摘要:原文地址文件混淆加密前言混淆加密主要是為了隱藏文件中關(guān)鍵的代碼,力度從輕到重包括靜態(tài)變量的隱藏函數(shù)的重復(fù)定義函數(shù)的隱藏以及整個(gè)類的隱藏。
現(xiàn)在部分 app 出于安全性(比如加密算法)或者用戶體驗(yàn)(熱補(bǔ)丁修復(fù)bug)會(huì)考慮將部分模塊采用熱加載的形式 Load。所以針對(duì)這部分的 dex 進(jìn)行加密是有必要的,如果 dex 是修復(fù)的加密算法,你總不想被人一下就反編譯出來吧。當(dāng)然也可以直接用一個(gè)加密算法對(duì) dex 進(jìn)行加密,Load 前進(jìn)行解密就可以了,但是最好的加密就是讓人分不清你是否加密了。一般逆向過程中拿到一個(gè)可以直接反編譯成 java 源碼的 dex 我們很可能就認(rèn)為這個(gè) dex 文件是沒有加密可以分析的。
原文地址: DEX文件混淆加密
0x00 前言混淆加密主要是為了隱藏 dex 文件中關(guān)鍵的代碼,力度從輕到重包括:靜態(tài)變量的隱藏、函數(shù)的重復(fù)定義、函數(shù)的隱藏、以及整個(gè)類的隱藏?;煜蟮?dex 文件依舊可以通過 dex2jar jade 等工具的反編譯成 Java 源碼,但是里面關(guān)鍵的代碼已經(jīng)看不到了。
效果圖:
源碼地址和使用說明在 github 上 hidex-hack
0x01 dex格式分析dex 文件格式在上一篇有進(jìn)行了比較詳細(xì)的介紹,具體可看dex文件格式分析,這里簡(jiǎn)單的介紹一下整個(gè) dex 文件的布局。
1.header(dex頭部)
header 概述了整個(gè) dex 文件的分布情況,包括了:magic, checksum, signature, file_size, header_size, endian_tag, link, map, string_ids, type_ids, proto_ids, field_ids, method_ids, class_defs, data。
checksum 和 signature 是校驗(yàn)值,修改后需要對(duì)其進(jìn)行修復(fù)
string_ids, type_ids, proto_ids, field_ids, method_ids 作為類型數(shù)組節(jié)區(qū)(我瞎起的)保存了不同類型的值
class_defs 存儲(chǔ)了類的定義也是我們修改的重點(diǎn)
data 是數(shù)據(jù)存儲(chǔ)區(qū),包括所有的數(shù)據(jù)
2.類型數(shù)組節(jié)區(qū)
類型數(shù)組節(jié)區(qū)包括了string_ids, type_ids, proto_ids, field_ids, method_ids。分別表示:字符串,類型,函數(shù)簽名,屬性,函數(shù)。每個(gè)節(jié)區(qū)都保存了對(duì)應(yīng)類型數(shù)據(jù)數(shù)組,可以用 010Editor 分析二進(jìn)制文件數(shù)據(jù)。
屬性示例:
3.類定義
類定義是修改的重點(diǎn),這里保存了所有類的結(jié)構(gòu),也是整個(gè) dex 文件中結(jié)構(gòu)最復(fù)雜的部分。其中包括了:靜態(tài)屬性變量、成員數(shù)形變量,虛函數(shù),直接函數(shù),靜態(tài)函數(shù)等數(shù)據(jù)。
通過分析 dex 文件格式,現(xiàn)在可以實(shí)現(xiàn)的混淆加密主要包括四種:
靜態(tài)變量隱藏
函數(shù)重復(fù)定義
函數(shù)隱藏
類定義隱藏
四種混淆加密的實(shí)現(xiàn)方式都是通過修改 class_def 結(jié)構(gòu)體中字段實(shí)現(xiàn)的??梢酝ㄟ^ json 格式了解一下 class_def 的結(jié)構(gòu)(這里只列出來要用到的字段):
{ "class_def": { "class_idx": 01 "static_values_off": 000, "class_data_off": 001, "class_data": { "direct_methods_size": 001, "virtual_methods_size": 002, "virtual_methods":[ { "code_off": 003 }, { "code_off": 004 } ] } } }
字段含義:
class_idx: 類名序號(hào),值是type_ids的一個(gè)index
class_def: 類定義結(jié)構(gòu)體
static_values_off: 靜態(tài)變量值偏移
class_data_off: 類定義偏移
class_data: 類定義結(jié)構(gòu)體
direct_methods_size: 直接函數(shù)個(gè)數(shù)
virtual_methods_size: 虛函數(shù)個(gè)數(shù)
virtual_methods: 虛函數(shù)結(jié)構(gòu)體
code_off: 函數(shù)代碼偏移
通過上面的字段介紹其實(shí)很容易得到四個(gè)功能的實(shí)現(xiàn)方案,下面一個(gè)一個(gè)介紹。
1.靜態(tài)變量隱藏static_vaules_off 保存了每個(gè)類中靜態(tài)變量的值的偏移量,指向 data 區(qū)里的一個(gè)列表,格式為 encode_array_item,如果沒有此項(xiàng)內(nèi)容,該值為0。所以要實(shí)現(xiàn)靜態(tài)變量賦值隱藏只需要將 static_values_off 值修改為0。
實(shí)現(xiàn)效果:
這里的靜態(tài)數(shù)組數(shù)據(jù)沒有成功隱藏,因?yàn)槲乙膊恢涝趺锤恪?
2.函數(shù)重復(fù)定義class_def -> class_data -> virtual_methods -> code_ff 表示的是某個(gè)類中某個(gè)函數(shù)的代碼偏移地址。這里需要提到一個(gè)概念:Java 中所有函數(shù)實(shí)現(xiàn)都是虛函數(shù),這一點(diǎn)和 C++ 是不一樣的,所有這里修改的都是 virtual_methods 中 code_off。
實(shí)現(xiàn)方式:讀取第一個(gè)函數(shù)的代碼偏移地址,將接下來的函數(shù)偏移地址都修改為第一的值。
實(shí)現(xiàn)效果:
class_def -> class_data -> virtual_methods_size 和 class_def -> class_data -> direct_methods_size 記錄了類定義中函數(shù)的個(gè)數(shù),如果沒有定義函數(shù)則該值為0。所以只要將該值改為0,函數(shù)定義就會(huì)被隱藏。
實(shí)現(xiàn)效果:
class_def -> class_data_off 保存了具體類定義的偏移地址,也就是 class_def -> class_data 的地址,如果該值為0則所有實(shí)現(xiàn)將被隱藏。隱藏后會(huì)把類定義的所有東西都隱藏包括成員變量,成員函數(shù),靜態(tài)變量,靜態(tài)函數(shù)。
實(shí)現(xiàn)效果:
上面一個(gè)章節(jié)主要介紹了功能實(shí)現(xiàn)的原理,接下來要介紹具體實(shí)現(xiàn)了。要實(shí)現(xiàn)修改 class_def 中字段,首先要把整個(gè) dex 文件結(jié)構(gòu)解析出來,當(dāng)然可以只是我們需要的字段。在工具中我定義的 dex 結(jié)構(gòu)如下,因?yàn)?class_def 結(jié)構(gòu)比較復(fù)雜所以獨(dú)立了一個(gè)包定義:
→ tree -L 2 . ├── DexFile.java ├── FieldIds.java ├── Header.java ├── MapList.java ├── MethodIds.java ├── ProtoIds.java ├── StringIds.java ├── TypeIds.java └── cladef ├── ClassData.java ├── ClassDefs.java ├── Code.java ├── EncodedField.java ├── EncodedMethod.java ├── EncodedValue.java └── StaticValues.java
也許你可能會(huì)疑問,我們功能實(shí)現(xiàn)時(shí)候只需要修改 class_def 為什么還需要讀取 string_ids 這些區(qū)段。這是因?yàn)橄裆厦嫣岬降?class_def -> class_idx 保存的其實(shí)是 type_ids 中的序號(hào),而 type_ids 中保存的是 string_ids 的序號(hào)。
為了靈活配置,運(yùn)行工具的時(shí)候我們只需要配置好要隱藏的類名,比如需要隱藏某個(gè)類的實(shí)現(xiàn) hack_me_size: cc.gnaixx.samp.core.EntranceImpl, 配置文件的具體實(shí)現(xiàn)下個(gè)章節(jié)介紹。
DexFile.java 定義了整個(gè) dex 文件結(jié)構(gòu), 實(shí)現(xiàn)比較簡(jiǎn)單只有一個(gè) read(byte[] dexBuff) 函數(shù)讀取整個(gè) dex 文件格式。
DexFile.java:
public class DexFile { public static final int HEADER_LEN = 0x70; public Header header; public StringIds stringIds; public TypeIds typeIds; public ProtoIds protoIds; public FieldIds fieldIds; public MethodIds methodIds; public ClassDefs classDefs; public MapList mapList; //reader dex public void read(byte[] dexBuff){ //read header byte[] headerbs = subdex(dexBuff, 0, HEADER_LEN); header = new Header(headerbs); //read string_ids stringIds = new StringIds(dexBuff, header.stringIdsOff, header.stringIdsSize); //read type_ids typeIds = new TypeIds(dexBuff, header.typeIdsOff, header.typeIdsSize); //read proto_ids protoIds = new ProtoIds(dexBuff, header.protoIdsOff, header.protoIdsSize); //read field_ids fieldIds = new FieldIds(dexBuff, header.fieldIdsOff, header.fieldIdsSize); //read method_ids methodIds = new MethodIds(dexBuff, header.methodIdsOff, header.methodIdsSize); //read class_defs classDefs = new ClassDefs(dexBuff, header.classDefsOff, header.classDefsSize); //read map_list mapList = new MapList(dexBuff, header.mapOff); } }
第一步要先讀取 header 因?yàn)樗4媪似渌?jié)區(qū)的偏移地址和個(gè)數(shù)。
Header.java:
public class Header { public byte[] magic = new byte[MAGIC_LEN]; public int checksum; public byte[] signature = new byte[SIGNATURE_LEN]; public int fileSize; public int headerSize; public int endianTag; public int linkSize; public int linkOff; public int mapOff; public int stringIdsSize; public int stringIdsOff; public int typeIdsSize; public int typeIdsOff; public int protoIdsSize; public int protoIdsOff; public int fieldIdsSize; public int fieldIdsOff; public int methodIdsSize; public int methodIdsOff; public int classDefsSize; public int classDefsOff; public int dataSize; public int dataOff; public Header(byte[] headerBuff) { Reader reader = new Reader(headerBuff, 0); this.magic = reader.subdex(MAGIC_LEN); this.checksum = reader.readUint(); this.signature = reader.subdex(SIGNATURE_LEN); //...... } public void write(byte[] dexBuff){ Writer writer = new Writer(dexBuff, 0); writer.replace(magic, MAGIC_LEN); writer.writeUint(checksum); writer.replace(signature, SIGNATURE_LEN); //..... } }
知道了各個(gè)節(jié)區(qū)的偏移地址和個(gè)數(shù)接下來的讀取就比較簡(jiǎn)單了,比如 string_ids 節(jié)區(qū)的讀取。
StringIds.java:
public class StringIds { class StringId { int dataOff; //字符串偏移位置 Uleb128 utf16Size; //字符串長(zhǎng)度 byte data[]; //字符串?dāng)?shù)據(jù) public StringId(int dataOff, Uleb128 uleb128, byte[] data) { this.dataOff = dataOff; this.utf16Size = uleb128; this.data = data; } } StringId stringIds[]; public StringIds(byte[] dexBuff, int off, int size) { this.stringIds = new StringId[size]; Reader reader = new Reader(dexBuff, off); for (int i = 0; i < size; i++) { int dataOff = reader.readUint(); Uleb128 utf16Size = getUleb128(dexBuff, dataOff); byte[] data = subdex(dexBuff, dataOff + 1, utf16Size.getVal()); StringId stringId = new StringId(dataOff, utf16Size, data); stringIds[i] = stringId; } } public String getData(int id) { //return "(" + id + ")" + new String(stringIds[id].data); return new String(stringIds[id].data); } }
其他節(jié)區(qū)的讀取和 string_ids 類似,但是 class_def 節(jié)區(qū)結(jié)構(gòu)比較復(fù)雜,讀取起來可能比較麻煩。但是其實(shí)我們要用的值并不是很多,只需要關(guān)注那幾個(gè)字段就好了。
ClassDefs.java:
public class ClassDefs { public class ClassDef { public int classIdx; //class類型,對(duì)應(yīng)type_ids public int accessFlags; //訪問類型,enum public int superclassIdx; //supperclass類型,對(duì)應(yīng)type_ids public int interfacesOff; //接口偏移,對(duì)應(yīng)type_list public int sourceFileIdx; //源文件名,對(duì)應(yīng)string_ids public int annotationsOff; //class注解,位置位于data區(qū),對(duì)應(yīng)annotation_direcotry_item public HackPoint classDataOff; //class具體用到的數(shù)據(jù),位于data區(qū),格式為class_data_item,描述class的field,method,method執(zhí)行代碼 public HackPoint staticValueOff; //位于data區(qū),格式為encoded_array_item public StaticValues staticValues; // classDataOff不為0時(shí)存在 public ClassData classData; // staticValueOff不為0存在 public ClassDef(int classIdx, int accessFlags, int superclassIdx, int interfacesOff, int sourceFileidx, int annotationsOff, HackPoint classDataOff, HackPoint staticValueOff) { this.classIdx = classIdx; this.accessFlags = accessFlags; this.superclassIdx = superclassIdx; this.interfacesOff = interfacesOff; this.sourceFileIdx = sourceFileidx; this.annotationsOff = annotationsOff; this.classDataOff = classDataOff; this.staticValueOff = staticValueOff; } public void setClassData(ClassData classData){ this.classData = classData; } public void setStaticValue(StaticValues staticValues){ this.staticValues = staticValues; } } int offset; //偏移位置 int size; //大小 public ClassDef classDefs[]; public ClassDefs(byte[] dexBuff, int off, int size) { this.offset = off; this.size = size; Reader reader = new Reader(dexBuff, off); classDefs = new ClassDef[size]; for (int i = 0; i < size; i++) { int classIdx = reader.readUint(); int accessFlags = reader.readUint(); int superclassIdx = reader.readUint(); int interfacesOff = reader.readUint(); int sourcFileIdx = reader.readUint(); int annotationOff = reader.readUint(); HackPoint classDataOff = new HackPoint(HackPoint.UINT, reader.getOff(), reader.readUint()); HackPoint staticValueOff = new HackPoint(HackPoint.UINT, reader.getOff(), reader.readUint()); ClassDef classDef = new ClassDef( classIdx, accessFlags, superclassIdx, interfacesOff, sourcFileIdx, annotationOff, classDataOff, staticValueOff); if(staticValueOff.value != 0){ Reader reader1 = new Reader(dexBuff, staticValueOff.value); Uleb128 staticSize = reader1.readUleb128(); StaticValues staticValues = new StaticValues(staticSize); classDef.setStaticValue(staticValues); } if(classDataOff.value != 0){ classDef.setClassData(new ClassData(dexBuff, classDataOff.value)); } classDefs[i] = classDef; } } public void write(byte[] dexBuff){ Writer writer = new Writer(dexBuff, offset); for(int i=0; i這里需要介紹一下 dex 特有的一種數(shù)據(jù)類型 LEB128 官方介紹如下:
LEB128 ("Little-Endian Base 128") is a variable-length encoding for arbitrary signed or unsigned integer quantities. The format was borrowed from the DWARF3 specification. In a .dex file, LEB128 is only ever used to encode 32-bit quantities.
Each LEB128 encoded value consists of one to five bytes, which together represent a single 32-bit value. Each byte has its most significant bit set except for the final byte in the sequence, which has its most significant bit clear. The remaining seven bits of each byte are payload, with the least significant seven bits of the quantity in the first byte, the next seven in the second byte and so on. In the case of a signed LEB128 (sleb128), the most significant payload bit of the final byte in the sequence is sign-extended to produce the final value. In the unsigned case (uleb128), any bits not explicitly represented are interpreted as 0.
也就是說 LEB128 是基于 1 個(gè) Byte 的一種不定長(zhǎng)度的編碼方式 。若第一個(gè) Byte 的最高位為 1 ,則表示還需要下一個(gè) Byte 來描述 ,直至最后一個(gè) Byte 的最高 位為 0 。每個(gè) Byte 的其余 Bit 用來表示數(shù)據(jù)。
代碼中用 ULeb128.java(unsigned 無符號(hào)) 表示是該結(jié)構(gòu),通過分析Android源碼 Leb128.h可以知道 LEB128 雖然表示的是不定長(zhǎng)格式,但是在 Android 中只用到了4 個(gè)byte,所以只需要用int表示就可以了。
ULeb128.java:
public class Uleb128 { byte[] realVal; //存儲(chǔ)的byte數(shù)據(jù) int val; //表示的整型數(shù)據(jù) public Uleb128(byte[] realVal, int val){ this.realVal = realVal; this.val = val; } public int getSize(){ return this.realVal.length; } public int getVal(){ return this.val; } public byte[] getRealVal(){ return this.realVal; } }Bytes to ULEB128:
//Reader.java public Uleb128 readUleb128() { int value = 0; int count = 0; byte realVal[] = new byte[4]; boolean flag = false; do { flag = false; byte seg = buffer[offset]; if ((seg & 0x80) == 0x80) { //高8位為1 flag = true; } seg = (byte) (seg & 0x7F); value += seg << (7 * count); realVal[count] = buffer[offset]; count++; offset++; } while (flag); return new Uleb128(BufferUtil.subdex(realVal, 0, count), value); }Integer to ULEB128:
//Trans.java public static Uleb128 intToUleb128(int val) { byte[] realVal = new byte[]{0x00, 0x00, 0x00, 0x00}; //int 最大長(zhǎng)度為4 int bk = val; int len = 0; for (int i = 0; i < realVal.length; i++) { len = i + 1; //最少長(zhǎng)度為1 realVal[i] = (byte) (val & 0x7F); //獲取低7位的值 if (val > (0x7F)) { realVal[i] |= 0x80; //高位為1 加上去 } val = val >> 7; if (val <= 0) break; } Uleb128 uleb128 = new Uleb128(BufferUtil.subdex(realVal, 0, len), bk); return uleb128; }0x04 HackPoint格式HackPoint 表示修改后的數(shù)據(jù)結(jié)構(gòu),代碼中把所有要修改的的字段都用 HackPoint 類型表示。HackPoint 類型有三個(gè)字段 type、offset、value,都是 int 類型分別表示:類型、偏移地址、原始值。類型主要有三種 uint(unsigned int)、ushort(unsigned short 2byte)、uleb128。這三種數(shù)據(jù)用 int 存儲(chǔ)都足夠了。
HackPoint.java:public class HackPoint implements Cloneable { public static final int UINT = 0x01; public static final int USHORT = 0x02; public static final int ULEB128 = 0x03; public int type; //數(shù)據(jù)類型 public int offset; //偏移地址 public int value; //原始值 public HackPoint(int type, int offset, int val) { this.type = type; this.offset = offset; this.value = val; } @Override public HackPoint clone() { HackPoint hp = null; try { hp = (HackPoint) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return hp; } }在修改完后會(huì)把所有的 HackPoint 數(shù)據(jù)寫在 dex 文件的末尾。本來 dex 文件末尾是 map_list 區(qū)段,數(shù)據(jù)格式是 :
struct map_list{ ushort type; ushort unused; uint size; uint offset; };剛好是 12 byte, 所以 HackPoint 寫入 dex 文件的格式為:
0x05 配置文件配置文件的定義比較簡(jiǎn)單看一下示例就知道了:
#################################################### # hack_class: 隱藏類定義 # hack_sf_val: 隱藏靜態(tài)變量 # hack_me_size: 隱藏methods # hack_me_def: 重復(fù)函數(shù)定義(以第一個(gè)為準(zhǔn)) ##################################################### #隱藏靜態(tài)變量值 hack_sf_val: cc.gnaixx.samp.core.EntranceImpl #重復(fù)函數(shù)定義(以第一個(gè)為準(zhǔn)) hack_me_def: cc.gnaixx.samp.core.EntranceImpl #隱藏函數(shù)實(shí)現(xiàn) hack_me_size: cc.gnaixx.samp.core.EntranceImpl #隱藏整個(gè)類實(shí)現(xiàn) hack_class: cc.gnaixx.samp.core.EntranceImpl cc.gnaixx.samp.BuildConfig當(dāng)多個(gè)類需要實(shí)現(xiàn)同一個(gè)功能的時(shí)候只需要用空格分隔就可以了
配置文件讀取代碼:
public static Map0x06 dex混淆隱藏> readConfig(String path) { try { Map > config = new HashMap<>(); FileReader fr = new FileReader(path); BufferedReader br = new BufferedReader(fr); String line; while ((line = br.readLine()) != null) { if (!line.startsWith("#") && !line.equals("")) { String conf[] = line.split(":"); if (conf.length != 2) { log("warning", "error config at :" + line); System.exit(0); } String key = conf[0]; String values[] = conf[1].split(" "); List valueList = new ArrayList<>(); for (int i = 0; i < values.length; i++) { if (values[i] != null && !values[i].equals("")) { valueList.add(values[i]); } } config.put(key, valueList); } } fr.close(); br.close(); return config; } catch (Exception e) { e.printStackTrace(); } return null; } dex 文件混淆隱藏主要包括三個(gè)步驟:
修改 HackPoint 并保存到 dex 文件末尾
修復(fù)Header
1.修改 HackPoint通過取得的配置文件中的配置類遍歷 class_def_item:
//查找配置文件所在類位置 private void seekHP(ClassDefs.ClassDef[] classDefItem, Listconf, String type, SeekCallBack callBack){ if (conf == null) { return; } for (int i = 0; i < conf.size(); i++) { String classname = conf.get(i); boolean isDef = false; for (int j = 0; j < classDefItem.length; j++) { String className = dexFile.typeIds.getString(dexFile, classDefItem[j].classIdx); //查找順序 class_idx => type_ids => string_ids className = pathToPackages(className); //獲取類名 if (className.equals(classname)) { callBack.doHack(classDefItem[j], this.hackPoints); //具體操作 log(type, conf.get(i)); isDef = true; } } if (isDef == false) { log("warning", "con"t find class:" + classname); } } } //具體操作回調(diào)處理 interface SeekCallBack { void doHack(ClassDefs.ClassDef classDefItem, List hackPoints); } 隱藏靜態(tài)變量值:
//隱藏靜態(tài)變量初始化 private void hackSfVal(ClassDefs.ClassDef[] classDefItem, Listconf) { seekHP(classDefItem, conf, Constants.HACK_SF_VAL, new SeekCallBack() { @Override public void doHack(ClassDefs.ClassDef classDefItem, List hackPoints) { HackPoint point = classDefItem.staticValueOff.clone(); //獲取靜態(tài)變量數(shù)據(jù)偏移 hackPoints.add(point); //添加修改點(diǎn) classDefItem.staticValueOff.value = 0; //將靜態(tài)變量的偏移改為0(隱藏賦值) } }); } 函數(shù)重復(fù)定義:
//重復(fù)函數(shù)定義 private void hackMeDef(ClassDefs.ClassDef[] classDefItem, Listconf){ seekHP(classDefItem, conf, Constants.HACK_ME_DEF, new SeekCallBack() { @Override public void doHack(ClassDefs.ClassDef classDefItem, List hackPoints) { //以第一個(gè)為默認(rèn)值 int virtualMeSize = classDefItem.classData.virtualMethodsSize.value; int virtualMeCodeOff = 0; for (int i = 0; i < virtualMeSize; i++) { if (i == 0) { virtualMeCodeOff = classDefItem.classData.virtualMethods[i].codeOff.value; }else{ HackPoint point = classDefItem.classData.virtualMethods[i].codeOff.clone(); hackPoints.add(point); classDefItem.classData.virtualMethods[i].codeOff.value = virtualMeCodeOff; } } } }); } 函數(shù)隱藏:
//隱藏函數(shù)定義 private void hackMeSize(ClassDefs.ClassDef[] classDefItem, Listconf){ seekHP(classDefItem, conf, Constants.HACK_ME_SIZE, new SeekCallBack() { @Override public void doHack(ClassDefs.ClassDef classDefItem, List hackPoints) { HackPoint directPoint = classDefItem.classData.directMethodsSize.clone(); //同時(shí)需改虛函數(shù)和直接函數(shù) HackPoint virtualPoint = classDefItem.classData.virtualMethodsSize.clone(); hackPoints.add(directPoint); hackPoints.add(virtualPoint); classDefItem.classData.directMethodsSize.value = 0; classDefItem.classData.virtualMethodsSize.value = 0; } }); } 隱藏類:
//隱藏靜態(tài)變量初始化 private void hackSfVal(ClassDefs.ClassDef[] classDefItem, Listconf) { seekHP(classDefItem, conf, Constants.HACK_SF_VAL, new SeekCallBack() { @Override public void doHack(ClassDefs.ClassDef classDefItem, List hackPoints) { HackPoint point = classDefItem.staticValueOff.clone(); //獲取靜態(tài)變量數(shù)據(jù)偏移 hackPoints.add(point); //添加修改點(diǎn) classDefItem.staticValueOff.value = 0; //將靜態(tài)變量的偏移改為0(隱藏賦值) } }); } 添加 HackPoint 數(shù)據(jù)到 dex 文件:
//保留修改信息 private void appendHP() { byte[] pointsBuff = new byte[]{}; for (int i = 0; i < hackPoints.size(); i++) { byte[] pointBuff = hackpToBin(hackPoints.get(i)); pointsBuff = BufferUtil.append(pointsBuff, pointBuff, pointBuff.length); } dexBuff = BufferUtil.append(dexBuff, pointsBuff, pointsBuff.length); } //hackPoint 轉(zhuǎn) 二進(jìn)制 public static byte[] hackpToBin(HackPoint point) { ByteBuffer bb = ByteBuffer.allocate(4 * 3); bb.put(intToBin_Lit(point.type)); bb.put(intToBin_Lit(point.offset)); bb.put(intToBin_Lit(point.value)); return bb.array(); } //小端二進(jìn)制 public static byte[] intToBin_Lit(int integer){ byte[] bin = new byte[]{ (byte) ((integer >> 0) & 0xFF), (byte) ((integer >> 8) & 0xFF), (byte) ((integer >> 16) & 0xFF), (byte) ((integer >> 24) & 0xFF) }; return bin; }dex 文件都是以小端數(shù)據(jù)保存
2.修復(fù)HeaderHeader 中修復(fù)的數(shù)據(jù)有三個(gè):
文件長(zhǎng)度
checksum
signature
修改代碼:
//修改header private void hackHeader() { //修改文件長(zhǎng)度 Header header = dexFile.header; header.fileSize = this.dexBuff.length; header.write(dexBuff); //需要先修改文件長(zhǎng)度,才能計(jì)算signature checksum //修復(fù) signature 校驗(yàn) log("old_signature", binToHex(dexFile.header.signature)); byte[] signature = signature(dexBuff, SIGNATURE_LEN + SIGNATURE_OFF); header.signature = signature; log("new_signature", binToHex(signature)); header.write(dexBuff); //需要先寫sinature,才能計(jì)算checksum,凸 //修復(fù) checksum 校驗(yàn) log("old_checksum", intToHex(dexFile.header.checksum)); int checksum = checksum_Lit(dexBuff, CHECKSUM_LEN + CHECKSUM_OFF); header.checksum = checksum; log("new_checksum", intToHex(checksum)); header.write(dexBuff); } //計(jì)算signature public static byte[] signature(byte[] data, int off) { int len = data.length - off; byte[] signature = SHA1(data, off, len); return signature; } //sha1算法 public static byte[] SHA1(byte[] decript, int off, int len) { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.update(decript, off, len); byte messageDigest[] = digest.digest(); return messageDigest; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return null; } //計(jì)算checksum 值 public static int checksum_Lit(byte[] data, int off) { byte[] bin = checksum_bin(data, off); int value = 0; for (int i = 0; i < UINT_LEN; i++) { int seg = bin[i]; if (seg < 0) { seg = 256 + seg; } value += seg << (8 * i); } return value; } //計(jì)算checksum public static byte[] checksum_bin(byte[] data, int off) { int len = data.length - off; Adler32 adler32 = new Adler32(); adler32.reset(); adler32.update(data, off, len); long checksum = adler32.getValue(); byte[] checksumbs = new byte[]{ (byte) checksum, (byte) (checksum >> 8), (byte) (checksum >> 16), (byte) (checksum >> 24)}; return checksumbs; }該部分代碼地址: HidexHandle.java
0x07 dex還原相對(duì)于加密解密過程簡(jiǎn)單了很多,只要根據(jù) HackPoint 數(shù)據(jù)一一修復(fù)就好了。這里簡(jiǎn)單的說下修復(fù)步驟:
讀取 Header 中 map_list 的偏移地址和個(gè)數(shù),因?yàn)?HackPoint 數(shù)據(jù)保存在 map_list 之后
讀取 HackPoint 數(shù)據(jù)并修復(fù) dex 文件
修復(fù) Header 中的 file_size、checksum、signature
java 實(shí)現(xiàn)修復(fù)關(guān)鍵源碼:
//修復(fù)dex文件 public byte[] redex() { int mapOff = getUint(dexBuff, MAP_OFF_OFF); //獲取map_off int mapSize = getUint(dexBuff, mapOff); //獲取map_size int hackInfoStart = mapOff + UINT_LEN + (mapSize * MAP_ITEM_LEN); //獲取 hackinfo 開始地址 int hackInfoLen = dexBuff.length - hackInfoStart; //獲取hackinfo 長(zhǎng)度 hackInfoBuff = subdex(dexBuff, hackInfoStart, hackInfoLen); //獲取hack數(shù)據(jù) int dexLen = dexBuff.length - hackInfoLen; dexBuff = subdex(dexBuff, 0, dexLen); //截取原始dex長(zhǎng)度 HackPoint[] hackPoints = Trans.binToHackP(hackInfoBuff); //修復(fù)hack點(diǎn) for (int i = 0; i < hackPoints.length; i++) { log("hackPoint", JSON.toJSONString(hackPoints[i])); recovery(hackPoints[i]); } byte[] fileSize = intToBin_Lit(dexLen); //修復(fù)文件長(zhǎng)度 replace(dexBuff, fileSize, FILE_SIZE_OFF, UINT_LEN); byte[] signature = signature(dexBuff, SIGNATURE_LEN + SIGNATURE_OFF); //修復(fù)signature校驗(yàn) replace(dexBuff, signature, SIGNATURE_OFF, SIGNATURE_LEN); byte[] checksum = checksum_bin(dexBuff, CHECKSUM_LEN + CHECKSUM_OFF); //修復(fù)checksum校驗(yàn) replace(dexBuff, checksum, CHECKSUM_OFF, CHECKSUM_LEN); log("fileSize", dexLen); log("signature", binToHex(signature)); log("checksum", binToHex_Lit(checksum)); return this.dexBuff; } //還原原始值 private void recovery(HackPoint hackPoint) { Writer writer = new Writer(this.dexBuff, hackPoint.offset); if (hackPoint.type == HackPoint.USHORT) { writer.writeUshort(hackPoint.value); } else if (hackPoint.type == HackPoint.UINT) { writer.writeUint(hackPoint.value); } else if (hackPoint.type == HackPoint.ULEB128) { Uleb128 uleb128 = Trans.intToUleb128(hackPoint.value); writer.writeUleb128(uleb128); } }c++ 實(shí)現(xiàn)工具本身就是為了實(shí)現(xiàn)安全加固,那么用 java 實(shí)現(xiàn)意義就小了很多,所以工具包里面的實(shí)現(xiàn)我是用 NDK 開發(fā)的。
修復(fù)關(guān)鍵源碼:
//解密dex void recode(char* source, uint sourceLen, char* target, uint* targetLen){ uint mapOff = readUint(source, MAP_OFF_OFF); //獲取map_off uint mapSize = readUint(source, mapOff); //獲取map_size LOGD("mapInfo: {map_off:%d, map_size:%d}", mapOff, mapSize); uint hackInfoOff = mapOff + UINT_LEN + (mapSize * MAP_ITEM_LEN); //定位hackInfo位置 uint hackInfoLen = sourceLen - hackInfoOff; //hackInfo長(zhǎng)度 char* hackInfo = (char *) calloc(hackInfoLen, sizeof(char)); memcpy(hackInfo, source + hackInfoOff, hackInfoLen); //復(fù)制hackInfo LOGD("hackInfo: {hackInfo_off:%d, hackInfo_len}", hackInfoOff, hackInfoLen); uint hackPointSize = hackInfoLen / sizeof(HackPoint); //獲取hackPoint結(jié)構(gòu)體 HackPoint* hackPoints = (HackPoint *) calloc(hackPointSize, sizeof(HackPoint)); initHP(hackPoints, hackInfo, hackPointSize); //將hockInfo 轉(zhuǎn)化為結(jié)構(gòu)體 *targetLen = hackInfoOff; memcpy(target, source, *targetLen); //恢復(fù)原始長(zhǎng)度 //恢復(fù)數(shù)據(jù) for(int i=0; i完整源碼地址: hidex.cpp
0x09 總結(jié)整體功能還是比較簡(jiǎn)單,實(shí)現(xiàn)的代碼也不是很復(fù)雜,但是這些都需要基于對(duì) dex 文件格式的了解的前提下。
另外該工具存在一個(gè)缺點(diǎn),dex 的加載問題。Android中加載 dex 的 DexClassLoad 只支持文件路徑加載,不像 java 中的 ClassLoad 可以支持二進(jìn)制流加載,所以在加載 dex 是就存在加密后的 dex 緩存,這是非常危險(xiǎn)的。所以下個(gè)研究的點(diǎn)也就是自定義 DexClassLoad 實(shí)現(xiàn)不落地加載。(很多安全加固廠商老早就實(shí)現(xiàn)了?)。
雖然功能不算強(qiáng)大,也有不少缺點(diǎn),不過也花了自己不少時(shí)間研究,對(duì) dex 文件格式也有點(diǎn)了解,也算值得了。原文地址: DEX文件混淆加密
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/66643.html
摘要:網(wǎng)易資深安全工程師鐘亞平在今年的安卓巴士全球開發(fā)者論壇上做了安卓逆向與保護(hù)的演講完整演講內(nèi)容請(qǐng)見這里一文了解安卓逆向分析與保護(hù)機(jī)制,其中就談到了關(guān)于代碼混淆的問題。就是一個(gè)混淆代碼的開源項(xiàng)目,能夠?qū)ψ止?jié)碼進(jìn)行混淆縮減體積優(yōu)化等處理。 歡迎訪問網(wǎng)易云社區(qū),了解更多網(wǎng)易技術(shù)產(chǎn)品運(yùn)營(yíng)經(jīng)驗(yàn)。 在大公司怎么做android代碼混淆的?發(fā)現(xiàn)他們的軟件用apktool反編譯居然沒看到classes....
摘要:為了防止這種現(xiàn)象,我們可以對(duì)字節(jié)碼進(jìn)行混淆。動(dòng)態(tài)鏈接庫是目標(biāo)文件的集合,目標(biāo)文件在動(dòng)態(tài)鏈接庫中的組織方式是按照特殊方式形成的。 一、已知防護(hù)策略 1.不可或缺的混淆 Java 是一種跨平臺(tái)、解釋型語言,Java 源代碼編譯成的class文件中有大量包含語義的變量名、方法名的信息,很容易被反編譯為Java 源代碼。為了防止這種現(xiàn)象,我們可以對(duì)Java字節(jié)碼進(jìn)行混淆?;煜粌H能將代碼中的類...
閱讀 2503·2021-11-24 09:39
閱讀 3427·2021-11-15 11:37
閱讀 2279·2021-10-08 10:04
閱讀 3986·2021-09-09 11:54
閱讀 1899·2021-08-18 10:24
閱讀 1074·2019-08-30 11:02
閱讀 1811·2019-08-29 18:45
閱讀 1668·2019-08-29 16:33