NeoQuest 2018: Читерство да и только

  • Tutorial


Всем доброго времени суток. На прошлой неделе закончился очередной очный этап 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. Как позже выяснилось, там была уязвимость типа race condition — нужно было изменять комбинацию во время ее проверки

Со вторым ключом всё немного сложнее. Если попробовать запустить второй образ (для этого нужно собрать дамп в таком порядке: заголовок -> образ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
  • +17
  • 4,1k
  • 5
Поделиться публикацией
Ой, у вас баннер убежал!

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

      VladikSS, думаю, если глубоко не вдаваться в подробности уязвимости Spectre, данный битый файл получен с помощью этой уязвимости. В кратце про Meltdown и Spectre, при попытке чтения сегментов памяти через кэш cpu, есть некий шанс приближенный к 100% (99.8% например) получить абсолютно идентичный дамп.

      • 0
        Здравая критика это конечно хорошо, но:
        а) Для ребят которые только окончили институт или недавно в теме реверса такие задания реально интересны и полезны
        б) Если у вас есть на примете ресурсы с «более веселыми» заданиями — поделитесь пожалуйста. Чтож почем зря негодовать в комментариях? Уверен все читатели не поскупятся на «плюсик»!
      • 0

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


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

        • +1
          В 9-м пересчитывать CRC32 для всего образа достаточно долго. Вместо этого можно заранее посчитать CRC от начала до первого битого байта, и сразу после последнего битого. Второе можно сделать, «отмотав» CRC от конца до нужного места (я использовал github.com/theonlypwner/crc32). Зная эти два значения, можно пересчитывать CRC только для 3-х битых байтов, что гораздо быстрее:
          from binascii import crc32
          from struct import pack
          
          value = 0x7ffcce87
          target = 0xea4af7fe
          
          for i in range(0x1000000):
              data = pack("I", i)[:-1]
              crc = crc32(data, value)
              if crc == target:
                  print("FOUND: %s" % data.hex())
          

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое