繁华一个CM的反调试技术实现与逆向
本帖最后由 wnagzihxain 于 2017-2-12 17:14 编辑蓦然发现移动端的阅读效果比较差,用markdown生成了html格式的提供下载,效果非常好
http://pan.baidu.com/s/1c246nj6
0x00 前言
繁华上次的CM所使用的几个反调试,来分享一下如何一步一步实现这些反调试技术以及如何逆向分析
0x01 作者WriteUp以及出题思路
吾爱论坛:[世事繁华皆成空---某CrackMe出题思路](http://www.52pojie.cn/thread-564068-1-1.html)
Github:(https://github.com/Qrilee/crackme)
下面所有知识点的代码都是从作者Github仓库fork的,繁华说还改了一些代码,所以可能会跟实际大家看到的代码有一点不一样,但是反调试那部分的实现是一样的
0x02 注册native函数
这部分针对没有NDK开发经验的同学,NDK编程中使用C和C++是有些差异的,下面统一使用的是C++
2.1 静态注册
我们先来实现具体静态注册的代码,假设大家的NDK开发环境已经配置完成
使用Android Studio创建Android工程
声明调用的so文件,并定义一个native函数`getStringFromNative()`
MainActivity.java
package com.wnagzihxain.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends Activity {
static {
System.loadLibrary("totoc");
}
public native static String getStringFromNative();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, getStringFromNative(), Toast.LENGTH_LONG).show();
}
}
执行`Make Project`
```
Build -> Make Project
```
接着可以在`app/build/intermediates/classes/debug`下面看到生成的class文件
然后在`app/src/main/java`下面执行
```
C:\Users\wangz\Desktop\MyApplication\app\src\main\java>javah com.wnagzihxain.myapplication.MainActivity
```
生成一个头文件
```
com_wnagzihxain_myapplication_MainActivity.h
```
我们修改一下头文件名为`totoc.h`
在`app/src/main`下新建`jni`文件夹,跟`java`同目录
将刚刚生成的头文件移动到`jni`下面
新建`C++ Source`文件`totoc.cpp`
将头文件中生成的函数信息复制进去,修改参数并添加返回语句
//
// Created by wnagzihxain on 2016/12/17 0017.
//
#include <iostream>
#include <jni.h>
#include "totoc.h"
using namespace std;
/*
* Class: com_wnagzihxain_myapplication_MainActivity
* Method: getStringFromNative
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_wnagzihxain_myapplication_MainActivity_getStringFromNative(JNIEnv *env, jclass obj)
{
return env->NewStringUTF("Hello From JNI!");
}
关于函数定义前面的`JNIEXPORT`和`JNICALL`是什么意思,可参考下面这个
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
修改`build.gradle`
默认的配置如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.wnagzihxain.myapplication"
minSdkVersion 19
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
}
改一下:
apply plugin: 'com.android.model.application'
model {
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.wnagzihxain.myapplication"
minSdkVersion.apiLevel 19
targetSdkVersion.apiLevel 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles.add(file("proguard-rules.pro"))
}
}
ndk {
moduleName "totoc"
stl "stlport_static"
ldLibs.addAll(["log", "z", "android"])
abiFilters.addAll(['armeabi', 'armeabi-v7a'])
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
}
讲解一下参数:
moduleName "totoc"//so的名字
stl "stlport_static"//以静态链接方式使用的stlport版本的STL
ldLibs.addAll(["log", "z", "android"])
abiFilters.addAll(['armeabi', 'armeabi-v7a'])
关于NDK编译的配置我在这里推荐几篇文章,虽然是`mk`,但是参数的解释大概是一样的
网易云捕的博客:(http://crash.163.com/#news/!newsId=24)
CSDN-oZuiJiaoWeiYang的专栏:(http://blog.csdn.net/ozuijiaoweiyang/article/details/50845899)
修改项目的`build.gradle`
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
改为
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.8.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
改完后`Sync`
等待构建完成我们就可以编译签名我们的APK了
一段漫长的等待后,APK编译签名好了
运行起来,弹出了Toast
拖进JEB
我们可以看到`MainActivity`的代码接近源码,右边的`Libraries`文件夹下有两个so文件,对应着我们在`build.gradle`中的配置
使用IDA Pro打开`armeabi-libtotoc.so`
在`Exports`里找到我们写的native方法,同时左边`Functions Window`也可以看到
双击该函数跳转到汇编代码
如果要切换逻辑调用图,可以按空格键
讲讲每一句汇编的意思,虽然我们是以C++代码写的函数,但是在反汇编后,还是以C的逻辑解析,比如C++是`env->NewStringUTF("totoc")`,而C是`(*env)->NewStringUTF(env,"totoc")`,那么IDA反汇编后,在调用函数的逻辑上,R0寄存器存储的就是env,这里包括后面说的env,指的是`JNIEnv *`类型,也就是一级指针,二级指针会使用`(&env)`表示
表示Thumb指令集
.text:00000D48 CODE16
.text:00000D48
.text:00000D48 ; =============== S U B R O U T I N E =======================================
.text:00000D48
.text:00000D48
导出函数
.text:00000D48 EXPORT Java_com_wnagzihxain_myapplication_MainActivity_getStringFromNative
函数名
.text:00000D48 Java_com_wnagzihxain_myapplication_MainActivity_getStringFromNative
将R3,LR寄存器的值压入栈中,称为压栈操作
.text:00000D48 PUSH {R3,LR}
将立即数`0xA7`赋值给R3:`R3 = 0xA7`
.text:00000D4A MOVS R3, #0xA7
取R0地址上的值,赋值给R2:`R2 = *env`
.text:00000D4C LDR R2,
将`aHelloFromJni - 0xD58`计算出来的值赋值给R1:`R1 = aHelloFromJni - 0xD58`
.text:00000D4E LDR R1, =(aHelloFromJni - 0xD58)
R3的值二进制左移两位:`R3 = R3 << 2`,也就是:`0xA7 << 2 => 0xA7 * 4 => 668`
.text:00000D50 LSLS R3, R3, #2
取`R2 +R3`地址上的值赋值给R3,R2此时为`(*env)`,R3为668,那么赋值完成后R3就是`[(*env) + R3]`的一个偏移地址,指向函数`NewStringUTF()`
.text:00000D52 LDR R3,
这一句看起来的意思是:`R1 = R1 + PC`,此时R1的值需要结合上面的语句来分析,`.text:00000D4E`处R1的值为`aHelloFromJni - 0xD58`,在这里合并起来:`aHelloFromJni - 0xD58 + PC`,PC为当前指令地址,那么就是`aHelloFromJni - 0xD58 + 0x0D54`,到这里一切看起来很合理,其实不是的,这里涉及到一个取址的问题,在指令执行过程中,会经历取址,译码,执行的过程,当我们在执行第一条指令的时候,第二条指令正在译码,第三条的指令正在取址,也就是说,当我们在执行`.text:00000D54`地址指令的时候,PC寄存器取址的值应该是`.text:00000D58`,这里是Thumb指令,2字节一条指令,所以往后2条指令就是`.text:00000D58`,那么再回来看这一句汇编,就应该是:`aHelloFromJni - 0xD58 + 0x0D58` -> `aHelloFromJni`,相当于一个定位的过程
.text:00000D54 ADD R1, PC ; "Hello From JNI!"
调用R3并根据跳转过去的地址位置上的数字是否为1,为1表示接下来的指令需要以Thumb指令集解释,并将CPSR的T置为1,如果位为0,说明接下来是要以ARM指令集解释指令,并将CPSR的T置为0,也就是:`(*env)->NewStringUTF(env, "Hello From JNI!")`
.text:00000D56 BLX R3
将栈中存储的R3和PC寄存器的值恢复到寄存器,称为弹出或者出栈
.text:00000D58 POP {R3,PC}
函数结束
.text:00000D58 ; End of function Java_com_wnagzihxain_myapplication_MainActivity_getStringFromNative
.text:00000D58
.text:00000D58 ; ---------------------------------------------------------------------------
2.2 动态注册
上面是静态注册的分析,接下来我们来讲讲动态注册的代码实现,为什么要动态注册,因为在加载so的时候会先调用`JNI_OnLoad`,这个用于动态注册函数以及一些初始化,所以我们反调试放在这里可以直接执行而不需要调用
添加一下代码
//
// Created by wnagzihxain on 2016/12/17 0017.
//
#include <iostream>
#include <jni.h>
#include "totoc.h"
using namespace std;
static const char *gClassName = "com/wnagzihxain/myapplication/MainActivity";
/*
* Class: com_wnagzihxain_myapplication_MainActivity
* Method: getStringFromNative
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL getStringFromNative(JNIEnv *env, jclass obj) {
return env->NewStringUTF("Hello From JNI!");
}
static JNINativeMethod gMethods[] = {
{"getStringFromNative", "()Ljava/lang/String;", (void *) getStringFromNative},
};
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return result;
}
if (registerNativeMethods(env, gClassName, gMethods, sizeof(gMethods) / sizeof(gMethods)) == JNI_FALSE) {
return -1;
}
return JNI_VERSION_1_6;
}
so加载起来的时候,会先调用`JNI_OnLoad()`函数,而我们就在这个函数里面进行注册native函数
先通过vm变量获取env
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return result;
}
通过定义一个`registerNativeMethods()`函数调用`env->RegisterNatives()`
if (registerNativeMethods(env, gClassName, gMethods, sizeof(gMethods) / sizeof(gMethods)) == JNI_FALSE) {
return -1;
}
先`FindClass()`,获取到clazz之后进行注册,`gMethods`是待注册的方法表,`numMethods`是要注册函数的数量
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
要注册函数所在类的名字
static const char *gClassName = "com/wnagzihxain/myapplication/MainActivity";
方法表,每组三个参数,第一个是Java层的函数名,第二个是方法签名,第三个是native实现函数的指针
static JNINativeMethod gMethods[] = {
{"getStringFromNative", "()Ljava/lang/String;", (void *) getStringFromNative},
};
编译签名生成APK,解压出so文件,载入IDA
可以看到导出函数名和刚才明显不一样了
同时还有一个`JNI_OnLoad()`
双击过去,并按空格切换到逻辑调用图
可以把偏移地址显示出来
```
Options -> General
```
然后看程序调用图,发现都是2字节指令,说明这里还是使用Thumb指令集
程序入口处代码
导出函数和函数名
.text:00000D80 EXPORT JNI_OnLoad
.text:00000D80 JNI_OnLoad
这个是临时变量,存储在栈中,是一个相对的偏移值,通过使用和寄存器进行加减,定位该变量在栈中的位置,然后再操作,比如在x86汇编中``这种写法是很常见的
.text:00000D80 var_14= -0x14
将0赋值给R3:`R3 = 0x0`
.text:00000D80 00 23 MOVS R3, #0
将R0,R1,R2,R4,R5,LR寄存器的值压栈保存
.text:00000D82 37 B5 PUSH {R0-R2,R4,R5,LR}
将R3存储到栈``,`var_14`的值是`-0x14`,也就是把R3存储到栈顶指针加4的地址,这里的含义可以猜测是初始化变量`var_14`,也就是:`var_14 = 0x0`
.text:00000D84 01 93 STR R3,
R0是`JNI_OnLoad()`函数第一个参数,类型是`JavaVM *`,这里是获取`(*vm)`:`R3 = (*vm)`
.text:00000D86 03 68 LDR R3,
取临时变量`var_14`变量在栈中的地址:`R1 = SP + 0x18 + var_14`
.text:00000D88 01 A9 ADD R1, SP, #0x18+var_14
这里LDR是伪指令,表示把`0x10006`写进R2:`R2 = 0x10006`
.text:00000D8A 0F 4A LDR R2, =0x10006
将`R3 + 0x18`地址上的值赋值给R3:`R3 = `,也就是`(*vm)->GetEnv()`
.text:00000D8C 9B 69 LDR R3,
调用R3并根据跳转过去的地址判断是否切换指令集,对应的代码:`(*vm)->GetEnv(vm, (void **) &env,JNI_VERSION_1_6)`,R1表示`(&env)`,也就是说执行完后,栈中`SP + 0x18 + var_14`这个位置就是`(&env)`,这个位置上存储的是`JNIEnv *`类型的env变量
.text:00000D8E 98 47 BLX R3
调用`(*vm)->GetEnv()`后返回值存储在R0中,判断R0是否为0决定跳转分支
.text:00000D90 00 28 CMP R0, #0
如果R0不为0说明执行出错,则跳转到`loc_DC0`
.text:00000D92 15 D1 BNE loc_DC0
也就是跳转到,这里是失败的分支
.text:00000DC0 loc_DC0
.text:00000DC0 01 20 MOVS R0, #1
.text:00000DC2 40 42 NEGS R0, R0
如果`(*vm)->GetEnv()`执行完返回0,则进入下面的分支
将栈中`SP + 0x18 + var_14`地址上的值取出来赋值给R4:`R4 = `,也就是`R4 = env`,这个env是`JNIEnv *`类型
.text:00000D94 01 9C LDR R4,
这个很熟悉了,定位`aComWnagzihxain`
.text:00000D96 0D 49 LDR R1, =(aComWnagzihxain - 0xD9E)
将R4地址上的值取出赋值给R3:`R3 = `,也就是`R3 = (*env)`
.text:00000D98 23 68 LDR R3,
PC此时指向`.text:00000D9E`,所以:`R1 = aComWnagzihxain`
.text:00000D9A 79 44 ADD R1, PC ; "com/wnagzihxain/myapplication/MainActiv"...
将R4的值赋值给R0:`R0 = R4`,也就是:`R0 = env`
.text:00000D9C 20 1C MOVS R0, R4
将`R3 + 0x18`地址的值赋值给R3:`R3 = `,这里是`(*env)->FindClass()`函数的指针
.text:00000D9E 9B 69 LDR R3,
调用R3,也就是:`(*env)->FindClass(env, "com/wnagzihxain/myapplication/MainActivity")`,执行完后,R0存储的就是返回的jclass对象
.text:00000DA0 98 47 BLX R3
R0减去0后赋值给R1:`R1 = R0 - 0`,此时R1为返回的jclass类型对象
.text:00000DA2 01 1E SUBS R1, R0, #0
等于0则跳转到失败分支
.text:00000DA4 0C D0 BEQ loc_DC0
如果不等于0则跳转到下面的分支
将立即数`0xD7`赋值给R3:`R3 = 0xD7`
.text:00000DA6 D7 23 MOVS R3, #0xD7
R4此时为env,也就是:`R2 = (*env)`
.text:00000DA8 22 68 LDR R2,
将R3的值左移2位:`R3 = R3 << 2`,也就是`R3 = R3 * 4 = 860`
.text:00000DAA 9B 00 LSLS R3, R3, #2
将R4的值赋值给R0,也就是:`R0 = env`
.text:00000DAC 20 1C MOVS R0, R4
取`R2 + R3`地址上的值赋值给R5:`R5 = `,这是`RegisterNatives()`函数
.text:00000DAE D5 58 LDR R5,
定位off_4004
.text:00000DB0 07 4A LDR R2, =(off_4004 - 0xDB8)
将1赋值给R3:`R3 = 0x1`,根据下面语句的逻辑判断,这是注册的函数数量
.text:00000DB2 01 23 MOVS R3, #1
此时R2的值为off_4004:`R2 = off_4004`,也就是要注册的函数表
.text:00000DB4 7A 44 ADD R2, PC ; off_4004
调用R5并根据跳转地址决定切换指令集,结合上面R0到R3的值,也就是:`(*env)->RegisterNatives(env, clazz, off_4004, 1)`
.text:00000DB6 A8 47 BLX R5
通过判断注册的结果决定跳转分支
.text:00000DB8 00 28 CMP R0, #0
如果返回值小于0则跳转到失败分支
.text:00000DBA 01 DB BLT loc_DC0
等于0跳转到下面的代码,这个是设置返回值`JNI_VERSION_1_6`
.text:00000DBC 02 48 LDR R0, =0x10006
.text:00000DBE 01 E0 B locret_DC4
最后的代码,恢复一开始存储的寄存器值
.text:00000DC4 locret_DC4
.text:00000DC4 3E BD POP {R1-R5,PC}
.text:00000DC4 ; End of function JNI_OnLoad
分析完汇编的流程后,我们使用IDA的F5功能分析一下伪代码
导入`JNI.h`头文件
`JNI_OnLoad()`第一个参数是`JavaVM*`,修复一下,然后改一下变量名
signed int __fastcall JNI_OnLoad(JavaVM *vm, int a2, int a3)
{
int v3; // r4@2
int v4; // r1@2
signed int result; // r0@4
int v6; // @1
int v7; // @1
v7 = a3;
v6 = 0;
if ( (*vm)->GetEnv(vm, (void **)&v6, 65542)
|| (v3 = v6,
(v4 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)v6 + 24))(
v6,
"com/wnagzihxain/myapplication/MainActivity")) == 0)
|| (*(int (__fastcall **)(int, int, char **, signed int))(*(_DWORD *)v3 + 860))(v3, v4, off_4004, 1) < 0 )
{
result = -1;
}
else
{
result = 65542;
}
return result;
}
可以看到已经识别出`GetEnv()`方法了,这个方法第二个参数是二级指针`(&env)`,因为我们在定义变量的时候使用的是`JNIEnv *env = NULL`,这时的env是一个一级指针,所以在源码以及伪代码中看到的都是`(void **) &v6`,修改一下类型和命名,同时`Force Call Type`
逻辑也是比较的清楚,先获取`(&env)`,然后获取`clazz`,接着注册native函数
signed int __fastcall JNI_OnLoad(JavaVM *vm, int a2, int a3)
{
JNIEnv *vEnv; // r4@2
jclass v4; // r1@2
signed int result; // r0@4
JNIEnv *env; // @1
int v7; // @1
v7 = a3;
env = 0;
if ( (*vm)->GetEnv(vm, (void **)&env, 65542)
|| (vEnv = env, (v4 = (*env)->FindClass(env, "com/wnagzihxain/myapplication/MainActivity")) == 0)
|| (*vEnv)->RegisterNatives(vEnv, v4, (const JNINativeMethod *)off_4004, 1) < 0 )
{
result = -1;
}
else
{
result = 65542;
}
return result;
}
这里的函数表`off_4004`我们可以跟过去看一下是什么形式的
在源码中有说过,待注册函数表三个参数为一组,第一个是在Java层的函数名,第二个是方法签名,第三个是在native层的地址,三个参数为一组这个小知识点很重要,当待注册函数比较多的时候就比较容易区分开
.data:00004004 off_4004 DCD aGetstringfromn ; DATA XREF: JNI_OnLoad+34o
.data:00004004 ; .text:off_DD0o
.data:00004004 ; "getStringFromNative"
.data:00004008 DCD aLjavaLangStrin ; "()Ljava/lang/String;"
.data:0000400C DCD _Z19getStringFromNativeP7_JNIEnvP7_jclass+1
那么`getStringFromNative()`函数我们在静态注册那部分已经分析过了,这里就不多讲
关于编译so过程中可能出现的问题以及注意事项,我推荐一篇文章:
(http://ph0b.com/android-abis-and-so-files/)
还有一些扩展的文章也可以看看
[与 .so 有关的一个长年大坑](https://zhuanlan.zhihu.com/p/21359984)
(http://www.jianshu.com/p/b758e36ae9b5)
使用C++进行NDK开发,记得在`cpp`里导入生成的`.h`头文件,因为C++和C的`JNIENV`接口不一样,需要`extern "C"`,否则运行起来会崩溃
0x03 反调试
这个CrackMe使用的反调试技术是`TracerPid`反调试,检测23946端口反调试,读取/proc/%d/wchan反调试,inotify机制反调试
3.1 TracerPid反调试
经常会在一些脱壳文章里面看到`TracerPid`,`ptrace`什么的,那这些都是什么意思呢?
我们来查看本程序的`/proc/{PID}/status`文件
C:\Users\wangz>adb shell
root@jflte:/ # ps |grep "wnagzihxain"
u0_a123 29139 281 960944 31468 ffffffff 4005a8e0 S com.wnagzihxain.myapplication
root@jflte:/ # cat /proc/29139/status
Name: n.myapplication /*进程的程序名*/
State:S (sleeping)
Tgid: 29139 /*线程组号*/
Pid: 29139 /*进程pid process id*/
PPid: 281 /*父进程的pid parent processid*/
TracerPid: 0 /*跟踪进程的pid*/
Uid: 10123 10123 10123 10123 /*uid euid suid fsuid*/
Gid: 10123 10123 10123 10123 /*gid egid sgid fsgid*/
FDSize: 256 /*文件描述符的最大个数,file->fds*/
Groups: 50123 /*启动该进程的用户所属的组的id*/
VmPeak: 960944 kB /*进程地址空间的大小*/
VmSize: 923620 kB /*进程虚拟地址空间的大小reserved_vm:进程在预留或特殊的内存间的物理页*/
VmLck: 0 kB /*进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘*/
VmPin: 0 kB
VmHWM: 31468 kB /*文件内存映射和匿名内存映射的大小*/
VmRSS: 31468 kB /*应用程序正在使用的物理内存的大小,就是用ps命令的参数rss的值 (rss)*/
VmData: 18428 kB /*程序数据段的大小(所占虚拟内存的大小),存放初始化了的数据*/
VmStk: 136 kB /*进程在用户态的栈的大小*/
VmExe: 20 kB /*程序所拥有的可执行虚拟内存的大小,代码段,不包括任务使用的库 */
VmLib: 61212 kB /*被映像到任务的虚拟内存空间的库的大小*/
VmPTE: 192 kB /*该进程的所有页表的大小*/
VmSwap: 13144 kB
Threads: 12 /*共享使用该信号描述符的任务的个数*/
SigQ: 0/14462 /*待处理信号的个数/目前最大可以处理的信号的个数*/
SigPnd: 0000000000000000 /*屏蔽位,存储了该线程的待处理信号*/
ShdPnd: 0000000000000000 /*屏蔽位,存储了该线程组的待处理信号*/
SigBlk: 0000000000001204 /*存放被阻塞的信号*/
SigIgn: 0000000000000000 /*存放被忽略的信号*/
SigCgt: 00000002000094f8 /*存放被俘获到的信号*/
CapInh: 0000000000000000 /*能被当前进程执行的程序的继承的能力*/
CapPrm: 0000000000000000 /*进程能够使用的能力,可以包含CapEff中没有的能力,这些能力是被进程自己临时放弃的*/
CapEff: 0000000000000000 /*是CapPrm的一个子集,进程放弃没有必要的能力有利于提高安全性*/
CapBnd: fffffff000000000
Cpus_allowed: f /*可以执行该进程的CPU掩码集*/
Cpus_allowed_list: 0-3
voluntary_ctxt_switches: 258 /*进程主动切换的次数*/
nonvoluntary_ctxt_switches: 201 /*进程被动切换的次数*/
由于其余字段对我们的CrackMe分析来说并不是很重要,有兴趣扩展的同学可以看这篇文章
Linux内核之旅:[四]](http://www.kerneltravel.net/?p=294)
那么我们来使用IDA attach这个程序
先设置`ro.debuggable`为1
root@jflte:/data # ./mprop ro.debuggable 1
运行`android_server`
C:\Users\wangz>adb shell
root@jflte:/ # cd data/local
root@jflte:/data/local # ./as
IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015
Listening on port #23946...
转发23946端口
C:\Users\wangz>adb forward tcp:23946 tcp:23946
开DDMS或者Android Device Monitor
选中我们要调试的程序,一定要点击选中
使用IDA attach
观察`TracerPid`字段,发现变成了8319,`State`字段也变成了`tracing stop`
root@jflte:/ # cat /proc/29139/status
Name: n.myapplication
State:t (tracing stop)
Tgid: 29139
Pid: 29139
PPid: 281
TracerPid: 8319
Uid: 10123 10123 10123 10123
Gid: 10123 10123 10123 10123
FDSize: 256
Groups: 50123
VmPeak: 961076 kB
VmSize: 923620 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 32216 kB
VmRSS: 32216 kB
VmData: 18428 kB
VmStk: 136 kB
VmExe: 20 kB
VmLib: 61212 kB
VmPTE: 192 kB
VmSwap: 13132 kB
Threads: 12
SigQ: 0/14462
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000001204
SigIgn: 0000000000000000
SigCgt: 00000002000094f8
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: fffffff000000000
Cpus_allowed: f
Cpus_allowed_list: 0-3
voluntary_ctxt_switches: 577
nonvoluntary_ctxt_switches: 315
那么8319是什么?
使用ps命令查看
root@jflte:/ # ps |grep 8319
root 831982771270010968 ffffffff b6f324c4 S ./as
原来是我们的android_server程序,那么看到这里,大家应该有一些理解了,当我们使用IDA attach程序的时候,在`/proc/{PID}/status`文件的`TracerPid`字段会写入调试程序的PID
也就是说使用`TracerPid`反调试的原理就是检测这个字段是否为0,为0说明没有被调试,不为0说明正在被调试,检测调试器直接退出就可以达到反调试的效果
接下来我们来实现具体的代码
创建一个`AntiDebug`文件夹,创建一个`antidebug.cpp`,一个`antidebug.h`
头文件可以在创建`C++ Source File`的时候选中`Create an associated header`自动创建
先在`antidebug.h`里引入各种头文件
//
// Created by wnagzihxain on 2016/12/25 0025.
//
#ifndef MYAPPLICATION_ANTIDEBUG_H
#define MYAPPLICATION_ANTIDEBUG_H
#include <stdio.h>
#include <sys/ptrace.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <android/log.h>
#include <sys/syscall.h>
#include <sys/inotify.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <sys/queue.h>
#include <sys/select.h>
#endif //MYAPPLICATION_ANTIDEBUG_H
在`antidebug.cpp`实现一下反调试函数
//
// Created by wnagzihxain on 2016/12/25 0025.
//
#include "antidebug.h"
#define NULL 0
#define CHECK_TIME 10
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "totoc", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "totoc", __VA_ARGS__)
pthread_t id_anti_debug = NULL;
void readStatus() {
FILE *fd;
char filename;
char line;
pid_t pid = syscall(__NR_getpid);
LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);// 读取proc/pid/status中的TracerPid
while (1) {
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line);
LOGI("########## status = %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("########## FBI WARNING ##########");
LOGI("######### FIND DEBUGGER #########");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(CHECK_TIME);
}
}
void checkAnti() {
LOGI("Call readStatus...");
readStatus();
}
void anti_debug() {
LOGI("Call anti_debug...");
if (pthread_create(&id_anti_debug, NULL, (void *(*)(void *)) &checkAnti, NULL) != 0) {
LOGE("Failed to create a debug checking thread!");
exit(-1);
};
pthread_detach(id_anti_debug);
}
首先是`anti_debug()`函数,用于创建线程执行`chackAnti()`函数
`chackAnti()`函数里面调用`readStatus()`,`readStatus()`是真正实现读取`TracerPid`字段实现反调试的函数,那么这里为什么要多写一个函数用于调用呢?
这里纯属个人瞎猜:程序不仅仅可以通过检测`/proc/{PID}/status`文件的`TracerPid`字段是否为0来判断是否被调试,还有其它方法,比如检测23946端口,这个端口是`android_server`在占用,还可以各种方法,每一个方法要是都写在一个`readStatus()`里那就太混乱了,所以我们可以将每个反调试方法实现在各自单独的函数里,然后在`checkAnti()`里面集中调用
那么仔细读一下`anti_debug()`函数,是比较容易理解的,在子线程中循环检测,时间间隔是10秒,发现`TracerPid`字段的值不为0就发送一个`SIGKILL`信号,这个信号简单粗暴,其它信号进程都可以忽略掉,唯独这个信号不行,无条件终止指定进程,意思就是说:**你个不要脸的都要调试我了,我就自杀!!!!!!**
我们attach一下看看效果
- android_sever跑起来监听23946端口
- mprop设置ro.debuggable变量为1
- 转发23946端口
12-26 17:05:15.539 32038-32038/com.wnagzihxain.myapplication I/totoc: Call anti_debug...
12-26 17:05:15.549 32038-32054/com.wnagzihxain.myapplication I/totoc: Call readStatus...
12-26 17:05:15.549 32038-32054/com.wnagzihxain.myapplication I/totoc: PID : 32038
12-26 17:05:15.549 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:05:15.609 32038-32038/com.wnagzihxain.myapplication I/totoc: getString : Hello From JNI
12-26 17:05:25.548 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:05:35.548 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:05:45.548 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:05:55.548 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:06:05.547 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
12-26 17:06:15.547 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 0, TracerPid: 0
从LogCat中可以看出来,确实是在不断地检测
我们来使用IDA attach
attch成功后会断在这里
点击左上角的三角运行起来
可以看到程序退出,并且Android Device Monitor显示程序的状态是`DEAD`,LogCat里输出检测到调试器的信息
12-26 17:08:24.383 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## status = 4036, TracerPid: 4036
12-26 17:08:24.383 32038-32054/com.wnagzihxain.myapplication I/totoc: ########## FBI WARNING ##########
12-26 17:08:24.383 32038-32054/com.wnagzihxain.myapplication I/totoc: ######### FIND DEBUGGER #########
IDA也是一个`不太好`的状态,这个界面可以多留意一下,以后经常会遇到的
基本效果虽然是达到了,但是我们发现,在IDA attach上程序之后,会断下来,并不会立刻断开,当我们运行起来之后,才会断开,所以我们可以改进一下,fork一个子进程出来,当父进程被attach之后,由子进程来检测父进程的`/proc/{PID}/status`文件,就可以实现IDA attach程序的时候立刻kill掉自己
改一下代码
void readStatus() {
FILE *fd;
char filename;
char line;
pid_t pid = syscall(__NR_getpid);
LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);// 读取proc/pid/status中的TracerPid
if (fork() == 0) {
while (1) {
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line);
LOGI("########## status = %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("########## FBI WARNING ##########");
LOGI("######### FIND DEBUGGER #########");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(CHECK_TIME);
}
} else {
LOGE("fork error");
}
}
`fork()`函数的作用是生成和父进程一模一样的子进程,它会和父进程一起执行一样的代码
当执行`fork()`函数的时候,会有三种返回值,一个是大于0的数,这个是返回给父进程的,这个数表示子进程的PID,一个是等于0,这个是返回给子进程的,那么在正常情况下是这样的,在不正常的情况下返回-1,表示`fork()`函数调用失败
既然正常情况下有两个返回值,那么我们就可以使用这两个不同的返回值来进行区分当前是父进程在执行还是子进程在执行
这里要注意一下源码的实现,我们使用的是返回值为0的情况,也就是说只实现了子进程,执行的时候,检测的是父进程的`/proc/{PID}/status`文件
运行起来,使用`ps`命令,可以看到有两个进程,10965是父进程
root@jflte:/ # ps |grep "wnagzihxain"
u0_a124 10965 281 952676 31684 ffffffff 4005a8e0 S com.wnagzihxain.myapplication
u0_a124 10992 10965 909256 11836 c00a30b4 4005a028 S com.wnagzihxain.myapplication
LogCat的输出发现了`fork error`,这里并不是真正的fork失败,而是这是父进程,fork后返回值为子进程的PID,而我们LogCat输出端是父进程的日志,所以看到的是`fork error`,在子进程里,确实是在不断的检测父进程的`/proc/{PID}/status`文件
12-26 17:47:56.037 10965-10965/com.wnagzihxain.myapplication I/totoc: Call anti_debug...
12-26 17:47:56.037 10965-10990/com.wnagzihxain.myapplication I/totoc: Call readStatus...
12-26 17:47:56.037 10965-10990/com.wnagzihxain.myapplication I/totoc: PID : 10965
12-26 17:47:56.047 10965-10990/com.wnagzihxain.myapplication E/totoc: fork error
12-26 17:47:56.097 10965-10965/com.wnagzihxain.myapplication I/totoc: getString : Hello From JNI
使用IDA attach父进程,选择PID为10965那个,根据LogCat的输出确定父进程
卡了一下,IDA挂了
再使用ps命令,发现两个进程都挂了
root@jflte:/ # ps |grep "wnagzihxain"
1|root@jflte:/ #
那这个效果是不错的
我们现在来attch子进程
依旧是先ps命令查看进程情况,21330是父进程,21353是子进程,千万记得,是通过LogCat判断哪个是父进程!!!!!!
root@jflte:/ # ps |grep "wnagzihxain"
u0_a124 21330 281 944556 31536 ffffffff 4005a8e0 S com.wnagzihxain.myapplication
u0_a124 21353 21330 909256 12008 c00a30b4 4005a028 S com.wnagzihxain.myapplication
我们attach子进程
发生了什么,竟然没有挂掉??????
是我们的代码出现错误了吗??????
从调试情况来看,是的,逻辑写的不太好
难道是繁华的代码有问题??????
带着疑问,我们尝试运行起来,也没有挂掉
那么看来是代码考虑的情况不完全了
我们再来仔细的思考一下代码是不是哪里有问题
void readStatus() {
FILE *fd;
char filename;
char line;
pid_t pid = syscall(__NR_getpid);
LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);// 读取/proc/pid/status中的TracerPid
if (fork() == 0) {
while (1) {
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line);
LOGI("########## status = %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("########## FBI WARNING ##########");
LOGI("######### FIND DEBUGGER #########");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(CHECK_TIME);
}
} else {
LOGE("fork error");
}
}
蓦然间,我们发现代码里只针对父进程做了反调试保护,而子进程却没有任何保护
我们在子进程中读取的是父进程的`/proc/{PID}/status`文件,kill的也是父进程
fd = fopen(filename, "r");
......
kill(pid, SIGKILL);
难怪我们刚才没有`FBI WARNING`
搞清楚这些后,来修改一下代码
使用`PTRACE_TRACEME`
void readStatus() {
FILE *fd;
char filename;
char line;
pid_t pid = syscall(__NR_getpid);
LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);//读取/proc/pid/status中的TracerPid
if (fork() == 0) {
int pt = ptrace(PTRACE_TRACEME, 0, 0, 0); //子进程反调试
if (pt == -1)
exit(0);
while (1) {
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line);
LOGI("########## status = %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
LOGI("########## FBI WARNING ##########");
LOGI("######### FIND DEBUGGER #########");
kill(pid, SIGKILL);
return;
}
break;
}
}
sleep(CHECK_TIME);
}
} else {
LOGE("fork error");
}
}
我们在子进程中使用ptrace,将请求类型设为`PTRACE_TRACEME`,表示让父进程跟踪自己,而进程在同一时间,只能被一个调试器调试或者跟踪,所以这里就是一个父进程,一个子进程,子进程通过读取父进程的`/proc/{PID}/status`文件保护父进程不被调试,同时让父进程跟踪自己,保护自己不被调试,如果ptrace失败,说明有调试器已经在调试自己,直接退出
运行起来,来看一下是不是和我们预期的一样
root@jflte:/ # ps |grep "wnagzihxain"
u0_a124 22633 281 943504 31644 ffffffff 4005a8e0 S com.wnagzihxain.myapplication
u0_a124 22653 22633 909256 11872 c00a30b4 4005a028 S com.wnagzihxain.myapplication
root@jflte:/ # cat /proc/22633/status |grep "TracerPid"
TracerPid: 0
root@jflte:/ # cat /proc/22653/status |grep "TracerPid"
TracerPid: 22633
从结果来看,子进程确实是被父进程跟踪了
直观的效果我们使用IDA attach
使用IDA进行逆向分析,编译的时候记得把LogCat给注释掉
左边有几个我们很眼熟的函数
进入`JNI_OnLoad()`函数
可以发现跟上一个版本的`JNI_OnLoad()`相比这里只是多了一个BL调用
跟进这个调用,也就是我们的反调试函数
分析一下代码
导出函数和函数名
.text:0000126C ; _DWORD anti_debug(void)
.text:0000126C EXPORT _Z10anti_debugv
.text:0000126C _Z10anti_debugv
将0赋值给R1:`R1 = 0`
.text:0000126C MOVS R1, #0 ; attr
将R4和LR寄存器的值压栈
.text:0000126E PUSH {R4,LR}
重定位操作:`R4 = id_anti_debug_ptr - 0x127A`,这个地方有个小技巧,IDA识别出来的字符串有时候是用指针来表示的,如果表示指针的变量后面还有一个`_ptr`,表示的是这是指针的指针,或者说指针的地址,比如`*string = "Goodmorning"`,`string`就表示它的指针或者首地址,那么`string_ptr`表示的就是指针的地址,也就是`string = *string_ptr`
.text:00001270 LDR R4, =(id_anti_debug_ptr - 0x127A)
重定位操作:`R2 = _Z9checkAntiv_ptr - 0x127A`,这个也是表示指针的指针
.text:00001272 LDR R2, =(_Z9checkAntiv_ptr - 0x127E)
将R1的值赋值给R3:`R3 = R1`
.text:00001274 MOVS R3, R1 ; arg
此时PC为`0x127A`:`R4 = id_anti_debug_ptr`
.text:00001276 ADD R4, PC ; id_anti_debug_ptr
取出R4地址上的值赋值给R4:`R4 = `,取出这个指针的地址上的值,这下R4真的存储的是`id_anti_debug`的地址或者说它的指针
.text:00001278 LDR R4, ; id_anti_debug
此时PC为`0x127E`:`R2 = _Z9checkAntiv_ptr`
.text:0000127A ADD R2, PC ; _Z9checkAntiv_ptr
取出R2地址上的值赋值给R2:`R2 = `,R2此时存储的是`checkAnti(void)`方法的指针
.text:0000127C LDR R2, ; checkAnti(void) ; start_routine
将R4的值赋值给R0:`R0 = R4`
.text:0000127E MOVS R0, R4 ; newthread
调用`j_j_pthread_create()`,此时R0为线程标识符的指针,R1为0,R2为`checkAnti(void)`函数指针,R3为0
.text:00001280 BL j_j_pthread_create
对比返回值是否为0
.text:00001284 CMP R0, #0
返回值为0表示线程创建成功,当线程结束跳转到正常结束分支
.text:00001286 BEQ loc_1290
正常结束分支
.text:00001290 loc_1290 ; th
.text:00001290 LDR R0,
.text:00001292 BL j_j_pthread_detach
.text:00001296 POP {R4,PC}
.text:00001296 ; End of function anti_debug(void)
当返回值不为0表示线程创建失败,则退出
.text:00001288 MOVS R0, #1
.text:0000128A NEGS R0, R0
.text:0000128C BL j_j_exit
进入创建线程运行的函数`checkAnti(void)`
调用`readStatus(void)`函数
00001266 BL _Z10readStatusv ; readStatus(void)
继续跟入
这里是一个非常常见的IDA识别错误,从整个逻辑调用来看,这个函数有多个入口,但是从我们的经验来说,函数入口应该有变量的或者参数,那么明显,左下角红色框框里面才是入口,我们来修正一下
这个修正我在Ericky师傅那个CrackMe的WriteUp里面有提到过,那个也是需要先修正
点击这个函数的BX指令或者说是选中
单击`Remove Function Tail`
然后这个代码块就悬空了
选中红框内的BL指令,然后点击`Force BL call`
那么整个函数的逻辑调用就修复了
继续继续看代码
最开始定义五个变量,其实从变量的命名之间我们是可以推敲出一点东西的,比如`0x11C`和`0x9C`之间的间隔是128,说明这里有一个变量占用128byte,那它可能是一个数组,同样,`0x9C`和`0x1C`之间间隔128byte,也可能是一个数组
.text:000011A4 var_124= -0x124
.text:000011A4 var_120= -0x120
.text:000011A4 var_11C= -0x11C
.text:000011A4 s= -0x9C
.text:000011A4 var_1C= -0x1C
将R4,R5,R6,R7,LR寄存器的值压栈
.text:000011A4 PUSH {R4-R7,LR}
重定位`__stack_chk_guard_ptr`,在前面我们提到,后面跟着ptr的是这个变量指针的指针,这里是:`R4 = __stack_chk_guard_ptr - 0x11B0`
.text:000011A6 LDR R4, =(__stack_chk_guard_ptr - 0x11B0)
抬高栈顶,开辟`0x114`byte大小的空间
.text:000011A8 SUB SP, SP, #0x114
`0x14`是20,将20赋值给R0:`R0 = 0x14`
.text:000011AA MOVS R0, #0x14 ; sysno
这个是一个保护机制,可以不用管
.text:000011AC ADD R4, PC ; __stack_chk_guard_ptr
.text:000011AE LDR R4, ; __stack_chk_guard
将SP加上`#0x128+var_11C`的值赋值给R6,其实这里是不需要计算出来的,因为这里的`var_11C`虽然代表的是偏移,但是在程序中使用的时候,可以看到跟这个变量有关的计算都是指向同一个内存地址,如果是有过一些分析经验的同学一定是可以理解我说的这句话的,如果没有经验的同学尝试着多分析几个小程序,再回头来看看我这句话,肯定会有所理解的
.text:000011B0 ADD R6, SP, #0x128+var_11C
取R4地址上的值给R3,往上两行可以看到这是`__stack_chk_guard`的指针
.text:000011B2 LDR R3,
将R3也就是`*__stack_chk_guard`存储到``,其实`var_1C`就可以重命名为`__stack_chk_guard`
.text:000011B4 STR R3,
调用`j_j_syscall()`函数,这一句的源码对应:`pid_t pid = syscall(__NR_getpid)`
.text:000011B6 BL j_j_syscall
上一句的返回值是当前进程的PID,存储在R0中,这里将R0的值赋值给R5:`R5 = PID`
.text:000011BA MOVS R5, R0
重定位`aProcDStatus`,这里是:`R1 = aProcDStatus - 0x11C4`
.text:000011BC LDR R1, =(aProcDStatus - 0x11C4)
将R5赋值给R2,R5存储的是PID,也就是:`R2 = PID`
.text:000011BE MOVS R2, R5
重定位过后,R1指向`aProcDStatus`:`R1 = "/proc/%d/status"`
.text:000011C0 ADD R1, PC ; "/proc/%d/status"
将R6赋值给R0,往上翻,找到R6,R6存储的是`var_11C`这个变量,这个变量在前面我们简单提到过,占用128byte的空间
.text:000011C2 MOVS R0, R6 ; s
调用`j_j_sprintf()`函数,还原一下:`sprintf(var_11C, "/proc/%d/status", PID)`,这样一看就清楚多了,`var_11C`表示的就是该进程`status`文件的路径
.text:000011C4 BL j_j_sprintf
调用`fork()`函数,没有参数
.text:000011C8 BL j_j_fork
将R4存储到``,R4是`__stack_chk_guard`的指针的指针
.text:000011CC STR R4,
将R0减0赋值给R7:`R7 = R0`
.text:000011CE SUBS R7, R0, #0
判断返回结果跳转
.text:000011D0 BNE loc_1240
不为0就跳到最后结束线程,这一段是安全性的校验
.text:00001240
.text:00001240 loc_1240
.text:00001240 01 9B LDR R3,
.text:00001242 43 9A LDR R2,
.text:00001244 1B 68 LDR R3,
.text:00001246 9A 42 CMP R2, R3
.text:00001248 01 D0 BEQ loc_124E
如果`fork()`的返回值不为0,进入反调试逻辑
将R7赋值给R3,R7的直观含义是`fork()`返回值减去0,那么这里就是:`R3 = 0`
.text:000011D2 3B 1C MOVS R3, R7
将R7赋值给R1:`R1 = 0`
.text:000011D4 39 1C MOVS R1, R7
将R7赋值给R2:`R2 = 0`
.text:000011D6 3A 1C MOVS R2, R7
调用`j_j_ptrace()`函数,这里四个参数`R0-R3`全是0,还原一下:`ptrace(0, 0, 0, 0)`,对应源码:`ptrace(PTRACE_TRACEME, 0, 0, 0)`
.text:000011D8 01 F0 42 FB BL j_j_ptrace
返回值给R0,这里R0加1赋值给R3,这里是有点不一样的,`j_j_ptrace()`调用失败会返回`-1`,就是说,R0如果为`-1`,加上1结果就是0,再将这个结果赋值给R3,这个就影响后面的逻辑跳转了
.text:000011DC 43 1C ADDS R3, R0, #1
这个跳转就看`j_j_ptrace()`方法返回值了
.text:000011DE 15 D1 BNE loc_120C
返回值为`-1`时的分支,直接就退出程序了
.text:000011E0 38 1C MOVS R0, R7 ; status
.text:000011E2 01 F0 45 FB BL j_j_exit
当返回值不为`-1`
.text:0000120C
.text:0000120C loc_120C
重定位`aR`,那么:`R1 = aR - 0x1214`
.text:0000120C 14 49 LDR R1, =(aR - 0x1214)
R6的值往上翻找一找,对应的是`"/proc/%d/status"`,虽然这里有字符串提醒
.text:0000120E 30 1C MOVS R0, R6 ; filename
重定位完成,此时:`R1 = "r"`
.text:00001210 79 44 ADD R1, PC ; "r"
调用`j_j_fopen()`函数,还原一下:`fopen("/proc/PID/status", "r")`,需要注意的是,此时PID的值已经获取到了,并不是一个未知的变量或者说是参数
.text:00001212 01 F0 4D FB BL j_j_fopen
返回的结果为R0,赋值给R4,它返回的结果是一个`FILE *`类型的参数
.text:00001216 04 1C MOVS R4, R0
无条件跳转到`loc_11F6`
.text:00001218 ED E7 B loc_11F6
跟下来
.text:000011F6
.text:000011F6 loc_11F6
将`SP + #0x128 + s`相加赋值给R7,s变量占用的空间也是128byte,说明可能也是数组
.text:000011F6 23 AF ADD R7, SP, #0x128+s
将R7赋值给R0:`R0 = R7`,R7是s变量的指针
.text:000011F8 38 1C MOVS R0, R7 ; s
将`0x80`赋值给R1,`0x80`刚好是128:`R1 = 0x80`
.text:000011FA 80 21 MOVS R1, #0x80 ; n
将R4赋值给R2,此时R4的值是一个`FILE *`类型的参数
.text:000011FC 22 1C MOVS R2, R4 ; stream
调用`j_j_fgets()`函数,还原一下:`fgets((char *)s, 128, (FILE *)R4)`,这个地方有个换行符的问题需要注意,当两行之间是一个空行,fgets读取并不会退出,因为还没有结束,而且读取空行的长度不是0,是1,因为换行符占一个字节
.text:000011FE 01 F0 47 FB BL j_j_fgets
判断是否读取到了数据
.text:00001202 00 28 CMP R0, #0
根据是否还有数据进行跳转判断
.text:00001204 EF D1 BNE loc_11E6
没数据了跳到`loc_1240`,也就是结束的分支
如果还有数据,不为空,跳到`loc_11E6`
.text:000011E6
.text:000011E6 loc_11E6
重定位`aTracerpid`:`R1 = aTracerpid - 0x11EE`
.text:000011E6 1D 49 LDR R1, =(aTracerpid - 0x11EE)
将R7的值赋值给R0,R7的值是s变量的指针,或者说是s数组的首地址:`R0 = (char *)s`
.text:000011E8 38 1C MOVS R0, R7 ; s1
重定位完成:`R1 = "TracerPid"`
.text:000011EA 79 44 ADD R1, PC ; "TracerPid"
将9赋值给R2:`R2 = 9`
.text:000011EC 09 22 MOVS R2, #9 ; n
调用`j_j_jstrcmp()`方法,还原一下:`strcmp(s, "TracerPid", 9)`,这个意思就是在比较读取到的数据前9个字节和`"TracerPid"`这个字符串比较
.text:000011EE 01 F0 47 FB BL j_j_strncmp
判断是否相等,也就是说是否读取到了`"TracerPid"`这个字段
.text:000011F2 00 28 CMP R0, #0
根据结果进行跳转
.text:000011F4 11 D0 BEQ loc_121A
如果不一样,跳回`loc_11F6`继续循环读取
如果一样,说明读取到了`"TracerPid"`这个字符串,那么跳转到`loc_121A`
.text:0000121A
.text:0000121A loc_121A
将`0x8E`赋值给R0:`R0 = 0x8E`
.text:0000121A 8E 20 MOVS R0, #0x8E
`var_120`在定义中是一个4字节的变量,那么R3此时指向的就是这个变量
.text:0000121C 02 AB ADD R3, SP, #0x128+var_120
将R0加上R3赋值给R0:`R0 = R0 + R3`
.text:0000121E C0 18 ADDS R0, R0, R3 ; nptr
调用`j_j_atoi()`方法,这个方法用于将字符串转换为整型,还原一下:`atoi(R0)`,那这句在这里什么意思呢?R0指向的是什么数据呢?`var_120`前面也没有赋值,我们再回到前面,R7是s数组的首地址,我们每读取一行都会存在s里,s变量与`var_120`隔着132byte,而且数据是往下填充,那么这里的`0x8E`的作用是影响`var_120`,这里千万要理解,并不是影响`SP, #0x128+var_120`这一整个,而是影响`var_***`,因为我们在定位栈中变量就是靠的这个偏移,所以这里应该是`0x8E + var_120 = 0x8E - 0x120 = -0x92`,如果按照IDA的命名,这个差值应该是`var_92`,完整的表达方式应该是`SP, #0x128+var_92`,那现在就清楚了,`atoi()`函数的参数是以``为起始地址的字符串,那么这个地址的数据又要和s关联起来分析,s在栈中的位置是`SP, #0x128+(-0x9C)`,`0x9C`和`0x92`差值是10,也就是说,`atoi()`方法的参数是读取的字符下标为10开始的字符串,这里已经弄清楚要读取的是前9个字节为`"TracerPid"`的字符串,但是`"TracerPid"`只有9个字节,从0开始数下标的话只到8,为什么会从10开始呢?这里是因为`"TracerPid"`后面还有个":",所以这样加起来,`"TracerPid:"`就有10个字符,下标到第9,下标为10的字符开始刚好是`"TracerPid"`对应的值,我们调用`atoi()`就可以获取到需要的数据
.text:00001220 01 F0 4E FB BL j_j_atoi
将返回值R0赋值给R7:`R7 = R0`,R0此时是`TracerPid`的值
.text:00001224 07 1C MOVS R7, R0
将R4赋值给R0,R4是`FILE *`类型的变量
.text:00001226 20 1C MOVS R0, R4 ; stream
调用`j_j_fclose()`关闭文件流指针,也就是上面说的`FILE *`变量
.text:00001228 01 F0 52 FB BL j_j_fclose
将6赋值给R0:`R0 = 6`
.text:0000122C 06 20 MOVS R0, #6 ; sysno
将R4赋值给R1:`R1 = R4`
.text:0000122E 21 1C MOVS R1, R4
系统调用,源码对应:`syscall(__NR_close, fd);`
.text:00001230 01 F0 FE FA BL j_j_syscall
判断R7是否为0,R7是`TracerPid`的值
.text:00001234 00 2F CMP R7, #0
根据结果跳转
.text:00001236 E6 D0 BEQ loc_1206
不为0说明被调试,直接kill自己,源码对应:`kill(pid, SIGKILL)`
.text:00001238 28 1C MOVS R0, R5 ; pid
.text:0000123A 09 21 MOVS R1, #9 ; sig
.text:0000123C 01 F0 50 FB BL j_j_kill
如果为0,睡眠10秒
.text:00001206
.text:00001206 loc_1206 ; seconds
.text:00001206 0A 20 MOVS R0, #0xA
.text:00001208 01 F0 4A FB BL j_j_sleep
然后就继续回去读取文件,循环走下去
分析完了汇编,看一下F5之后的伪代码,有几处是要特别注意的,比如`syscall(__NR_getpid)和syscall(20)`,`ptrace(TRACE_TRACEME, 0, 0, 0)和ptrace(0, 0, 0, 0)`等这些反编译后对应的关系
__pid_t readStatus(void)
{
__int32 v0; // r5@1
__pid_t result; // r0@1
int v2; // r7@1
FILE *v3; // r4@7
int v4; // r7@8
char v5; // @1
char s; // @4
__int16 v7; // @8
int v8; // @1
v8 = _stack_chk_guard;
v0 = j_j_syscall(20);
j_j_sprintf(&v5, "/proc/%d/status", v0);
result = j_j_fork();
v2 = result;
if ( !result )
{
if ( j_j_ptrace(0, 0, 0, 0) == -1 )
j_j_exit(v2);
LABEL_7:
v3 = j_j_fopen(&v5, "r");
do
{
if ( !j_j_fgets(&s, 128, v3) )
{
LABEL_6:
j_j_sleep(0xAu);
goto LABEL_7;
}
}
while ( j_j_strncmp(&s, "TracerPid", 9u) );
v4 = j_j_atoi((const char *)&v7);
j_j_fclose(v3);
j_j_syscall(6, v3);
if ( !v4 )
goto LABEL_6;
result = j_j_kill(v0, 9);
}
if ( v8 != _stack_chk_guard )
j_j___stack_chk_fail(result);
return result;
}
关于这里的`syscall()`系统调用,推荐一篇文章
up哥小号的ChinaUnix博客:(http://blog.chinaunix.net/uid-28362602-id-3424404.html)
3.2 23946端口检测反调试
当我们在使用IDA调试安卓应用的时候,需要先把`android_server`传到手机里,运行起来后会默认监听`23946`端口
在PC上开启端口转发,这时候我们才能使用IDA挂接上应用
那么我们就可以通过检测`/proc/net/tcp`文件里是否有`00000000:5D8A`,`0x5D8A`的十进制就是`23946`
root@jflte:/ # cat /proc/net/tcp
sllocal_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uidtimeout inode
0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4656 1 00000000 100 0 0 10 -1
运行`android_server`
root@jflte:/data/local # ./as
IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015
Listening on port #23946...
查看`/proc/net/tcp`文件,可以看到多了`00000000:5D8A`
root@jflte:/ # cat /proc/net/tcp
sllocal_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uidtimeout inode
0: 00000000:5D8A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 36501 1 00000000 100 0 0 10 -1
1: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4656 1 00000000 100 0 0 10 -1
我们关掉`android_server`,重新开启,监听`1995`端口,注意`-p`参数是指`port`,和后面的端口数字之间没有空格
root@jflte:/data/local # ./as -p1995
IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015
Listening on port #1995...
查看`/proc/net/tcp`文件,可以看到`00000000:5D8A`变成了`00000000:07CB`,`07CB`的十进制是`1995`
root@jflte:/ # cat /proc/net/tcp
sllocal_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uidtimeout inode
0: 00000000:07CB 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 42557 1 00000000 100 0 0 10 -1
1: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4656 1 00000000 100 0 0 10 -1
添加一个检测的函数,并在子进程中循环调用,节奏跟着检测`TracerPid`,10秒一次,把`TracerPid`反调试的kill命令注释掉
//
// Created by wnagzihxain on 2016/12/25 0025.
//
#include "antidebug.h"
#define NULL 0
#define CHECK_TIME 10
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "totoc", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "totoc", __VA_ARGS__)
pthread_t id_anti_debug = NULL;
void checkAndroidServer() {
char szLines = {0};
FILE *fp = fopen("/proc/net/tcp", "r");
if(fp != NULL) {
while (fgets(szLines, sizeof(szLines), fp)) {
if (strstr(szLines, "00000000:5D8A")) {
kill(getpid(), SIGKILL);
break;
}
}
fclose(fp);
}
LOGI("There is no android_server");
}
void readStatus() {
FILE *fd;
char filename;
char line;
pid_t pid = syscall(__NR_getpid);
//LOGI("PID : %d", pid);
sprintf(filename, "/proc/%d/status", pid);//读取/proc/pid/status中的TracerPid
if (fork() == 0) {
int pt = ptrace(PTRACE_TRACEME, 0, 0, 0); //子进程反调试
if (pt == -1)
exit(0);
while (1) {
checkAndroidServer();
fd = fopen(filename, "r");
while (fgets(line, 128, fd)) {
if (strncmp(line, "TracerPid", 9) == 0) {
int status = atoi(&line);
//LOGI("########## status = %d, %s", status, line);
fclose(fd);
syscall(__NR_close, fd);
if (status != 0) {
//LOGI("########## FBI WARNING ##########");
//LOGI("######### FIND DEBUGGER #########");
//kill(pid, SIGKILL);
//return;
}
break;
}
}
sleep(CHECK_TIME);
}
} else {
//LOGE("fork error");
}
}
void checkAnti() {
//LOGI("Call readStatus...");
readStatus();
}
void anti_debug() {
//LOGI("Call anti_debug...");
if (pthread_create(&id_anti_debug, NULL, (void *(*)(void *)) &checkAnti, NULL) != 0) {
//LOGE("Failed to create a debug checking thread!");
exit(-1);
};
pthread_detach(id_anti_debug);
}
运行起来,再次确定`/proc/net/tcp`文件
root@jflte:/ # cat /proc/net/tcp
sllocal_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uidtimeout inode
0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4656 1 00000000 100 0 0 10 -1
运行`android_server`
root@jflte:/data/local # ./as
IDA Android 32-bit remote debug server(ST) v1.19. Hex-Rays (c) 2004-2015
Listening on port #23946...
查看`/proc/net/tcp`文件,确定有`00000000:5D8A`字符串
root@jflte:/ # cat /proc/net/tcp
sllocal_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uidtimeout inode
0: 00000000:5D8A 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 71246 1 00000000 100 0 0 10 -1
1: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 4656 1 00000000 100 0 0 10 -1
等了10秒,发现什么反应都没有,啥情况?
使用过ps命令查看进程,发现只剩一个进程
root@jflte:/ # ps |grep "wnagzihxain"
u0_a124 11963 280 943496 32280 ffffffff 400688e0 S com.wnagzihxain.myapplication
看到这里,貌似发现了点什么,我们来梳理一下流程
首先我们运行程序,fork出两个进程,父进程并不参与反调试,子进程进入反调试分支,反调试分支有两个反调试点,一个是`23946`端口检测反调试,一个是`TracerPid`字段检测反调试,这里的`TracerPid`字段检测反调试我们是测试过可以成功运行的,那么问题很可能是出在`23946`端口检测这个反调试分支上
当我们运行`android_server`的时候,子进程正在反调试循环中,检测肯定是检测到了,问题出现了,我们kill掉的是子进程,因为`getpid()`获取的是本进程`PID`,那这里就很有意思了,我们相当于自己把反调试进程kill掉了
所以我们刚才使用ps命令只看到了一个进程,而那个进程现在看来是父进程,父进程是不参与反调试的,也就是说我们运行`android_server`后,承担反调试任务的子进程因为一个理论上恒成立的条件被kill掉,我们可以使用IDA attach上应用
为了验证,我们使用IDA attach应用
确实能成功attach,验证了我们上面的分析
注释掉所有的LogCat,恢复`TracerPid`反调试分支里kill命令,编译签名
我们在逻辑调用图中看到了多了一个BL调用分支
双击进入
遇到了我们在之前分析时的问题,怎么修复在上面已经详细的讲过了,这里留给大家自己去研究一下
修复完是这样的
我们在前面讲的变量相关的技巧,这里可以实践一下,s目测是一个数组,`0x414`减去`0x1C`为1016字节,奇怪,我们明明定义的是1024个字节,这里难道是编译器优化什么的导致空间减少了吗?带着这个疑问,我们继续分析
.text:00001264 s= -0x414
.text:00001264 var_1C= -0x1C
.text:00001264
将R4,R5,R6,LR三个寄存器的值压栈保存
.text:00001264 PUSH {R4-R6,LR}
将`0xFFFFFBF8`这个地址赋值给R4:`R4 = 0xFFFFFBF8`
.text:00001266 LDR R4, =0xFFFFFBF8
将0赋值给R1:`R1 = 0x0`
.text:00001268 MOVS R1, #0 ; c
将R4加上SP赋值给SP:`SP = SP + R4`
.text:0000126A ADD SP, R4
定位`__stack_chk_guard_ptr`,安全机制
.text:0000126C LDR R4, =(__stack_chk_guard_ptr - 0x1276)
定位`var_1C`变量
.text:0000126E ADD R2, SP, #0x418+var_1C
R2是`var_1C`变量在栈空间的地址,这个地址加8,相当于往下移两个4字节单位,也就是说给上面那个变量空出8字节空间,再结合我们刚才说的s只有1016个字节,加上这8个字节刚好是1024个字节,这里就解释清楚了
.text:00001270 ADDS R2, #8
安全机制,再多说一句,这个`_ptr`后缀是指`__stack_chk_guard`的指针
.text:00001272 ADD R4, PC ; __stack_chk_guard_ptr
安全机制
.text:00001274 LDR R4, ; __stack_chk_guard
将`SP + 0x418 + s`的值赋值给R6:`R6 = s`
.text:00001276 ADD R6, SP, #0x418+s
将R6的值赋值给R0:`R0 = R6 = s`
.text:00001278 MOVS R0, R6 ; s
R4此时的值为`__stack_chk_guard_ptr`的指针:`R3 = *__stack_chk_guard_ptr`
.text:0000127A LDR R3,
将R3也就是`*__stack_chk_guard_ptr`存储到R2存储的地址,也就是`SP + 0x418+var_1C + 8`,关键是`var_1C + 8`
.text:0000127C STR R3,
`0x400`的十进制是1024,赋值给R2:`R2 = 0x400`
.text:0000127E MOVS R2, #0x400 ; n
调用`memset()`函数,R0此时是s,R1是0,R2是`0x400`,也就是初始化s数组:`memset(s, 0, 0x400)`
.text:00001282 BL j_j_memset
重定位`aProcNetTcp`
.text:00001286 LDR R0, =(aProcNetTcp - 0x128E)
重定位`aR`
.text:00001288 LDR R1, =(aR - 0x1290)
重定位完成:`R0 = "/proc/net/tcp"`
.text:0000128A ADD R0, PC ; "/proc/net/tcp"
重定位完成:`R1 = "r"`
.text:0000128C ADD R1, PC ; "r"
调用`fopen()`函数,R0位`"/proc/net/tcp"`,R1为`"r"`,还原一下:`fopen("/proc/net/tcp", "r")`
.text:0000128E BL j_j_fopen
将返回值R0减去0存储在R5:`R5 = R0 - 0`
.text:00001292 SUBS R5, R0, #0
根据结果跳转
.text:00001294 BEQ loc_12C4
如果返回的fp指针为空,直接跳到最后,不做任何操作结束函数
.text:000012C4 loc_12C4
.text:000012C4 ADD R3, SP, #0x418+var_1C
.text:000012C6 ADDS R3, #8
.text:000012C8 LDR R2,
.text:000012CA LDR R3,
.text:000012CC CMP R2, R3
.text:000012CE BEQ loc_12D4
打开成功后会进入一个循环
将0x80赋值给R1,`0x80`十进制是128
.text:00001296 loc_1296
.text:00001296 MOVS R1, #0x80
R6此时是s的首地址,将其赋值给R0:`R0 = R6 = s`
.text:00001298 MOVS R0, R6 ; s
将R1二进制左移三位:`R1 = R1 * 2 * 2 * 2`,结果是:`R1 = 1024`
.text:0000129A LSLS R1, R1, #3
将R5赋值给R2,R5此时是`FILE *`类型的变量,类似`R2 = R5 = (FILE *)fp`
.text:0000129C MOVS R2, R5 ; stream
调用`fgets()`函数,还原一下:`fgets(s, 1024, fp)`
.text:0000129E BL j_j_fgets
读取结果是否为空,为空说明读取到最后
.text:000012A2 CMP R0, #0
读取到最后则跳出
.text:000012A4 BEQ loc_12BE
跳到`loc_12BE`,调用`fclose()`函数关闭`fp`
.text:000012BE loc_12BE ; stream
.text:000012BE MOVS R0, R5
.text:000012C0 BL j_j_fclose
然后正常退出本次反调试
如果读取到的数据不为空,则进入判断分支
定位`a000000005d8a`
.text:000012A6 LDR R1, =(a000000005d8a - 0x12AE)
将R6赋值给R0,此时R6为s的首地址,也就是读取到的数据存储的栈空间首地址
.text:000012A8 MOVS R0, R6 ; haystack
重定位完成:`R1 = "00000000:5D8A"`
.text:000012AA ADD R1, PC ; "00000000:5D8A"
调用`strstr()`函数,判断R1是否为R0的子字符串,也就是判断`"00000000:5D8A"`是否在读取的数据里
.text:000012AC BL j_j_strstr
对比返回结果
.text:000012B0 CMP R0, #0
根据结果跳转
.text:000012B2 BEQ loc_1296
如果判断出`"00000000:5D8A"`在读取的子字符串里,跳到kill分支
先调用`getpid()`获取本进程`PID`
.text:000012B4 BL j_j_getpid
调用`kill()`函数kill掉自己
.text:000012B8 MOVS R1, #9 ; sig
.text:000012BA BL j_j_kill
上面有提到过,这样只会kill掉子进程,相当于自己去掉了目前为止所有的反调试措施,非常奇怪的一个地方,这里可能就是作者说的后面修改的地方
不过从侧面来看,说明我们这种检测还是有效的,只要我们合理的修改一些地方,比如在检测`23946`端口的时候传一下参数,那么就可以把父进程kill掉了
3.3 读取/proc/%d/wchan反调试
当程序被调试的时候,我们读取这个文件的数据和未被调试时的数据是不一样的
跑起来,查看进程信息,选择PID为1321的那个
root@jflte:/ # ps |grep "wnagzihxain"
u0_a17 1321283 940868 30472 ffffffff 400cc8e0 S com.wnagzihxain.myapplication
u0_a17 13571321906420 11460 c00a30b4 400cc028 S com.wnagzihxain.myapplication
查看`/proc/1321/wchan`文件
root@jflte:/ # cat proc/1321/wchan
sys_epoll_waitroot@jflte:/ #
使用IDA attach PID为1321的进程
attach上之后,再查看`/proc/1321/wchan`文件
root@jflte:/ # cat proc/1321/wchan
ptrace_stoproot@jflte:/ #
根据这种情况,我们可以读取`/proc/1321/wchan`文件实现反调试
添加三个宏
#define WCHAN_ELSE 0;
#define WCHAN_RUNNING 1;
#define WCHAN_TRACING 2;
然后实现函数,记得在头文件添加定义,不然在`JNI_OnLoad()`是没法调用的
int getWchanStatus() {
char *wchaninfo = new char;
int result = WCHAN_ELSE;
char *cmd = new char;
pid_t pid = syscall(__NR_getpid);
sprintf(cmd, "cat /proc/%d/wchan", pid);
LOGI("cmd= %s", cmd);
if (cmd == NULL) {
return WCHAN_ELSE;
}
FILE *ptr;
if ((ptr = popen(cmd, "r")) != NULL) {
if (fgets(wchaninfo, 128, ptr) != NULL) {
LOGI("wchaninfo = %s", wchaninfo);
}
}
if (strncasecmp(wchaninfo, "sys_epoll\0", strlen("sys_epoll\0")) == 0) {
result = WCHAN_RUNNING;
}
else if (strncasecmp(wchaninfo, "ptrace_stop\0", strlen("ptrace_stop\0")) == 0) {
result = WCHAN_TRACING;
}
return result;
}
在`readStatus()`函数里添加调用
//LOGI("PID : %d", pid);
int ret = getWchanStatus();
if (2 == ret) {
kill(pid, SIGKILL);
}
生成APK,解压出so文件,IDA载入,会发现多了一个函数的调用,这个就是`getWchanStatus()`方法
.text:0000341A BL _Z14getWchanStatusv ; getWchanStatus(void)
.text:0000341E STR R5,
.text:00003420 CMP R0, #2
.text:00003422 BNE loc_342C
根据返回的结果跳转,如果返回2,跳转到kill分支`loc_342C`
.text:00003424 MOVS R0, R4 ; pid
.text:00003426 MOVS R1, #9 ; sig
.text:00003428 BL j_j_kill
进入`getWchanStatus()`方法,会发现识别有问题,`Force BL call`就可以修复了
寄存器数据压栈
_Z14getWchanStatusv
PUSH {R4-R6,LR}
将0x80赋值给R0,十进制是128:`R0 = 0x80`
MOVS R0, #0x80 ; unsigned int
定义`uint`类型的数组,`uint->char`:`char xxx`
BL _Znaj ; operator new[](uint)
R0此时是创建的数组首地址,赋值给R5,我们将其设为s1
MOVS R5, R0
`R0 = 0x80`
MOVS R0, #0x80 ; unsigned int
定义`uint`类型的数组,`uint->char`:`char xxx`
BL _Znaj ; operator new[](uint)
R0此时是创建的数组首地址,赋值给R4,我们将其设为s2
MOVS R4, R0
将`0x14`赋值给R0:`0x14`是`__NR_getpid`
MOVS R0, #0x14 ; sysno
调用获取PID:`syscall(__NR_getpid)`
BL j_j_syscall
将`aCatProcDWchan - 0x32E0`赋值给R1:`R1 = aCatProcDWchan - 0x32E0`
LDR R1, =(aCatProcDWchan - 0x32E0)
上面调用完`j_j_syscall`之后,R0为PID
MOVS R2, R0
重定位`aCatProcDWchan`,R1指向`"cat /proc/%d/wchan"`
ADD R1, PC ; "cat /proc/%d/wchan"
R4为创建的第二个字符数组首地址,赋值给R0:`R0 = s2`
MOVS R0, R4 ; s
将`aTotoc - 0x32EC`赋值给R6:`R3 = aTotoc - 0x32EC`
LDR R6, =(aTotoc - 0x32EC)
调用`sprintf()`函数:`sprintf(s2, "cat /proc/%d/wchan", PID)`
BL j_j_sprintf
将`aCmdS - 0x32F0`赋值给R2:`R2 = aCmdS - 0x32F0`
LDR R2, =(aCmdS - 0x32F0)
重定位完成后,R6指向`"totoc"`
ADD R6, PC ; "totoc"
将`"totoc"`的指针赋值给R1
MOVS R1, R6
重定位完成,R2指向`"cmd = %s"`
ADD R2, PC ; "cmd = %s"
将R4赋值给R3,R4为创建的s数组首地址:`R3 = s2`
MOVS R3, R4
将`0x4`赋值给R0:`R0 = 0x4`
MOVS R0, #4
调用log,此时s为`"cat /proc/{PID}/wchan"`:`LOGI("cmd = %s", cmd)`
BL j_j___android_log_print
将`aR - 0x32FE`赋值给R1:`R1 = aR - 0x32FE`
LDR R1, =(aR - 0x32FE)
将R4也就是s数组的首地址赋值给R0,R0指向`"cat /proc/{PID}/wchan"`
MOVS R0, R4 ; command
重定位完成,R1指向`"r"`
ADD R1, PC ; "r"
调用`popen()`方法,这个方法可以执行命令行,同时将显示的数据存到一个文件句柄里
BL j_j_popen
此时R0存的就是返回的句柄,类型为`FILE *`
SUBS R2, R0, #0 ; stream
BEQ loc_331E
如果这里返回的值不为空
结合上面,这里还原一下代码:`fgets(s1, 128, R2)`
MOVS R0, R5 ; s
MOVS R1, #0x80 ; n
BL j_j_fgets
判断返回的结果进行跳转
CMP R0, #0
BEQ loc_331E
如果返回的结果不为空,输出获取的数据
LDR R2, =(aWchaninfoS - 0x331A)
MOVS R0, #4
MOVS R1, R6
ADD R2, PC ; "wchaninfo = %s"
MOVS R3, R5
还原代码:`LOGI("wchaninfo = %s", s1)`
BL j_j___android_log_print
上面是一个if代码块,这里开始是一个新的if-else结构
将`aSys_epoll - 0x3326`赋值给R4:`R4 = aSys_epoll - 0x3326`
loc_331E
LDR R4, =(aSys_epoll - 0x3326)
将s1的指针赋值给R0
MOVS R0, R5 ; s1
重定位完成,R4指向`"sys_epoll"`
ADD R4, PC ; "sys_epoll"
将s2的指针赋值给R1
MOVS R1, R4 ; s2
将`0x9`赋值给R2:`R2 = 0x9`
MOVS R2, #9 ; n
调用`strncasecmp()`,还原一下代码:`strncasecmp(s1, "sys_epoll\0", 9)`
BL j_j_strncasecmp
将返回值赋值给R3
MOVS R3, R0
将1赋值给R0:`R0 = 1`
MOVS R0, #1
判断s1是否包含`"sys_epoll\0"`
CMP R3, #0
如果包含,跳到结束分支,返回值为1
BEQ locret_3346
如果不包含`"sys_epoll\0"`
将R4赋值给R1:`R1 = s2`
MOVS R1, R4
将R5赋值给R0:`R0 = s1`
MOVS R0, R5 ; s1
将s2往后加`0xB`个字节,什么意思呢?
ADDS R1, #0xB ; s2
网上找给s2赋值的地方`LDR R4, =(aSys_epoll - 0x3326)`,双击进入`aSys_epoll`,看`"ptrace_stop\0"`刚好在`"sys_epoll"`偏移`0xB`的位置
.rodata:00008700 aSys_epoll DCB "sys_epoll",0 ; DATA XREF: getWchanStatus(void)+62o
.rodata:00008700 ; .text:off_335Co ...
.rodata:0000870A DCB 0
.rodata:0000870B DCB 0x70 ; p
.rodata:0000870C DCB 0x74 ; t
.rodata:0000870D DCB 0x72 ; r
.rodata:0000870E DCB 0x61 ; a
.rodata:0000870F DCB 0x63 ; c
.rodata:00008710 DCB 0x65 ; e
.rodata:00008711 DCB 0x5F ; _
.rodata:00008712 DCB 0x73 ; s
.rodata:00008713 DCB 0x74 ; t
.rodata:00008714 DCB 0x6F ; o
.rodata:00008715 DCB 0x70 ; p
.rodata:00008716 DCB 0
.rodata:00008717 DCB 0
将`0xB`赋值给R2:`R2 = 0xB`
MOVS R2, #0xB ; n
还原一下代码:`strncasecmp(s1, "ptrace_stop\0", 0xB)`
BL j_j_strncasecmp
然后根据返回结果决定返回值
NEGS R3, R0
ADCS R0, R3
LSLS R0, R0, #1
静态分析完,我们动态调试一下
这里测试需要先生成APK,解压出so文件,提前在IDA下好断点
7775841A BL _Z14getWchanStatusv ; getWchanStatus(void)
调试模式启动应用,输出LogCat
$ adb shell ps |grep "wnagzihxain"
u0_a137 10146 283 908004 18140 ffffffff 400ccaac S com.wnagzihxain.myapplication
$ adb logcat -v process |grep 10146
IDA attach,跑起来断在断点处,F7跟进去
F8单步走完这段最后一句
.text:77758310 LDR R2, =(aWchaninfoS - 0x7775831A)
.text:77758312 MOVS R0, #4
.text:77758314 MOVS R1, R6
.text:77758316 ADD R2, PC ; "wchaninfo = %s"
.text:77758318 MOVS R3, R5
.text:7775831A BL j_j___android_log_print
LogCat记录
I(10146) cmd = cat /proc/10146/wchan(totoc)
I(10146) wchaninfo = ptrace_stop(totoc)
然后走完这个函数,跳出,发现R0的值为2
R0 00000002
一开始的宏定义
#define WCHAN_TRACING 2;
源码对应
int ret = getWchanStatus();
if (2 == ret) {
kill(pid, SIGKILL);
}
F9,跑飞了
不过有个小问题,在单步的时候,有时候会读到0,不知道为什么
I(32081) wchaninfo = 0(totoc)
3.4 inotify机制反调试
int read_event(int fd) {
char buffer = {0};
size_t index = 0;
struct inotify_event *ptr_event;
ssize_t r = read(fd, buffer, 16384);
if (r <= 0) {
LOGE("read_event");
return r;
}
while (index < r) {
ptr_event = (struct inotify_event *) &buffer;
//此处监控事件的读和打开,如果出现则直接结束进程
if ((ptr_event->mask & IN_ACCESS) || (ptr_event->mask & IN_OPEN)) {
//事件出现则杀死父进程
LOGI("hhhahahahahahahahahaahah");
int ret = kill(getpid(), SIGKILL);
return 0;
}
index += sizeof(struct inotify_event) + ptr_event->len;
}
return 0;
}
int event_check(int fd) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
return select(FD_SETSIZE, &rfds, NULL, NULL, NULL);
}
void runInotify() {
keep_running = 1;
pid_t ppid = syscall(__NR_getpid);
int fd;
char buf;
fd = inotify_init();//初始化
if (fd == -1) { //错误处理
LOGE("inotify_init error");
return;
}
int wd;
sprintf(buf, "/proc/%d/maps", ppid);
wd = inotify_add_watch(fd, buf, IN_ALL_EVENTS); //添加监视
if (wd == -1) { //错误处理
LOGE("inotify_add_watch");
return;
}
while (keep_running) {
if (event_check(fd) > 0) {
read_event(fd);
}
}
return;
}
void checkNotify() {
runInotify();
}
void anti_notify() {
LOGI("Call anti debug...");
if (pthread_create(&id_notify, NULL, (void *(*)(void *)) &checkNotify, NULL) != 0) {
LOGE("Failed to create a debug checking thread!");
exit(-1);
};
pthread_detach(id_notify);
}
全局添加两个变量
pthread_t id_notify = NULL;
int keep_running;
最后在`JNI_OnLoad()`函数里注释掉第一个反调试调用
添加这个反调试函数的调用
//anti_debug();
anti_notify();
程序正常跑起来,使用IDA attach,还没attach上,就被kill了,效果刁刁的
恢复第一个反调试函数的调用,编译签名,IDA载入so文件
在`JNI_OnLoad()`函数里关于第二个反调试的调用
创建子线程调用`checkNotify()`
`checkNotify()`函数很简单,调用`runInotify()`
.text:000038A8 _Z11checkNotifyv ; DATA XREF: anti_notify(void)+20o
.text:000038A8 ; .got:_Z11checkNotifyv_ptro
.text:000038A8 PUSH {R3,LR}
.text:000038AA BL _Z10runInotifyv ; runInotify(void)
.text:000038AE POP {R3,PC}
进入`runInotify()`函数,代码比较多,按空格看逻辑调用图
左上角孤零零一个,`Force BL call`即可修复
关键在这里,在一个循环里调用`read_event()`
`read_event()`函数的逻辑也是很清楚,这里不过多分析,感兴趣的同学可以按照前面分析汇编指令的方法试着去分析一下
那么这个CrackMe使用到的几个反调试技术都已经分析完毕了,从作者源码中可以看到,还有不少反调试的代码并没有用到,以后有机会再写一下那些代码的用法
qtfreet00 发表于 2017-2-10 23:04
赶紧搞吧,这版式我看的想吐
看看效果图:http://addon.discuz.com/?@zxsq_markdown.plugin
测试站点:http://www.tecbbs.com/group-299-1.html
我看一直都没更新,怕bug多,先去那网站试试? 三世 发表于 2017-3-7 23:54
非常感谢楼主这么详细的分析,让我对arm指令又熟悉了不少,真的很感谢,很少有人能把帖子写的这么详细了。
...
对应着去改就行了,或者修改系统源码最彻底 @Hmily 赶紧上个精华贴了 额 师傅写的好高端,我怎么大部分都看不懂啊 qtfreet00 发表于 2017-2-10 15:38
额 师傅写的好高端,我怎么大部分都看不懂啊
祖师爷一天到晚在我这装菜鸟,我这些简单玩意都是抄你的代码的 来学习大神的教程了,辛苦了分享这么好的东西,谢谢 精品了,赶紧占个前排来围观 我是进来给你点赞的! wnagzihxain 发表于 2017-2-10 15:42
祖师爷一天到晚在我这装菜鸟,我这些简单玩意都是抄你的代码的
{:1_908:}可是我arm真的一般般,一般都是看f5 {:1_937:}所以看你讲了那么多arm我就头疼 都是大神啊