首先我们需要了解一代壳的原理,一代壳是对dex文件进行加密,反编译只能看见壳程序的代码,只能通过IDA动态调试或者使用Xposed等HOOK框架,hook相关API在App运行时dump出解密后的dex文件,这两种方法都是通过内存dump出解密后的dex文件来进行脱壳的。
针对上面一代壳的简单描述,我们引出二代壳的功能:防止内存dump出dex文件。
将需要保护的源码隐藏起来,通过的就是修改dex文件结构来删除指令集,这样即使dump出的dex文件也是不完整的。
这里需要了解dex文件结构,这里大概说一下,dex文件结构中的倒数第二个class def段存储着源码中类的各种详细信息,我们关注和修改的就是其中encode_method结构体,这个结构体保存中类中方法的详细信息,也是源码的逻辑结构,需要保护起来的,这个结构体里的的code_item就是这个方法中的代码信息,我们只要把指令集(指令集构成的每一行代码)置空,也就是删除了这个方法内部逻辑代码,这个方法也就成了空方法,即使dump出来也没什么作用。
进行下面dex文件格式解析过程,需要对dex文件格式有一定的了解,可以看尼古拉斯赵四的dex文件解析的博客。
1、首先需要遍历dex文件的class段
public static void parseClassIds(byte[] srcByte){
int idSize = ClassDefItem.getSize();
int countIds = classIdsSize;
// System.out.println("Total " + String.valueOf(countIds) + " classes(自定义类)\n");
for(int i=0;i<countIds;i++){
ClassDefItem item = new ClassDefItem();
byte[] classItemByte = Utils.copyByte(srcByte, classIdsOffset+i*idSize, idSize);
byte[] classIdxByte = Utils.copyByte(classItemByte, 0, 4);
item.class_idx = Utils.byte2int(classIdxByte);
byte[] accessFlagsByte = Utils.copyByte(classItemByte, 4, 4);
item.access_flags = Utils.byte2int(accessFlagsByte);
byte[] superClassIdxByte = Utils.copyByte(classItemByte, 8, 4);
item.superclass_idx = Utils.byte2int(superClassIdxByte);
byte[] iterfacesOffByte = Utils.copyByte(classItemByte, 12, 4);
item.iterfaces_off = Utils.byte2int(iterfacesOffByte);
byte[] sourceFileIdxByte = Utils.copyByte(classItemByte, 16, 4);
item.source_file_idx = Utils.byte2int(sourceFileIdxByte);
byte[] annotationsOffByte = Utils.copyByte(classItemByte, 20, 4);
item.annotations_off = Utils.byte2int(annotationsOffByte);
byte[] classDataOffByte = Utils.copyByte(classItemByte, 24, 4);
item.class_data_off = Utils.byte2int(classDataOffByte);
byte[] staticValueOffByte = Utils.copyByte(classItemByte, 28, 4);
item.static_value_off = Utils.byte2int(staticValueOffByte);
classIdsList.add(item);
}
2、解析class段下的每个类的类数据,也就是解析每个classItemData中的方法字段。
//directMethods
EncodedMethod[] staticMethodsAry = new EncodedMethod[item.direct_methods_size];
for(int i=0;i<item.direct_methods_size;i++){
/**
* public byte[] method_idx_diff;
public byte[] access_flags;
public byte[] code_off;
*/
EncodedMethod directMethod = new EncodedMethod();
directMethod.method_idx_diff = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += directMethod.method_idx_diff.length;
directMethod.access_flags = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += directMethod.access_flags.length;
directMethod.code_off = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += directMethod.code_off.length;
staticMethodsAry[i] = directMethod;
}
//virtualMethods
EncodedMethod[] instanceMethodsAry = new EncodedMethod[item.virtual_methods_size];
for(int i=0;i<item.virtual_methods_size;i++){
/**
* public byte[] method_idx_diff;
public byte[] access_flags;
public byte[] code_off;
*/
EncodedMethod instanceMethod = new EncodedMethod();
instanceMethod.method_idx_diff = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += instanceMethod.method_idx_diff.length;
instanceMethod.access_flags = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += instanceMethod.access_flags.length;
instanceMethod.code_off = Utils.readUnsignedLeb128(srcByte, dataOffset);
dataOffset += instanceMethod.code_off.length;
instanceMethodsAry[i] = instanceMethod;
}
3、进一步向结构体内部解析,找到code结构体的指令集数组。
/ System.out.printf("\tDirect methods\t-\n");
if(item.direct_methods.length != 0) {
for(int i=0; i<item.direct_methods.length; i++) {
int methodIndex = Utils.decodeUleb128(item.direct_methods[i].method_idx_diff);
int accessflag = Utils.decodeUleb128(item.direct_methods[i].access_flags);
int code_off = Utils.decodeUleb128(item.direct_methods[i].code_off);
if(code_off == 0) {
System.out.printf("\t\t null code item");
continue;
}
//解析code_item结构体
byte[] codeItemByte = Utils.copyByte(srcByte, code_off, 16);
ClassCodeItem mClassCodeItem = new ClassCodeItem();
mClassCodeItem.registersSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 0, 2));
mClassCodeItem.insSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 2, 2));
mClassCodeItem.outsSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 4, 2));
mClassCodeItem.triesSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 6, 2));
mClassCodeItem.debugInfoOff = Utils.byte2int(Utils.copyByte(codeItemByte, 8, 4));
mClassCodeItem.insnsSize = Utils.byte2int(Utils.copyByte(codeItemByte, 12, 4));
byte[] instruction_byte = Utils.copyByte(srcByte, code_off+16, mClassCodeItem.insnsSize*2);
for(int j=0; j<mClassCodeItem.insnsSize; j++) {
mClassCodeItem.insns.add(Utils.byte2Short(Utils.copyByte(instruction_byte, 2*j, 2)));
}
System.out.printf("\t\t name\t:%s\n", stringList.get(methodIdsList.get(methodIndex).name_idx));
System.out.printf("\t\t instructions:%s\n", mClassCodeItem.insns.toString());
System.out.printf("\t\t 指令置空:\n");
if(flag == 0) {
dexByte = set_instru2null(srcByte, code_off+16, mClassCodeItem.insnsSize*2);
byte[] null_instruction = Utils.copyByte(dexByte, code_off+16, mClassCodeItem.insnsSize*2);
flag++;
}else {
dexByte = set_instru2null(dexByte, code_off+16, mClassCodeItem.insnsSize*2);
}
byte[] null_byte = Utils.copyByte(dexByte, code_off+16, mClassCodeItem.insnsSize*2);
System.out.println("\t\t" + Utils.bytesToHexString(null_byte)+"\n");
}
}
if(item.virtual_methods.length != 0) {
for(int i=0; i<item.virtual_methods.length; i++) {
int methodIndex = Utils.decodeUleb128(item.virtual_methods[i].method_idx_diff);
int accessflag = Utils.decodeUleb128(item.virtual_methods[i].access_flags);
int code_off = Utils.decodeUleb128(item.virtual_methods[i].code_off);
if(code_off == 0) {
System.out.printf("\t\t null code item");
continue;
}
//解析code_item结构体
byte[] codeItemByte = Utils.copyByte(srcByte, code_off, 16);
ClassCodeItem mClassCodeItem = new ClassCodeItem();
mClassCodeItem.registersSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 0, 2));
mClassCodeItem.insSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 2, 2));
mClassCodeItem.outsSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 4, 2));
mClassCodeItem.triesSize = Utils.byte2Short(Utils.copyByte(codeItemByte, 6, 2));
mClassCodeItem.debugInfoOff = Utils.byte2int(Utils.copyByte(codeItemByte, 8, 4));
mClassCodeItem.insnsSize = Utils.byte2int(Utils.copyByte(codeItemByte, 12, 4));
byte[] instruction_byte = Utils.copyByte(srcByte, code_off+16, mClassCodeItem.insnsSize*2);
for(int j=0; j<mClassCodeItem.insnsSize; j++) {
mClassCodeItem.insns.add(Utils.byte2Short(Utils.copyByte(instruction_byte, 2*j, 2)));
}
System.out.printf("\t\t name\t:%s\n", stringList.get(methodIdsList.get(methodIndex).name_idx));
System.out.printf("\t\t instructions:%s\n", mClassCodeItem.insns.toString());
System.out.printf("\t\t 指令置空:\n");
if(flag == 0) {
dexByte = set_instru2null(srcByte, code_off+16, mClassCodeItem.insnsSize*2);
flag++;
}else {
dexByte = set_instru2null(dexByte, code_off+16, mClassCodeItem.insnsSize*2);
}
byte[] null_byte = Utils.copyByte(dexByte, code_off+16, mClassCodeItem.insnsSize*2);
System.out.println("\t\t" + Utils.bytesToHexString(null_byte)+"\n");
}
}
4、上面代码解析出指令数组后,使用了set_instru2null方法将指令偏移处指定大小的字节流置0,来返回一个指令集为0的dex文件的字节流。
public static byte[] set_instru2null(byte[] src, int start, int len) {
if(src == null){
return null;
}
if(start > src.length){
return null;
}
if((start+len) > src.length){
return null;
}
if(start<0){
return null;
}
if(len<=0){
return null;
}
byte[] resultByte = new byte[src.length];
for(int i=0; i<src.length-1; i++) {
if(i<start) {
resultByte[i] = src[i];
}else if((i-start) < len){
resultByte[i] = 0;
}else {
resultByte[i] = src[i];
}
}
return resultByte;
}
上面的代码主要都是对dex文件格式的解析,需要对dex文件格式有了解,可以参考我github上的工具readdex.jar。然后将下图中所示的指令集置0,也就隐藏了代码。
下面通过Jadx打开经过更改的dex文件的对比,可以从图中明显看出改过指令的dex文件方法内部的代码全部被隐藏了。
dex文件头中有两个字段,随着dex文件格式的修改是要进行改变的,否则安装apk的时候,会通不过系统校验。
checksum:文件校验码,除 magic 和此字段之外的文件剩下内容的 adler32 校验和,用于检测文件损坏情况;
signature:SHA-1 签名,除 magic、checksum 和此字段之外的文件的内容的 SHA-1 签名(哈希),用于对文件进行唯一标识。
也就需要写两个方法分别进行adler32校验和SHA1摘要。
先进行SHA1摘要,然后再进行CRC计算:
//替换校验值
public static void resetDexCheckSum(byte[] src) {
byte[] SHA1byte = new byte[src.length-33];
System.arraycopy(src, 32, SHA1byte, 0, src.length-33);
byte[] sha1 = getSHA1(SHA1byte);
replaceByte(dexByte, 12, sha1);
byte[] checkByte = checksum_bin(dexByte, 12);
replaceByte(dexByte, 8, checkByte);
}
//替换指定位置的字节数组
public static void replaceByte(byte[] src, int offset, byte[] repByte) {
for(int i=0; i<repByte.length; i++) {
src[offset+i] = repByte[i];
}
}
//获取SHA1值
public static byte[] getSHA1(byte[] bt) {
MessageDigest mMessageDigest;
byte[] messageDigest = null;
try {
mMessageDigest = MessageDigest.getInstance("SHA-1");
mMessageDigest.update(bt);
messageDigest = mMessageDigest.digest();
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < messageDigest.length; i++) {
String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
if (shaHex.length() < 2) {
hexString.append(0);
}
hexString.append(shaHex);
}
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return messageDigest;
}
//计算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;
}
本文只是一种对类方法的一种隐藏,如果你对dex文件有一定了解的话还可以做到对类字段、静态字段隐藏、类方法的重复定义。
[1] Android中实现「类方法指令抽取方式」加固方案原理解析 [2] DEX文件混淆加密
*本文作者:xiongchaochao,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。