Poniższy post został pierwotnie opublikowany na forum 4programmers.net w temacie "Hello world bez bibliotek i asm" (link).
--post start--
Kawałek kodu i ode mnie - bardziej ofc chodziło mi o zilustrowanie metody niż o zawsze-działający-kod :)
Kod napisany jest pod linuxa (32-bity x86), natomiast tą samą metodę można użyć na 64-bity oraz na Windowsa 32- / 64-bity.
Kod nie korzysta z żadnych bibliotek (nawet nie szuka żadnych w pamięci), ani nie ma żadnych wstawek asma/innych (przynajmniej jawnie ;>).
Wyjaśnienie zasady działania pod kodem.
volatile unsigned int something_wicked_this_way_comes(
int a, int b, int c, int d) {
a ^= 0xC3CA8900; b ^= 0xC3CB8900; c ^= 0xC3CE8900; d ^= 0x80CDF089;
return a+b+c+d;
}
void* find_the_witch(unsigned short witch) {
unsigned char *p = (unsigned char*)something_wicked_this_way_comes;
int i;
for(i = 0; i < 50; i++, p++) {
if(*(unsigned short*)p == witch) return (void*)p;
}
return (void*)0;
}
typedef void (*gadget)() __attribute__((fastcall));
int main(void) {
gadget eax_from_esi_call_int = (gadget)find_the_witch(0xF089);
gadget set_esi = (gadget)find_the_witch(0xCE89);
gadget set_ebx = (gadget)find_the_witch(0xCB89);
gadget set_edx = (gadget)find_the_witch(0xCA89);
if(!eax_from_esi_call_int) return 1;
if(!set_esi) return 3;
if(!set_ebx) return 4;
if(!set_edx) return 5;
set_edx(12), set_ebx(1), set_esi(4);
eax_from_esi_call_int("Hello World\n");
return 0;
}
Kod korzysta z metody bardzo podobnej do metody exploitacji języków JITowanych przy zabezpieczonej pamięci via XD/NX/XN/DEP/etc - tj. niejawnie starałem się w pamięć wykonywalną wrzucić kilka "gadżetów" (w rozumieniu ret2libc aka return oriented programing - http://gynvael.coldwind.pl/?id=144) i następnie je użyć do zrobienia syscalla do systemu (tak więc żadne biblioteki nie są potrzebne, natomiast oczywiście zachodzi bezpośrednia interakcja z kernelem).
Owe gadzety są [umieszczone] w funkcji something_wicked_with_way_comes, a konkretniej są to stałe przy xor'ach:
a ^= 0xC3CA8900; b ^= 0xC3CB8900; c ^= 0xC3CE8900; d ^= 0x80CDF089;
Powyższy kod w assemblerze / kodzie maszynowym zostanie zapisany następująco:
[...]
6: 35 00 89 ca c3 xor eax,0xc3ca8900
b: 89 45 08 mov DWORD PTR [ebp+0x8],eax
e: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
11: 35 00 89 cb c3 xor eax,0xc3cb8900
16: 89 45 0c mov DWORD PTR [ebp+0xc],eax
19: 8b 45 10 mov eax,DWORD PTR [ebp+0x10]
1c: 35 00 89 ce c3 xor eax,0xc3ce8900
21: 89 45 10 mov DWORD PTR [ebp+0x10],eax
24: 8b 45 14 mov eax,DWORD PTR [ebp+0x14]
27: 35 89 f0 cd 80 xor eax,0x80cdf089
[...]
Czyli gdyby disassemblować "źle", czyli z przesunięciem o jeden/dwa bajty, to dostanie się trochę inny kod:
6: 35 00 89 ca c3 → mov edx, ecx ; ret
11: 35 00 89 cb c3 → mov ebx, ecx ; ret
1c: 35 00 89 ce c3 → mov esi, ecx ; ret
27: 35 89 f0 cd 80 → mov eax, esi ; int 0x80
Tak więc dzięki temu jestem pewien, że w pamięci wykonywalnej są potrzebne mi gadgety. [Oczywiście, gdyby kompilator wygenerował inny kod, to tych opkodów w pamięci w cale by nie musiało być; natomiast w tym konkretnym wypadku bazując na powyższym listingu można stwierdzić, że opkody w pamięci jednak są; dop. własny]
Idąc dalej, używam funkcji find_the_witch do ich znalezienia w kodzie maszynowym funkcji something_wicked_this_way_comes [parametr funkcji find_the_witch to pierwsze dwa bajty szukanego gadżetu jako uint16_t, czyli little endian; dop. własny]:
gadget eax_from_esi_call_int = (gadget)find_the_witch(0xF089);
gadget set_esi = (gadget)find_the_witch(0xCE89);
gadget set_ebx = (gadget)find_the_witch(0xCB89);
gadget set_edx = (gadget)find_the_witch(0xCA89);
I teraz jeszcze jedna istotna sprawa - typ gadget to:
typedef void (*gadget)() attribute((fastcall));
Są tu dwie istotne rzeczy:
1. W C argumenty () oznaczają "dowolne nieokreślone argumenty" (w przeciwieństwie do C++ gdzie to jest tożsame z (void) aka brak argumentów).
2. Ustawiona jest konwencja wywołania na fastcall, czyli parametry funkcji polecą do rejestrów zamiast na stos (a konkretniej, pierwszy argument poleci do rejestru ecx w tym konkretnym przypadku).
Dalej po prostu "składam" zwykły asmowy hello world za pomocą tych gadgetów:
set_edx(12), set_ebx(1), set_esi(4);
eax_from_esi_call_int("Hello World\n");
Co runtime będzie wykonane jako:
(main) mov ecx, 12
mov eax, set_edx
call eax
(gadżet) mov edx, ecx
ret
(main) ...
... ...
(gadżet) ...
int 0x80
Pomijając części z main:
[gadzet 1] mov edx, 12 (długość napisu)
[gadzet 2] mov ebx, 1 (stdout)
[gadzet 3] mov esi, 4 (sys_write)
[fastcall to załatwi] mov ecx, address "Hello World\n"
[gadzet 4] mov eax, esi
[gadzet 4] int 0x80
Oczywiście w tej postaci po wypisaniu hello world program się "wywali", natomiast można go dość łatwo przerobić na taki który się mimo wszystko nie "wywala" :)
Test:
$ gcc -m32 test.c -O0
$ ./a.out
Hello World
Segmentation fault (core dumped)
$
--post stop--
Jeśli chodzi o problem niezbyt łagodnego wyjścia, to bardzo fajne rozwiązanie podał Azarien w kolejnym poście - mianowicie utworzył on jeszcze jedną funkcję nazwaną graceful_exit, która za pomocą istniejących gadgetów wywoływała syscall exit, a następnie dodał jej wywołanie przed return w funkcji something_wicked_this_way_comes, ale bezpośrednio za d ^= 0x80CDF089; - czyli po wykonaniu gadzętu 89 F0 CD 80 (przypominam: nie ma tu C3 czyli instrukcji ret) CPU "poleci dalej", a dalej jest wywołanie funkcji graceful_exit.
Fragment kodu o którym mowa wyżej (zmiany Azarien'a są pogrubione; dodam, że była jeszcze jedna zmiana - przerzucenie struktury gadget wyżej w kodzie, ale ją pominę dla przejrzystości):
void graceful_exit()
{
set_ebx(0);
set_esi(1);
eax_from_esi_call_int(0);
}
volatile unsigned int something_wicked_this_way_comes(
int a, int b, int c, int d) {
a ^= 0xC3CA8900; b ^= 0xC3CB8900; c ^= 0xC3CE8900; d ^= 0x80CDF089;
graceful_exit();
return a+b+c+d;
}
Bardzo eleganckie rozwiązanie moim zdaniem :)
Warto również rzucić okiem na post MSM'a oraz dyskusję w komentarzach do niego - metoda MSM'a opiera się znaną technikę z poszukaniem adresu biblioteki kernel32 w liście załadowanych bibliotek w PEB, znalezienie GetProcAddress w tablicy importów i załadowanie potrzebnych funkcji do wyświetlenia "Hello World" (chociaż nie do końca jest to metoda zgodna z "bez bibliotek", niemniej jednak warto rzucić okiem).
I tyle.
Comments:
@ged_
Nie, z uwagi na non-executable stack (zalozylem ze takowy jest).
Rowniez nie chcialem uzywac tablic ani standardowego ((void(*)())"\xc3")(); - za blisko temu do "jakiejs wstawki asma", ktorych mialo nie byc.
chodzilo mi o cos innego. czasami kompilator inicjuje tablice lokalne nie za pomoca rep movsd z sekcji danych, tylko jako serie mov. mozna taka sytuacje wymusic w ten sposob:
void foo(){
int x;
unsigned int t[]={0x11111111,0x22222222,0x33333333,0x44444444, &x, 0};
int i;
for(i=0;t[i];i++){
printf("0x%08x\n", t[i]);
}
printf("foo");
}
(printf("foo") zeby bylo latwo znalezc w IDA, printf(.., t[i]) zeby optymalizator nie wycial).
teraz kompilator nie moze zrobic rep movsd, bo przeszkadza mu &x, wiec generuje taki kod:
00401392 C745 D8 11111111 MOV DWORD PTR SS:[EBP-28],11111111
00401399 C745 DC 22222222 MOV DWORD PTR SS:[EBP-24],22222222
004013A0 C745 E0 33333333 MOV DWORD PTR SS:[EBP-20],33333333
004013A7 C745 E4 44444444 MOV DWORD PTR SS:[EBP-1C],44444444
(costam...)
sprawdzone na mingw gcc
Ah, zgadza się, jest takie zachowanie. Świetny pomysł swoją drogą :)
Przyznaję, że nawet dużo o wyborze sposobu nie myślałem - XOR był (jak pisałem) klasycznie używany przy JITach, więć na niego padło.
"I don't want to live on this planet anymore"
Nie przepadasz za kodem bazującym na undefined behavior ? ;)
Nigdy bym nie probowal takiego programu napisac ...
a juz tego typu rozwiazania daja do myslenia jak duzo jeszcze nie wiem ;P No ale nigdy nie pisalem w C to moze dlatego :P
Nie są kodem asm tylko kodem maszynowym, taka drobna różnica ;)
Bardzo trafne spostrzeżenie! :) (zastanawiałem się kiedy ktoś na to uwagę zwróci)
Zgadzam się, że jest to kwestia dyskusyjna. Mój punkt widzenia jest taki:
- wstawki asma i kodu maszynowego są explicit i gwarantują, że dany kod będzie w pamięci; tak więc do wstawek zaliczyłbym np.:
__asm("KOD TUTAJ");
oraz
((void(*)())"KOD MASZYNOWY TUTAJ")(); (lub podobne)
- natomiast w przypadku kodu o z posta nie chodzi o bezpośrednią wstawkę, tylko o próbę przemycenia czegoś, co disassemblowane od środka będzie czymś, co możemy wykorzystać;
Zauważ, że z punktu widzenia C ta liczba to stała, a w najlepszym wypadku argument XOR'a (chociaż OK, we wstawce explicit z kodem maszynowym też mamy stałą, ale na poziomie logicznym widać rzutowanie na funkcje i wywołanie).
A punktu widzenia kompilatora generującego kod assembly będą to jakieś XOR costam, STAŁA, więc również te instrukcje się jawnie nie pojawiają.
W zasadzie dopiero na poziomie kodu maszynowego przy wspomnianym nieprawidłowym offsetcie pojawia się alternatywna interpretacja, a więc de facto "wstawka" którą chcemy uzyskać. I to nie zawsze - w tym wypadku się pojawiła, ale inny kompilator lub choćby inne flagi kompilacji mogą sprawić, że jednak się nie pojawi.
Bottom line: na potrzeby tematu założyłem, że wstawka asma/etc jest explicit i gwarantuje pojawienie się kodu; tak się wg tej definicji mój kod nie wykorzystuje wstawek asma/etc ;)
Druga sprawa, pewnie trochę przesadzona: czym jest biblioteka i czy kernel nie jest w pewnym sensie biblioteką, skoro oferuje gotowe funkcje, którym można przesłać własne argumenty? :)
Przeczytałem teraz cały wątek na 4programmers i IMHO post Rev'a zawiera chyba odpowiedź na wszystkie aspekty problemu, głownie chodzi mi tu o jasno określone "NIE", przy czym dołożyłbym jeszcze kwestię komunikacji karty gfx z monitorem przez odpowiedni protokół (DVI, HDMI) i biblioteki kodu w układach w samym monitorze, które przecież trudnią się zapalaniem odpowiednich pikseli w odpowiednim momencie. Że już nie wspomnę o usłudze oferowanej przez ciekły kryształ pozwalającej na blokowanie niektórych partii światła - to też zostało jakoś i przez "coś" zaprogramowane, ale pytania tej natury to już chyba nie IT :)
Mimo wszystko, pytanie było o biblioteki i wstawki asm, i myślę, że nie należy tego uogólniać na "Not Invented Here". Można by dyskutować, czy kernel można uznać za bibliotekę, a kod maszynowy w stałych za wstawkę asm - ale na pewno ani sprzęt, ani interfejsy urządzeń peryferialnych, ani firmware monitora biblioteką ani wstawką asm nie są.
Co do ostatniego, to mimo, iż w monitorze może znajdować się kod korzystający z bibliotek czy system operacyjny, to z punkty widzenia aplikacji biblioteką nie są, ponieważ działają całkowicie oddzielnie - biblioteka jest czymś o zdecydowanie węższej definicji niż "coś co świadczy jakieś usługi" czy nawet "kod który świadczy jakieś usługi programom".
Poza tym, w prostszych monitorach (bez TV lub innych wymyślnych funkcjonalności) raczej stosuje się specyficzne układy (typu ASIC lub FPGA) służące do dekodowania sygnału i sterowania matrycą ;) .
Bo tam niby jest jedna biblioteka - "cstdio", ale wszystkie funkcje wyjścia są niedozwolone.
Przypuszczam, że rowiązanie może wyglądać tak: if(jakasfunkcja("Hello World!")){}
Czy można prosić o jakąś podpowiedź? :)
int main()
{
char napis[] = "Hello World!\n";
char asembler[] = {
0xB8,0x04,0x00,0x00,0x00, // movl $4,%eax
0xBB,0x01,0x00,0x00,0x00, // movl $1,%ebx
0xB9,0xFF,0xFF,0xFF,0xFF, // movl $-1,%ecx
0xBA,0x0D,0x00,0x00,0x00, // movl $13,%edx
0xCD,0x80, // int $0x80
0xC3 // ret
};
*((unsigned int *)(asembler+11)) = (unsigned int)napis; // wpisanie adresu do napisu zamiast liczbe -1 w movl $-1,%ecx
((void (*)(void))asembler)(); // wywołanie kodu jako funkcji
return 0;
}
Oczywiście trochę na szybko pisany. Dałoby się na pewno poradzić sobie bez tych 0xffffffff (-1) i wpisywania wskaźnika, ale akurat nie jestem tak biegły w asemblerze.
Kod po prostu wywołuje syscala write (eax=4) podając numer deskryptora stdout (ebx=1), wskaźnikiem na string (ecx - cudaczne przypisanie), i ilość znaków (edx=13).
Pozdro
Hehe to nie kwestia przekombinowania, tylko:
1. Interpretacji zasad, w szczególności punktu "ani wstawek assembly (w dowolnej formie)" - "w dowolnej formie" dotyczy również "w formie opcode'ów" / w formie podobnej do Turbo Pascal inline() (http://turbo-pascal.4coders.info/kurs-instrukcja-inline.html) która znajduje się w Twoim kodzie.
2. Twój kod zakłada, że stos jest wykonywalny, co na współczesnych systemach jest niestety (stety) niepoprawnym założeniem. Przykładowo:
> cat /proc/self/maps | grep "[stack]"
7fff1f2f4000-7fff1f315000 rw-p 00000000 00:00 0 [stack]
(brakuje "x" dla stacku)
Jak pisałem w poście, mój kod jest podobno do exploitacji z wykorzystaniem JIT, więc z założenia wyszukuje gadgety w pamięci wykonywalnej.
Na wszelki wypadek dodam, że metoda której użyłeś jest mi oczywiście znana. Zresztą, pisałem o niej np. w http://4programmers.net/C/Artyku%C5%82y/Kruczki_i_sztuczki_C_cz._1 (2004) :)
gdybyś użył tego kodu w moim projekcie to bym Cię wywalił na zbity pysk ;) Przecież nikt po Tobie tego nie przejmie :)
Haha spokojnie, jakbym w kodzie produkcyjnym coś takiego napisał, to bym się sam wywalił ;D
Add a comment: