【客户端安全】对抗静态分析之运行时修复dex!【转】

很多公司为了保护自己的软件和产品,都会进行加壳处理,以防止软件被破解。壳是一个比较大的概念,发展的比较迅速。未来(或者现在?)加壳的主流会逐渐变为最新的第四代壳,也就是VMP技术,各个厂商自己搞一套解释器,但是这种方式对运行效率影响比较大,这篇讲的是一种现在采用得比较多的办法,即在运行时对dex进行修改,来解决这个问题,想知道具体方法就往下看吧~

跟我一起摇摆

学起来~

一 写在前面

这次要讲的原理是针对源APK(也就是真正运行的APK),将dex文件中的某部分指令提取出来,保存为一个文件。然后将dex文件中的这部分指令修改为错误的字节码,比如0x00 0x00 0x00 0x000x00 。。。运行时,壳程序将这部分指令修复。

修复的方法简单点可以直接替换回去,如果复杂点可以在dex文件的范围外new一块区域,然后将原本指向这部分指令的指针指向new出来的区域(也就是在dex外部),这样即便你通过动态分析对dvmDexFileOpenPartial这样的API下断能dump出dex文件,还是没有办法正确反编译,因为正确的指令还是没有加入进来。

这个系列本来题目想写对抗反编译,可是想想对抗反编译的这个范围有点大,总结如下

先来看灵魂作图~

自己比较熟悉的是静态分析,所以就从这里入手吧,省的吹大了,挖了坑填不上那就不好了。当然也会写些动态分析的东西。

废话少说,开始吧,最近又把脱壳拿出来看了,就从这里开始吧

整个过程分为两个阶段

第一阶段:提取、替换正确的dex中的指令

第二阶段:运行时修复替换之后错误的dex文件

1、定位——提取——替换

首先,在dex中定位code的位置,我们看看android源码是怎么写的

源码位置dalvik\libdex\dexFile.h

/*

* Direct-mapped “code_item”.

*

* The “catches” table is used when throwing an exception,

* “debugInfo” is used when displaying an exception stack trace or

* debugging. An offset of zero indicates that there are no entries.

*/

struct DexCode {

u2  registersSize;

u2  insSize;

u2  outsSize;

u2  triesSize;

u4  debugInfoOff;       /* file offset to debug info stream */

u4  insnsSize;          /* size of the insns array, in u2 units */

u2  insns[1];

/* followed by optional u2 padding */

/* followed by try_item[triesSize] */

/* followed by uleb128 handlersSize */

/* followed by catch_handler_item[handlersSize] */

};

这个结构,从上到下分别表示

~~~~~~~~~~~~~~

该函数寄存器的数目

传入的参数个数

调用其他函数时需要的参数个数

try结构的数目

debug信息的偏移

指令列表大小

该函数的指令

~~~~~~~~~~~~~~

看注释,这个结构我们一般不叫它DexCode,而是code_item(关于各种item,在深入Androguard源码中有解释)

而指向它的指针,位于encoded_method结构中的codeoff(源码位置dalvik\libdex\DexClass.h)

/* expanded form of encoded_method */

struct DexMethod {

u4 methodIdx;    /* index to a method_id_item */

u4 accessFlags;

u4 codeOff;      /* file offset to a code_item */

};

那么这个DexMethod来自于哪里呢?在这里(同一文件下)

/* expanded form of class_data_item. Note: If a particular item is

* absent (e.g., no static fields), then the corresponding pointer

* is set to NULL. */

struct DexClassData {

DexClassDataHeader header;

DexField*          staticFields;

DexField*          instanceFields;

DexMethod*         directMethods;

DexMethod*         virtualMethods;

};

也就是我们所说的class_data_item。而这个item又是由class_def_item中的classDataOff指引的

/*

* Direct-mapped “class_def_item”.

*/

struct DexClassDef {

u4  classIdx;           /* index into typeIds for this class */

u4  accessFlags;

u4  superclassIdx;      /* index into typeIds for superclass */

u4  interfacesOff;      /* file offset to DexTypeList */

u4  sourceFileIdx;      /* index into stringIds for source file name */

u4  annotationsOff;     /* file offset to annotations_directory_item */

u4  classDataOff;       /* file offset to class_data_item */

u4  staticValuesOff;    /* file offset to DexEncodedArray */

};

上图中的DexMethodId结构保存的是dex文件中函数的元信息,包括class、参数与返回值、函数名,都是String池索引结构。根据这个索引信息我们可以确定唯一的指定的函数。

我们通过dexClassLoader将源APK加载到内存中,通过cat  /proc/self/maps 可以查看当前进程的内存结构,找到源APK的dex文件在内存中的起始位置和终止位置。不过这个内存中的dex是并不是初始的dex文件了,是优化后的odex文件(安利一下,优化过程可见我的博客优化过程分析 和加载过程分析,对于dex和odex结构的区别,可见dex和odex的比较实验,安利完成)。适当偏移之后,才是dex文件的开头位置。然后根据前述的结构,找到指定函数的指令位置,将指令拷贝到一个其他文件中,然后将这部分指令修改,比如改为0x23 0x33 0x33……

