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

2020-10-11  本文已影响0人  Sharkchilli

前言

以前我们介绍了加密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文件不会有太多无关的东西,有利于我们分析。


image.png

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


修改前.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<String,CodeItem> directMethodCodeItemMap = new HashMap<String,CodeItem>();
    public static Map<String,CodeItem> virtualMethodCodeItemMap = new HashMap<String,CodeItem>();
    ...

    /*************************** 解析代码内容 ***************************/
    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;
    
}

这个再什么时候赋值呢?


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呢?


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<String, CodeItem> codeItemMap = new HashMap<String, CodeItem>();

    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)

    }

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

到这里抽取指令项目就完成了,完整代码我放到了github上:
https://github.com/bigGreenPeople/parse_androiddex-master

运行

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


image.png

运行项目后得到new_CoreDex.dex


image.png
再次使用010 Editor查看new_CoreDex.dex
修改后.png

可以看到指令为0了!

使用JEB打开dex


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},就是原来的指令

项目完整代码:https://github.com/bigGreenPeople/AndroidInlineHook

运行

测试效果.png

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

引用

Android中实现「类方法指令抽取方式」加固方案原理解析

上一篇下一篇

猜你喜欢

热点阅读