巅峰极客挑战赛2023-逆向&misc

 此次比赛复盘,m1_read、Misc-一起学生物是根据wp来做的,而goRe-U与ezlua则是自己尝试做一做。总的来说,里面好多题都没见过,尤其是m1_read与ezlua,这两道题让我发现自己的脚本能力(frida、gdb、z3等)还是比较差。继续加油吧。

0x00 m1_read

 这个题做完调试文件给删了…所以没在github上同步更新。

1
这个看起来是一个门禁系统的写卡程序,不过开发人员只写了半天并没有搞完,此外还有一个写卡程序相关的数据包

 比赛中,这道题我看了一天。最后才知道是W&M的师傅出的题。

菜鸟的挣扎

 这是一道RFID的门禁卡程序,分别给了m1_read.exe与out.bin,其中out.bin是RFID卡中存储的数据,m1_read.exe则是读写RFID卡的工具。

image-20230727093855149

 那天我的发现有:

(1)上图的hint。

(2)RFID的数据存储结构如下:

image-20230727094439818

 RFID卡共有16个扇区,每个扇区都有4个区块,共有1024个字节。

(3)RFID的数据块使用crypto-1算法加密(存疑),链接为:crypto算法

大爹的wp&自己的探索

 链接为wp。其中,我并不理解为什么要用到AES加密,猜测是出题人将m1_read.exe魔改了,在其中读入读出数据时采用了加密处理。所以打算现在网上找到原始版本的程序。

 找到了32位的此程序,链接如下:链接,这是youtube的链接,里面有相关的下载地址。关键是bindiff还出问题了,在ida7.7上总是有问题,最后降成ida7.5+bindiff6才行。然鹅,比较一看,根本木有重合。

 那这样的话,只能自己看m1_read.exe了。

image-20230727162941115

 通过ResourceHacker获得UpdateBlock控件的ID(因为要写文件),可以发现ID=0x3FC。在IDA中找0x3FC,并筛选出rdata段中的条目,并将其转为AFX_MSGMAP_ENTRY结构体。最后可以获得消息映射表:

image-20230727185914187

 最后,可以定位到updateBlock对应的函数:sub_1400028A0。通过分析此函数,如下所示,其中hint函数比较重要,它控制着结果的输出:

image-20230727202014223

 于是跟进到hint函数(需要说明一下,这里我找到hint函数的过程有很大一部分原因是根据AES函数分析调用反推出来的,但是正常分析时,也可以分析到此函数,只不过花费的时间可能长一些),可以发现:

image-20230727202936534

 pbSendBuffer是向卡中写入的数据,且str1_copy与pbSendBuffer挨得很近,猜测是将pbSendBuffer与str1_copy一块写入到卡中。接下来,我们再来分析whiteAES函数(此函数名是我后来加的)。

image-20230727225615753

 由上图可以发现,先经过了一系列操作,最后经过异或,得出结果。

 看了大爹的wp,发现这一系列操作是AES白盒,目的是为了掩盖密钥。大爹的思路是:

(1)使用frida(windows下使用frida,技能点get),随机生成数据并经过whiteAES,得到一系列数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# windows exe hook

import sys
import frida

def on_message(message, data):
print("[%s] => %s" % (message, data))

session = frida.attach("m1_read.exe")

script = session.create_script("""
var baseAddr = Module.findBaseAddress("m1_read.exe");
var f1 = baseAddr.add(0x4BF0);
var f2 = baseAddr.add(0x4C2C);
var whiteAES = new NativeFunction(f1, "pointer", ["pointer", "pointer"])
var count = 9;
Interceptor.attach(f2, {
onEnter: function(args) {
count++;
if(count == 9) {
// 将rdi增加0-15,将rdi指向的地址随机写入数据
this.context.rdi.add(Math.floor(Math.random() * 16)).writeU8(Math.floor(Math.random() * 256))
}
}
})

for (let index = 0; index < 33; index++) {
// 1234567890abcdef 可任意修改
var l = Memory.allocAnsiString("1234567890abcdef");
var b = Memory.alloc(16);
whiteAES(l, b);
console.log(b.readByteArray(16));
count = 0;
}
""")
script.on('message', on_message)
script.load()
sys.stdin.read()
session.detach()

