WP-[2025.1软件系统安全赛]happylock
HappyLock
比赛的时候写了好久,可惜还是差一些,没有写出来...
赛中进度
模拟器里运行一下,这是一个手势密码验证程序,通过输入正确手势密码得到一个哈希值作为flag
框架oncomplete
方法
jadx
反编译,核心在com.crackme.happylock.MainActivity
类的oncomplete
方法中,这个方法实现了将用户输入的手势密码进行编码加密处理以及与预设值进行对比
解析如下,其中StringObf.decode
方法就是 base64 解码
1 | // com.andrognito.patternlockview.listener.PatternLockViewListener |
可以看到,调用PatternLockUtils.enc
方法对用户输入进行加密处理,调用Utils.cmp
方法比较加密后的图案和预设的图案,正确会将结果用Submit dart{}
包裹起来显示,错误会显示Try again
HookUtils.cmp
方法
于是先尝试直接在onComplete
里hook比较函数Utils.cmp
,将返回值修改为正确,看一下加密结果的格式
1 | function hookUtilsCmp() { |
不出所料,结果是一个40位十六进制的哈希值,可能是SHA-1
做的哈希
同时,安卓端也显示了加密结果
但是上面的代码也可以看到,虽然绕过了cmp
的检验,这里显示的哈希值还是我们随机输入的手势密码的结果,并不是目标密码的结果
1 | Toast $r82 = Toast.makeText($r52, $r6.append($r4).append(StringObf.decode("fQ==")).toString(), 1); |
那么就要去看Utils.cmp
方法的具体实现了
主要实现的是反射调用clz.cmp
方法
反射调用clz.cmp
方法
于是hook查看一下反射调用的clz
的具体信息
1 | function hookUtilsCmp() { |
这个时候程序运行崩溃了,打印了clz
的相关信息,可以看到
clz
实际上是一个Java.Field
类型对象,它的value
属性是一个名为com.crackme.happylock.Check
的类,比较可疑
于是我们通过clz.value
来访问
1 | function hookUtilsCmp() { |
遗憾的是虽然 clz
被成功获取为
class com.crackme.happylock.Check
,但是崩溃仍然发生,后续还换了各种方法,还是一直崩溃
同时注意到,其实 jadx
中并没有com.crackme.happylock.Check
类,可能是动态加载的
动态加载Check
类
先尝试一下用java.use()
强制加载
1 | function hookUtilsCmp() { |
果然不行,应该就是动态加载的
同时观察到com.crackme.happylock.Utils
里有一个loadclass
方法,猜测Check
类是由它加载的,但是
jadx 并没有把它反编译出来,没法看到它的具体实现
hook检查一下它是否调用了 DexClassLoader
或其他类似的类加载器
1 | function hookDexClassLoader() { |
可以看到,Check
类的加载确实调用了DexClassLoader.loadClass
方法,而且在下面还看到了SHA-1
字样,验证了之前的猜测,手势密码加密编码之后是用SHA-1
做哈希的
我赛中的进度就至此了,后面一直在捣鼓反射的事情,再到后面还尝试了全局扫描SHA-1
哈希算法的调用情况,试图扫到目标手势密码编码后做哈希的过程,都失败了...
而且,其实gpt当时给了我正确解法,但我一直忽略了,在纠结反射、自己的loadclass
方法什么的...
赛后复现
dumpclasses.dex
正确解法就是去 dump
运行时的dex
文件classes.dex
,这里用到了一个新工具GDA
,具体的介绍见作者知乎
需要注意的是先将
Gdump
push到安卓端,同时将GDA
的adb
环境变量放在模拟器adb
环境变量之上
打开 GDA,点击导航栏安卓图标的 dump
功能打开GDA Device Dumper
,在左侧选择com.crackme.happylock
进程,右侧筛选DEX
文件,可以看到/data/data/com.crackme.happylock/files/classes.dex
,右键Dump Module
即可
一开始我也一直 dump 不出来,只能说多试几次,重启一下之类的就可以了,或者就是 GDA 的 Gdump、adb 没有配好,就是我上面说的
也可以全局查找,显示了 PID、地址范围等信息
也可以在下方输入要 dump 的文件的 PID、start_addr 和 size
dump 出来的文件在GDA.exe
的同级目录下,拖到 jadx
里分析,但是报错了
日志显示错误为Bad checksum
,看大佬的解释是 hook
之后填充字节改变了sum
值
修改 dump 出来的classes.dex
的sum
值即可
但是 jadx 还是反编译失败了
同样,也可以选择拖到 GDA 里面分析,但仍然反编译不出来什么东西
在学习大佬写的文章复现的时候,还看到了一个frida-dump
的脚本库,在这里跑出来的结果也是一样的
根据参考的两篇文章,这里是字节码有问题,代码被抽取,这道题实际上就是用 ShadowHook 实现类抽取
修复classes.dex
Java 层卡住了,就要去 Native 层看一下了
ida 分析 libhappylock.so 文件(不得不说 ida9.0 确实好用,8.3 没有
arm、mips 什么的反编译器,还得去用 7.7 反编译,现在 9.0
都有了,定位到JNI_OnLoad
函数
一般来说,JNI_OnLoad
函数里会有本地方法注册RegisterNatives
,但这里并没有
到这里,对于JNI_OnLoad
函数的分析,我参考的两篇大佬的文章用了不一样的方法,我这里就复现了一种
使用Bindiff
恢复符号
至于怎么看出来是 ShadowHook 的,我就不知道了...
ShadowHook 是字节跳动的一个开源 inline hook 框架
于是下载抖音 apk,提取libshadowhook.so
文件,用 ida
打开,保存得到 i64 文件,ida
打开libhappylock.so
文件,ctrl+6
调用bindiff
,选择 Diff
Database,选择libshadowhook.so
的 i64 文件
需要注意
Bindiff
不支持中文路径,所选择的 i64 文件路径不要有中文,也不要有除下划线外的特殊符号
在 Matched Functions 窗口中选中所有匹配的函数符号
右键点击 Import symbols/comments 即可恢复匹配的符号
可以看到,确实有大量 ShadowHook
相关函数,而且JNI_OnLoad
函数中就有调用
ShadowHook 分析
其中,由于有符号信息,shadowhook_hook_func_addr
函数就是调用shadowhook_hook_sym_name
函数
参考ShadowHook 手册,ShadowHook 支持通过“函数地址”或“库名 + 函数名”指定 hook 位置
参考手册,shadowhook_hook_sym_name
函数声明如下
1 |
|
参数
lib_name
(必须指定):需要被 hook 的函数所在的库名sym_addr
(必须指定):需要被 hook 的函数的符号名new_addr
(必须指定):新函数(proxy 函数)的绝对地址orig_addr
(不需要的话可传NULL
):返回原函数地址
这种方式可以 hook “当前已加载到进程中的动态库”,也可以 hook “还没有加载到进程中的动态库”,即如果 hook 时动态库还未加载,ShadowHook 内部会记录当前的 hook “诉求”,后续一旦目标动态库被加载到内存中,将立刻执行 hook 操作
参考以上,观察这里的shadowhook_hook_sym_name
函数的参数,前两个参数如下,但是需要注意的是这里显示的数据不是正确的,交叉引用可以看到对这些值有动态修改
sub_12414
函数如下,可以看到其中对0x430C0-0x430C6
有一个异或
4
的操作,对0x430B8
有一个异或0xE1E1E1E1E1E1E1E1
的操作
而查看sub_12414
的交叉引用就能发现它在.init_array
段,也就意味着这些异或操作程序一运行就会进行
本来想动调的,但是有点问题,那就手动异或回去吧,可以看到lib_name
是libc.so
,sym_name
是execve
,execve
是一个系统调用,用来启动一个新程序
看到不是目标函数我就没继续下去了,看大佬的文章,后面代理函数sub_121E4
中判断系统调用是否为dex2oat
,如果是则不执行,如果不是则执行
下面shadowhook_hook_sym_name_callback
里也调用了shadowhook_hook_sym_name
同样地,之前的sub_12414
函数中也看到了,对0x430C8
有一个异或0x7D7D7D7D7D7D7D7D
的操作,异或之后得到lib_name
为libart.so
我这里 ida
识别的有点问题,需要手动把下面0x430D0
的0x12
带上
这里符号恢复得也有点问题,别人的恢复除了这里的sym_name
,也就是sub_1216C
,是__android_log_print
函数,而代理函数正是我们苦苦寻求的目标函数
这个函数实现的功能是检测dex
文件大小,如果匹配到目标dex
则执行回填操作,从偏移为0x178(376)
开始的0x98
大小的字节,回填的内容就是0x42D90
处内容
而检测dex
文件大小为0x3AC
,实际上就是之前
dump 出来的dex
文件的有效部分,后面都是0
同时,偏移为0x217(535)
的地方也要赋值为0x430A0
处的值,同样也要进行异或
把这些内容按照上面的说的偏移位置填入,即修复成功
终于反编译成功!!!
很简单的异或逻辑,exp
如下
1 | enc = [118, 17, 2, 80, 9, 125, 6, 22, 113, 66, 0, 81, 94, 41, 87, 20, 122, 65, 88, 5, 94, 41, 7, 19, 118, 22, 3, 2, 90, 41, 87, 71, 117, 68, 4, 7, 95, 116, 4, 67] |