/ / Debugger und CPU-Emulator erkennen selbstmodifizierten Code nicht - Python, Debugging, Assembly, Reverse-Engineering, selbst-modifizierend

Debugger und CPU-Emulator erkennen selbst-modifizierten Code nicht - Python, Debugging, Assembly, Reverse-Engineering, selbst-modifizierend

Problem:

Ich habe eine Elf ausführbar gemacht, die selbst eine modifiziertseines Bytes. Es ändert einfach eine 0 für eine 1. Wenn ich die ausführbare Datei normal ausführe, kann ich sehen, dass die Änderung erfolgreich war, weil sie genau wie erwartet ausgeführt wird (mehr dazu weiter unten). Das Problem tritt beim Debuggen auf: Der Debugger (mit radare2) gibt den falschen Wert zurück, wenn er das modifizierte Byte betrachtet.

Kontext:

Ich habe eine Reverse Engineering Herausforderung gemacht, inspiriert von Kleinste Elfe. Sie können den "Quellcode" (wenn Sie es so nennen können) dort sehen: https://pastebin.com/Yr1nFX8W.

Zusammenstellen und ausführen:

nasm -f bin -o tinyelf tinyelf.asm
chmod +x tinyelf
./tinyelf [flag]

Wenn das Flag richtig ist, gibt es 0 zurück. Jeder andere Wert bedeutet, dass Ihre Antwort falsch ist.

./tinyelf FLAG{wrong-flag}; echo $?

... gibt "255" aus.

! Lösung SPOILER!

Es ist möglich, es statisch umzukehren. Sobald das erledigt ist, werden Sie herausfinden, dass jedes Zeichen in der Flagge durch diese Berechnung gefunden wird:

flag[i] = b[i] + b[i+32] + b[i+64] + b[i+96];

... wobei i der Index des Zeichens ist und b die Bytes der ausführbaren Datei selbst ist. Hier ist ein c-Skript, das die Herausforderung ohne einen Debugger löst:

#include <stdio.h>

int main()
{
char buffer[128];
FILE* fp;

fp = fopen("tinyelf", "r");
fread(buffer, 128, 1, fp);

int i;
char c = 0;
for (i = 0; i < 32; i++) {
c = buffer[i];

// handle self-modifying code
if (i == 10) {
c = 0;
}

c += buffer[i+32] + buffer[i+64] + buffer[i+96];
printf("%c", c);
}
printf("n");
}

Sie können sehen, dass mein Solver einen speziellen Fall behandelt: Wenn i == 10, ist c = 0. Das ist, weil es der Index des Bytes ist, das während der Ausführung geändert wird. Ich löse den Solver und rufe tinyelf damit an:

FLAG{Wh3n0ptiMizaTioNGOesT00F4r}
./tinyelf FLAG{Wh3n0ptiMizaTioNGOesT00F4r} ; echo $?

Ausgabe: 0. Erfolg!

Großartig, lass uns jetzt versuchen, es dynamisch zu lösen, indem wir python und radare2 benutzen:

import r2pipe

r2 = r2pipe.open("./tinyelf")

r2.cmd("doo FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAA}")
r2.cmd("db 0x01002051")

flag = ""
for i in range(0, 32):
r2.cmd("dc")
eax = r2.cmd("dr? al")
c = int(eax, 16)
flag += chr(c)

print("nn" + flag)

Es fügt einen Haltepunkt für den Befehl ein, der die eingegebenen Zeichen mit den erwarteten Zeichen vergleicht und dann erhält, mit was verglichen wird (al). Das sollte funktionieren. Aber hier ist die Ausgabe:

FLAG {Wh3n0 tiMiza ioNGOesT00F4r}

2 falsche Werte, von denen einer am Index 10 liegt (das modifizierte Byte). Seltsam, vielleicht ein Bug mit radare2? Lass uns als nächstes Einhorn (einen CPU-Emulator) ausprobieren:

from unicorn import *
from unicorn.x86_const import *
from pwn import *

ADDRESS = 0x01002000

mu = Uc(UC_ARCH_X86, UC_MODE_32)
code = bytearray(open("./tinyelf").read())

mu.mem_map(ADDRESS, 20 * 1024 * 1024)

mu.mem_write(ADDRESS, str(code))

mu.reg_write(UC_X86_REG_ESP, ADDRESS + 0x2000)
mu.reg_write(UC_X86_REG_EBP, ADDRESS + 0x2000)

mu.mem_write(ADDRESS + 0x2000, p32(2)) # argc
mu.mem_write(ADDRESS + 0x2000 + 4, p32(ADDRESS + 0x5000)) # argv[0]
mu.mem_write(ADDRESS + 0x2000 + 8, p32(ADDRESS + 0x5000)) # argv[1]
mu.mem_write(ADDRESS + 0x5000, "x" * 32)

flag = ""

def hook_code(uc, address, size, user_data):
global flag
eip = uc.reg_read(UC_X86_REG_EIP)

if eip == 0x01002051:
c = uc.reg_read(UC_X86_REG_EAX) & 0x7f
#print(str(c) + " " + chr(c))
flag += chr(c)