(2)将得到的数据异或66,就得到白盒操作后的数据。

(3)使用phoenixAES,例如第3个例子处理数据,可以得到第N轮的密钥。再使用Stark反推出第1轮的密钥,为00000000000000000000000000000000。这一步其实有Hint,大爹没看到。

(4)用拿到的密钥解密out.bin中的数据:0B987EF5D94DD679592C4D2FADD4EB89 ^ 0x66。就可以得到flag{cddc8d28dabb4ea9}

0x01 g0Re-U

1
gogogo

 发现是UPX的壳。工具脱壳(UPX Unpacker)失败,打算动调找OEP。

苦逼动调upx

image-20230729110419250

 将上图所示内存dump下来,发现是ELF头部以及各种Go表,说明还未解密完。

image-20230729114118806

 sub_45D700貌似是一个解密函数,用于对上上图所示的内存进行解密。

image-20230729115123796

 F8上图出现”input flag”字样。之后跟进到sub_434C00函数,定位到sub_434CA0函数,发现:

image-20230729124346850

 记得之前Go程序的Start入口有这个函数,猜测我已找到OEP。将模块0x400000-0x483000 dump出来,因为这是goRe原始模块的文件。如下图所示:

image-20230729134050910

 但是go_parser找不到first_moduledata,所以无法解析。那怎么办?

 一筹莫展之际,发现文件中有函数aes加密,如下图所示:

image-20230729150416292

 动态调试,发现为10轮AES加密。加密的明文是输入的字符串,扩展为16字节。加密密钥为:wvgitbygwbk2b46d。那么就可以猜测,本题是明文加密然后与某密文比较。

 如何找到密文呢?又一次一筹莫展之际,发现,如果将0x400000-0x578580dump出来,就可以正常运行go_parser脚本。这是偶然发现的,因为经过调试发现解密过程还包括0x483000后的内容。

程序分析

 经过分析,此程序共有3步:

(1)输入input,使用AES加密,密钥为:wvgitbygwbk2b46d,iv为0。(需要注意的是,这里的加密是按16字节作为一个Block加密)

(2)将加密结果使用Base64转换,Base64码表为:

1
456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFG

(3)将转换后的结果做如下处理:

1
(step2_result[i]^0x1A)+key[i&0xf]

 最终可以得到wp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
