Copy Link
Add to Bookmark
Report
7x02 theory_of_disassembling
Teorija Disassemblovanja i malo trikova oko pisanja protektora
S verom u Boga, deroko of ARTeam
Za neke od svojih potreba sam poceo da pisem disassembler, tacnije za jedan
crackme sa www.crackmes.de za koji mi je disassembler po mojoj meri bio vise
nego potreban, mozda bi bolje islo da sam koristio IDC, ali eto hteo sam da to
ucinim na sebi svojstven nacin. Posle toga krenuo sam da pisem svoj primitivni
paker koji radi samo na BCC aplikacijama, i crackme zasticen sa tim pakerom sam
takodje objavio na www.crackmes.de. U ovom tekstu bih hteo da pojasnim logiku
istog protektora i da vam pokazem kako sam ja to uradio.
Najveci problem za nas predstavlja razlikovanje code i data, tj. data koji je
ubacen u kod, kad imamo relokacije onda ovo odista i nije veliki problem jer na
osnovu relokacija znamo sta je gde, no podjimo da nemamo relokacije i da hocemo
samo da zastitimo OEP kao sto to radi ASProtect recimo.
Da bi postigli slican efekat moramo pre svega da imamo neki disasm engine, na
svu srecu, ovde je vise nego dovoljan LDE (Length decoding engine) koji ce nam
samo reci duzinu instrukcija, potom moramo znati kako cemo disassemblovati nas
kod i kako OEP u velikom broju slucajeva izgleda. Ako pogledamo kod neke BCC
aplikacije to nam dodje ovako:
.text:00401000 start proc near
.text:00401000 jmp short loc_401012
.text:00401000 ; ---------------------------------------------------------------------------
.text:00401002 dw 6266h
.text:00401004 dd 2B2B433Ah, 4B4F4F48h
.text:0040100C db 90h, 0E9h
.text:0040100E dd offset ___CPPdebugHook
.text:00401012 ; ---------------------------------------------------------------------------
.text:00401012
.text:00401012 loc_401012: ; CODE XREF: startj
.text:00401012 mov eax, TlsIndex
.text:00401017 shl eax, 2
.text:0040101A mov dword_409113, eax
.text:0040101F push edx
.text:00401020 push 0 ; lpModuleName
.text:00401022 call GetModuleHandleA
.text:00401027 mov edx, eax
.text:00401029 call nullsub_2
.text:0040102E pop edx
.text:0040102F call nullsub_1
.text:00401034 call nullsub_3
.text:00401039 push 0
.text:0040103B call __ExceptInit
.text:00401040 pop ecx
.text:00401041 push offset off_4090B8
.text:00401046 push 0 ; lpModuleName
.text:00401048 call GetModuleHandleA
.text:0040104D mov dword_409117, eax
.text:00401052 push 0
.text:00401054 jmp __startup
...
.text:00406788 __startup proc near
.text:00406788
.text:00406788 var_C = dword ptr -0Ch
.text:00406788 var_8 = dword ptr -8
.text:00406788 arg_0 = dword ptr 8
.text:00406788
.text:00406788 push ebp
.text:00406789 mov ebp, esp
.text:0040678B add esp, 0FFFFFFF4h
.text:0040678E push ebx
...
.text:00406904 pop edi
.text:00406905 pop esi
.text:00406906 pop ebx
.text:00406907 mov esp, ebp
.text:00406909 pop ebp
.text:0040690A retn
.text:0040690A __startup endp ; sp = -20h
main() ili WinMain() ce biti pozvan iz __startup, a takodje i ExitProcess ce
biti pozvan odatle tako da slobodno mozemo da disassemblujemo bez brige za icim,
dalji problem za nas predstavljaju jcc/call koje moramo rebasovati buduci da
planiramo da izmedju originalnih instrukcija ubacimo i junk instrukcije kako bi
nas kod bio malo tezi za tracovanje. S obzirom da ubacujemo junk postavlja se
pitanja sredjivanja jcc/call kako bi skakali na prave instrukcije. Dobro, pred
nas se postavlja nekoliko pitanja koja moramo da resimo:
- pravilno da dekodiramo instrukcije
- pravilno da otkrijemo jcc/call i da te podatke negde sacuvamo
- nacin na koji cemo to uciniti je takodje jedan od problema
Prvi problem resavamo veoma lako, a to je da koristimo neki LDE, ja u svom kodu
koristim svoj LDE mada na raspolaganju nam stoji gomila LDE kao sto je Z0mbie
-jev, rgb-ov, underX-ov, ollydbg disasm engine, nasm disasm engine i gomila drugih.
Drugi problem je malo slozeniji ali je opet u sustini prost kad malo bolje
razmislimo i sve napisemo na papiru. Pre svega moramo da vodimo racuna o nekim
situacijama koje se mogu desiti tokom naseg izgradjivanja novog OEP-a sa ubacenim
laznim instrukcijama:
1. loop je short sto znaci da moze da skace samo za displacement -+128
2. jcc-ovi mogu biti short sto znaci da im displacement moze biti samo -+128
3. jmp short moze biti samo -+128
Sve ovo moramo zameniti sa odgovarajucim instrukcijama jer moze se desiti da jcc
short kad ubacimo junk izmedju instrukcija ne moze da skoci tamo gde treba, isto
cinimo i sa jmp (eb) pretvaramo u jmp near (e9), a loop pak mozemo zemniti sa
dec ecx, jz __label setom instrukcija, ali problem je opet eflags jer loop ne
dira eflags :P No svom srecom ja do sada nisam video nijedan oep generisan od
strane nekog kompajlera koji koristi loop, kazu da je spora instrukcija, mada,
opet, heh, nista joj ne fali...
Takodje imamo jedan problem koji je nas prijatelj The Mental Driller nazvao
Future Table, naime to su instrukcije na koje neki jmp/jcc pokazuje a koje jos
nisu dekodirane te ne mozemo da ih obelezimo kao instrukcije na koje jcc/jmp
pokazuje sto je bitno kod meta engina kako bismo znali da li instrukcija moze da
se shrinkuje ili expanduje itd... S obzirom da mi ovde ne radimo meta, vec vise
obsfukiramo OEP taj problem se u nas ne postavlja, ali moramo odmah razmisljati
i o assembleru, tj. delu koda koji ce instrukcije da propisno realocira. Taj kod
mora nekako znati gde je instrukcija na koju jcc/jmp pokazuje, posle malo
razmisljanja dosao sam do fenomenalne ideje, da sve instrukcije organizujem u
sturkture kako bi kasnije znao sta je gde, tako da moja struktura izgleda ovako:
entry_point_struct struct
ep_opcode_type dd ? ;tip instrukcije
ep_original_raw dd ? ;gde je instrukcija u originalnom fajlu (RAW)
ep_destination dd ? ;koristi se samo kod call da se izracuna gde on vodi, takodje i kod TYPE_STOP
ep_link_list dd ? ;koristi se kod jcc/loop/jecxz kako bi znao gde da linkujem instrukciju
ep_va_pos dd ? ;pozicija u memoriji prilikom linkovanja instrukcije koristi se samo kad je jcc
ep_size dd ? ;velicina instrukcije, ovo se koristi samo kad je normal u pitanju
ep_instruction db 16 dup(0) ;sama instrukcija ukoliko nije neki od specijalnih slucajeva
entry_point_struct ends
TYPE_CALL equ 1 ;call
TYPE_JCC equ 2 ;jcc
TYPE_JMP equ 3 ;jmp
TYPE_NORMAL equ 4 ;sve instrukcije koje ne menjaju eip
TYPE_STOP equ 5 ;instrukcija koja oznacava redirekciju eipa na originalan kod
;drugim recima, ovde je kraj mog ukradenog koda
Sada cu podrobnije da objasnim strukturu koju sam gore prikazao, naime ideja je veoma prosta:
ep_opcode_type mi sluzi da prilikom linkovanaj znam koje instrukcije da trazim, to su
TYPE_CALL/TYPE_JCC i TYPE_JMP ciji displacement mora biti sredjen kako bi pokazivao na
pravu adresu.
ep_original_raw - raw instrukcije prilikom disassemblovanja
ep_destination - pokazuje virtuelnu adresu na koju call treba da skoci
ep_link_list - slicno kao i ep_destination ali sa osobitom razlikom buduci da
je destination izracunat preko RAWa, koristi se prilikom linkovanja
i to samo kad je rec o jcc/jmp
ep_va_pos - pozicija instrukcije u memoriji prilikom assemblovanja
ep_size - velicina instrukcije
ep_instruction - buffer gde se cuva originalna instrukcija
Krenimo sad logikom disassemblera i uzmimo da imamo sledeci kod:
00: call __1st_call
05: test eax, eax
07: jz __label1
09: xor eax, eax
0B: __label1: dec eax
Kod je vise nego glup ali ce nam objasniti kako radi ovaj engine:
pozivamo LDE i proveravamo da li je instrukcija call/jcc/jmp (short/near),
prva instrukciaj je call tako da cemo entry_point_struct ovako popuniti:
entry_point_struct struct
ep_opcode_type dd TYPE_CALL
ep_original_raw dd 00
ep_destination dd offset __1st_call
ep_link_list dd 0
ep_va_pos dd ? <--- koristimo samo kod assemblovanja
ep_size dd 5
ep_instruction db 0e8h <--- imamo destination tako da nam treba
entry_point_struct ends samo tip instrukcije, cak sta vise ni
to nam ne treba jer asembler zna da je
TYPE_CALL i sta da assembluje
Sledeca instrukciaj je test eax, eax
entry_point_struct struct
ep_opcode_type dd TYPE_NORMAL (instrukciaj koja ne modifikuje EIP)
ep_original_raw dd 05
ep_destination dd 0
ep_link_list dd 0
ep_va_pos dd ? <--- koristimo samo kod assemblovanja
ep_size dd 5
ep_instruction db xor eax, eax
entry_point_struct ends
Potom sledi jz instrukciaj koja je ovde short i koju pretvaramo u near koristeci 0F prefix,
za ovo pretvaranje pogledajte IA 32 manual volume 2.
entry_point_struct struct
ep_opcode_type dd TYPE_JCC
ep_original_raw dd 07
ep_destination dd 0
ep_link_list dd 0B
ep_va_pos dd ? <--- koristimo samo kod assemblovanja
ep_size dd 6 <--- near jz je 6 bajtova
ep_instruction db jz near
entry_point_struct ends
Naime kod relokacije jcc/jmp ep_link_list nam koristi da znamo koju strukturu
trazimo, a kad nadjemo strukturu preko ep_va_pos znamo gde se nalazi instrukcija
na koju jcc treba da skoci sto onda prosto i asemblujemo.
Tok asemblovanja je sad prost kad imamo ovu strukturu i u zavisnosti od
kompajlera odlucujemo dokle da tracujemo, kad je rec o BCC aplikacijama
disassemblujemo oep do prvog ret koji je ujedno i kraj __starup rutine.
Naime kod asemblovanja radimo sledece, uzimamo niz struktura i pisemo jednu
instrukciju zavisnoti od njenog tipa, a izmedju pisanja svake instrukcije pisemo
junk instrukcije koje nece modifikovati ni registre ni eflags sto se cini
prostim ubacivanjem makroa koji ce na pocetku imati pusha/pushf set instrukcija,
a na kraju popf/popa instrukcije.
Takodje prilikom assemblovanaj instrukcija u poseban buffer moramo da ubacimo
njihovu adresu u ep_va_pos strukturu za svaku instrukciju. Posto smo ovo
ucinili, nas sledeci korak je da realociramo sve jcc i call insrukcije, kao i
jmp instrukcije sto cinimo veoma prosto:
reallocate_jcc:
__reallocate_jcc: lea ebx, [ebp+pe_structs] <--- uzimamo niz struktura
__cycle_j: cmp [ebx.ep_opcode_type], 0 <--- kraj strukture????
je __done_jcc <--- da, i zavrsavamo sa radom
cmp [ebx.ep_opcode_type], TYPE_JMP <--- JMP instrukcije
jne __check_jcc <--- ne, proveravamo jcc
mov esi, [ebx.ep_link_list] <--- ep_link_list je zapravo RAW instrukcije
na koju treba da skocimo, zamislite to kao
ID strukture
call find_entry <--- i trazimo ep_struct ciji je ep_original_raw
jednak sa ep_link_list iz ove strukture
mov eax, [ebx.ep_va_pos] <--- memorijska adresa naseg JMP
mov ecx, [esi.ep_va_pos] <--- memorijska adresa instrukcije na koju skacemo
add eax,5 <--- izracunavamo displacement koji trebamo
sub ecx, eax <--- da stavimo kod jmp kako bi se skocilo na
mov [eax-4], ecx <--- pravu adresu
add ebx, size entry_point_struct <--- idemo na sledecu strukturu
jmp __cycle_j <--- i idemo opet u krug
__check_jcc: cmp [ebx.ep_opcode_type], TYPE_JCC <--- istu logiku kao i gore
jne __spin_j <--- primenjumeo ovde
mov esi, [ebx.ep_link_list]
call find_entry
mov eax, [ebx.ep_va_pos]
mov ecx, [esi.ep_va_pos]
add eax, 6
sub ecx, eax
mov [eax-4], ecx
add ebx, size entry_point_struct
jmp __cycle_j
__spin_j: add ebx, size entry_point_struct
jmp __cycle_j
__done_jcc: retn
find_entry: push eax
lea eax, [ebp+pe_structs]
sub eax, size entry_point_struct
__cycle_fe: add eax, size entry_point_struct
cmp [eax.ep_opcode_type], 0
je __e_fe
cmp [eax.ep_original_raw], esi
jne __cycle_fe
__e_fe: xchg eax, esi
pop eax
retn
Nadam se da ste process assemblovanja razumeli? Slicno cinimo i sa call, s tom
razlikom sto ovde koristimo samo ep_displacement field, normalno vi mozete ici i
do ludih ideja i obsfukirati call buduci da znate sta se treba pozvati i koristiti
kod kao ovaj:
call_dispatcher: pusha
call __delta_cd
__delta_cd: pop ebp
sub ebp, offset __delta_cd
mov esi, [esp+20h] ;uzimamo ret adresu sa stacka
sub esi, 5 ;sad nalazimo gde se nalazi call
mov ebx, [ebp+ep_mem1]
sub ebx, size entry_point_struct
__find_call: add ebx, size entry_point_struct
cmp [ebx.ep_va_pos], esi ;trazimo odgovarajucu strukturu...
jne __find_call
mov esi, [ebx.ep_destination]
mov dword ptr[ebp+destination+1], esi
popa ;vrcemo registre
destination: push 0deadc0deh ;i idemo na pravo mesto...
retn
ep_mem1 dd ? ;pokazuje na strukture tipa entry_point
size_call_dispatcher = $-call_dispatcher
Naime call_dispatcher, kako bi ucinio popravljanje oep-a malo tezim sve
instrukcije tipa TYPE_CALL relocira da budu:
call call_dispatcher koji ce zatim na osnovu adrese od call da nadje
ep_destination i da kod tamo redirektuje.
To je cela logika obsfukiranja OEP-a, veoma prosto kao sto se da videti, malo
maste i to je to.
Sada cu vam prikazati neke ideje koje ovaj moj BCC protektor koristi kako bi
svoj kod ucinio manje debugging friendly i onemogucio postavljanje BreakPointova
u njemu, naime rec je o metamorfnom enginu koji je prezentovao Lord Julus u svom
clanku u 29a zine u vezi sa metamorfnim enginom. Razmislimo prvo odmah sta je to
sto mnoge pakere cinim relativno laganim za pracenjem:
1. kod koji se izvrsava iz alociranih buffera je uvek na istoj adresi sto nam
omogucuje lako postavljanje BPM (hardwerskih breakpointova) ne samo tokom vise
izvrsavanja programa na istom sistemu nego cak i posle restarta adrese se ne
menjaju. Zasto mi ASProtect pada napamet!!??
2. Onemoguciti postavljanje BPXova (f2 u olly ili BPX address u softice)
Resenje za ovo nam daje sam Lord Julus kroz metamorfni engine kao i trik sa
rdtsc i alociranjem buffera (moze i GetTickCount ili da direktno citamo Timer iz
SharedUserData sto ujedno cini i GetTickCount):
<++>
call ebx, GPTR, 5000h <-- GlobalAlloc
;
; ovaj trik sad sa rdtsc sluzi kako bismo izbegli postavljanje hwbpa kroz razlicite sesije debugera
;
xchg eax, edi
rdtsc
and eax, 0FFFh
add eax, edi
mov [ebp+stolen_oep], eax
<++>
Naime mi pocetak naseg buffera odredjujemo tako da pocetak buffera uvek zavisi
od rdtsc, cime je pocetak oep-a uvek razlicit od onog koji je bio u prethodnoj
sesiji debugovanja. Veoma cool, zar ne :P Na taj nacin HWBP na odredjenoj adresi
ce vec prilikom sledeceg debugovanja biti invalid i moracete rucno da trazite
gde program odredjuje pocetak OEPa. Doduse to u mom protektoru nije tesko jer
sam ga namerno uradio kako bi ljudi mogli lako da ga tracuju i da vezbaju svoje
RCE vestine kroz mali, ali veoma lepo skockan protektor.
Onemogucavanje BPX se cini kroz MetaEgnine gde je predvidjeno da se sve
procedure koje treba da se izvrse prekopiraju u jedan buffer i tamo izvrse, tako
da ako postavite BPX negde, desice se da sledeca procedura koja treba da se izvrsi
prebrise proceduru na koju ste stavili BPX.
call MetamorphizeEngine, META_ASSEMBLE_NORMAL
...
MetamorphizeEngine: pusha
mov esi, [esp+24h] ;index za odrejdnei engine
call __m_delta
__m_delta: pop ebp
sub ebp, offset __m_delta
lea ebx, [ebp+MetaProcedures] ;strukura nasih argumenata
lea eax, [ebx+esi*8] ;sad uzimamo odgovarajucu proceduru
mov esi, [eax]
add esi, ebp ;i cinimo je da bude relativna sa delta offsetom
mov ecx, [eax+4] ;uzimamo velicinu
mov edi, [ebp+meta_buffer] ;destinacija :)
cld
rep movsb ;kopiramo
popa ;sredjujemo stack
call [ebp+meta_buffer] ;pozivamo proceduru
retn 4 ;i vracamo se nazad, prosto zar ne
MetaProcedures dd offset getkernelbase ;u apizloader.inc
dd size_getkernelbase
dd offset getprocaddress ;u apizloader.inc
dd size_getprocaddress
dd offset assemble_jcc
dd size_assemble_jcc
dd offset assemble_normal
dd size_assemble_normal
dd offset assemble_call
Dakle, svaka procedura bica kopirana u meta_buffer i odande biva izvrsena,
takodje meta_buffer koristi rdtsc prilikom alociranja cime izbegavamo opet hwbp
na istu adresu kao i u prethodnoj sesiji debugovanja. No doduse HWBP moze biti
postavljen, ali cemo se uvek zakucavati na razlicite procedure, a s obzirom da
pozivamo meta_buffer prilikom assemblovanja nekih 50-100 instrukcija, moze biti
dosadno da se svaki ovaj deo koda tracuje... Zar ne :)
Pa to je to, to je ukratko teorija disasemblovanja, ako bismo imali neki dobar
MetaEngine, ali pravi meta engine koji ce da disasembluje instrukcije i da ih
menja sa sebi slicnima, onda bismo mogli da napravimo OEP koji niko zivi ne bi
prepoznavao, recimo, kad nadjemo u entry_point_strukturi:
push 0dead0c0de
mi to mozemo zameniti sa recimo:
sub esp, 4
mov [esp], 0deadc0deh
ili:
mov ebx, eax
sa:
push eax
pop ebx
itd, itd... i tada bismo imali veoma napredan Poly OEP koji bismo mogli da
iskoristimo i dobro prodamo nekoj kompaniji kojoj je to neophodno potrebno za
svoju zastitu...
To je to, u prilog vam dajem crackme sa ovim protektorom.
Nekoliko stvari na koje biste trebali da obratite paznju. Pomenuo sam koriscenje
rdtsc-a da se uvek odredi random pocetak buffera. Vama je vec poznato da postoji
nekoliko drivera koji na nasu zalost postavljaju rdtsc na privileged instruction
i fakuju ga sa 0. Takodje smo svesni mogucnosti hookovanja GetTickCount, ali ono
sto nismo rekli je da GetTickCount izvlaci podatke iz UserSharedData koji sluzi
za thread schedulling, a to je gotovo nemoguce patchovati iz ring3. Ako bismo
taj buffer patchovali sa 0 iz ring0 onda bismo disablovali thread scheduling i
windows ne bi mogao da menja threadove. Postoji caka i da se ova provera zaobidje
ali evo kako:
UsareShardData je takodje mapiran i u ring0 memoriji na drugoj adresi.
Zahvaljujuci paging sistemu koji postoji na ia32 cpu, mogli bismo fakovati
PDE/PTE tabele i tako naterati windows da misli da je nasa memorija alocirana u
ring0 zapravo UserSharedData, tada se system timer ne bi menjao, a opet bi
GetTickCount vracao 0 ili sta god da mi odredimo. Doduse ovo je malo naprednija
tehnika i zahteva dobro poznavanje ia32 i windows internala, tako da ovo ostavljam
za neki drugi clanak, a dotle, mislite o tome :)
S verom u Boga, deroko/ARTeam