好友
阅读权限25
听众
最后登录1970-1-1
|
本帖最后由 Enigma_G 于 2017-4-29 10:23 编辑
0x00 闲言碎语
发现自己的贴被繁华版主加了精,怪感动的,谢谢版主
上了一天的课怪累的。。。晚上开论坛发现有朋友问我一道PC的竞赛题,PC逆向自己接触的不多,然后看了下那个竞赛的第一道Android方向的题目,有一点声明一下,我这只是分享一些个人经验与技巧从分析apk的角度入手而不是解CTF题,这道题的算法其实不难 ,论坛里也有一篇精华是分析这个的
附上链接 http://www.52pojie.cn/thread-602744-1-1.html
还有安全客上的分析,这个比较简陋。随意看看就好,不适合新手 http://bobao.360.cn/ctf/learning/191.html
那么
嘻嘻,大家不嫌弃的话可以看看
那么进入正题
----------------
0x01 Java层分析
我的习惯是先去分析AndroidMainifest文件,因为这里可以得到很多东西,权限啦,注册了哪些Activity啦,reciver啦,等等之类的,还能看出它有没有加壳之类的,那么我们首先用jeb打开这个文件
[XML] 纯文本查看 复制代码 <?xml version="1.0" encoding="utf-8"?>
<manifest package="com.humen.crackme010" platformBuildVersionCode="23" platformBuildVersionName="6.0-2166767" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="21" />
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme">
<activity android:label="@string/app_name" android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:label="@string/title_activity_window" android:name=".WindowActivity" />
</application>
</manifest>
下面这两行代码有过开发经验的朋友应该知道,这是加载的第一个avtivity
[XML] 纯文本查看 复制代码 <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
那么我们跟进这个MainActivity查看
[Java] 纯文本查看 复制代码 import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View$OnClickListener;
import android.view.View;
import android.widget.Toast;
public class MainActivity extends Activity {
static {
System.loadLibrary("humen"); //加载so类库,还有一种加载方式是调用load()函数,至于加载的过程,我前段时间一直在看源码,有机会以后和大家分享分享
}
public MainActivity() {
super();
}
public void checkPass(String arg6) {
String v1 = CheckUtil.checkPass(arg6);
if(arg6 == null || (arg6.equals(""))) {
Toast.makeText(this.getApplicationContext(), this.getString(2131034114), 1).show();
}
else if(v1.equals("a")) {
this.startActivity(new Intent(((Context)this), WindowActivity.class));
}
else {
Toast.makeText(this.getApplicationContext(), this.getString(2131034115), 1).show();
}
}
protected void onCreate(Bundle arg3) {
super.onCreate(arg3);
this.setContentView(2130903040);
this.findViewById(2131165184).setOnClickListener(new View$OnClickListener() {
public void onClick(View arg4) {
MainActivity.this.checkPass(MainActivity.this.findViewById(2131165185).getText().toString().trim());//.trim()是去掉前后两端存在的空格
}
});
}
}
那么思路其实很清晰,我们继续跟进CheckUtil.checkPass函数
[Java] 纯文本查看 复制代码 package com.humen.crackme010;
public class CheckUtil {
public CheckUtil() {
super();
}
public static native String checkPass(String arg0) { //声明native函数
}
}
那么Java层分析完毕了,接下来就是分析so文件了,正好前篇那篇文章提到过一些修改变量类型,可能还不是很详细,这里就详细给大家分享下我个人的经验
0x02 so文件分析
找到对应的Java_com_humen_crackme010_CheckUtil_checkPass()函数
那么明显这两个参数类型识别有问题
前面文章说过,第二个参数要么是jclass类型,要么是jobject类型,那么因为在java层声明的时候是static方法,所以这里应该是jclass类型,摁快捷键Y或者你右键也行,改为jclass,再就是第三个参数,就是我们java层传入的参数,不记得的话调回去看一看
是一个String类型参数,那么JNI对应的就是jstring类型,同样修改,呢么修复后代码如下:
[C++] 纯文本查看 复制代码
int __fastcall Java_com_humen_crackme010_CheckUtil_checkPass(_JNIEnv *a1, jclass a2, jstring a3)
{
_JNIEnv *v3; // r7@1
const char *v4; // r0@1
const char *v5; // r4@1
size_t v6; // r0@1
signed int v7; // r2@1
signed int v8; // r6@1
int v9; // r3@3
char v10; // r0@3
signed int v11; // r1@3
int i; // r3@3
char v13; // r5@5
signed int v14; // r5@6
signed int v15; // r8@6
signed int j; // r3@9
const char *v17; // r1@13
v3 = a1;
v4 = (const char *)getStringUnicodeChars(a1, (int)a3, "utf-8");
v5 = v4;
v6 = strlen(v4);
v7 = 0;
v8 = v6;
while ( 1 )
{
v7 += 2;
if ( v7 >= v8 )
break;
v9 = (int)&v5[v7];
v10 = v5[v7 - 1];
*(_BYTE *)(v9 - 1) = v5[v7 - 2];
v11 = 0;
*(_BYTE *)(v9 - 2) = v10;
for ( i = (int)(v5 + 4); ; *(_BYTE *)(i - 8) = v13 )
{
v11 += 4;
i += 4;
if ( v11 >= v8 )
break;
v13 = *(_BYTE *)(i - 4);
*(_BYTE *)(i - 4) = *(_BYTE *)(i - 8);
}
}
v14 = 0;
v15 = strlen(v5);
while ( v14 < v15 )
_android_log_print(4, "humen", "%c", v5[v14++]);
printf("%s", v5);
for ( j = 0; v5[j] == (unsigned __int8)t[j]; ++j )
;
if ( j <= v8 )
v17 = "b";
else
v17 = "a";
return _JNIEnv::NewStringUTF(v3, v17);
}
那么从解题的角度来看,已经可以开始分析算法了,但是前面说过我是从分析apk的角度,
这行代码从函数名字可以很容易看出他是转换Unicode码,但是这里参数类型让我觉得奇怪
[C++] 纯文本查看 复制代码 v4 = (const char *)getStringUnicodeChars(a1, (int)a3, "utf-8");
那么跟进看看:
很明显,这里IDA没有正确识别
注意这几行代码
[C++] 纯文本查看 复制代码 v6 = (*(int (__fastcall **)(_JNIEnv *, const char *))(*(_DWORD *)v4 + 24))(v4, "java/lang/String");
[C++] 纯文本查看 复制代码 v7 = (*(int (__fastcall **)(_JNIEnv *, int, const char *, const char *))(*(_DWORD *)v4 + 132))(
v4,
v6,
"getBytes",
"(Ljava/lang/String;)[B");
[C++] 纯文本查看 复制代码 v9 = _JNIEnv::CallObjectMethod(v4, v5, v7, v8);
那么看到CallObjectMethod这个函数了,这个函数是干吗的呢?还记得之前那篇文章讲过JNI.h这个文件么?去这里找到这个函数
下面是这个函数的三个重载
[C++] 纯文本查看 复制代码 jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);[/size] jobject (*CallObjectMethodV)(JNIEnv*, jobject, jmethodID, va_list);
jobject (*CallObjectMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);
那么我们得到的第一个信息便是这个函数的返回值是一个jobject类型,但是这里返回的是int类型,呢么双击CallObjectMethod函数,我们跟进分析它哪识别错了
[C++] 纯文本查看 复制代码 int __fastcall _JNIEnv::CallObjectMethod(int a1, int a2, int a3, int a4)
{
int varg_r3; // [sp+14h] [bp-4h]@1
varg_r3 = a4;
return (*(int (__cdecl **)(int, int))(*(_DWORD *)a1 + 140))(a1, a2);
}
可以发现全部识别错了
开始对照手动修改,修复完后
[C++] 纯文本查看 复制代码
jobject __fastcall _JNIEnv::CallObjectMethod(JNIEnv *a1, jobject a2, jmethodID a3, int a4)
{
int varg_r3; // [sp+14h] [bp-4h]@1
varg_r3 = a4;
return (*a1)->CallObjectMethodV(a1, a2, a3, (int **__attribute__((__org_typedef(va_list))) )&varg_r3);
}
那么再回到之前的函数查看
那么再修复一下v4的类型为JNIEnv*,之前的你会发现前面多了下划线,那么这应该就是导致识别出错的地方
修改后成功参数识别,后面的函数也识别出来了,那么你会发现其实a1的参数类型也错了,同样修改,然后把函数修复一下,还记得么,右键force call type
是不是好看了很多,还没修复完,先不急,我们先看看几个重要的JNI函数
这个是一个找class的函数
[C++] 纯文本查看 复制代码 v6 = (*v4)->FindClass(v4, "java/lang/String");
看看JNI.h是怎样定义的
[C++] 纯文本查看 复制代码 jclass (*FindClass)(JNIEnv*, const char*);
那么,这里没有识别错,看下一个
[C++] 纯文本查看 复制代码 v7 = (*v4)->GetMethodID(v4, v6, "getBytes", "(Ljava/lang/String;)[B");
JNI.h中的定义
[C++] 纯文本查看 复制代码 jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
可以发现有几处识别出错了,对应着改
再看下一个
[C++] 纯文本查看 复制代码 v8 = _JNIEnv::NewStringUTF((_JNIEnv *)v4, v3);
会发现这里显示v4出错了,但其实v4类型就是JNIEnv*,那么很可能是函数定义处出错了,双击跟过去
[C++] 纯文本查看 复制代码
int __fastcall _JNIEnv::NewStringUTF(_JNIEnv *this, const char *a2)
{
return (*(int (**)(void))(*(_DWORD *)this + 668))();
}
又是_JNIEnv的锅,这下会修改了,不怕。修复后
[C++] 纯文本查看 复制代码 jstring __fastcall _JNIEnv::NewStringUTF(JNIEnv *this, const char *a2)
{
return (*this)->NewStringUTF(this, a2);
}
退回刚刚函数
那么识别出来了。
下面的修复,大同小异,就不再一一演示步骤了,截图再上传好累的,最终修复结果
[C++] 纯文本查看 复制代码 void *__fastcall getStringUnicodeChars(JNIEnv *a1, jobject a2, const char *a3)
{
const char *v3; // r6@1
JNIEnv *v4; // r4@1
jobject v5; // r5@1
jclass v6; // r0@1
jmethodID v7; // r8@1
jstring v8; // r6@1
jobject v9; // r6@1
signed int v10; // r5@1
jbyte *v11; // r8@1
void *v12; // r7@2
v3 = a3;
v4 = a1;
v5 = a2;
_android_log_print(4, "humen", "-------------------------------3");// 打印调试
v6 = (*v4)->FindClass(v4, "java/lang/String");
v7 = (*v4)->GetMethodID(v4, v6, "getBytes", "(Ljava/lang/String;)[B");
v8 = _JNIEnv::NewStringUTF(v4, v3);
_android_log_print(4, "humen", "-------------------------------4");// 打印调试
v9 = _JNIEnv::CallObjectMethod(v4, v5, v7, (int)v8);
v10 = (*v4)->GetArrayLength(v4, v9);
v11 = (*v4)->GetByteArrayElements(v4, v9, 0);
_android_log_print(4, "humen", "-------------------------------5");// 打印调试
if ( v10 <= 0 ) // 元素为空
{
v12 = 0;
}
else
{
v12 = malloc(v10 + 1);
memcpy(v12, v11, v10); // 拷贝
*((_BYTE *)v12 + v10) = 0;
}
_android_log_print(4, "humen", "-------------------------------6");
(*v4)->ReleaseByteArrayElements(v4, v9, v11, 0);// 释放空间
return v12;
}
那么可以继续往下分析函数的作用了
GetMethodID -> 每一个函数都有对应的ID,呢么我们怎样得到呢?前面JNI那篇文章说了JNI注册时会将函数的签名传进去,也就是这里的 v6, "getBytes", "(Ljava/lang/String;)[B",v6就是java/lang/String
NewStringUTF ->字符串转化,没啥多讲
CallObjectMethod -> 函数调用,这也是为啥它有那么多重载的原因,因为你调用的函数返回值有多种类型,这里就需要传入刚刚的MethodID了,比较要告诉它调用哪个函数,对吧
GetArrayLength ->获得数组长度,不多说
GetByteArrayElements -> 获得数组元素
ReleaseByteArrayElements - >最后释放数组空间
_android_log_print ->打印调试用的方法
那么,这个getStringUnicodeChars函数算是修复完毕了
那么贴出,Java_com_humen_crackme010_CheckUtil_checkPass函数代码以及一点简单地解释
[C++] 纯文本查看 复制代码 jstring __fastcall Java_com_humen_crackme010_CheckUtil_checkPass(JNIEnv *env, jclass a2, jstring input)
{
JNIEnv *v3; // r7@1
const char *v4; // r0@1
const char *v5; // r4@1
size_t v6; // r0@1
signed int v7; // r2@1
signed int length; // r6@1
int v9; // r3@3
char v10; // r0@3
signed int v11; // r1@3
int i; // r3@3
char v13; // r5@5
signed int v14; // r5@6
signed int v15; // r8@6
signed int j; // r3@9
const char *v17; // r1@13
v3 = env;
v4 = (const char *)getStringUnicodeChars(env, input, "utf-8");
v5 = v4;
v6 = strlen(v4); // 获取长度
v7 = 0;
length = v6;
while ( 1 )
{
v7 += 2;
if ( v7 >= length )
break;
v9 = (int)&v5[v7];
v10 = v5[v7 - 1];
*(_BYTE *)(v9 - 1) = v5[v7 - 2];
v11 = 0;
*(_BYTE *)(v9 - 2) = v10;
for ( i = (int)(v5 + 4); ; *(_BYTE *)(i - 8) = v13 )
{
v11 += 4;
i += 4;
if ( v11 >= length )
break;
v13 = *(_BYTE *)(i - 4);
*(_BYTE *)(i - 4) = *(_BYTE *)(i - 8);
}
} // 转换
v14 = 0;
v15 = strlen(v5); // 获取长度
while ( v14 < v15 )
_android_log_print(4, "humen", "%c", v5[v14++]);// 打印
printf("%s", v5);
for ( j = 0; v5[j] == (unsigned __int8)t[j]; ++j )// S!@#@1FD23154A34
;
if ( j <= length )
v17 = "b";
else
v17 = "a";
return _JNIEnv::NewStringUTF(v3, v17);
}
算法就不讲了,就是转换了
-------------------------
0x03 总结
这道题本身是不难的,但是可以学到IDA的小技巧,但是有一点,如果你分析汇编当我没说23333,这个应该是不会出现类型识别错误的问题233333
嘻嘻,只是个人分享的一点点小经验,又让师傅们笑话了
马上五一,祝大家五一快乐,以后有机会再接着分享
|
免费评分
-
查看全部评分
|