Reverse engineering Linux

Linux Crackovani linux aplikaci. K čemu je komu dovednost pro crackování programů psaných pro operační systém Linux? Postup popsaný v článku Reverse engineering Linux nabízí primárně pochopit jak celá věc funguje – běh aplikací. Ne každý Linux sofwtare je free – To pro někoho může znamenat výzvu pro „konverzi“ komerční aplikace na freeware. Zkušenost lze dále uplatnit na jiných platforomách (třeba operační systém Windows).

Poznatky vám pomůžou precizně ladit aplikace a dobývat nove boxy.

Cim se budem zabyvat:
– decompilace
– staticka analyza souboru
– dynamicka alanyza souboru
– tracovani/patchovani procesu
– ELF

Pozadovana znalost:

– assembler AT&T / Intel
– C
– linux memory management

Obtiznost:
3/5 ###00

Pracovat budem s operačním systémem Damn Vulnerable Linux Live distribuce urcena k prohlubovani znalosti o pocitacove bezpecnosti a je k mani v ZIP cca 1GB. Balik obsahuje i soubory *.vmx pro VMware Player (virtualizacni nastroj, ktery je zdarma pro *nix i windows). To nam poskytne standardizovane prostredi s uz nachystanymi programy a ukoly. Konkretne se podivame na 2 crackmes obsazene v DVL.

DVL neni k praci s timto clankem potreba. Staci jakykoliv linux s potrebnymi programy. Crackmes se daji stahnout zvlast.

Xserver nebudem vubec potrebovat, takze po prihlaseni provedem upravu shellu.

$ splash -s
$ killall gpm
$ gpm -m /dev/mouse -t ps2

Tim odstranime boot splash screen a opravime rozsah mysi. Pokud se bez Xserveru neobejdete, mate na vyber KDE nebo FluxBox. Ja doporucuju zustat v textovem rezimu hlavne pokud DVL spoustite ve virtual machine (setri prostredky).

Cyrex LinuxCrackme1

Jednoduche crackme pro zacatecniky. Uplne prvni krok v reverse engineering. Otevri slozku, kde je crackme a spust ho.

$ cd /dvl/crackmes_package/level_01/Cyrex_LinuxCrackme1/
$ ./crackme
-[ Linux CrackMe (Level:2) by cyrex ]-
-[ TODO: You have to get the valid Password ]-
Enter Password: asd
-[ Ohhhh, your skills are bad try again later ]-

Za predpokladu, ze jde o jednoduche crackme pro zacatecniky, lze usoudit, ze heslo bude statickeho razu ulozene nekde v aplikaci. Dejme tomuto usudku sanci. Prozkoumame soubor o staticke retezce.

$ strings crackme

-[ Linux CrackMe (Level:2) by cyrex ]-
-[ TODO: You have to get the valid Password ]-
Enter Password:
47ghf6fh37fbgbgj
-[ Good, You’re ready to begin linux reversing ]-
-[ Ohhhh, your skills are bad try again later ]-

Troufam si rict, ze „47ghf6fh37fbgbgj“ je to heslo. A taky ze je. To bylo jednoduchy, ale skutecny zivot takovy nebude. Podivejme se trosku na overovani hesla. Provedeme malou statickou analyzu. Zobraz funkci main a s minimalni znalosti assembleru dokazeme urcit, co se deje.

