考察知识点
JNI_Onload 中通过 RegisterNatives 动态注册 jni 函数
.init_array
前置知识
http://eternalsakura13.com/2018/02/08/jni2/
http://eternalsakura13.com/2018/02/08/jnienv/
赛题链接
https://github.com/eternalsakura/ctf_pwn/blob/master/android逆向/mobicrackNDK.apk
分析
看一下apk是什么样的。
用jadx反编译,然后导出android工程,用as打开
查看AndroidManifest.xml
在java代码中定位
可以看出,调用了一个native方法testFlag,将输入的flag字符串传入testFlag进行验证,验证成功则弹出输入的字符串,否则就wrong answer。
用IDA打开so文件查看。
发现没有testFlag对应的c函数,怀疑是动态注册
找到JNI_Onload()
用y把参数都改改
首先,JNI_Onload()的参数是JavaVM *vm
再看GetEnv,它的第一个参数是JavaVM,然后第二个参数就是用来存得到的env指针
1 | struct JNIInvokeInterface { |
所以我们把v7修改为env。
v2=env,所以v2也是JNIEnv *
类型。
再看v4,v4是FindClass的返回值,类型是jclassjclass (*FindClass)(JNIEnv*, const char*);
这样修改后我们的反编译代码就好看多了,这种技巧非常有用,除非你已经很熟练了,否则这样多改改最好。(改类型按y,改名字按n)
这样我们就找到了函数映射表。
很显然abcdefghijklmn就是testFlag的native实现,双击切过去重命名为Java_com_testFlag。
具体的验证算法就在这里。
算法分析
像刚刚那样修正一下参数类型。
前8位校验
1 | for (int i = 0; i < 8; i++) |
将s2和seed比较,seed的内容是
后8位校验
首先调用了java层的calcKey方法,计算得到一个key
Calc.java
1 | package com.example.mobicrackndk; |
然后将后八位处理一下,存入字符串
1 | for (int i = 8; i < 16; i++) |
于是写出脚本计算flag
1 | seed='QflMn`fH' |
算出QgnPrelO4cRackEr,然而wrong answer.
继续分析
在JNI_Onload之前执行的只能是.init_array
段了。
.init_array
根据 linker 源码, section 的执行顺序为 .preinit_array -> .init -> .init_array 。但 so 是不会执行 .preinit_array 的, 可以忽略。
.init_array 是一个函数指针数组。编写代码时在函数声明时加上 __attribute__((constructor))
使之成为共享构造函数,即可使该函数出现在 .init_array section 中。
IDA 动态调试时 ‘ctrl+s’ 查看 section 信息即可定位这两个 setction,特别的,对于 .init_array,可通过搜索 Calling %s @ %p for '%s'
定位。
代码分析
进入__init_my
看看
确实在这里对seed字符串进行了修改。
1 | for (int i = 0; i < 8; i++) |
所以最终我们的reverse脚本是
1 | seed = 'QflMn`fH' |
flag是NdkMobiL4cRackEr
参考链接
https://www.zybuluo.com/cxm-2016/note/566623
https://github.com/toToCW/CTF-Mobile/blob/master/2015海峡两岸CTF/一个APK,逆向试试吧ndk/WriteUp/2015海峡两岸CTF-一个APK,逆向试试吧.md