Jak zmínil BSH, váš kód shellu neobsahuje bajty zprávy. Skok na MESSAGE
a volání GOBACK
rutina těsně před definováním msg
byte byl dobrý krok, protože adresa msg by byla na vrcholu zásobníku jako návratová adresa, kterou by bylo možné přepnout na ecx
, kde je uložena adresa msg.
Ale váš i BSH kód má mírné omezení. Obsahuje NULL bytes ( \x00 )
který by byl považován za konec řetězce při dereferencování ukazatelem funkce.
Existuje chytrý způsob, jak to obejít. Hodnoty, které uložíte do eax, ebx and edx
jsou dostatečně malé na to, aby je bylo možné přímo zapsat do spodních kousků příslušných registrů najednou přístupem al, bl and dl
respektive. Horní nibble může obsahovat nevyžádanou hodnotu, takže může být xored.
b8 04 00 00 00 ------ mov $0x4,%eax
se stává
b0 04 ------ mov $0x4,%al
31 c0 ------ xor %eax,%eax
Na rozdíl od předchozí instrukční sady neobsahuje nová instrukční sada žádný bajt NULL.
Takže konečný program vypadá takto:
global _start
section .text
_start:
jmp message
proc:
xor eax, eax
mov al, 0x04
xor ebx, ebx
mov bl, 0x01
pop ecx
xor edx, edx
mov dl, 0x16
int 0x80
xor eax, eax
mov al, 0x01
xor ebx, ebx
mov bl, 0x01 ; return 1
int 0x80
message:
call proc
msg db " y0u sp34k 1337 ? "
section .data
Sestavení a propojení :
$ nasm -f elf hello.asm -o hello.o
$ ld -s -m elf_i386 hello.o -o hello
$ ./hello
y0u sp34k 1337 ? $
Nyní extrahujte shell kód z binárky hello :
$ for i in `objdump -d hello | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\\x$i" ; done
výstup:
\xeb\x19\x31\xc0\xb0\x04\x31\xdb\xb3\x01\x59\x31\xd2\xb2\x12\xcd\x80\x31\xc0\xb0\x01\x31\xdb\xb3\x01\xcd\x80\xe8\xe2\xff\xff\xff\x20\x79\x30\x75\x20\x73\x70\x33\x34\x6b\x20\x31\x33\x33\x37\x20\x3f\x20
Nyní můžeme mít náš ovladač, který spustí shell kód.
#include <stdio.h>
char shellcode[] = "\xeb\x19\x31\xc0\xb0\x04\x31\xdb"
"\xb3\x01\x59\x31\xd2\xb2\x12\xcd"
"\x80\x31\xc0\xb0\x01\x31\xdb\xb3"
"\x01\xcd\x80\xe8\xe2\xff\xff\xff"
"\x20\x79\x30\x75\x20\x73\x70\x33"
"\x34\x6b\x20\x31\x33\x33\x37\x20"
"\x3f\x20";
int main(int argc, char **argv) {
(*(void(*)())shellcode)();
return 0;
}
V moderních kompilátorech existují určité bezpečnostní funkce, jako je ochrana NX, která zabraňuje spuštění kódu v datovém segmentu nebo zásobníku. Měli bychom tedy explicitně specifikovat kompilátor, který je zakáže.
$ gcc -g -Wall -fno-stack-protector -z execstack launcher.c -o launcher
Nyní launcher
lze vyvolat ke spuštění kódu shellu.
$ ./launcher
y0u sp34k 1337 ? $
U složitějších shell kódů by byla další překážka. Moderní linuxová jádra mají ASLR nebo Address Space Layout Randomization
Možná budete muset tuto funkci deaktivovat, než vložíte kód shellu, zvláště když dojde k přetečení vyrovnávací paměti.
[email protected]:~# echo 0 > /proc/sys/kernel/randomize_va_space
Když vložíte tento shell kód, nevíte, co je na message
:
mov ecx, message
v injektovaném procesu to může být cokoliv, ale nebude to "Hello world!\r\n"
protože je v sekci dat, zatímco vyhazujete pouze textovou sekci. Můžete vidět, že váš shell kód nemá "Hello world!\r\n"
:
"\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"
"\xb9\x00\x00\x00\x00"
"\xba\x0f\x00\x00\x00"
"\xcd\x80\xb8\x01\x00"
"\x00\x00\xbb\x00\x00"
"\x00\x00\xcd\x80";
Toto je běžný problém při vývoji shellkódu, způsob, jak to obejít, je tento:
global _start
section .text
_start:
jmp MESSAGE ; 1) lets jump to MESSAGE
GOBACK:
mov eax, 0x4
mov ebx, 0x1
pop ecx ; 3) we are poping into `ecx`, now we have the
; address of "Hello, World!\r\n"
mov edx, 0xF
int 0x80
mov eax, 0x1
mov ebx, 0x0
int 0x80
MESSAGE:
call GOBACK ; 2) we are going back, since we used `call`, that means
; the return address, which is in this case the address
; of "Hello, World!\r\n", is pushed into the stack.
db "Hello, World!", 0dh, 0ah
section .data
Nyní vypište textovou sekci:
$ nasm -f elf shellcode.asm
$ ld shellcode.o -o shellcode
$ ./shellcode
Hello, World!
$ objdump -d shellcode
shellcode: file format elf32-i386
Disassembly of section .text:
08048060 <_start>:
8048060: e9 1e 00 00 00 jmp 8048083 <MESSAGE>
08048065 <GOBACK>:
8048065: b8 04 00 00 00 mov $0x4,%eax
804806a: bb 01 00 00 00 mov $0x1,%ebx
804806f: 59 pop %ecx
8048070: ba 0f 00 00 00 mov $0xf,%edx
8048075: cd 80 int $0x80
8048077: b8 01 00 00 00 mov $0x1,%eax
804807c: bb 00 00 00 00 mov $0x0,%ebx
8048081: cd 80 int $0x80
08048083 <MESSAGE>:
8048083: e8 dd ff ff ff call 8048065 <GOBACK>
8048088: 48 dec %eax <-+
8048089: 65 gs |
804808a: 6c insb (%dx),%es:(%edi) |
804808b: 6c insb (%dx),%es:(%edi) |
804808c: 6f outsl %ds:(%esi),(%dx) |
804808d: 2c 20 sub $0x20,%al |
804808f: 57 push %edi |
8048090: 6f outsl %ds:(%esi),(%dx) |
8048091: 72 6c jb 80480ff <MESSAGE+0x7c> |
8048093: 64 fs |
8048094: 21 .byte 0x21 |
8048095: 0d .byte 0xd |
8048096: 0a .byte 0xa <-+
$
Řádky, které jsem označil, jsou naše "Hello, World!\r\n"
řetězec:
$ printf "\x48\x65\x6c\x6c\x6f\x2c\x20\x57\x6f\x72\x6c\x64\x21\x0d\x0a"
Hello, World!
$
Takže náš obal C bude:
char code[] =
"\xe9\x1e\x00\x00\x00" // jmp (relative) <MESSAGE>
"\xb8\x04\x00\x00\x00" // mov $0x4,%eax
"\xbb\x01\x00\x00\x00" // mov $0x1,%ebx
"\x59" // pop %ecx
"\xba\x0f\x00\x00\x00" // mov $0xf,%edx
"\xcd\x80" // int $0x80
"\xb8\x01\x00\x00\x00" // mov $0x1,%eax
"\xbb\x00\x00\x00\x00" // mov $0x0,%ebx
"\xcd\x80" // int $0x80
"\xe8\xdd\xff\xff\xff" // call (relative) <GOBACK>
"Hello wolrd!\r\n"; // OR "\x48\x65\x6c\x6c\x6f\x2c\x20\x57"
// "\x6f\x72\x6c\x64\x21\x0d\x0a"
int main(int argc, char **argv)
{
(*(void(*)())code)();
return 0;
}
Pojďme to otestovat pomocí -z execstack
abychom povolili read-implies-exec (celoprocesové, navzdory "zásobníku" v názvu), abychom mohli spouštět kód v .data
nebo .rodata
sekce:
$ gcc -m32 test.c -z execstack -o test
$ ./test
Hello wolrd!
Funguje to. (-m32
je nezbytný také na 64bitových systémech. int $0x80
32bitové rozhraní ABI nefunguje s 64bitovými adresami, jako je .rodata
ve spustitelném souboru PIE. Strojový kód byl také sestaven pro 32-bit. Stává se, že stejná sekvence bajtů by se dekódovala na ekvivalentní instrukce v 64bitovém režimu, ale není tomu tak vždy.)
Moderní GNU ld
vloží .rodata
v samostatném segmentu od .text
, takže může být nespustitelný. Dříve stačilo použít const char code[]
vložit spustitelný kód na stránku s daty pouze pro čtení. Alespoň pro shell kód, který se nechce sám upravovat.