Всем доброго времени суток. На прошлой неделе закончился очередной очный этап 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')
]])

Что делает код:
  1. Сначала мы инициируем новое тестовый контекст
  2. Затем подгружаем библиотеки для работы с FS
  3. И через 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. Как позже выяснилось, пароль можно было очень быстро перебрать, но зачем, если проще пропатчить нужный участок кода)

Со вторым ключом всё немного сложнее. Если попробовать запустить второй образ (для этого нужно собрать дамп в таком порядке: заголовок -> образ2 -> образ1, или просто поменять параметры загрузки в u-boot), то образ не загрузится, а будет ругаться на неверное значение CRC32:


После долгих поисков, а так же сравнив размер образа и количество записей в логе, находим следующее:
  1. Каждый блок в логе длинной: 0xE1
  2. Всего блоков: 0x48D1
  3. В блоке 0x2580 произошла критическая ошибка
  4. Началась она со смещения в блоке: [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

Ошибки #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

Комментарии (2)


  1. ValdikSS
    19.03.2018 22:15

    В чем задача у задания Spectre? Кто-то действительно считает, что исправлять битый (пусть и умышленно) файл — весело?


  1. alexbers
    20.03.2018 13:45

    Я решал 9-ый без ревёрса, сдампил всю память машины через gdb, посмотрел как мигают лампочки, восстановил память и выставил нужную последовательность.


    Примерно в середине квеста организаторы запатчили таск с луа и его стало невозможно пройти указанным способом.