静态分析原生层程序¶
Warning
The current page still doesn't have a translation for this language.
You can read it through Google Translate.
Besides, you can also help to translate it: Contributing.
基本方法¶
静态分析原生层程序基本的过程如下
- 提取 so 文件
- ida 反编译 so 文件阅读 so 代码
- 根据 java 层的代码来分析 so 代码。
- 根据 so 代码的逻辑辅助整个程序的分析。
原生层静态分析例子¶
2015-海峡两岸-一个APK,逆向试试吧¶
反编译¶
利用jadx反编译apk,确定应用的主活动
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:versionCode="1" android:versionName="1.0" package="com.example.mobicrackndk">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" />
<application android:theme="@style/AppTheme" android:label="@string/app_name" android:icon="@drawable/ic_launcher" android:allowBackup="true">
<activity android:label="@string/app_name" android:name="com.example.mobicrackndk.CrackMe">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
不难看出,程序的主活动为 com.example.mobicrackndk.CrackMe。
分析主活动¶
不难看出,程序的基本情况就是利用 native 函数 testFlag 判断用户传入的 pwdEditText 是否满足要求。
public native boolean testFlag(String str);
static {
System.loadLibrary("mobicrackNDK");
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) R.layout.activity_crack_me);
this.inputButton = (Button) findViewById(R.id.input_button);
this.pwdEditText = (EditText) findViewById(R.id.pwd);
this.inputButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
CrackMe.this.input = CrackMe.this.pwdEditText.getText().toString();
if (CrackMe.this.input == null) {
return;
}
if (CrackMe.this.testFlag(CrackMe.this.input)) {
Toast.makeText(CrackMe.this, CrackMe.this.input, 1).show();
} else {
Toast.makeText(CrackMe.this, "Wrong flag", 1).show();
}
}
});
}
分析so文件¶
自然我们首先会去直接找 testFlag 函数,凡是并没有直接找到。我们只好首先分析 JNI_Onload 函数,如下
signed int __fastcall JNI_OnLoad(JNIEnv *a1)
{
JNIEnv *v1; // r4
int v2; // r5
char *v3; // r7
int v4; // r1
const char *v5; // r1
int v7; // [sp+Ch] [bp-1Ch]
v1 = a1;
v7 = 0;
printf("JNI_OnLoad");
if ( ((*v1)->FindClass)(v1, &v7, 65540) )
goto LABEL_7;
v2 = v7;
v3 = classPathName[0];
fprintf((&_sF + 168), "RegisterNatives start for '%s'", classPathName[0]);
v4 = (*(*v2 + 24))(v2, v3);
if ( !v4 )
{
v5 = "Native registration unable to find class '%s'";
LABEL_6:
fprintf((&_sF + 168), v5, v3);
LABEL_7:
fputs("GetEnv failed", (&_sF + 168));
return -1;
}
if ( (*(*v2 + 860))(v2, v4, off_400C, 2) < 0 )
{
v5 = "RegisterNatives failed for '%s'";
goto LABEL_6;
}
return 65540;
}
可以发现,程序在这里动态注册了类和相应的函数 off_400C。仔细看一下该函数
.data:0000400C off_400C DCD aTestflag ; DATA XREF: JNI_OnLoad+68↑o
.data:0000400C ; .text:off_1258↑o
.data:0000400C ; "testFlag"
.data:00004010 DCD aLjavaLangStrin_0 ; "(Ljava/lang/String;)Z"
.data:00004014 DCD abcdefghijklmn+1
.data:00004018 DCD aHello ; "hello"
.data:0000401C DCD aLjavaLangStrin_1 ; "()Ljava/lang/String;"
.data:00004020 DCD native_hello+1
.data:00004020 ; .data ends
可以发现,确实就是 testflag 函数,其对应的函数名为 abcdefghijklmn。
分析abcdefghijklmn¶
可以发现,程序主要在三个部分对输入的 v10 进行了判断
- 判断1
if ( strlen(v10) == 16 )
说明输入的字符串长度为16。
- 判断2
v3 = 0;
do
{
s2[v3] = v10[v3] - v3;
++v3;
}
while ( v3 != 8 );
v2 = 0;
v12 = 0;
if ( !strcmp(seed[0], s2) )
- 判断3
v9 = ((*jniEnv)->FindClass)();
if ( !v9 )
{
v4 = "class,failed";
LABEL_11:
_android_log_print(4, "log", v4);
exit(1);
}
v5 = ((*jniEnv)->GetStaticMethodID)();
if ( !v5 )
{
v4 = "method,failed";
goto LABEL_11;
}
_JNIEnv::CallStaticVoidMethod(jniEnv, v9, v5);
v6 = ((*v1)->GetStaticFieldID)(v1, v9, "key", "Ljava/lang/String;");
if ( !v6 )
_android_log_print(4, "log", "fid,failed");
((*v1)->GetStaticObjectField)(v1, v9, v6);
v7 = ((*jniEnv)->GetStringUTFChars)();
while ( v3 < strlen(v7) + 8 )
{
v13[v3 - 8] = v10[v3] - v3;
++v3;
}
v14 = 0;
v2 = strcmp(v7, v13) <= 0;
根据汇编代码,可知第三个判断中调用了calcKey类中的静态方法
.text:00001070 LDR R0, [R5]
.text:00001072 LDR R2, =(aCalckey - 0x1080)
.text:00001074 LDR R3, =(aV - 0x1084)
.text:00001076 LDR R4, [R0]
.text:00001078 MOVS R1, #0x1C4
.text:0000107C ADD R2, PC ; "calcKey"
.text:0000107E LDR R4, [R4,R1]
.text:00001080 ADD R3, PC ; "()V"
并在之后获得了key的内容。
public static String key;
public static void calcKey() {
key = new StringBuffer("c7^WVHZ,").reverse().toString();
}
}
获取flag¶
根据这三个判断,我们可以得到输入的字符串内容
s = "QflMn`fH,ZHVW^7c"
flag = ""
for idx,c in enumerate(s):
flag +=chr(ord(c)+idx)
print flag
结果如下
QgnPrelO4cRackEr
输入之后并不对。
再次分析¶
想到这里就要考虑下,程序是不是在哪里修改了对应的字符串。这里首先看一下seed。对 x 进行交叉引用,发现其在 _init_my 中使用了,如下
size_t _init_my()
{
size_t i; // r7
char *v1; // r4
size_t result; // r0
for ( i = 0; ; ++i )
{
v1 = seed[0];
result = strlen(seed[0]);
if ( i >= result )
break;
t[i] = v1[i] - 3;
}
seed[0] = t;
byte_4038 = 0;
return result;
}
所以最初程序对 seed 进行了修改。
再次获取flag¶
修改脚本如下
s = "QflMn`fH,ZHVW^7c"
flag = ""
for idx,c in enumerate(s):
tmp = ord(c)
if idx<8:
tmp-=3
flag +=chr(tmp+idx)
print flag
flag 如下
➜ 2015-海峡两岸一个APK,逆向试试吧 python exp.py
NdkMobiL4cRackEr
当然该题目也可以使用动态调试。