Czasem przyjemnie jest zapomnieć o Undefined Behavior w C i po prostu napisać coś co działa tu i teraz, a niekoniecznie będzie działać jutro (przy nowej wersji kompilatora; innych opcjach kompilacji) lub w innym miejscu (innej platformie). Kilka tygodni temu nadarzyła się ku temu okazja za sprawą tematu na pewnym forum zatytułowanym "Hello world bez bibliotek i asm" (stąd nazwa niniejszego postu) - autor pytał czy możliwym jest napisać w C "Hello World" bez użycia bibliotek (w tym include'ów) ani wstawek assembly (w dowolnej formie). O ile na początku topic był powiązany jeszcze z poprawnym językiem C, to dość szybko przeszło na kod bardzo niskopoziomowy (zapisany nadal w C), zależny od danego systemu, architektury CPU czy nawet sposobu kompilacji użytego przez kompilator. Poniżej zamieszczam mój pomysł na wypisanie "Hello World" na konsoli GNU/Linux, natomiast zachęcam również do rzucenia okiem na sam topic (link powyżej).

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:

2012-07-10 12:37:02 = prog4mer
{
Geniusz :)
}
2012-07-10 14:38:07 = Bartek
{
Wyrazy podziwu...
}
2012-07-10 16:53:00 = ged_
{
a probowales z lokalna tablica uint'ow?
}
2012-07-10 18:53:57 = Gynvael Coldwind
{
:)

@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.
}
2012-07-10 20:32:38 = Infern0_
{
Mistrz!
}
2012-07-10 20:50:59 = ged_
{
@gyn:

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


}
2012-07-10 21:25:52 = Gynvael Coldwind
{
@ged_
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.
}
2012-07-10 21:31:23 = carstein
{
Świetny kawałek kodu. Ja się właśnie na początku zastanawiałem, czemu po prostu nie zadeklarować wartości jako zmienne, a dopiero potem, metodą 'misia' (stawiamy pluszowego misia przed nami i mu tłumaczymy co i jak robimy) doszedłem do tego, że prawie na pewno stos będzie NX.
}
2012-07-11 21:12:35 = Ouh
{
Po przeczytaniu tego tematu nasunela mi sie jedna mysl ....
"I don't want to live on this planet anymore"


}
2012-07-11 21:19:04 = Gynvael Coldwind
{
@Ouh
Nie przepadasz za kodem bazującym na undefined behavior ? ;)
}
2012-07-11 21:29:19 = Ouh
{
@Gynvael Coldwind

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
}
2012-07-11 22:05:17 = Ezoman
{
Wow... nic z tego nie rozumiem :P Niesamowite
}
2012-07-11 22:14:21 = A
{
Hm... :)
}
2012-07-12 07:19:49 = antek
{
Być może czegoś nie czaję, ale czym są liczby typu 0xC3CA8900 jeśli nie kodem asm, a tym samym wstawkami asma?
}
2012-07-12 09:04:02 = Rolek
{
@antek
Nie są kodem asm tylko kodem maszynowym, taka drobna różnica ;)
}
2012-07-12 09:29:43 = Gynvael Coldwind
{
@antek
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 ;)

}
2012-07-12 09:31:38 = Gynvael Coldwind
{
Ah, i jeszcze jedna uwaga do powyższego - to samo można spróbować osiągnąć bez stałych (o czym pisaliśmy z ged_'em wyżej), np. również po prostu przydługawym kodem w którym nie ma explicit potrzebnych stałych nawet w kodzie, ale jakoś tak wychodzi z kompilacji, że się takie ciągi bajtów w pamięci pojawią.
}
2012-07-12 11:06:27 = antek
{
@Gynvael Coldwind: Twój kod jest dobry na demonstrację działania ROP, choć dość rozmyty termin "wstawka asm" stawia wielki znak zapytania przy tezie uzyskania odpowiedzi na oryginalne pytanie z forum.

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 :)
}
2012-07-12 17:42:58 = olo16
{
@antek
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ą ;) .
}
2012-11-15 13:27:46 = wasmaro
{
A jak z tym zadankiem: "securitytraps.no-ip.pl/challs/cpptrap7" ?
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ź? :)
}
2012-12-12 03:11:30 = Marek
{
Hmm mam wrażenie, że trochę przekombinowałeś z tymi funkcjami. Skoro i tak wywołujesz asemblera a koniecznie nie chcesz dyrektywy asm to możesz to zrobić prościej niż jakieś cudaczne xory. To przykład:

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
}
2012-12-12 08:47:11 = Gynvael Coldwind
{
@Marek
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) :)
}
2012-12-12 16:44:46 = Marek
{
Masz rację, rzeczywiście Twoje rozwiązanie znacząco się różni.
}
2013-09-05 19:34:24 = Nice
{
Mega fajna ciekawostka. I szczerze Ci gratuluję pomysłu (bo trzeba mieć łeb do takich rzeczy), ale

gdybyś użył tego kodu w moim projekcie to bym Cię wywalił na zbity pysk ;) Przecież nikt po Tobie tego nie przejmie :)
}
2013-09-05 21:01:02 = Gynvael Coldwind
{
@Nice
Haha spokojnie, jakbym w kodzie produkcyjnym coś takiego napisał, to bym się sam wywalił ;D
}

Add a comment:

Nick:
URL (optional):
Math captcha: 8 ∗ 9 + 8 =