2、修复

修复时,壳程序同样先通过dexClassLoader加载APK(此时源APK已被修改,是无法正确执行的),按之前的步骤找到目标函数指令。再将保存正确指令的文件mmap到内存中。然后将指令直接替换;或者将指向错误DexCode 的codeOff改为mmap返回的地址位置

二 实践

接下来就简单实践一下。首先要做一个简单的源APK,名字就叫TestApk.apk

package com.example.testapk;

 

import android.app.Activity;

import android.os.Bundle;

import android.widget.TextView;

 

public class MainActivity extends Activity {

 

@Override

public void onCreate(Bundle saveInstanceState) {

super.onCreate(saveInstanceState);

 

TextView context = new TextView(this);

context.setText(EncryptString.encrypt(“Hello,World”));

setContentView(context);

}

}

如果正确执行的话,会如下所示

那么这堆乱码就是“Hello,World”经过EncryptString.encrypt加密之后的结果。我们提取替换的目标函数就定为这个函数。找到之后会替换为如下

String path = getFilesDir().getAbsolutePath();

 

DexClassLoader dexClassLoader = new DexClassLoader(“/data/local/tmp/TestApk.apk”,

path, null, getClassLoader());

将TestApk.apk放到/data/local/tmp下,然后由FixDex加载,定位EncryptString.encrypt函数位置的工作我们放到native层

package com.example.fixdex;

 

public class FindCode {

static {

System.loadLibrary(“findcode”);

}

 

public FindCode() {

}

 

public native int findCode(String dexName, String className, String methodName);

}

JNIEXPORT jint JNICALL Java_com_example_fixdex_FindCode_findCode

(JNIEnv *env, jobject clazz, jstring dexName, jstring className, jstring methodName) {

const char *dexNameLocal;

const char *classNameLocal;

const char *methodNameLocal;

dexNameLocal = env->GetStringUTFChars(dexName, JNI_FALSE);

classNameLocal = env->GetStringUTFChars(className, JNI_FALSE);

methodNameLocal = env->GetStringUTFChars(methodName, JNI_FALSE);

return nativeParserDex(dexNameLocal, classNameLocal, methodNameLocal);

}

在将Java字符串转化为char *之后,由nativeParserDex执行定位工作

首先要定位内存中TestApk.dex的位置

static void* get_module_base( pid_t pid, const char* module_name)

{

FILE *fp;

long addr = 0;

char *pch;

char filename[32];

char line[1024];

 

if ( pid < 0 ){

/* self process */

strcpy(filename, “/proc/self/maps”);

}else{

snprintf( filename, sizeof(filename), “/proc/%d/maps”, pid);

}

 

fp = fopen( filename, “r” );

 

if ( fp != NULL ){

while ( fgets( line, sizeof(line), fp ) ){

 

if ( strstr( line, module_name) ){

pch = strtok( line, “-” );

addr = strtoul( pch, NULL, 16 );

if ( addr == 0x8000 ) addr = 0;

 

break;

}

}

 

fclose( fp ) ;

}

 

return (void *)addr;

}

我们也可以手动查看当前的内存分布

void *dexBase = searchDexStart(base);

ALOGD(“found dex start[%p]”, dexBase);

if(checkDexMagic(dexBase) == false){

ALOGE(“Error! invalid dex format at: %p”, dexBase);

return 0;

}

base的值就是get_module_base返回的地址,其实searchDexStart函数十分简单,只需要偏移odex的头部,就是dex的起始位置,不清楚的同学可以看我上面安利的关于这两种文件比较的博客。找到dex头部之后,验证文件魔数也就是checkDexMagic函数

const DexCode  *code =

dexFindClassMethod(&gDexFile, className, methodName);

if(code == NULL){

ALOGE(“Error can not found setScoreHidden”);

return 0;

}

position = (u4 *)code – (u4 *)dexBase;

ALOGD(“codeoff:%d”, ((u4 *)code – (u4 *)dexBase));

return position * 4 + 16;

dexFindClassMethod返回函数指令在内存中的位置,position就是指令位置相对dex文件头部的偏移,注意这个计算结果是一个unsigned int型指针,所以返回的时候要转化为字节数,这样就返回了指向相对头部的字节偏移数。
下面是dexFindClassMethod函数的代码

static const DexCode *dexFindClassMethod(DexFile *dexFile, const char *clazz, const char *method)

{

ALOGD(“finding: %s->%s”, clazz, method);

DexClassData* classData = dexFindClassData(dexFile, clazz);

if(classData == NULL) return NULL;

 

const DexCode* code = dexFindMethodInsns(dexFile, classData, method);

 

if(code != NULL) {

dumpDexCode(code);

}

dumpDexClassDataMethod(dexFile, classData);

 

ALOGD(“found[%p]: %s->%s”, code, clazz, method);

return code;

}

是不是也很简单,也很简单就不说了,233333。

