去年 CBCTF 的时候,我当时为这个比赛出了一道逆向题叫做 CBNET
, 可是直到结束之后才终于有了一解,我那时非常自责,感觉自己把题目出难了导致大家都做不出来,那次比赛结束之后几个师傅找我问这题,我发现原来是我自己设计的自定义算法难住了大家,大家不会写逆向脚本导致的。所以今年的 CBCTF, 我总结了去年出题的经验,给自己今年出题定的目标的就是希望每一个人都可以去尝试着去做一下,并且不在算法的层面去为难大家.
auuuu3 的出题灵感主要在上半年看到过一篇文章,说是有 hacker 在压缩包里藏了一个无毒的 autoit exe 解释器,然后这个解释器运行恶意的 au3 脚本来实现恶意行为,当时我就在想要是把这个 au3 脚本编译成 exe 之后会不会很有趣呢~所以就开始了尝试,写了一个 demo 自己做了一下,发现直接用 ida 逆向的难度相当的高,需要极强的逆向水平才可以做出来,所以我就去网上寻找可以逆向 autoit 的工具,一开始用官方自带的 exe2aut 是可以成功得到源码的,随后我去翻看了 autoit 官方的文档,发现这个 exe2aut 在 autoitv3.2.5.1 之后就被移除了,为了稍稍增加一些难度,所以我就选用了高版本的 autoit 来编译 exe, 然后就发现不管是官方的 exe2aut, 还是 github 上高 star 的 AutoDec
, autoitqds
, myAut2Exe
这些工具全部都反编译源码报错了,正当我担心自己又要把中等题出成困难题的时候,我又发现了一个工具,他叫 autoit-ripper
, 可以完美的反编译高版本的 autoit, 思路也是解析 autoit 虚拟机,有兴趣的可以去看一看它的源码,还是挺有趣的,至于逆向算法,我仅仅是用 shellcode 加载了一个 dll, 这个 dll 里面也只有一个 xxtea, 应该逆向起来是非常轻松的
vm_flutter 的出题灵感是我在八月末做完腾讯游戏安全竞赛安卓初赛题之后,对这题中的 vm 有了较深的感悟之后决定设计的,在解那题的 vm 的时候,我对于它的 opcode 指令真的是全然不知,所以我去 hook 了 vm 相关的函数成功的解出了 vm, 要是想看这场比赛题解的可以移步我博客的这篇文章。回到 vm_flutter, 这题在比赛结束之后只有一解我感觉其实挺意外的,应该可以有更多人可以做出来这题。出 vm_flutter 的时候,我想要设计一个大家不知道 opcode 的环境,我想最好的方法就是用 flutter 框架,然后把 opcode 放在 dart 层中,但是我又希望大家可以很快的找出 vm 的函数比如 push
, pop
, add
等等函数所在的位置,所以我将这些函数全部都定义在了 java 层中,这样大家用 jadx 或 jeb 反编译之后,就可以很快找到他们了。接下来的需要解决的问题就是如何让 flutter 的 dart 层去调用原生安卓中的 java 层中的函数,这里我用的方法是我上半年在逆向一个 flutter 框架的游戏叫做装扮少女的时候学习到的,这个 app 使用 MethodChannel
管道通信的方式实现 dart 层和原生安卓之间的通信,而通讯的消息我也没有修改,甚至字符串都是 vm 有关的字符串。但是方法名混淆其实是在我的预期之外,这其实是 build apk 的时候 flutter 自动把方法名混淆了的,不是我干的!!(本来我是想让大家一下就可以知道 java 层函数的含义的), 然后我还把最终的 check 函数也放在了 java 层中,而不是在 vm 中去 check,dart 层通过 check 的结果去弹出 right 或 wrong, 所以这样应该能更加降低大家解题的难度了~
# auuuu3
本题是由 autoit 编写而成的 exe,本质上也是一个虚拟机,通过解析 autoit 动态脚本语言来执行命令,倘若使用 IDA 或 od 直接逆向,那么是需要花费一段时间在这题上的。并且根据官方的说法,在 v3.2.5.1 之后的 autoit 版本中将不再拥有自带的反编译工具,本题所使用的 autoit 版本在 v3.2.5.1 之上。
不过好在已经有人帮助我们分析出了虚拟指令对应的含义,我们直接使用工具即可快速得到源码。
查壳,这是用 autoit 编译而成的 exe,无壳。
使用 AutoIt-Ripper
得到该 exe 的源码
https://github.com/nazywam/AutoIt-Ripper
通过搜索字符串 wrong
定位到关键函数
分析加密流程,首先判断输入是否满足 38 位,如果满足,则将输入经过 ENC
函数加密, ENC
函数如下
Func ENC ( $DATA , $KEY ) | |
$DATA = Binary ( $DATA ) | |
Local $DATALEN = BinaryLen ( $DATA ) | |
If $DATALEN = 0 Then | |
Return "" | |
ElseIf $DATALEN < 8 Then | |
$DATALEN = 8 | |
EndIf | |
Local $OPCODE = "0x83EC14B83400000099538B5C2420558B6C242056578B7C9DFCF7FB89C683C606C74424180000000085F68D76FF0F8EEA000000896C24288D4BFF8D549D00894C2410895424148974242081442418B979379E8B4C2418C1E90281E103000000894C241C31F6397424107E568B5424288BCF8B6CB204C1E9058D14AD0000000033CA8BD58BC7C1EA03C1E00433D003CA8B5424188BDE81E303000000335C241C8B4424308B1C9833D533DF03D333CA8B542428010CB28B0CB2463974241089CF7FAA8B5424288BCF8B2AC1E9058D14AD0000000033CA8BD58BC7C1EA03C1E00433D003CA8B5424188BDE81E303000000335C241C8B4424308B1C9833D533DF03D3FF4C242033CA8B542414014AFC8B4AFC8B54242089CF420F8F2DFFFFFF5F31C05E5D5B83C414C21000" | |
Local $CODEBUFFER = DllStructCreate ( "byte[" & BinaryLen ( $OPCODE ) & "]" ) | |
DllStructSetData ( $CODEBUFFER , 1 , $OPCODE ) | |
Local $V = DllStructCreate ( "byte[" & Ceiling ( $DATALEN / 4 ) * 4 & "]" ) | |
DllStructSetData ( $V , 1 , $DATA ) | |
Local $K = DllStructCreate ( "byte[16]" ) | |
DllStructSetData ( $K , 1 , $KEY ) | |
DllCall ( "user32.dll" , "none" , "CallWindowProc" , "ptr" , DllStructGetPtr ( $CODEBUFFER ) , "ptr" , DllStructGetPtr ( $V ) , "int" , Ceiling ( $DATALEN / 4 ) , "ptr" , DllStructGetPtr ( $K ) , "int" , 0 ) | |
Local $RET = DllStructGetData ( $V , 1 ) | |
$CODEBUFFER = 0 | |
$V = 0 | |
$K = 0 | |
Return $RET | |
EndFunc |
可以发现函数动态加载了一个 dll, 然后从 dll 调用加密函数进行加密
使用如下脚本将 OPCODE
以字节的形式写入文件,方便使用 IDA 进行分析
import binascii | |
opcode = "83EC14B83400000099538B5C2420558B6C242056578B7C9DFCF7FB89C683C606C74424180000000085F68D76FF0F8EEA000000896C24288D4BFF8D549D00894C2410895424148974242081442418B979379E8B4C2418C1E90281E103000000894C241C31F6397424107E568B5424288BCF8B6CB204C1E9058D14AD0000000033CA8BD58BC7C1EA03C1E00433D003CA8B5424188BDE81E303000000335C241C8B4424308B1C9833D533DF03D333CA8B542428010CB28B0CB2463974241089CF7FAA8B5424288BCF8B2AC1E9058D14AD0000000033CA8BD58BC7C1EA03C1E00433D003CA8B5424188BDE81E303000000335C241C8B4424308B1C9833D533DF03D3FF4C242033CA8B542414014AFC8B4AFC8B54242089CF420F8F2DFFFFFF5F31C05E5D5B83C414C21000" | |
hex_bytes = binascii.a2b_hex(opcode) | |
with open("enc.dll",'wb') as f: | |
f.write(hex_bytes) |
伪代码如下,可以发现加密算法为 xxtea
编写 exp 得到 flag
import binascii | |
from ctypes import * | |
import struct | |
def MX(z, y, total, key, p, e): | |
temp1 = (z.value >> 5 ^ y.value << 2) + (y.value >> 3 ^ z.value << 4) | |
temp2 = (total.value ^ y.value) + (key[(p & 3) ^ e.value] ^ z.value) | |
return c_uint32(temp1 ^ temp2) | |
def decrypt(n, v, key): | |
delta = 0x61C88647 | |
rounds = 6 + 52 // n | |
total = c_uint32(-rounds * delta) | |
y = c_uint32(v[0]) | |
e = c_uint32(0) | |
while rounds > 0: | |
e.value = (total.value >> 2) & 3 | |
for p in range(n - 1, 0, -1): | |
z = c_uint32(v[p - 1]) | |
v[p] = c_uint32((v[p] - MX(z, y, total, key, p, e).value)).value | |
y.value = v[p] | |
z = c_uint32(v[n - 1]) | |
v[0] = c_uint32(v[0] - MX(z, y, total, key, 0, e).value).value | |
y.value = v[0] | |
total.value += delta | |
rounds -= 1 | |
return v | |
if __name__ == "__main__": | |
ct = "7218181A02F79F4B5773E8FFE83FE732DF96259FF2B86AAB945468A132A83D83CF9D750E316C8675" | |
ct = binascii.a2b_hex(ct) | |
flag = "" | |
key = "Wowww111auUu3" | |
v = struct.unpack('<10I', ct) | |
k = struct.unpack('<4I', key.encode() + b'\x00' * 3) | |
v = list(v) | |
k = list(k) | |
n = 10 | |
res = decrypt(n, v, k) | |
for r in res: | |
print(r.to_bytes(4, 'little').decode(), end='') |
# vm_flutter
虽然这个 apk 是使用 flutter 编写的,但是其实在本题中 flutter 仅仅是纸老虎般的存在。
在本题的题目描述中给出了两个提示,第一是对 flag 的加密算法有且只有一个 vm,第二是全部 vm 相关的函数都定义在 Java 层中,dart 层仅仅只是调用 java 层中定义的函数。所以根据这两个提示,就可以联想到使用 frida 等 hook 框架去 hook java 层中 vm 相关的函数,打印出虚拟指令来进行分析,如果可以想到这一点,那么这题就迎刃而解了。倘若使用 reflutter 等 flutter 逆向工具,那么将会在 dart 虚拟机中越陷越深:(
在本题中,opcode 是在 dart 层被定义的,但是这其实无关紧要,因为 vm 相关的函数是在 java 层中定义的,我们对 vm 函数 hook 的过程其实就是对 opcode 的 “解读” 过程,通过 hook 的操作,我们就可以将 vm 对内部栈或内存的操作映射为可读的、可以理解的虚拟指令来进行分析。
所以设计本题也是基于这种思想,使用目前安卓层面逆向难度非常高的 flutter 框架,来模拟类似 vmprotect 这类强壳 opcode 未知的场景,甚至在 vmprotect 中 opcode 会在间隔不定时间后随机变换。虽然我们不知道 opcode,但是万变不离其宗,vm 解析 opcode 之后最终要执行的操作是不会变的。
获取 flag 的过程如下
使用 jadx 反编译,发现方法名被混淆,但是进入 com.dasctf.vm_flutter.vm_flutter.MainActivity
可以看到 vm 相关的字符串
通过给 c2
赋不同的值来调用 vm 中的函数
根据字符串的提示,我们便知道了被混淆的 vm 的函数都是什么含义
同时我们还在这里发现了最终的校验函数,可以推测最终的 flag 应该有 33 位
使用 frida hook vm 函数来获取 vm 指令
function hook(){ | |
Java.perform(function(){ | |
const activity = Java.use("k.b"); | |
activity.a.implementation = function(){ | |
console.log("Lshift"); | |
} | |
activity.b.implementation = function(){ | |
console.log("Rshift"); | |
} | |
activity.c.implementation = function(){ | |
console.log("add"); | |
} | |
activity.d.implementation = function(){ | |
console.log("and"); | |
} | |
activity.e.implementation = function(x){ | |
console.log("load "+x); | |
} | |
activity.f.implementation = function(){ | |
console.log("mul"); | |
} | |
activity.g.implementation = function(){ | |
console.log("or"); | |
} | |
activity.h.implementation = function(){ | |
console.log("pop"); | |
} | |
activity.i.implementation = function(x){ | |
console.log("push "+x); | |
} | |
activity.j.implementation = function(x){ | |
console.log("store "+x); | |
} | |
activity.k.implementation = function(){ | |
console.log("sub"); | |
} | |
activity.l.implementation = function(){ | |
console.log("xor"); | |
} | |
}) | |
} | |
setImmediate(hook,0); |
使用下列命令注入脚本
frida -U -l .\hook.js -f "com.dasctf.vm_flutter.vm_flutter"
我们输入 33 位的数字,例如 000000000000000000000000000000000
,frida 打印的内容如下
push 48 | |
store 0 | |
push 176 | |
push 11 | |
load 0 | |
add | |
xor | |
store 0 | |
push 48 | |
store 1 | |
push 198 | |
push 18 | |
load 1 | |
add | |
xor | |
store 1 | |
push 48 | |
store 2 | |
push 66 | |
push 5 | |
load 2 | |
add | |
xor | |
store 2 | |
push 48 | |
store 3 | |
push 199 | |
push 18 | |
load 3 | |
add | |
xor | |
store 3 | |
push 48 | |
store 4 | |
push 170 | |
push 14 | |
load 4 | |
add | |
xor | |
store 4 | |
push 48 | |
store 5 | |
push 32 | |
push 13 | |
load 5 | |
add | |
xor | |
store 5 | |
push 48 | |
store 6 | |
push 31 | |
push 14 | |
load 6 | |
add | |
xor | |
store 6 | |
push 48 | |
store 7 | |
push 60 | |
push 18 | |
load 7 | |
add | |
xor | |
store 7 | |
push 48 | |
store 8 | |
push 26 | |
push 13 | |
load 8 | |
add | |
xor | |
store 8 | |
push 48 | |
store 9 | |
push 89 | |
push 18 | |
load 9 | |
add | |
xor | |
store 9 | |
push 48 | |
store 10 | |
push 60 | |
push 17 | |
load 10 | |
add | |
xor | |
store 10 | |
push 48 | |
store 11 | |
push 119 | |
push 19 | |
load 11 | |
add | |
xor | |
store 11 | |
push 48 | |
store 12 | |
push 60 | |
push 17 | |
load 12 | |
add | |
xor | |
store 12 | |
push 48 | |
store 13 | |
push 90 | |
push 5 | |
load 13 | |
add | |
xor | |
store 13 | |
push 48 | |
store 14 | |
push 104 | |
push 13 | |
load 14 | |
add | |
xor | |
store 14 | |
push 48 | |
store 15 | |
push 174 | |
push 19 | |
load 15 | |
add | |
xor | |
store 15 | |
push 48 | |
store 16 | |
push 146 | |
push 11 | |
load 16 | |
add | |
xor | |
store 16 | |
push 48 | |
store 17 | |
push 179 | |
push 5 | |
load 17 | |
add | |
xor | |
store 17 | |
push 48 | |
store 18 | |
push 67 | |
push 15 | |
load 18 | |
add | |
xor | |
store 18 | |
push 48 | |
store 19 | |
push 73 | |
push 11 | |
load 19 | |
add | |
xor | |
store 19 | |
push 48 | |
store 20 | |
push 50 | |
push 12 | |
load 20 | |
add | |
xor | |
store 20 | |
push 48 | |
store 21 | |
push 92 | |
push 19 | |
load 21 | |
add | |
xor | |
store 21 | |
push 48 | |
store 22 | |
push 170 | |
push 19 | |
load 22 | |
add | |
xor | |
store 22 | |
push 48 | |
store 23 | |
push 160 | |
push 9 | |
load 23 | |
add | |
xor | |
store 23 | |
push 48 | |
store 24 | |
push 166 | |
push 15 | |
load 24 | |
add | |
xor | |
store 24 | |
push 48 | |
store 25 | |
push 47 | |
push 8 | |
load 25 | |
add | |
xor | |
store 25 | |
push 48 | |
store 26 | |
push 155 | |
push 19 | |
load 26 | |
add | |
xor | |
store 26 | |
push 48 | |
store 27 | |
push 115 | |
push 9 | |
load 27 | |
add | |
xor | |
store 27 | |
push 48 | |
store 28 | |
push 60 | |
push 13 | |
load 28 | |
add | |
xor | |
store 28 | |
push 48 | |
store 29 | |
push 52 | |
push 12 | |
load 29 | |
add | |
xor | |
store 29 | |
push 48 | |
store 30 | |
push 42 | |
push 5 | |
load 30 | |
add | |
xor | |
store 30 | |
push 48 | |
store 31 | |
push 96 | |
push 19 | |
load 31 | |
add | |
xor | |
store 31 | |
push 48 | |
store 32 | |
push 72 | |
push 7 | |
load 32 | |
add | |
xor | |
store 32 |
分析一下 vm 指令,这里有个相同的结构,经过分析后可以发现这是标准的栈式虚拟机,先将操作数压入栈中,然后进行运算时从栈顶取回,所以此处的 vm 加密是对输入加上一个数,再去异或一个数
push 48 | |
store 0 | |
push 176 | |
push 11 | |
load 0 | |
add | |
xor | |
store 0 |
编写 exp 得到 flag
import re | |
output = '''push 48 | |
store 0 | |
push 176 | |
push 11 | |
load 0 | |
add | |
xor | |
store 0 | |
push 48 | |
store 1 | |
push 198 | |
push 18 | |
load 1 | |
add | |
xor | |
store 1 | |
push 48 | |
store 2 | |
push 66 | |
push 5 | |
load 2 | |
add | |
xor | |
store 2 | |
push 48 | |
store 3 | |
push 199 | |
push 18 | |
load 3 | |
add | |
xor | |
store 3 | |
push 48 | |
store 4 | |
push 170 | |
push 14 | |
load 4 | |
add | |
xor | |
store 4 | |
push 48 | |
store 5 | |
push 32 | |
push 13 | |
load 5 | |
add | |
xor | |
store 5 | |
push 48 | |
store 6 | |
push 31 | |
push 14 | |
load 6 | |
add | |
xor | |
store 6 | |
push 48 | |
store 7 | |
push 60 | |
push 18 | |
load 7 | |
add | |
xor | |
store 7 | |
push 48 | |
store 8 | |
push 26 | |
push 13 | |
load 8 | |
add | |
xor | |
store 8 | |
push 48 | |
store 9 | |
push 89 | |
push 18 | |
load 9 | |
add | |
xor | |
store 9 | |
push 48 | |
store 10 | |
push 60 | |
push 17 | |
load 10 | |
add | |
xor | |
store 10 | |
push 48 | |
store 11 | |
push 119 | |
push 19 | |
load 11 | |
add | |
xor | |
store 11 | |
push 48 | |
store 12 | |
push 60 | |
push 17 | |
load 12 | |
add | |
xor | |
store 12 | |
push 48 | |
store 13 | |
push 90 | |
push 5 | |
load 13 | |
add | |
xor | |
store 13 | |
push 48 | |
store 14 | |
push 104 | |
push 13 | |
load 14 | |
add | |
xor | |
store 14 | |
push 48 | |
store 15 | |
push 174 | |
push 19 | |
load 15 | |
add | |
xor | |
store 15 | |
push 48 | |
store 16 | |
push 146 | |
push 11 | |
load 16 | |
add | |
xor | |
store 16 | |
push 48 | |
store 17 | |
push 179 | |
push 5 | |
load 17 | |
add | |
xor | |
store 17 | |
push 48 | |
store 18 | |
push 67 | |
push 15 | |
load 18 | |
add | |
xor | |
store 18 | |
push 48 | |
store 19 | |
push 73 | |
push 11 | |
load 19 | |
add | |
xor | |
store 19 | |
push 48 | |
store 20 | |
push 50 | |
push 12 | |
load 20 | |
add | |
xor | |
store 20 | |
push 48 | |
store 21 | |
push 92 | |
push 19 | |
load 21 | |
add | |
xor | |
store 21 | |
push 48 | |
store 22 | |
push 170 | |
push 19 | |
load 22 | |
add | |
xor | |
store 22 | |
push 48 | |
store 23 | |
push 160 | |
push 9 | |
load 23 | |
add | |
xor | |
store 23 | |
push 48 | |
store 24 | |
push 166 | |
push 15 | |
load 24 | |
add | |
xor | |
store 24 | |
push 48 | |
store 25 | |
push 47 | |
push 8 | |
load 25 | |
add | |
xor | |
store 25 | |
push 48 | |
store 26 | |
push 155 | |
push 19 | |
load 26 | |
add | |
xor | |
store 26 | |
push 48 | |
store 27 | |
push 115 | |
push 9 | |
load 27 | |
add | |
xor | |
store 27 | |
push 48 | |
store 28 | |
push 60 | |
push 13 | |
load 28 | |
add | |
xor | |
store 28 | |
push 48 | |
store 29 | |
push 52 | |
push 12 | |
load 29 | |
add | |
xor | |
store 29 | |
push 48 | |
store 30 | |
push 42 | |
push 5 | |
load 30 | |
add | |
xor | |
store 30 | |
push 48 | |
store 31 | |
push 96 | |
push 19 | |
load 31 | |
add | |
xor | |
store 31 | |
push 48 | |
store 32 | |
push 72 | |
push 7 | |
load 32 | |
add | |
xor | |
store 32''' | |
pattern = r'push\s+(\d+)' | |
final = [255, 149, 26, 146, 200, 115, 150, 68, 36, 222, 185, 240, 74, 45, 4, 234, 236, 215, 62, 114, 178, 46, 205, 209, | |
214, 83, 233, 34, 82, 74, 67, 36, 204] | |
matches = re.findall(pattern, output) | |
#print(matches) | |
for i in range(len(final)): | |
print(chr((final[i] ^ (int(matches[i * 3 + 1]))) - int(matches[i * 3 + 2])), end='') |