福建海峡两岸CTF 2015:一个APK,逆向试试吧

考察知识点

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
2
3
4
5
6
7
8
9
10
11
12
13
14
struct JNIInvokeInterface {
...
jint (*GetEnv)(JavaVM*, void**, jint);
};

struct _JavaVM {
const struct JNIInvokeInterface* functions;
#if defined(__cplusplus)
...
jint GetEnv(void** env, jint version)
{ return functions->GetEnv(this, env, version); }
...
#endif /*__cplusplus*/
};


所以我们把v7修改为env。
v2=env,所以v2也是JNIEnv *类型。
再看v4,v4是FindClass的返回值,类型是jclass
jclass (*FindClass)(JNIEnv*, const char*);

这样修改后我们的反编译代码就好看多了,这种技巧非常有用,除非你已经很熟练了,否则这样多改改最好。(改类型按y,改名字按n)

这样我们就找到了函数映射表。

很显然abcdefghijklmn就是testFlag的native实现,双击切过去重命名为Java_com_testFlag。
具体的验证算法就在这里。

算法分析

像刚刚那样修正一下参数类型。

前8位校验

1
2
3
4
for (int i = 0; i < 8; i++)
{
s2[i] = input[i] - i;
}

将s2和seed比较,seed的内容是

后8位校验

首先调用了java层的calcKey方法,计算得到一个key
Calc.java

1
2
3
4
5
6
7
8
9
package com.example.mobicrackndk;

public class Calc {
public static String key;

public static void calcKey() {
key = new StringBuffer("c7^WVHZ,").reverse().toString();
}
}

然后将后八位处理一下,存入字符串

1
2
3
4
for (int i = 8; i < 16; i++)
{
s3[i - 8] = input[i] - i;
}

于是写出脚本计算flag

1
2
3
4
5
6
7
seed='QflMn`fH'
key='c7^WVHZ,'[::-1]
cyphertext=seed+key
plaintext=[]
for i in range(16):
plaintext.append(chr(ord(cyphertext[i])+i))
print "".join(c for c in plaintext)

算出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
2
3
4
5
for (int i = 0; i < 8; i++)
{
t[i] = seed[i] - 3;
}
seed = t

所以最终我们的reverse脚本是

1
2
3
4
5
6
7
8
seed = 'QflMn`fH'
seed = "".join(chr(ord(c) - 3) for c in seed)
key = 'c7^WVHZ,'[::-1]
cyphertext = seed + key
plaintext = []
for i in range(16):
plaintext.append(chr(ord(cyphertext[i]) + i))
print "".join(c for c in plaintext)

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