android抽取方法,Android加固方案 之 类方法抽取指令

前言

以前我们介绍了加密dex文件的加固方案Android最初的加固,其实现在市场上用的比较多的是类方法抽取指令的加固方案,或者说是综合应用。由于商业问题此类的资料还是比较少的。所幸姜维写了几遍关于类方法抽取指令的文章,下方有他的链接。本文就是参照他的资料去实现的。

请读者务必阅读上一章Android免Root 修改程序运行时内存指令逻辑(Hook系统函数)

这是指令还原的前提

开发环境

Android4.4.4

Nexus5手机(ARM)

Android Studio3.5.1

eclipse

思路

主要分两步一是指令抽取,二是指令还原。

我们先开发一个dex文件使用ClassLoader去加载并执行其中的方法。

这个dex很可能被他人进行逆向,分析所以我们对其关键的方法进行抽取置空。

这样这个方法就是空的。那么什么时候还原呢?

就是上一章在dex文件加载进内存的时候,这样就要去hook dexFindClass函数。上一章我们是改变代码逻辑,这里我为了方便进行硬编码还原代码。

加载Dex项目开发

先开发那个需要加载的dex,这里我使用Eclipse开发。这样开发出来的dex文件不会有太多无关的东西,有利于我们分析。

8160170d9b10

image.png

这里非常简单就是返回一个字符串密码回去。我们就要在真正的项目中调用这个方法。将其编译后取出器dex文件改名为CoreDex.dex。

使用010 Editor看它的指令如下图

8160170d9b10

修改前.png

可以看到此方法指令为{26, 33, 17}

指令抽取

这里我们要将指令置为0,就要去解析dex文件。我用c写过一个解析工具https://github.com/bigGreenPeople/DexAnalysis

姜维也有一个用java写的解析器https://github.com/fourbrother/parse_androiddex

由于他的项目功能更全,我这里就使用了他的项目

他的是一个Eclipse项目导入后直接使用

我们需要修改ParseDexUtils.java,在解析的过程中将CodeItem结构体到Map中,方便我们后面去取得我们需要的方法指令

public class ParseDexUtils {

...

//类方法抽取Map

public static Map directMethodCodeItemMap = new HashMap();

public static Map virtualMethodCodeItemMap = new HashMap();

...

/*************************** 解析代码内容 ***************************/

public static void parseCode(byte[] srcByte) {

for (ClassDataItem item : dataItemList) {

int premid = 0;

//解析静态方法

for (EncodedMethod item1 : item.direct_methods) {

int offset = Utils.decodeUleb128(item1.code_off);

CodeItem items = parseCodeItem(srcByte, offset);

int index = Integer.valueOf(

Utils.bytesToHexString(item1.method_idx_diff).trim(),

16) + premid;

premid = index;

MethodIdsItem methodItem = methodIdsList.get(index);

//获得方法名称

String methodName = stringList.get(methodItem.name_idx);

int classIndex = typeIdsList.get(methodItem.class_idx).descriptor_idx;

//获得类名

String className = stringList.get(classIndex);

//使用方法的签名作为key

directMethodCodeItemMap.put(getMethodSignStr(methodItem), items);

//directMethodCodeItemList.add(items);

System.out.println("class name:"+className+":"+methodName+"-----direct method item:" + items);

}

premid = 0;

//解析对象方法

for (EncodedMethod item1 : item.virtual_methods) {

int offset = Utils.decodeUleb128(item1.code_off);

CodeItem items = parseCodeItem(srcByte, offset);

int index = Integer.valueOf(

Utils.bytesToHexString(item1.method_idx_diff).trim(),

16) + premid;

premid = index;

MethodIdsItem methodItem = methodIdsList.get(index);

//获得方法名称

String methodName = stringList.get(methodItem.name_idx);

int classIndex = typeIdsList.get(methodItem.class_idx).descriptor_idx;

//获得类名

String className = stringList.get(classIndex);

virtualMethodCodeItemMap.put(getMethodSignStr(methodItem), items);

//virtualMethodCodeItemList.add(items);

System.out.println("class name:"+className+":"+methodName+"-----virtual method item:" + items);

}

}

}

...

}

在解析方法得到代码结构CodeItem 的时候,将每个方法保存到上面定义的静态Map中。

这里定义的两个Map,一个保存所有的静态方法,一个保存所有的对象方法

那么用什么作为这个方法的key呢?当然是这个方法的签名了。getMethodSignStr就是用来获得方法的签名。其代码如下

//得到方法的唯一签名

