Всем доброго времени суток. На прошлой неделе закончился очередной очный этап NeoQuest. А значит пришло время публиковать разбор некоторых заданий. Знаю многие ждали этого разбора, поэтому всех интересующихся прошу под кат.
Адский реверсер – моё ампЛУА!
В задании нам предложено скачать бинарник, и ознакомиться с его исходным кодом, для поиска в нём уязвимостей, скачиваем и распаковываем.
В архиве лежит всего одна директория, и как не трудно догадаться, содержит исходный код LUA, взятый с git. Смотрим что было изменено:
Как видно был добавлен новый файл larray.c, в котором судя по всему и содержится уязвимый код. Хорошо, теперь попробуем определить расположение флага. Подключившись к серверу и нажав TAB два раза, видим в текущей директории файл FLAG__.TXT
ОК. Вызов принят.
В LUA наверняка можно выполнить консольные команды или просто попробовать открыть файл. Однако не всё так просто, в исходный код не только был добавлен новый файл, но и исключены некоторые функции:
gh0st3rs@user-pc:lua$ git diff lbaselib.c
diff --git a/lbaselib.c b/lbaselib.c
index 00452f2..52ec9c6 100644
--- a/lbaselib.c
+++ b/lbaselib.c
@@ -480,18 +480,18 @@ static int luaB_tostring (lua_State *L) {
static const luaL_Reg base_funcs[] = {
{"assert", luaB_assert},
{"collectgarbage", luaB_collectgarbage},
- {"dofile", luaB_dofile},
+ // {"dofile", luaB_dofile},
{"error", luaB_error},
{"getmetatable", luaB_getmetatable},
{"ipairs", luaB_ipairs},
- {"loadfile", luaB_loadfile},
- {"load", luaB_load},
+ // {"loadfile", luaB_loadfile},
+ // {"load", luaB_load},
#if defined(LUA_COMPAT_LOADSTRING)
- {"loadstring", luaB_load},
+ // {"loadstring", luaB_load},
#endif
{"next", luaB_next},
{"pairs", luaB_pairs},
- {"pcall", luaB_pcall},
+ // {"pcall", luaB_pcall},
{"print", luaB_print},
{"rawequal", luaB_rawequal},
{"rawlen", luaB_rawlen},
@@ -502,7 +502,7 @@ static const luaL_Reg base_funcs[] = {
{"tonumber", luaB_tonumber},
{"tostring", luaB_tostring},
{"type", luaB_type},
- {"xpcall", luaB_xpcall},
+ // {"xpcall", luaB_xpcall},
/* placeholders */
{LUA_GNAME, NULL},
{"_VERSION", NULL},
gh0st3rs@user-pc:lua$ git diff linit.c
diff --git a/linit.c b/linit.c
index 3c2b602..d7e03c9 100644
--- a/linit.c
+++ b/linit.c
@@ -41,17 +41,18 @@
*/
static const luaL_Reg loadedlibs[] = {
{LUA_GNAME, luaopen_base},
- {LUA_LOADLIBNAME, luaopen_package},
+ // {LUA_LOADLIBNAME, luaopen_package},
{LUA_COLIBNAME, luaopen_coroutine},
{LUA_TABLIBNAME, luaopen_table},
- {LUA_IOLIBNAME, luaopen_io},
- {LUA_OSLIBNAME, luaopen_os},
+ // {LUA_IOLIBNAME, luaopen_io},
+ // {LUA_OSLIBNAME, luaopen_os},
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_UTF8LIBNAME, luaopen_utf8},
- {LUA_DBLIBNAME, luaopen_debug},
+ // {LUA_DBLIBNAME, luaopen_debug},
#if defined(LUA_COMPAT_BITLIB)
{LUA_BITLIBNAME, luaopen_bit32},
+ {LUA_ARRAY, luaopen_array},
#endif
{NULL, NULL}
};
Но взглянув на изменения в makefile, можно заметить, что специально или по ошибке, был оставлен модуль TESTS.
gh0st3rs@user-pc:lua$ git diff makefile
diff --git a/makefile b/makefile
index 8160d4f..d9df7e8 100644
--- a/makefile
+++ b/makefile
@@ -53,12 +53,12 @@ LOCAL = $(TESTS) $(CWARNS) -g
# enable Linux goodies
-MYCFLAGS= $(LOCAL) -std=c99 -DLUA_USE_LINUX -DLUA_COMPAT_5_2
-MYLDFLAGS= $(LOCAL) -Wl,-E
+MYCFLAGS= $(LOCAL) -std=c99 -DLUA_USE_LINUX -DLUA_COMPAT_5_2 -fPIE -fPIC # -fsanitize=address -fno-omit-frame-pointer
+MYLDFLAGS= $(LOCAL) -Wl,-E # -fsanitize=address
MYLIBS= -ldl -lreadline
-CC= clang-3.8
+CC= gcc # clang-5.0
CFLAGS= -Wall -O2 $(MYCFLAGS)
AR= ar rcu
RANLIB= ranlib
@@ -74,7 +74,7 @@ LIBS = -lm
CORE_T= liblua.a
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o
- ltm.o lundump.o lvm.o lzio.o ltests.o
+ ltm.o lundump.o lvm.o lzio.o ltests.o larray.o
AUX_O= lauxlib.o
LIB_O= lbaselib.o ldblib.o liolib.o lmathlib.o loslib.o ltablib.o lstrlib.o
lutf8lib.o lbitlib.o loadlib.o lcorolib.o linit.o
@@ -194,5 +194,6 @@ lvm.o: lvm.c lprefix.h lua.h luaconf.h ldebug.h lstate.h lobject.h
ltable.h lvm.h
lzio.o: lzio.c lprefix.h lua.h luaconf.h llimits.h lmem.h lstate.h
lobject.h ltm.h lzio.h
+larray.o: larray.c
# (end of Makefile)
Погуглив как его можно использовать, приходим к простому коду для извлечения флага:
L1 = T.newstate()
T.loadlib(L1)
a,b,c = T.doremote(L1, [[
os = require'os';
os.execute('cat FLAG__.TXT')
]])
Что делает код:
- Сначала мы инициируем новое тестовый контекст
- Затем подгружаем библиотеки для работы с FS
- И через doremote исполняем в тестовом контексте системные команды
После исполнения получаем ключ: c91a8674a726823e9edad1a4262da4be7f216d74
QEMU+Ecos = QEcos
В этом году так же не обошлось без заданий с QEMU. В задании спрятано 2 ключа, найдя которые можно было получить +200 очков. Приступим:
Скачав все 3 файла, приступим к их изучению:
Первое с чем приходится столкнуться, это измененный порядок байт в дампе, определить это легко, выполнив команду:
$ strings dump.bin
....
:CCGbU( utnu3.6 1-0.ubu22utn.6 ) 0.371026040
На Python это решается довольно просто:
#!/usr/bin/python3
import sys
fixed = open(sys.argv[2], 'wb')
dump = open(sys.argv[1], 'rb').read()
[fixed.write(dump[x:x + 4][::-1]) for x in range(0, len(dump), 4)]
fixed.close()
После преобразования, с дампом можно работать:
И так, у нас есть 2 образа eCos и заголовок, образы отделены между собою нулями. через dd режем его на 3 части, они понадобятся далее.
Но в начале, попробуем запустить первый образ, чтобы узнать, что от нас требуется:
После загрузки нужно ввести пароль, и если он окажется не верным, получаем сообщение: AUTH FAIL
Распакуем образ и отправим его в IDA. Далее по перекрестным ссылкам находим функцию, которая выводит сообщение об ошибке:
Поднимаемся на уровень выше, где видим 2 условия, при который проверка не проходит:
Дело за малым:
- Патчим эти переходы
- Архивируем файл ecos.bin и вставляем его в распаковщик
- Используя утилиту mkimage собираем новый образ для u-boot
- И проверяем результат
После запуска нового образа на любой пароль получаем сообщение со строкой, которую нужно ввести в u-boot:
Auth process started…
===============
=== AUTH OK ===
===============use this key in u-boot:4a2#*a11gpiun%25
Вводим и получаем ещё один ключ (предварительно взяв от строки sha1 хеш): ddf5957cd43a3712e0c67d019a37223043ae6df5
P.S. Как позже выяснилось, пароль можно было очень быстро перебрать, но зачем, если проще пропатчить нужный участок кода)
Со вторым ключом всё немного сложнее. Если попробовать запустить второй образ (для этого нужно собрать дамп в таком порядке: заголовок -> образ2 -> образ1, или просто поменять параметры загрузки в u-boot), то образ не загрузится, а будет ругаться на неверное значение CRC32:
После долгих поисков, а так же сравнив размер образа и количество записей в логе, находим следующее:
- Каждый блок в логе длинной: 0xE1
- Всего блоков: 0x48D1
- В блоке 0x2580 произошла критическая ошибка
- Началась она со смещения в блоке: [0x44, 0x47) т.е. 3 байта
Сопоставив размеры блока с реальной позицией в дампе, определяем, что во втором образе архив ecos.bin.gz является поврежденным. Ничего не остаётся, как сбрутить недостающие 3 байта, имея оригинальную CRC32 образа и позицию в которой ошибка.
#!/usr/bin/python3
import sys
import binascii
import os
import subprocess
import struct
START_OFFSET=0xf5c5
END_OFFSET=0xf5c8
OUT_FILE=sys.argv[1]+'.patch'
dump = open(sys.argv[1], 'rb').read()
crc1 = struct.unpack('>I', dump[24:28])[0]
for x in range(0xa2, -1, -1):
for y in range(0xff, -1, -1):
for z in range(0xff, -1, -1):
number='%02x%02x%02x' % (x,y,z)
crc = binascii.crc32(dump[0x40:START_OFFSET] + binascii.unhexlify(number.encode()) + dump[END_OFFSET:])
if crc == crc1:
print('Possible fix: %s' % number)
print('Status: %s' % number)
Воспользовавшийь простейшим скриптом, запускаем перебор, и через какое-то время получаем верную комбинацию. Далее можно собрать дамп и запустить его, либо просто распаковать образ и используя grep найти нужную строку:
$ strings ecos.bin | grep KEY
KEY: xs26k=b$km*8_mNf
Взяв от полученной строки sha1 хеш, получаем ещё 1 ключ: 35f6e7d0d65097f29ad74a7aaf991f2166b0a492
Spectre
Тут авторы сильно заморочились и предложили нам найти и исправить так называемые опечатки в коде. Приведу сразу список исправлений, а затем расскажу, как их можно было найти:
0x7f0: 9d: 9c
0x8bc: 75: 74
0xd86: 3e: 3f
0x277e: f1: ee
0x2ac1: 03: 02
0x2c79: 00 00 10: 10 04 00
0x3b19: 74: 70
0x3b73: 6A: 75
0x3b75: 5f 5f: 6e 65
0x3be7: 77: 6f
0x52b0: 4f 4b: 4d 5a
Ошибки #9 #10
В самом начале в функции main по адресу: 0x000000013FE517E7 происходит вызов функции check_cpu():
Тут происходит проверка, соответствия модели процессора, но строка с которой происходит сравнение ошибочна. В отладке видим, что верным должно быть значение: GenuineIntel
Ошибка #11
Находясь в функции генерации первого ключа, видим, что он основан на строке: A hecatwnicosachoron or 120-cell is a regular polychoron having 120 dodecahedral cells. поиск в гугл, подсказал, правильное её написание: A hecatonicosachoron or 120-cell is a regular polychoron having 120 dodecahedral cells.
.text:000000013FE53323 lea rax, byte_13FE57030
.text:000000013FE5332A lea rbp, aAHecatwnicosac ; "A hecatwnicosachoron or 120-cell is a r"...
.text:000000013FE53331 sub rbp, rax
.text:000000013FE53334 mov eax, 1
Ошибка #5
Если взглянуть ниже, видим вызов функции по не верному смещению:
.text:000000013FE5337D call near ptr sub_13FE53070+3
.text:000000013FE53382 movzx ecx, [rsp+48h+arg_0]
.text:000000013FE53387 inc rbp
Ошибка #1
В функции по адресу 0x000000013FE51390 происходит формирование второго ключа:
Во время отладки можно заметить, что условный переход, после генерации первой части ключа происходит не верно:
.text:000000013FE513B9 mov rbx, rax
.text:000000013FE513BC call sub_13FE53460
.text:000000013FE513C1 test eax, eax
.text:000000013FE513C3 jnz short loc_13FE513C9
Ошибка #2
Следующее, что бросается в глаза, при дальнейшем просмотре этой функции, это не верный оффсет при вызове функции, которая обращается к реестру:
.text:000000013FE513EF call near ptr get_SoftwareType+1
.text:000000013FE513F4 test eax, eax
.text:000000013FE513F6 jz short loc_13FE51415
Ошибка #6
Зайдя глубже в функцию get_SoftwareType, и проверив аргументы функции RegOpenKeyExA понимаем, что значение: 0x80000003 явно не соответствует HKEY_LOCAL_MACHINE:
.text:000000013FE536A9 lea rax, [rsp+0D8h+hkey]
.text:000000013FE536AE lea rdx, SubKey ; "SOFTWARE\Microsoft\Windows NT\Curren"...
.text:000000013FE536B5 mov r9d, 20019h ; samDesired
.text:000000013FE536BB xor r8d, r8d ; ulOptions
.text:000000013FE536BE mov rcx, 0FFFFFFFF80000003h ; hKey
.text:000000013FE536C5 mov [rsp+0D8h+var_90], 64h
.text:000000013FE536CD mov [rsp+0D8h+phkResult], rax ; phkResult
.text:000000013FE536D2 call cs:RegOpenKeyExA
.text:000000013FE536D8 test eax, eax
Ошибка #7
Пролистав функцию генерации второго ключа, к следующей части, видим попытку получить домашнюю директорию для процесса explorer.exe, и вроде бы ничего не обычного, но вот из документации, можно узнать, что режим доступа указан не верно, и должен быть 0x410:
.text:000000013FE53871 mov r8d, [rsp+278h+pe.th32ProcessID] ; dwProcessId
.text:000000013FE53876 xor edx, edx ; bInheritHandle
.text:000000013FE53878 mov ecx, 100000h ; dwDesiredAccess
.text:000000013FE5387D call cs:OpenProcess
.text:000000013FE53883 mov rbx, rax
.text:000000013FE53886 test rax, rax
Ошибка #3
При отладке функции, которая генерирует третий ключ, замечаем, ещё один не верный условный переход, в результате, не учитывается ответ от вызова экзешника из ресурсов:
.text:000000013FE514B1 call load_exe
.text:000000013FE514B6 mov rdi, rax
.text:000000013FE514B9 test rax, rax
.text:000000013FE514BC jnz short loc_13FE514D7
.text:000000013FE514BE mov rdx, [rsp+28h+a2] ; a2
.text:000000013FE514C3 mov r8, rbx ; out_hash
.text:000000013FE514C6 mov rcx, rax ; a1
.text:000000013FE514C9 call calc_sha
Ошибка #8
Если извлечь из ресурсов файл tmp.exe, то при беглом изучении становится понятно, что единственный аргумент с которым он работает это -p:
.text:000000013FE51147 call memset
.text:000000013FE5114C xor eax, eax
.text:000000013FE5114E lea rdx, CommandLine ; "tmp.exe -t"
.text:000000013FE51155 mov [rsp+118h+ProcessInformation.hProcess], rax
Ошибка #12
При попытке извлечь файл tmp.exe из ресурсов, замечаем, что у него не верный заголовок, исправляем OK на MZ и всё работает:
Ошибка #4
Странно, что второй ключ полностью дублирует первый, ведь как мы помним, результат должен быть в регистре r15:
.text:000000013FE51872 call key2
.text:000000013FE51877 mov r15, rax
Но это ещё не всё в процессе отладки и патчинга мы натыкаемся на пару защитных мер. Первая это всем изъясненная IsDebuggerPresent:
.text:000000013FE517EE jz short loc_13FE51844
.text:000000013FE517F0 call cs:IsDebuggerPresent
.text:000000013FE517F6 test eax, eax
.text:000000013FE517F8 jz short loc_13FE51856
Вторая это проверка целостности файла на основе sha1 хеша:
.text:000000013FE516C3 mov dword ptr [rbp+original_hash], 0D8086BF9h
.text:000000013FE516CA mov dword ptr [rbp+original_hash+4], 0AA45EFE5h
.text:000000013FE516D1 mov dword ptr [rbp+original_hash+8], 492519ECh
.text:000000013FE516D8 mov dword ptr [rbp+original_hash+0Ch], 212C9756h
.text:000000013FE516DF mov [rbp+var_30], 5BB58EA1h
.text:000000013FE516E6 mov byte ptr [rbp+hash], bl
.text:000000013FE516E9 mov [rbp+hash+1], rax
.text:000000013FE516ED mov [rbp+var_17], rax
.text:000000013FE516F1 mov [rbp+var_F], ax
.text:000000013FE516F5 mov [rbp+var_D], al
.text:000000013FE516F8 call calc_sha
.text:000000013FE516FD mov rax, [rbp+hash]
.text:000000013FE51701 cmp rax, qword ptr [rbp+original_hash]
Функцию проверки целостности можно либо забить nop-ами, либо в самом конце просто поправить оригинальный хеш.
После всех этих изменений получаем сразу все 3 ключа:
First key: 2A 93 E7 6A F5 BB E0 92 83 E5 99 E6 63 6D 04 1C 95 9B 3C D7
Second key: B2 D7 CC 3F 58 03 EB C6 4D 14 8E A6 AB 2E FC 10 DE B1 45 8D
Third key: DB 0D 81 6E 50 63 BA 13 65 2F 35 7B 1F 7C E9 FC 1E A1 C1 C6
Автор: GH0st3rs