mu.hook_add(UC_HOOK_CODE, hook_code)

try:
mu.emu_start(0x01002004, ADDRESS + len(code))
except Exception:
print flag

Diesmal gibt der Löser aus: FLAG {Wh3n0otiMizaTioNGoesT00F4r}

Beachten Sie beim Index 10: "o" statt "p". Das ist ein Aus-1-Fehler genau dort, wo das Byte geändert wird. Das kann kein Zufall sein, oder?

Jeder hat eine Idee, warum diese beiden Skripte nicht funktionieren? Vielen Dank.

Antworten:

6 für die Antwort № 1

Es gibt kein Problem mit radare2, aber Ihre Analyse des Programms ist falsch, daher behandelt der Code, den Sie geschrieben haben, diesen RE falsch.

Lass uns beginnen mit

Wenn i == 10, ist c = 0. Das ist, weil es der Index des Bytes ist, das während der Ausführung geändert wird.

Das ist teilweise richtig. Es wird am Anfang auf Null gesetzt, aber nach jeder Runde gibt es diesen Code:

xor al, byte [esi]
or byte [ebx + 0xa], al

Also lasst uns verstehen, was hier passiert. al ist der aktuell berechnete Char der Flagge und esi verweist auf die FLAG, die als Argument eingegeben wurde und bei [ebx + 0xa] Wir haben derzeit 0 (am Anfang gesetzt), also das Char im Index 0xa wird nur dann null bleiben, wenn das berechnete Flag charist gleich dem in argument und da du r2 mit einem fake flag startest, fängt das an, ein problem vom 6. char zu sein, aber das ergebnis davon siehst du am ersten bei index 10. Um das zu mildern musst du dein aktualisieren skript ein bisschen.

eax = r2.cmd("dr? al")
c = int(eax, 16)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")

Was wir hier machen ist, dass nachdem der Brekpoint getroffen wurde und wir den berechneten Flag Char gelesen haben, wir zwei Anweisungen weiter bewegen (um zu erreichen 0x01002054) und dann setzen wir al zu 0x0 zu emulieren, dass unser Char bei [esi] tatsächlich dasselbe war wie das berechnete (so xor wird zurückkehren 0 in diesem Fall). Indem wir dies tun, behalten wir den Wert bei 0xa immer noch null sein.

Jetzt das zweite Zeichen. Dieses RE ist knifflig;) - es liest sich selbst und wenn du das vergisst, könntest du mit einem Fall wie diesem enden. Versuchen wir zu analysieren, warum dieses Zeichen ausgeschaltet ist. Es ist das 18. Zeichen des Flags (also der Index ist 17, da wir von 0 ausgehen) und wenn wir die Formel für Zeichenindizes überprüfen, die wir aus dem Binärcode lesen, haben wir festgestellt, dass es Indizes sind : 17(dec) = 11(hex), 17 + 32 = 49(dec) = 31(hex), 17 + 64 = 81(dec) = 51(hex), 17 + 96 = 113(dec) = 71(hex). Aber dieses 51(hex) sieht seltsam vertraut aus? Haben wir das nicht irgendwo vorher gesehen? Ja, es ist der Offset, mit dem Sie Ihren Haltepunkt setzen, um den zu lesen al Wert.

Dies ist der Code, der deinen zweiten Char bricht

r2.cmd("db 0x01002051")

Yup - dein Haltepunkt. Sie legen fest, dass an dieser Adresse unterbrochen werden soll, und ein weicher Haltepunkt setzt a 0xcc in der Speicheradresse, wenn der Opcode, der 3. Byte des 18. Chars liest, diesen Punkt trifft, wird es nicht 0x5b (der ursprüngliche Wert) es bekommt 0xcc. Um das zu beheben, müssen wir diese Berechnung korrigieren. Hier kann es wahrscheinlich auf eine klügere / elegantere Art und Weise gemacht werden, aber ich entschied mich für eine einfache Lösung, also tat ich das einfach:

if i == 17:
c -= (0xcc-0x5b)

Einfaches Subtrahieren wurde versehentlich hinzugefügt, indem ein Haltepunkt in den Code eingefügt wurde.

Der endgültige Code:

import r2pipe

r2 = r2pipe.open("./tinyelf")
print r2

r2.cmd("doo FLAG{AAAAAAAAAAAAAAAAAAAAAAAAAA}")
r2.cmd("db 0x01002051")

flag = ""
for i in range(0, 32):
r2.cmd("dc")
eax = r2.cmd("dr? al")
c = int(eax, 16)
if i == 17:
c -= (0xcc-0x5b)
r2.cmd("ds 2")
r2.cmd("dr al = 0x0")
flag += chr(c)

print("nn" + flag)

Das druckt die korrekte Flagge:

FLAG {Wh3n0ptiMizaTioNGoesT00F4r}

Wie beim Unicorn setzen Sie den Haltepunkt nicht so, dass das Problem 2 weggeht, und das Aus-bei-1 am 10. Index ist aus demselben Grund wie für r2.