public static String getMethodSignStr(MethodIdsItem methodItem){

int classIndex = typeIdsList.get(methodItem.class_idx).descriptor_idx;

//获得类名

String className = stringList.get(classIndex);

//获得方法名称

String methodName = stringList.get(methodItem.name_idx);

//获得方法签名

ProtoIdsItem protoIdsItem = protoIdsList.get(methodItem.proto_idx);

String protoName = stringList.get(protoIdsItem.shorty_idx);

//返回值

int returnIndex = typeIdsList.get(protoIdsItem.return_type_idx).descriptor_idx;

String returnName = stringList.get(returnIndex);

String sinName = className+methodName+"#"+returnName+"()"+protoName;

System.out.println("Shark:"+sinName);

return sinName;

}

这里还要注意一点,在修改指令的时候我们需要,指令的Offset(偏移),而CodeItem结构体中没有这个成员,我们需要添加上去。

CodeItem.java

package com.wjdiankong.parsedex.struct;

import com.wjdiankong.parsedex.Utils;

public class CodeItem {

public short registers_size;

public short ins_size;

public short outs_size;

public short tries_size;

public int debug_info_off;

public int insns_size;

public short[] insns;

//指令偏移

public int insnsOffset;

}

这个再什么时候赋值呢?

8160170d9b10

image.png

这里的offset就是这个CodeItem的偏移

修改parseCodeItem方法

private static CodeItem parseCodeItem(byte[] srcByte, int offset) {

CodeItem item = new CodeItem();

/**

* public short registers_size; public short ins_size; public short

* outs_size; public short tries_size; public int debug_info_off; public

* int insns_size; public short[] insns;

*/

byte[] regSizeByte = Utils.copyByte(srcByte, offset, 2);

item.registers_size = Utils.byte2Short(regSizeByte);

byte[] insSizeByte = Utils.copyByte(srcByte, offset + 2, 2);

item.ins_size = Utils.byte2Short(insSizeByte);

byte[] outsSizeByte = Utils.copyByte(srcByte, offset + 4, 2);

item.outs_size = Utils.byte2Short(outsSizeByte);

byte[] triesSizeByte = Utils.copyByte(srcByte, offset + 6, 2);

item.tries_size = Utils.byte2Short(triesSizeByte);

byte[] debugInfoByte = Utils.copyByte(srcByte, offset + 8, 4);

item.debug_info_off = Utils.byte2int(debugInfoByte);

byte[] insnsSizeByte = Utils.copyByte(srcByte, offset + 12, 4);

item.insns_size = Utils.byte2int(insnsSizeByte);

//赋值指令的偏移

item.insnsOffset = offset + 16;

short[] insnsAry = new short[item.insns_size];

int aryOffset = offset + 16;

for (int i = 0; i < item.insns_size; i++) {

byte[] insnsByte = Utils.copyByte(srcByte, aryOffset + i * 2, 2);

insnsAry[i] = Utils.byte2Short(insnsByte);

}

item.insns = insnsAry;

return item;

}

这里的insnsOffset就是offset + 16;为什么是加16呢?

8160170d9b10

image.png

高亮的部分刚好就是16个字节,所以+16就指向了指令部分了。

这样Map中的CodeItem就有指令的偏移了。

现在回到main方法中

因为上面保存工作是在解析的时候做的,我们不要修改原来的代码逻辑,直接在后面加我们的代码就行了

ParseDexMain.java

package com.wjdiankong.parsedex;

import java.io.ByteArrayOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.security.NoSuchAlgorithmException;

import java.util.HashMap;

import java.util.Map;

import com.wjdiankong.parsedex.struct.CodeItem;

public class ParseDexMain {

private static Map codeItemMap = new HashMap();

public static void main(String[] args) {

//---------------------------原先的解析逻辑 ----------------------------------

...

// ----------------------------方法抽取逻辑----------------------------------------

String className = "Lcom/shark/calculate/CoreUtils;";

String methodName = "getPwd#Ljava/lang/String;()L";

//构造出要抽取方法的方法签名

String signName = className + methodName;

//将两个map合并到codeItemMap

codeItemMap.putAll(ParseDexUtils.directMethodCodeItemMap);

// 遍历所有方法的信息

for (String key : codeItemMap.keySet()) {

System.out.println("key:" + key);

// 找到想抽取的方法

if (key.equals(signName)) {

CodeItem codeItem = codeItemMap.get(key);

// 获取方法对应的指令个数和偏移

int insns_size = codeItem.insns_size;

int insns_Offset = codeItem.insnsOffset;

// 构造空指令 每条指令占两个字节

byte[] nopBytes = new byte[insns_size * 2];

for (int i = 0; i < nopBytes.length; i++) {

nopBytes[i] = 0;

}

try {

// 替换原有指令

srcByte = Utils.replaceBytes(srcByte, nopBytes,

insns_Offset);

// 修改DEX file size文件头

Utils.updateFileSizeHeader(srcByte);// dex中32到35的位置为文件长度

// 修改DEX SHA1 文件头

Utils.updateSHA1Header(srcByte);

// dex中12到31位置,32到结束参与SHA1计算

// 修改DEX CheckSum文件头

Utils.updateCheckSumHeader(srcByte);// dex中8到11位置,12到文件结束计算checksum

String str = "dex/new_CoreDex.dex";

File file = new File(str);

if (!file.exists()) {

file.createNewFile();

}

FileOutputStream fileOutputStream = new FileOutputStream(

file);

fileOutputStream.write(srcByte);

fileOutputStream.flush();

fileOutputStream.close();

System.out.println("done!");

} catch (Exception e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

}

}

}

}

这里的逻辑很简单就是从保存的Map中找到我们要修改的方法,然后构造了一个空指令byte[]调用replaceBytes覆盖它。

因为修改了它的指令所以SHA1 和 CheckSum都有变化需要重新计算

最后调用updateFileSizeHeader、updateSHA1Header、updateCheckSumHeader修改DEX file size 、SHA1 、CheckSum

最后保存到dex/CoreDex.dex

先看下replaceBytes的实现

//用来覆盖字节数组

public static byte[] replaceBytes(byte[] source_byte, byte[] replace_byte,

int offset) {

for (int i = 0; i < replace_byte.length; i++) {

source_byte[offset++] = replace_byte[i];

}

return source_byte;

}

updateFileSizeHeader、updateSHA1Header、updateCheckSumHeader

/**

* 修改dex头 sha1值

*

* @param dexBytes

* @throws NoSuchAlgorithmException

*/

public static void updateSHA1Header(byte[] dexBytes)

throws NoSuchAlgorithmException {

MessageDigest md = MessageDigest.getInstance("SHA-1");

md.update(dexBytes, 32, dexBytes.length - 32);// 从32到结束计算sha-1

byte[] newdt = md.digest();

System.arraycopy(newdt, 0, dexBytes, 12, 20);// 修改sha-1值(12-31)

}

/**

* 修改dex头 file_size值

*

* @param dexBytes

*/

public static void updateFileSizeHeader(byte[] dexBytes) {

// 新文件长度

byte[] newfs = intToByte(dexBytes.length);

// 高位低位交换

for (int i = 0; i < 2; i++) {

byte tmp = newfs[i];

newfs[i] = newfs[newfs.length - 1 - i];

newfs[newfs.length - 1 - i] = tmp;

}

System.arraycopy(newfs, 0, dexBytes, 32, 4);// 修改(32-35)

}

/**

* 修改dex头,CheckSum 校验码

*

* @param dexBytes

*/

public static void updateCheckSumHeader(byte[] dexBytes) {

Adler32 adler = new Adler32();

adler.update(dexBytes, 12, dexBytes.length - 12);// 从12到文件末尾计算校验码

long value = adler.getValue();

int va = (int) value;

byte[] newcs = intToByte(va);

for (int i = 0; i < 2; i++) {

byte tmp = newcs[i];

newcs[i] = newcs[newcs.length - 1 - i];

newcs[newcs.length - 1 - i] = tmp;

}

System.arraycopy(newcs, 0, dexBytes, 8, 4);// 效验码赋值(8-11)

}

这些其实在以前的文章中都使用过了,只不过上面的源码中没有这种方法,所以直接拿过来再次使用了。

运行

将前面开发的CoreDex.dex放入到项目的dex文件夹下

8160170d9b10

image.png

运行项目后得到new_CoreDex.dex

8160170d9b10

image.png

再次使用010 Editor查看new_CoreDex.dex

8160170d9b10

修改后.png

可以看到指令为0了!

使用JEB打开dex

8160170d9b10

image.png

显示的也为nop

指令还原

指令还原就很简单了,就是上一章的东西改几个地方就行

首先是DexUtils.java,改为调用getPwd

package com.shark.androidinlinehook;

import android.content.Context;

import android.util.Log;

import java.io.File;

import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

public class DexUtils {

public static final String SHARK = "shark";

public static void exeCoreMethod(Context context) {

try {

//创建文件夹

File optfile = context.getDir("opt_dex", 0);

File libfile = context.getDir("lib_path", 0);

//得到当前Activity 的ClassLoader 以下的方法得到的都是同一个ClassLoader

ClassLoader parentClassloader = MainActivity.class.getClassLoader();

ClassLoader tmpClassLoader = context.getClassLoader();

//创建我们自己的DexClassLoader 指定其父节点为当前Activity 的ClassLoader

/*dexPath:目标所在的apk或者jar文件的路径,装载器将从路径中寻找指定的目标类。

dexOutputDir:由于dex 文件在APK或者 jar文件中,所以在装载前面前先要从里面解压出dex文件,这个路径就是dex文件存放的路径,

在 android系统中,一个应用程序对应一个linux用户id ,应用程序只对自己的数据目录有写的权限,所以我们存放在这个路径中。

libPath :目标类中使用的C/C++库。

最后一个参数是该装载器的父装载器,一般为当前执行类的装载器。*/

DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/CoreDex.dex",

optfile.getAbsolutePath(), libfile.getAbsolutePath(), MainActivity.class.getClassLoader());

Class> clazz=dexClassLoader.loadClass("com.shark.calculate.CoreUtils");

// Method calculateMoney=clazz.getDeclaredMethod("calculateMoney",int.class,int.class);

// Object obj=clazz.newInstance();

// int result = (int)calculateMoney.invoke(obj,2,3);

// Log.i(SHARK, "calculateMoney result:" + result);

//------------------------------------------------------------------------

//getPwd方法执行

Method getPwd=clazz.getDeclaredMethod("getPwd");

Log.i(SHARK, "getPwd result:" + getPwd.invoke(null));

} catch (Exception e) {

Log.i(SHARK, "exec exeCoreMethod err:" + Log.getStackTraceString(e));

}

}

}

