📜 ⬆️ ⬇️

NeoQuest 2018: Cheating and only



Good day to all. Last week the next full-time stage of NeoQuest ended . So it's time to publish an analysis of some tasks. I know many have been waiting for this analysis, so I ask all those interested in a cat.

Hellish reverser - my amplua!




In the task we are offered 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 it is not difficult to guess, it contains the source code LUA , taken with git . We look at what has been changed:


As you can see, the new file larray.c was added, which apparently contains the vulnerable code. Well, now let's try to determine the location of the flag. Having connected to the server and pressing TAB twice, we can see in the current directory the file FLAG __. TXT

OK. Challenge accepted.
In LUA you can probably execute console commands or just try to open the file. However, not everything is so simple, not only was the new file added to the source code, but some functions were also excluded:
git diff lbaselib.c
gh0st3rs@user-pc:lua$ git diff lbaselib.c diff --git a/lbaselib.cb/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.cb/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} }; 


But looking at the changes in the makefile, you can see that the TESTS module was left on purpose 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 -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) 


Googling how it can be used, we arrive at a simple code to extract the flag:
 L1 = T.newstate() T.loadlib(L1) 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 working 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, it was also not without assignments with QEMU . In the task 2 keys are hidden, having found which it was possible to receive +200 points. Let's start:
After downloading all 3 files, let's start studying them:


The first thing you have to deal with is a modified order of bytes 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:
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() 


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 between them. through dd cut it into 3 parts, they will be needed later.
But at the beginning, we will try to launch the first image to find out what is required of us:


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


We rise to the level above, where we see 2 conditions under which the test fails:
led_check


Things are easy:

After launching a new image to any password, we receive a message with a string that needs to be entered into the u-boot:
Auth process started ...

===============
=== AUTH OK ===
===============

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

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

PS As it turned out later, there was a race condition type vulnerability - it was necessary to change the combination while checking it

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 simply change the boot parameters in the u-boot), then the image will not load, but will swear at the wrong CRC32 value:


After a long search, as well as comparing the image size 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. A critical error has occurred in block 0x2580
  4. It began with an offset in the block: [0x44, 0x47) i.e. 3 bytes

Comparing the dimensions of the block with the real position in the dump, we determine that in the second image the archive ecos.bin.gz is damaged. There is nothing left but to truncate the missing 3 bytes, having the original CRC32 image and the position in which the error occurred.
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) 


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

Taking from the received line sha1 hash, we get another 1 key: 35f6e7d0d65097f29ad74a7aaf991f2166b0a492

Specter



Here the authors became very confused and suggested that we find and correct the so-called typos in the code. I will give a list of corrections right away, and then I will tell you how they could be found:
List of fixes 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

Errors # 9 # 10


At the very beginning, the function check_cpu () is called in the main function at the address: 0x000000013FE517E7 :


Here there is a check that the processor model matches, but the string with which the comparison is made is erroneous. In debugging, we see that the value should be correct: GenuineIntel

Error # 11


Being in the function of generating the first key, we see that it is based on the string: A docatca cells. Google search, prompted, 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 ar"... .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 the address 0x000000013FE51390 , a second key is generated:
During debugging, you can see that the conditional transition, after generating 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 further viewing this function 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 does not explicitly correspond to 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 function of generating a second key, to the next part, we see an attempt to get the home directory for the explorer.exe process, and it seems to be nothing ordinary, but from the documentation, you can see that the access mode is not specified correctly, 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 a function that generates the third key, we note that another incorrect conditional transition, as a result, does not take into account the response from the executable call from resources:
 .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 the resources, then after a quick study it becomes clear that the only argument with which it works 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 a valid header, we fix OK on MZ and everything works:


Error # 4


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



But this is not all in the process of debugging and patching, we stumble on a couple of protective measures. The first is the Explained IsDebuggerPresent :
 .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 to check the integrity of the file 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 scored by nop-s, or at the very end simply correct 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

Source: https://habr.com/ru/post/351484/


All Articles