$ objdump -d crackme | grep „“: -A 50
08048450 :
8048450: 55 push %ebp
8048451: 89 e5 mov %esp,%ebp
8048453: 83 ec 28 sub $0x28,%esp
8048456: 83 c4 f4 add $0xfffffff4,%esp
8048459: 68 20 86 04 08 push $0x8048620
; vlozi na zasobnik „ukazatel“
; na retezec „-[ Linux CrackMe (Level:2) by cyrex ]-“
804845e: e8 f9 fe ff ff call 804835c
; a zavola funkci printf ktera ho vypise
; dalse se opakuje s dalsimi retezci
8048463: 83 c4 10 add $0x10,%esp
8048466: 83 c4 f4 add $0xfffffff4,%esp
8048469: 68 60 86 04 08 push $0x8048660
; -[ TODO: You have to get the valid Password ]-
804846e: e8 e9 fe ff ff call 804835c
8048473: 83 c4 10 add $0x10,%esp
8048476: 83 c4 f4 add $0xfffffff4,%esp
8048479: 68 90 86 04 08 push $0x8048690
; Enter Password:
804847e: e8 d9 fe ff ff call 804835c
8048483: 83 c4 10 add $0x10,%esp
; vytvori prostor pro 16 bit lokalni promennou
8048486: 83 c4 f8 add $0xfffffff8,%esp
8048489: 8d 45 e0 lea 0xffffffe0(%ebp),%eax
; aby si funkce scanf vzala parametry musi se dat na zasobnik v opacnem poradi
804848c: 50 push %eax
; nejdriv adresa promenne kam se input ulozi
804848d: 68 a1 86 04 08 push $0x80486a1
; potom retezec „%s“ ktery slouzi jako format pro cteni
8048492: e8 95 fe ff ff call 804832c
; scanf(„%s“,(char *)eax)
; vola scanf a nacte hodnotu z inputu na adresu v eax = 0xffffffe0(%ebp)
8048497: 83 c4 10 add $0x10,%esp
804849a: 83 c4 f8 add $0xfffffff8,%esp
; chysta se volani funkce strcmp
804849d: 68 a4 86 04 08 push $0x80486a4
; na zasobnik „ukazatel“ na spravne heslo
80484a2: 8d 45 e0 lea 0xffffffe0(%ebp),%eax
80484a5: 50 push %eax
; na zasobnik „ukazatel“ na nactenou hodnotu z stdin
80484a6: e8 71 fe ff ff call 804831c
; strcmp((char *)eax,“47ghf6fh37fbgbgj“)
; kontrola zdali se input a heslo shoduji
80484ab: 83 c4 10 add $0x10,%esp
80484ae: 89 c0 mov %eax,%eax
80484b0: 85 c0 test %eax,%eax
80484b2: 75 12 jne 80484c6
; dal nepotrebujem nic vedet protoze cele overeni uz sme prosli a zname
; ukazatel na heslo 0x80486a4
80484b4: 83 c4 f4 add $0xfffffff4,%esp

Jendoduchy dukaz.

$ objdump -s crackme | grep -A 1 80486a0
80486a0 00257300 34376768 66366668 33376662 .%s.47ghf6fh37fb
80486b0 6762676a 00000000 00000000 00000000 gbgj…………

Ballmann CheckPasswd

Verze na stahnuti se trochu lisi, ja budu pracovat s crackme obsazene v Damn Vulnerable Linux (download check-password). O neco slozitejsi crackme.

$ cd /dvl/crackmes_package/level_01/Ballmann_checkpasswd/
$ ./check-password
I need a password!
$ ./check-password asd
Wrong password!

Vyborne, vime, kam se pise heslo. Je dobre, ze se mi podarilo vydedukovat, ze se heslo predava jako argument (nic uzasnyho), ale pro lepsi pochopeni ukazi, ze tomu tak doopravdy je. Provedu statickou analyzu inicializace procesu.

$ objdump -d ./check-password | less

080484df :
; int main(int argc, char *argv[])

; 0x8(%ebp) obsahuje hodnotu argumentu argc funkce main.
; zde se kontroluje, zda byl program spusten s nejakymi arumenty
80484fb: 83 7d 08 01 cmpl $0x1,0x8(%ebp)
; if(1 != argc) skoc na kontrolu hesla jinak pokracuj dal
80484ff: 75 18 jne 8048519
; vypise chybovou hlasku a skonci
8048501: c7 04 24 6d 86 04 08 movl $0x804866d,(%esp)
8048508: e8 63 fe ff ff call 8048370
804850d: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048514: e8 77 fe ff ff call 8048390