hooktest.cpp修改被Hook的函数逻辑。将正确的代码写回到dex中

const DexClassDef *newDexFindClass(const DexFile *pFile, const char *descriptor) {

//只关注需要修改的类Lcom/shark/calculate/CoreUtils;

int cmp = strcmp("Lcom/shark/calculate/CoreUtils;", descriptor);

if (cmp == 0) {

//执行原来的逻辑得到类结构信息

const DexClassDef *pClassDef = oldDexFindClass(pFile, descriptor);

if (pClassDef == NULL) {

return pClassDef;

}

//打印信息

LOGI("class def:%d", (int)pClassDef);

LOGI("class dex find class name:%s", descriptor);

//我们需要调用DexReadAndVerifyClassData得到DexClassData代码结构,所以需要得到其地址

//依然需要用IDA打开libdvm.so文件查看DexReadAndVerifyClassData函数的导出名称:

DexReadAndVerifyClassData getClassData = (DexReadAndVerifyClassData) dlsym(

dvmLib, "_Z25dexReadAndVerifyClassDataPPKhS0_");

const u1 *pEncodedData = dexGetClassData(pFile, pClassDef);

DexClassData *pClassData = getClassData(&pEncodedData, NULL);

DexClassDataHeader header = pClassData->header;

//打印对象方法数量

LOGI("method size:%d", header.directMethodsSize);

//得到首个对象方法的指针

DexMethod *pDexDirectMethod = pClassData->directMethods;

u1 *ptr = (u1 *) pDexDirectMethod;

//循环遍历每个方法

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

//这里每个方法都是相邻的,每个大小都是DexMethod结构体的大小

pDexDirectMethod = (DexMethod *) (ptr + sizeof(DexMethod) * i);

//得到方法名称

const DexMethodId *methodId = dexGetMethodId(pFile, pDexDirectMethod->methodIdx);

const char *methodName = dexStringById(pFile, methodId->nameIdx);

//如果是getPwd方法就进行替换逻辑

if (strcmp("getPwd", methodName) == 0) {

LOGI("pDexDirectMethod methodName:%s", methodName);

//打印指令

printMethodInsns(pFile, pDexDirectMethod);

//修改内存页属性

int start_add = (int) (pFile->baseAddr + pDexDirectMethod->codeOff);

int result = changeMemWrite(start_add);

LOGI("mp result:%d", result);

//获取方法对应DexCode结构

DexCode *dexCode = (DexCode *) dexGetCode(pFile, pDexDirectMethod);

//下面就是覆盖指令了

u2 new_ins[3] = {26, 33, 17};

memcpy(dexCode->insns, &new_ins, 3 * sizeof(u2));

printMethodInsns(pFile,pDexDirectMethod);

}

}

return pClassDef;

} else{

//执行原来的逻辑

return oldDexFindClass(pFile,descriptor);

}

}

因为getPwd是静态方法,所以这里要去静态方法中找。方法名也要改为getPwd。最后指令这里我们硬编码了{26, 33, 17},就是原来的指令

运行

8160170d9b10

测试效果.png

可以看到我们得到了正确的结果~

引用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值