首先找到对应的DexClassData,也就是EncryptString的DexClassData,利用DexClassData中的一个成员classIdx到TypeId池中找到descriptorIdx,这是一个字符串池的索引,这个字符串就是class的名称。具体实现如下

static DexClassData* dexFindClassData(const DexFile *dexFile, const char* clazz)

{

const DexClassDef* classdef;

u4 count = dexFile->pHeader->classDefsSize;

 

const u1* pEncodedData = NULL;

DexClassData* pClassData = NULL;

const char *descriptor = NULL;

 

ALOGD(“total count: %ld search:%s”, count, clazz);

 

for(u4 i=0; i<count; i++){

classdef = dexGetClassDef(dexFile, i);

 

descriptor = getTpyeIdString(dexFile, classdef->classIdx);

ALOGD(“%s”, descriptor);

 

pEncodedData = dexFile->baseAddr + classdef->classDataOff;

pClassData = dexReadAndVerifyClassData(&pEncodedData, NULL);

ALOGD(“[%p] [%p]”, pEncodedData, pClassData);

 

if(strcmp(descriptor, clazz) == 0){

ALOGD(“found %s”, clazz);

return pClassData;

}

 

if (pClassData == NULL) {

ALOGE(“Trouble reading class data (#%ld) for %s\n”, i, descriptor);

continue;

}

 

free(pClassData);

}

 

return NULL;

}

找到class之后再通过dexFindMethodInsns找到指令的地址

static const DexCode* dexFindMethodInsns(const DexFile *dexFile, const DexClassData*classData, const char* methodName)

{

int idx = 0;

DexMethod *method = NULL;

const DexMethodId* methodId = NULL;

DexCode* code = NULL;

 

method = classData->directMethods;

methodId = dexFile->pMethodIds;

 

for (int i = 0; i < (int) classData->header.directMethodsSize; i++) {

idx = classData->directMethods[i].methodIdx;

ALOGD(“:idx-%d [%06lx]: %s->%s”, idx, classData->directMethods[i].codeOff,

getTpyeIdString(dexFile, methodId[idx].classIdx), getString(dexFile, methodId[idx].nameIdx));

 

 

const DexCode* pCode = dexGetCode(dexFile, &classData->directMethods[i]);

 

if(strcmp(getString(dexFile, methodId[idx].nameIdx), methodName) == 0){

return pCode;

}

}

 

for (int i = 0; i < (int) classData->header.virtualMethodsSize; i++) {

idx = classData->virtualMethods[i].methodIdx;

ALOGD(“idx-%d [%06lx]: %s->%s”, idx, classData->virtualMethods[i].codeOff,

getTpyeIdString(dexFile, methodId[idx].classIdx), getString(dexFile, methodId[idx].nameIdx));

 

const DexCode* pCode = dexGetCode(dexFile, &classData->virtualMethods[i]);

 

if(strcmp(getString(dexFile, methodId[idx].nameIdx), methodName) == 0){

return pCode;

}

}

 

return code;

}

当目标函数的指令偏移返回之后

FindCode fd = new FindCode();

int codeoff = fd.findCode(“TestApk.dex”,

“Lcom/example/testapk/EncryptString;”, “encrypt”);

try {

File file = new File(“/data/local/tmp/classes.dex”);

byte[] dexByte = readFileBytes(file);

byte[] lengthHex = new byte[4];

System.arraycopy(dexByte, codeoff – 4, lengthHex, 0, 4);

int length = byteToInt(lengthHex, 0);

String strso = path + “/data.so”;

writeFile(strso, dexByte, codeoff – 16, length + 16);

System.arraycopy(exceptionCode, 0, dexByte, codeoff,

length > exceptionCode.length ? exceptionCode.length : length);

//修改DEX file size文件头

fixFileSizeHeader(dexByte);

//修改DEX SHA1 文件头

fixSHA1Header(dexByte);

//修改DEX CheckSum文件头

fixCheckSumHeader(dexByte);

String str = path + “/classes_fix.dex”;

writeFile(str, dexByte, 0, dexByte.length);

将正确的指令所在的整个DexCode结构,也就是指令起始往前移动16个字节,拷贝到data.so文件中,然后将指令修改

将data.so和classes_fix.dex拷贝出来,看一下

data.so

classes_fix.dex

红色下划线之前的四字节1F 00 00 00 就是insnsSize成员,也就是指令长度,大小为31。

所以红色下划线的31个字节被替换为exceptionCode的前31字节。

三 总结

替换完毕,之后修复也跟之前的过程差不多,代码我就不上传了,大家自己有兴趣可以自己实现一下(其实是我懒,2333333。。。),方法我说过了,定位然后改回去,与提取不同的是,要在native层中实现,直接在内存中修改(其实也很简单,两个指针,按位拷贝)。不想在native层实现也可以,将TestApk解压找到classes.dex修改之后再用一个新的dexClassLoader加载到内存,然后执行的时候用这个新的dexClassLoader就可以啦~

此条目发表在未分类, 经验技术分类目录。将固定链接加入收藏夹。