Trosku to objasnim. Neni pravidlo, ze 0x8(%ebp) obsahuje hodnotu argc. V tomto pripade tomu proste tak je. Ve skutecnosti se totiz pred funkci main volaji jeste inicializacni sekce, ktere jsou vlozene kompilatorem GCC. Prvopocatecni sekce se jmenuje _start. Vsechny tyto sekce jsou definove v hlavicce souboru formatu ELF.

Spusteni programu vypada tak, ze operacni system nacte soubor do pameti, zkontroluje, co je zac, koho je, atd. a pokud je vse OK, ulozi na zasobnik ruzne informace k procesu vcetne argumentu volani. Prvni argument je vzdy nazev procesu, nas ale zajimaji argumenty, ktere se za nej pripojuji. V pameti to pak vypada takto.

0x08048000
program (spustitelna cast)
data (retezce,hodoty a podobne)
staticke promenne
volne misto pro volani malloc, atd.
zasobnik
argumenty (vcetne nazvu programu)
promenne prostredi
nazev programu
NULL
0xBFFFFFFF

a detail spodni casti diagramu

argc
argv[0]
argv[1]

argv[argc-1]
NULL
env[0]
env[1]

env[n]
NULL

Zpatky k crackme. Vime, kam psat heslo, ted je treba zjistit jeho hodnotu. Pouzij stejny postup jako u prvniho crackme – vypis retezce v programu.

$ strings ./check-password | less

djjk/aWmbQkO6
I need a password!
Wrong password!
Accepted password!
/bin/csh

Vypada to na stejny pripad jako v prvnim crackme, ale neni tomu tak. Provedeme dynamickou analyzu za pomoci programu ltrace, ktery odchytava dynamicka volani.

$ ltrace ./check-password asd
__libc_start_main(0x80484df, 2, 0xbfec1cb4, 0x804856c, 0x80485bd
strncpy(0xbfec1bdc, 0xbfec273b, 20, 0, 0) = 0xbfec1bdc
crypt(0xbfec1bdc, 0x804865c, 20, 0, 0) = 0xb7ed7120
puts(„Wrong password!“Wrong password!
) = 16
exit(1, 0, 0xbfec1c28, 0x8048587, 0xb7ea5ffc
+++ exited (status 1) +++

Vsimni si volani strncpy a crypt. Strncpy je volan s dvema ukazately na zasobnik (viz diagram). Ve zdrojaku to nejspis vypadalo asi takto:
strncpy(pass,argv[1],20);
Dal se vola crypt s prvnim argumentem stejnym jako mel strncpy a druhy je ukazatel na nejaky retezec v sekci data. Podivame se na co problizne ukazuje.

$ objdump -s ./check-password | grep 8048654
8048654 03000000 01000200 646a0064 6a6a6b2f ……..dj.djjk/

Takze volani vypadalo asi takto: crypt(pass,“dj“);
Pokud si tuto funkci vyzkousis bokem a pouzijes stejne argumenty jake jsme dostali z analyzy, zjistis, ze retezec, ktery funkce crypt vraci je velice podobny tomu zahadnemu retezci djjk/aWmbQkO6. To by mohl byt kontrolni hash.

Projdi si celou aplikaci jen v assembleru a zjistis, ze uz neni co resit.

$ objdump -d ./check-password | less

V main je kontrola argumentu, pri spravnem poctu se zavola funkce check, kde se vypocita hash pro heslo predane argumentem aplikace a porovna se s retezcem djjk/aWmbQkO6 v sekci .data. Jestli se hashe shoduji, spusti /bin/csh

Zustava otazka, co se za onym hashem djjk/aWmbQkO6 skryva. Dal by se lousknout pomoci brute force metody, ale to neni ani trochu elegantni a muze to trvat dost dost .. dlouho. Dalsi moznosti je patchnout soubor a nahradit hash za jiny. To zni fajn, ale ja se v tomto pripade priklanim k jeste elegantnejsimu reseni a to je patchnuti obrazu aplikace v pameti.

Takzvany loader nacte aplikaci do pameti, upravi ji a az potom ji spusti. Tim nedojde k poskozeni souboru se stejnym efektem. Tato metoda se da pouzit i na injectovani shellcodu a dalsim dobromyslnostem.

#include
#include
#include
#include
#include
#
#define _BIN „check-password“ /* nazev crackme souboru */
#define _SALT „dj“ /* pridavek k pocitani hashe */
#define _W 4 /* sizeof(long) ktery vraci ptrace */
#define _PLEN 4 /* pocet long kolik zavira hash */
#
int main(int argc, unsigned char *argv[]){
#
if(argc<2){
fprintf(stderr,“usage: %s password\n“,argv[0]);
return 0;
}
#
int pid;
/* fork viz. `man fork` */
if((pid = fork()) == 0){
/*

nastav tracovani procesu na sebe(dite), tim se pri volani exec dostavi signal SIGTRAP, aby sem mohl jako rodic zmenit data aplikace jeste predtim, nez se proces volany pomoci exec spusti

*/
ptrace(PTRACE_TRACEME, 0,NULL,NULL);
/* predani kontroly */
execl(_BIN, _BIN, argv[1], NULL);
} else {
int status;
/* pockam az dite zmeni stav (obdrzi SIGTRAP pri volani exec) */
wait(&status);
/* pokud dite neni nazivu, je neco spatne a koncime */
if(WIFEXITED(status))
return -1;
/* dite obrdzelo SIGTERM a muzeme ho prznit */
if(WIFSTOPPED(status)){
/* hash noveho hesla */
char * pass = (char *)crypt(argv[1],_SALT);
printf(„new password hash: %s\n“,pass);
/* vytvor blok kterym nahradi puvodni hash */
char pload[_W*_PLEN];
strcpy(pload,pass);
/* posledni 2 hodnoty v bloku nepatri k hashi ale cizim datum*/
pload[(_W*_PLEN)-2] = 0x49;
pload[(_W*_PLEN)-1] = 0x20;
/* nastav adresu a delku hashe */
long *p = (long *)pload, *addr = (long *)0x0804865f;
long data, limit = (long)(addr + _PLEN);
/* prepis puvodni hash novym */
printf(„[i] run patch\n“
„0xADDRESS ASCII HEXDUMP ASCII HEXDUMP\n…\n“);
while(addr < (long *)limit){
/* cti data(long) z procesu(dite) */
data = ptrace(PTRACE_PEEKDATA,pid, addr ,NULL);
printf(„%p: %4.4s %x < %4.4s %x\n“,addr, &data, data, p,*p);
/* prepise hash v diteti novym hashem */
ptrace(PTRACE_POKEDATA, pid, addr, *p);
addr++; p++;
}
printf(„…\n[i] starting child process\n\n“);
/* prestan dite otravovat */
ptrace(PTRACE_DETACH,pid,0,0);
/* a pockej az umre */
wait(NULL);
}
}
#
return(1);
}

musi se kompilovat s -lcrypt

$ gcc loader.c -o loader -lcrypt;
$ ./loader airdump
new password hash: djfwob3BbChAg
[i] run patch
0xADDRESS ASCII HEXDUMP ASCII HEXDUMP

0x804865f: djjk 6b6a6a64 < djfw 77666a64
0x8048663: /aWm 6d57612f < ob3B 4233626f
0x8048667: bQkO 4f6b5162 < bChA 41684362
0x804866b: 6 20490036 < g 20490067

[i] starting child process
#
Accepted password!
$

Kam dál?