最近第一届OpenHarmony CTF专题赛要开始了,报名了看看题。其实去年开始就陆续出现鸿蒙应用逆向的题目,第一次出现是在2024的京麟杯吧,而且题目非常难。现在就零时抱佛脚一下,复现一下之前其实就挺想复现的鸿蒙逆向的题目。
题目描述
//太好了,是数学大师,我们有救啦
//Great! It’s the Math Master, we are saved!
附件给了一个entry-default-unsigned.hap文件
应用程序包基础知识-开发基础知识-基础入门 – 华为HarmonyOS开发者
看看官方介绍
HAP(Harmony Ability Package)是应用安装和运行的基本单元。HAP包是由代码、资源、第三方库、配置文件等打包生成的模块包,其主要分为两种类型:entry和feature。
- entry:应用的主模块,作为应用的入口,提供了应用的基础功能。
- feature:应用的动态特性模块,作为应用能力的扩展,可以根据用户的需求和设备类型进行选择性安装。
应用程序包可以只包含一个基础的entry包,也可以包含一个基础的entry包和多个功能性的feature包。
和apk文件一样,其实是个zip,解压可以看到里面的文件,本体的结构如下
可以看到这里的libs目录下的so文件肯定要关注,应该和分析安卓so文件的流程差不多。
然后是关注这里的ets/modules.abc,这里的abc文件,官方介绍如下
方舟字节码(ArkCompiler Bytecode)文件,是ArkCompiler的编译工具链以源代码作为输入编译生成的产物,其文件后缀名为.abc。在发布态,abc文件会被打包到HAP中。
不知道全称的以为华为命名文件这么草率😅,鸿蒙应用的开发看官网是使用自己研发的ArkTS语言,应该是对应安卓开发的Java/Kotlin语言,所以这里的abc文件也对应安卓的dex文件。
abc字节码文件的反编译工具有abc-decompiler,这里Java版本太低打开会报错
ohos-decompiler/abc-decompiler
然后搜了一下最近刚好出了在线的反编译器.abcD
HarmonyOS NEXT鸿蒙应用反编译器 .abcD 发布试用 – 知乎
abc-decompiler反编译的内容很多这种奇奇怪怪的字符,看起来不太舒服,估计还是基于Jadx的兼容不能做到完美
.abcD反编译出来的代码就比较干净,而且已经不像是Java代码了,abc-decompiler这边还是全是public属性的函数,当然我也不知道ArkTS语言是什么样的,不知道谁反编译的效果更好一些。
分析过程中一般需要关注的是源代码中main包的pages目录,这里包含了该程序中的所有的页面,这里只有一个Index页面。
abc-decompiler这边反编译了出入口函数func_main_0,用于进行初始化。.abcD网站则没用反编译出这样的函数名。
public Object func_main_0(Object functionObject, Object newTarget, Index this) {
newlexenvwithname([3, "Index", 0, "4newTarget", 1, "this", 2], 3);
_lexenv_0_1_ = newTarget;
_lexenv_0_2_ = this;
if (isIn("finalizeConstruction", ViewPU.prototype) == false) {
Reflect.set(ViewPU.prototype, "finalizeConstruction", #*#);
}
obj = ViewPU.#~@0=#Index(Object2, Object3, ViewPU, ["setInitiallyProvidedValue", "&entry/src/main/ets/pages/Index&.#~@0>#setInitiallyProvidedValue", 1, "updateStateVars", "&entry/src/main/ets/pages/Index&.#~@0>#updateStateVars", 1, "purgeVariableDependenciesOnElmtId", "&entry/src/main/ets/pages/Index&.#~@0>#purgeVariableDependenciesOnElmtId", 1, "aboutToBeDeleted", "&entry/src/main/ets/pages/Index&.#~@0>#aboutToBeDeleted", 0, 4]);
obj2 = obj.prototype;
obj2["message"].getter = obj2.#~@0>#message;
obj2["message"].setter = obj2.#~@0>#message^1;
obj2["inputBackgroundColor"].getter = obj2.#~@0>#inputBackgroundColor;
obj2["inputBackgroundColor"].setter = obj2.#~@0>#inputBackgroundColor^1;
obj2.handleInput = obj2.#~@0>#handleInput;
obj2.initialRender = obj2.#~@0>#initialRender;
obj2.rerender = obj2.#~@0>#rerender;
obj.getEntryName = obj.#~@0<#getEntryName;
_lexenv_0_0_ = obj;
registerNamedRoute(#*#^1, "", createobjectwithbuffer(["bundleName", "com.swdd.suapp2", "moduleName", "entry", "pagePath", "pages/Index", "pageFullPath", "entry/src/main/ets/pages/Index", "integratedHsp", "false", "moduleType", "followWithHap"]));
return null;
}
这里有一个handleInput ,我们追踪一下找到关键的逻辑
这里应该就是调用了libentry.so文件中的check函数来检查输入,要求函数的返回值为right。
再提到ArkTS的一些关键部分
TextInput是鸿蒙 ArkUI 中的一个重要组件,用于创建用户可交互的输入框。它通常用于响应用户的输入操作,那这里提交之后应该触发onSubmit事件,这里在abc-decompiler中就不知道这个事件触发后执行哪里的代码。.abcD可以看到,这里就直接跳转到handleInput函数,所以说反编译器果然都是各有优点
这里有个onClick事件也会触发handleInput函数,没有鸿蒙真机或者模拟器,就无法知道其应用布局,也无法动态分析,这也是鸿蒙逆向出题人和做题人都面对的困境,出题人出不了需要动态分析的,因为我们根本做不了,反正目前鸿蒙逆向仅靠静态分析是可以做的。
那我们就看so文件。
这是一个入口模块的注册函数,点击unk_F390,引用entry字符串可以找到
再追踪
这里的sub_3750函数应该就是abc层当中调用的check函数。或者
也能找到sub_3750函数,这里sub_3750函数的代码量很长,看了很多文章都能看出是对代码进行了混淆,如果是当时的我来做估计就看不出,然后被这一大坨代码恶心到,然后放弃了😢。
代码很复杂,不过可以通过一些打印语句提取关键信息
flag的长度是32,再跟踪这里的input变量
这里复制了元素,再跟踪copy
这里应该是通过两层for循环把输入分为4组,每次将8个字节传入了函数sub_57B0,同时还传入了一个数组,这些数组都是一些大数。前面元素的复制也是两层for循环,分为4组,和这里一样的操作。
这里的sub_57B0应该就是我们要进行解密的一个逻辑
可以发现对我们的输入进行处理,然后和大数数组的元素进行对比,这里的函数参数也是逐渐传递,所以s1很明显就是对输入处理之后的结果。
这几个函数依旧有混淆,但是可以喂给AI,可以加快分析
分析完之后大概就是这样
所以就是解这样的一个方程
参考其他战队的解题脚本可以直接解
import math
from Crypto.Util.number import long_to_bytes
s = [
999272289930604998,
1332475531266467542,
1074388003071116830,
1419324015697459326,
978270870200633520,
369789474534896558,
344214162681978048,
2213954953857181622,
]
def decrypt(enc):
e = enc * 2
d = e + 3
a = -1 + math.sqrt(1 + d)
a = int(a)
return a
for i in s:
a = decrypt(i)
print(long_to_bytes(a)[::-1].decode(), end="")
# SUCTF{Ma7h_WorldIs_S0_B3aut1ful}
或者z3解
from z3 import *
dst = [999272289930604998, 1332475531266467542, 1074388003071116830, 1419324015697459326, 978270870200633520, 369789474534896558, 344214162681978048, 2213954953857181622]
flag = b''
solver = Solver()
x = [Int(f"x{i}") for i in range(8)]
for i in range(8):
solver.add(x[i] > 0)
expr = (x[i]**2 + 2*x[i] - 3) / 2 - dst[i] # 整除
solver.add(0 <= expr)
solver.add(expr < 1)
if solver.check() == sat:
m = solver.model()
ans = [m[x[i]].as_long() for i in range(8)]
flag = b''.join([y.to_bytes(4, 'little') for y in ans])
print(flag)
# b'SUCTF{Ma7h_WorldIs_S0_B3aut1ful}'
所以flag就是SUCTF{Ma7h_WorldIs_S0_B3aut1ful}