美文网首页
Android加固方案 之 类方法抽取指令

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

作者: Sharkchilli | 来源:发表于2020-10-11 15:21 被阅读0次

    前言

    以前我们介绍了加密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中实现「类方法指令抽取方式」加固方案原理解析

    相关文章

      网友评论

          本文标题:Android加固方案 之 类方法抽取指令

          本文链接:https://www.haomeiwen.com/subject/hfcepktx.html