# @Time : 2023/08/01 18:13:14
# @Author: wd-2711
'''

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

# Excerpt from https://blog.csdn.net/qq_42967398/article/details/101778364
def My_base64_decode(inputs):
bin_str = []
for i in inputs:
if i != '=':
x = str(bin(s.index(i))).replace('0b', '')
bin_str.append('{:0>6}'.format(x))
outputs = ""
nums = inputs.count('=')
while bin_str:
temp_list = bin_str[:4]
temp_str = "".join(temp_list)
if(len(temp_str) % 8 != 0):
temp_str = temp_str[0:-1 * nums * 2]
for i in range(0,int(len(temp_str) / 8)):
outputs += chr(int(temp_str[i*8:(i+1)*8],2))
bin_str = bin_str[4:]
return outputs

# Step1: xor
target = [b'\xe6', b'\xce', b'\x89', b'\xc8', b'\xcf', b'\xc5', b'\xf5', b'\xc9', b'\xd2', b'\xd9', b'\xc0', b'\x91', b'\xce', b'\x7f', b'\xac', b'\xcc', b'\xe9', b'\xcf', b'\xb7', b'\xc0', b'\x96', b'\xd4', b'\xea', b'\x92', b'\xe2', b'\xd7', b'\xdf', b'\x84', b'\xcb', b'\xa5', b'\xae', b'\x93', b'\xa6', b'\xca', b'\xbe', b'\x97', b'\xdf', b'\xce', b'\xf0', b'\xc9', b'\xb7', b'\xe1', b'\xae', b'k', b'\xc4', b'\xb1', b'e', b'\xdb', b'\xce', b'\xed', b'\x92', b'\x93', b'\xd6', b'\x8c', b'\xed', b'\xc3', b'\xa3', b'\xda', b'\x94', b'\xa5', b'\xaa', b'\xb2', b'\xb5', b'\xa7']
key = "wvgitbygwbk2b46d"
inp = ""
for i in range(len(target)):
tmp = int.from_bytes(target[i], byteorder='big', signed=False)
inp += chr((tmp - ord(key[i&0xf]))^0x1A)

# Step2: base64_decode
s = "456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFGH"
output = My_base64_decode(inp)
output = [ord(i) for i in output]
ciphertext = bytes(output)

# Step3: AES_decrypt
for i in range(4):
key = b'wvgitbygwbk2b46d'
iv = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext_block = ciphertext[i*16:(i+1)*16]
plaintext = cipher.decrypt(ciphertext_block)
print(plaintext, end = "")

# flag{g0_1s_th3_b3st_1anguage_1n_the_wOrld!_xxx}

0x02 ezlua

1
luajit 5.1

 之前没做过lua的题,lua是大多嵌入式环境使用的脚本语言,好多游戏也是Lua写的。64位文件。

 Hint为luajit,说明这是用针对lua5.1版本的jit编译器编译的(代码转为机器码)。luajit是保护lua代码的一种方式,可以使用luajit-lang-toolkit进行反汇编。

 使用luajit对dump出的字节码进行反汇编,但是一直显示不兼容。

 准备看wp了,一直没找到兼容的版本啊。

 但是,今天看了一天lualualua,感觉和picStore那道题很像,只不过是luajit版本的。因此,很容易想到,是在加载lua字节码,即LuaL_loadbuffer函数中做了手脚,如下所示:

image-20230802212818568

luaL_loadbuffer函数分析

 首先,找到luaL_loadbuffer的源码,与反汇编后的代码比较:

image-20230803134835189

 容易发现,IDA反汇编结果实际上是将lua_loadx加载进了luaL_loadbuffer函数中(根据字符”?”)。因此,可以根据源码对函数进行命名。其中,cpparser作用是将 Lua 源代码解析为 Lua 函数,并将其保存在 Lua 虚拟机中,以供后续执行,于是跟进此函数:

image-20230803141520298

 在此判断加载的Buffer是binary文件还是text文件,由于是binary文件,于是跟进lj_bcread,与源代码比较如下:

image-20230803145553057

 跟进lj_bcread_proto,发现此函数可能被魔改了。经过分析,发现下左图红框的两行代码魔改为下右图红框的代码:

image-20230803155348688

 进一步分析,发现是在lj_bcread_proto中的bcread_bytecode函数中动了手脚,如下图所示:

image-20230803160914397

 源代码中,bc[i]为4字节,lj_bswap函数作用是改变字节序并返回。发现并没有魔改,寄!看来和picStore还不一样。

佬的wp

 对输入打内存断点。若输入1123456789112345678911234567891123456789,则在luajit字节码区域为:

image-20230803182449497

 中间有0x3。对输入打断点,卡在了bcread_uleb128中,回溯发现在bcread_kgc函数中,这是一个加载常量的函数,如下所示:

image-20230803184456539

 在此补充一下uleb编码:uleb通过字节的最高位来决定是否用到下一个字节。如果最高位为1,则用到下一个字节,直到某个字节最高位为0或已经读取了5个字节为止。对于函数bcread_uleb128而言,它读取1-5字节。

 其实,能正常反编译,就是自己没找对版本和x86/x64的而已。具体看luajit常用的反编译方法

Step1:dump出luajit字节码,使用bt模板解析,但是在解析常量时报错。猜测原因是:输入也会改变字节码的值,具体是改变字节码中存储常量的部分。因此,要输入符合uleb128格式的输入,再dump字节码。此时输入可以为:9999999911999999991199999999119999999911。再解析,便成功了。发现Luajit版本为2,如下所示:

image-20230804143805212

Step2:可以发现这是64位的luajit字节码文件,原因如下:第5字节为0x0E=b1110,其中采用2-slot frame info` 模式(FLAG_FR2 = 0b00001000),后者是 64 位引入的新特性。

Step3:目前,反编译luajit字节码文件的工具有ljd与luajit-decomp,由于ljd并未找到x64版本的,所以使用luajit-decomp进行反编译,具体步骤见luajit反编译。最后使用luajit-decompiler进行反编译,但是出错了,如下所示:

image-20230804153504085

 猜测是输入的最后使用11,多占了1字节,于是将输入改为9999999901999999990199999999019999999901。还是报错:

image-20230804163519467

 这是去除中间项的代码,把这行注释掉,并开启no_unwarp,最终得到反汇编代码。但是有7000行代码,猜测是将去除中间项的代码注释掉的结果,妈的。

 看了佬的wp,他对于此错误的改进为:修改./luajit-decompiler/ljd/rawdump/code.py为:

image-20230804171242947

(注:由于我并不是先转汇编再转lua代码,因此并没有调试出是在CALL处除了错误,其实,我就算知道了CALL出了错误,也不知道怎么改,呜呜呜。)

 再运行,得到:

image-20230804171355119

 针对浮点数的情况,佬的改进如下:修改./luajit-decompiler/ljd/rawdump/constants.py为:

image-20230804171644260

 得到的结果out.lua为(其中slot1与slot0为输入的uleb128编码):

image-20230804172838927

 其中,可以分析得出,slot2负责控制程序流。最后返回处理后的slot0与slot1。有一个很坑的点是:

1
slot0 = bit.bxor(bit.bor(bit.lshift(slot0, 3), bit.rshift(slot0, 61)), bit.bxor(slot0 + bit.bor(bit.rshift(slot1, 8), bit.lshift(slot1, 56)), slot2))

 上述式子的中间值slot1没赋值,应该是这样(也太坑了):

1
2
3
4
slot1 = bor(rshift(slot1, 8), lshift(slot1, 56));
slot1 = bxor(slot0 + slot1, slot2);
slot0 = bor(lshift(slot0, 3), rshift(slot0, 61));
slot0 = bxor(slot0, slot1);

 再分析IDA反汇编代码,luaL_checkcdata函数表示从栈底开始取值,取到的分别是slot0与slot1,并判断是否是某个值。

image-20230804173219028

Step4:通过运行out.lua,可以得到一共运行了32次上述4行代码,其中slot2都不同,最终可以写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import leb128

def decrypt(_slot0, _slot1, _slot2):
_slot0 ^= _slot1
_slot0 = ((_slot0 >> 3) | (_slot0 << 61)) & 0xffffffffffffffff
slot1_and_slot0 = _slot1 ^ _slot2
_slot1 = slot1_and_slot0 - _slot0
_slot1 &= 0xffffffffffffffff
_slot1 = ((_slot1 >> 56) | (_slot1 << 8)) & 0xffffffffffffffff
return _slot0, _slot1

def u64_to_uleb128(u):
high = u >> 32
low = u & 0xffffffff
return leb128.u.encode(low).hex() + leb128.u.encode(high).hex()

slot2_array = [0xdeadbeef12345678, 0x28539dc5904d8141, 0xf2ac321ccf237a7b, 0xf03df21e866b1a36, 0x584cde754c325b4b, 0x97407269ac231f8b,
0xd2960ba60ee82d09, 0xb34efc0e8d197592, 0x15011adba4d8613d, 0x1598470b72677cea, 0xb497efc6db87c606, 0xae0f3ba8a4eeb218,
0xab6036ab64121254, 0x663ae5cc72c5eb7f, 0x71af0f7e9c371b0e, 0xeb97fc6b58f9eb33, 0x774108a83f7c75f6, 0x5a6542d5c9968681,
0x5e6fb973117ccfb1, 0xea8134ba653ce534, 0xfc92946aa1cc9678, 0x38af8cc9553071e4, 0x99f7a1b258084992, 0x82e920e890bb99da,
0xc67f72528ed05d6c, 0x4cab3a53d2598281, 0x517358620b3249f9, 0xcf3d41fd5e5e0786, 0x626be66ab995efe3, 0x24d85b01f54e2ab1,
0xe9cd3a65e3f95992, 0x4bf5996751882d17]

slot2_array.reverse()

slot0 = 0xdd26c29515a28396
slot1 = 0xbd722d4baf99b9c7

for slot2 in slot2_array:
slot0, slot1 = decrypt(slot0, slot1, slot2)

print(u64_to_uleb128(slot1) + u64_to_uleb128(slot0))
# b0e09cfb05e5e4fdaa07d4e2a8fa05f480fda206

0x03 Misc-一起学生物

To be continued…

留言

© 2024 wd-z711

⬆︎TOP