去年 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 之上。

image-20231024100923652

不过好在已经有人帮助我们分析出了虚拟指令对应的含义,我们直接使用工具即可快速得到源码。

查壳,这是用 autoit 编译而成的 exe,无壳。

img

使用 AutoIt-Ripper 得到该 exe 的源码

https://github.com/nazywam/AutoIt-Ripper

image-20231024100948515

通过搜索字符串 wrong 定位到关键函数

image-20231024101011962

分析加密流程,首先判断输入是否满足 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

image-20231024101111920

编写 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 相关的字符串

image-20231024101207340

通过给 c2 赋不同的值来调用 vm 中的函数

image-20231024101214972

根据字符串的提示,我们便知道了被混淆的 vm 的函数都是什么含义

image-20231024101220002

同时我们还在这里发现了最终的校验函数,可以推测最终的 flag 应该有 33 位

image-20231024101225270

使用 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='')
更新于 阅读次数