在一些CTF以及某些检测环境的APP中,在Java层会出现一种很奇怪的混淆,经过熊仔哥(看雪ID:白熊)的指点,这种混淆是一种开源的混淆方案,最早我是使用trace smali的方式解决掉的,不过最近有一场ctf比赛,这个混淆又出现了,并且有大佬给出了新的思路,以此契机我开始学习了Java层混淆对抗的路子,并开发出了几个脚本。感谢与我一起写反混淆脚本的实习生,我们一起完善解决了这个方案与脚本。
Jadx-GUi
GDA
JEB
由于每种反编译器的反编译效果不同,表现出的伪代码形式也为不同。
我们选择JEB来进行主力分析工具,因为JEB自带有一些神奇魔法,能帮我们做很多的事情。
https://bbs.kanxue.com/thread-278648.htm
在oacia大佬的帖子里,jeb直接去除掉了控制流平坦化
但是又是什么神奇魔法让jeb的这种自动反混淆的能力失效了呢,让我来带你详细分析。
jeb官方支持反控制流平坦化的文档
https://www.pnfsoftware.com/blog/control-flow-unflattening-in-the-wild/
如果jeb帮我们完成了处理,那么在函数的头部会留下一些注释。
首先,打开自动重命名工具,将奇怪的字符串重新命名,增强阅读体验
我认为百分之20即可完美去除垃圾字符串(正因为不好看,所以好识别)
点击确定即可重新命名,获得一个比较好的阅读体验。
首先我们先摘出来一段控制流平坦化,分析其为什么不能自动反控制流平坦化
在switch(v1) v1如果全部都是已知数值的情况下,jeb可以直接反控制流平坦化
让我们看看是什么影响了v1的值
通过查看发现,有两种方式影响了v1的赋值,让我们点进去看一下
第一种形式:
第二种形式:
当我们解决这两种混淆方式以后,jeb大哥会直接反混淆成功。
由于静态变量不是定植,在其他的地方可能被引用并修改,jeb理解这个值可能发生变化,所以不进行优化。
熟悉混淆的小伙伴可能脑子里立马迸发出一个想法:
这不就是bcf吗? 通过全局变量的不透明谓词来干扰执行流程
让我们分析这个变量在后续有没有修改?
发现只有获取,并没有修改的形式。
那么我们该如何解决呢?其实非常简单。
https://bbs.kanxue.com/thread-257213.htm
参考葫芦娃大佬的帖子,我们可以改个标题了
JEB: 十步杀一人,两步秒BlackObfuscator
参考大佬的思路,我们可以把这个字段的权限从读写,改为只读
我们如何将这个字段变为只读字段呢?
熟悉java的朋友应该知道,final加入后只能读就不能写了
我们来观察一下这个字段获取的语句:
那么我们就可以引出第二种修改方式,将sget语句patch为const语句,也可以让我们的jeb直接意识到,这个是不可变的。
关键的语句:
const-string v0, "\u06E7\u06E6\u06E0" 定义了一个固定的字符串 赋值给v0寄存器
invoke-static CLS5547->MTH27577(Object)I, v0 调用静态方法,v0参数传入这个函数
000003F8 move-result v0 将结果放回v0寄存器(这里和上面两句使用的寄存器的一般一致,但是我在后面还是处理了)
调用的函数非常简单,就是取一个字符串的hashcode
众所周知,在字符串不变的情况下,hashcode也是不会变的,下面给出算法
第二种方式又是用一种巧妙的方法骗过了我们的jeb老大哥
如何解决?
查找所有静态调用的方法,查看方法体是否调用了hashcode(如果更快我觉得可以收集opcode特征打一个md5,但是没必要),如果是,手动计算hashcode,并patch回原来的调用处。
为了给原来的smali方法体擦干净屁股,我们需要将三条smali指令替换为一条。
也就是把
替换为const v0,计算后的hashcode
这个部分更多的是引出两种去混淆方案的形式,为第二篇AST解混淆做出预告
最早我使用的方案是使用jeb脚本的方式来修改,当然幻想是美好的,实际却是残酷的。
在这里我想引入两种概念,借用看雪另外一位大佬(https://bbs.kanxue.com/homepage-760871.htm)的两个帖子来带大家领略这两个反混淆的概念。
https://bbs.kanxue.com/thread-263011.htm
https://bbs.kanxue.com/thread-263012.htm(选看,本帖没用上)
下面我们开始讲如何用jeb。实现第一种形式的patch**(没有成功实现,大佬选看,只是记录踩坑过程)**
第一种想法,找到sget指令,获取到sget操作的字段,patch回去
尴尬的来了,找了半天jeb文档,没有相关写入方法。(有大佬有写入方法的话带带,我能写出jeb脚本)
insn是属于IDalvikInstruction
类下面的
我们打开jeb文档
https://www.pnfsoftware.com/jeb/apidoc/reference/com/pnfsoftware/jeb/core/units/code/android/dex/IDalvikInstruction.html
我们可以看到各种get方法,
也许可以拿到offset和offsetend 在针对性patch,但是我没有考虑这种方式。
总的来说,通过DEX字节码层面可以拿到所有我能拿到的信息,但是并没有一个方法来设置。
大体思路就是先拿到DEX模块,进一步拿到所有method,在拿到所有基本块,遍历所有基本块里的指令,如果遇到sget,则找到sget获取的Filed,然后修改sget这个指令。
也可以另外一种思路,收集所有静态字段,然后查找引用的地方,再去以字节码层面patch
同时也可以收集所有静态字段,拿到静态字段的权限,进行过滤,然后修改为只读(JEB可以拿到确切的权限,但是没法修改) 拿到权限那段代码我丢了,我后续会补上
既然jeb不能满足我们的需求,我们可以采用dex2lib这个库来进行反混淆操作。
首先先实现方案1,将所有静态字段增加final属性
show me the code!
很多读者有疑问,为什么hashcode没有解析也可以识别了,我的答案是jeb牛逼。
当然我也针对了hashcode写了对抗的脚本,大家可以当demo参考。
运行结果:
第三个思路:
收集所有sget指令,替换为const,这里涉及到一个搜索问题
第一个版本我是先拿到Feild,然后遍历所有类,找到相同的,导致速度很慢
第二个版本我先提前收集好所有Feild,然后建立一个hashmap做匹配,速度提升了不少
第一个版本:
第一个版本的搜索算法(别喷)
第二个版本:
提前收集字段
最后还原后,还有一些函数在外面调用,我准备下一篇文章讲一下具体api,然后手把手带着做一下
这个留给下一篇文章进行详细讲解然后解决,其实解决起来也非常的容易,大家可以尝试一下
坑点:app是多个dex的,有两种解决方式
第一种合并dex(我采用的)
第二种收集多个dex的静态字段,建立maps映射(朋友实现的)
我现在给出合并dex的思路和脚本(大家需要自行下载一下jar包)
提前预告:JSAST的思路都可以引入进来,做自己的反混淆插件,比如常量折叠,反控制流平坦化。
这一篇章就是基于JEB提供的一系列AST接口来实现,而且我推断官方内置的就是使用这种方式来实现的,总的来看Java层面的混淆比较难做,碍于Java字节码的机制,对于调用树恢复的程度很高。
Java层面的反混淆的对抗成本和开发成本是对等的(混淆和去混淆都用的一个库,基于dex层面做)
脚本还有一些瑕疵需要完善,近期会上传。
000003EE
const
-string v0,
"\u06E7\u06E6\u06E0"
000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
000003F8 move-result v0
000003EE
const
-string v0,
"\u06E7\u06E6\u06E0"
000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
000003F8 move-result v0
public
int
hashCode() {
int
h = hash;
if
(h ==
0
&& value.length >
0
) {
char
val[] = value;
for
(
int
i =
0
; i < value.length; i++) {
h =
31
* h + val[i];
}
hash = h;
}
return
h;
}
public
int
hashCode() {
int
h = hash;
if
(h ==
0
&& value.length >
0
) {
char
val[] = value;
for
(
int
i =
0
; i < value.length; i++) {
h =
31
* h + val[i];
}
hash = h;
}
return
h;
}
000003EE
const
-string v0,
"\u06E7\u06E6\u06E0"
000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
000003F8 move-result v0
000003EE
const
-string v0,
"\u06E7\u06E6\u06E0"
000003F2 invoke-
static
CLS5547->MTH27577(Object)I, v0
000003F8 move-result v0
DEX字节码层面(IDexUnit部件)
(
1
)访问DEX与Class
(
2
)遍历Field / Method
(
3
)访问某个Method
(
4
)访问指令
(
5
)访问基本块
(
6
)访问控制流数据流
DEX字节码层面(IDexUnit部件)
(
1
)访问DEX与Class
(
2
)遍历Field / Method
(
3
)访问某个Method
(
4
)访问指令
(
5
)访问基本块
(
6
)访问控制流数据流
# -*- coding: UTF-
8
-*-
from com.pnfsoftware.jeb.client.api
import
IScript
from com.pnfsoftware.jeb.core.units
import
UnitUtil
from com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
from com.pnfsoftware.jeb.core.units.code.android.dex
import
IDexClass
from com.pnfsoftware.jeb.core.actions
import
ActionContext
from com.pnfsoftware.jeb.core.actions
import
Actions
from com.pnfsoftware.jeb.client.api
import
IClientContext
from com.pnfsoftware.jeb.core
import
IRuntimeProject
from com.pnfsoftware.jeb.core.units
import
IUnit
from com.pnfsoftware.jeb.core.units.code
import
IFlowInformation
from com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
class
SGetRightOperandTree(IScript):
def run(self, ctx):
prj = ctx.getMainProject();
dexUnit =prj.findUnit(IDexUnit);
# Check
if
the unit is a DEX unit
if
not isinstance(dexUnit, IDexUnit):
print(
'The script must be run on a DEX unit.'
)
return
# Specify the
class
name you're interested in
method_sign =
'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;'
method = dexUnit.getMethod(method_sign)
dexMethodData = method.getData();
dexCodeItem= dexMethodData.getCodeItem();
for
idx,insn in enumerate(dexCodeItem.getInstructions()):
if
str(insn)==
"sget"
:
print insn
print idx,
"(01) getCode >>> "
,insn.getCode() # 二进制
print idx,
"(02) getOpcode >>> "
,insn.getOpcode() # 操作码
print idx,
"(03) getParameters:"
# 指令操作数
for
a,b in enumerate(insn.getParameters()):
print
"<"
,a,
">"
,b.getType(),b.getValue()
if
len(insn.getParameters()) >
1
:
fieldRef = insn.getParameters()[
1
]
print(
"Field Reference: "
, fieldRef)
if
len(insn.getParameters()) >
1
:
fieldRef = insn.getParameters()[
1
].getValue()
field = dexUnit.getField(fieldRef)
print(field.getName())
# currentFlags = field.getAddress()
# print(
"currentFlags"
,currentFlags)
fieldData = field.getData()
print(field.getStaticInitializer())
fieldValue=field.getStaticInitializer()
#
if
field:
# 那么就patch回去
# -*- coding: UTF-
8
-*-
from com.pnfsoftware.jeb.client.api
import
IScript
from com.pnfsoftware.jeb.core.units
import
UnitUtil
from com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
from com.pnfsoftware.jeb.core.units.code.android.dex
import
IDexClass
from com.pnfsoftware.jeb.core.actions
import
ActionContext
from com.pnfsoftware.jeb.core.actions
import
Actions
from com.pnfsoftware.jeb.client.api
import
IClientContext
from com.pnfsoftware.jeb.core
import
IRuntimeProject
from com.pnfsoftware.jeb.core.units
import
IUnit
from com.pnfsoftware.jeb.core.units.code
import
IFlowInformation
from com.pnfsoftware.jeb.core.units.code.android
import
IDexUnit
class
SGetRightOperandTree(IScript):
def run(self, ctx):
prj = ctx.getMainProject();
dexUnit =prj.findUnit(IDexUnit);
# Check
if
the unit is a DEX unit
if
not isinstance(dexUnit, IDexUnit):
print(
'The script must be run on a DEX unit.'
)
return
# Specify the
class
name you're interested in
method_sign =
'Lcom/example/bbandroid/strange;->encode([B)Ljava/lang/String;'
method = dexUnit.getMethod(method_sign)
dexMethodData = method.getData();
dexCodeItem= dexMethodData.getCodeItem();
for
idx,insn in enumerate(dexCodeItem.getInstructions()):
if
str(insn)==
"sget"
:
print insn
print idx,
"(01) getCode >>> "
,insn.getCode() # 二进制
print idx,
"(02) getOpcode >>> "
,insn.getOpcode() # 操作码
print idx,
"(03) getParameters:"
# 指令操作数
for
a,b in enumerate(insn.getParameters()):
print
"<"
,a,
">"
,b.getType(),b.getValue()
if
len(insn.getParameters()) >
1
:
fieldRef = insn.getParameters()[
1
]
print(
"Field Reference: "
, fieldRef)
if
len(insn.getParameters()) >
1
:
fieldRef = insn.getParameters()[
1
].getValue()
field = dexUnit.getField(fieldRef)
print(field.getName())
# currentFlags = field.getAddress()
# print(
"currentFlags"
,currentFlags)
fieldData = field.getData()
print(field.getStaticInitializer())
fieldValue=field.getStaticInitializer()
#
if
field:
# 那么就patch回去
@Override
public
Rewriter<Field> getFieldRewriter(Rewriters rewriters) {
return
new
FieldRewriter(rewriters) {
@Override
public
Field rewrite(Field field) {
int
accessFlags = field.getAccessFlags();
if
((accessFlags & AccessFlags.PUBLIC.getValue()) !=
0
&&
(accessFlags & AccessFlags.STATIC.getValue()) !=
0
&&
(accessFlags & AccessFlags.FINAL.getValue()) ==
0
) {
accessFlags |= AccessFlags.FINAL.getValue();
System.out.println(
"Modified field "
+ field.getName() +
" to public static final"
);
return
new
ImmutableField(
field.getDefiningClass(),
field.getName(),
field.getType(),
accessFlags,
field.getInitialValue(),
field.getAnnotations(),
field.getHiddenApiRestrictions()
);
}
return
super
.rewrite(field);
}
};
}
@Override
public
Rewriter<Field> getFieldRewriter(Rewriters rewriters) {
return
new
FieldRewriter(rewriters) {
@Override
public
Field rewrite(Field field) {
int
accessFlags = field.getAccessFlags();
if
((accessFlags & AccessFlags.PUBLIC.getValue()) !=
0
&&
(accessFlags & AccessFlags.STATIC.getValue()) !=
0
&&
(accessFlags & AccessFlags.FINAL.getValue()) ==
0
) {
accessFlags |= AccessFlags.FINAL.getValue();
System.out.println(
"Modified field "
+ field.getName() +
" to public static final"
);
return
new
ImmutableField(
field.getDefiningClass(),
field.getName(),
field.getType(),
accessFlags,
field.getInitialValue(),
field.getAnnotations(),
field.getHiddenApiRestrictions()
);
}
return
super
.rewrite(field);
}
};
}
@Override
public
Rewriter<MethodImplementation> getMethodImplementationRewriter(Rewriters rewriters) {
return
new
MethodImplementationRewriter(rewriters) {
@Override
public
MethodImplementation rewrite(MethodImplementation methodImpl) {
if
(methodImpl ==
null
) {
return
null
;
}
List<Instruction> originalInstructions =
new
ArrayList<>();
for
(Instruction instruction : methodImpl.getInstructions()) {
originalInstructions.add(instruction);
}
List<Instruction> newInstructions =
new
ArrayList<>();
for
(
int
i =
0
; i < originalInstructions.size(); i++) {
Instruction instruction = originalInstructions.get(i);
if
(instruction.getOpcode() == Opcode.INVOKE_STATIC) {
if
(instruction
instanceof
ReferenceInstruction) {
ReferenceInstruction refInstr = (ReferenceInstruction) instruction;
Reference reference = refInstr.getReference();
if
(reference
instanceof
MethodReference) {
MethodReference methodRef = (MethodReference) reference;
String methodKey = getMethodKey(methodRef);
Method calledMethod = methodMap.get(methodKey);
if
(calledMethod !=
null
&& calledMethod.getImplementation() !=
null
) {
if
(methodContainsHashCodeInvocation(calledMethod)) {
List<Integer> parameterRegisters = getInvokeInstructionParameterRegisters(instruction);
if
(parameterRegisters.size() >
0
) {
int
paramRegister = parameterRegisters.get(
0
);
String stringValue = findStringAssignedToRegister(paramRegister, originalInstructions, i);
if
(stringValue !=
null
) {
int
hashCode = stringValue.hashCode();
int
resultRegister = getResultRegister(originalInstructions, i);
if
(resultRegister != -
1
) {
Instruction constInstr = createConstInstruction(resultRegister, hashCode);
if
(constInstr !=
null
) {
newInstructions.add(constInstr);
System.out.println(
"Replaced invoke-static with const for method "
+ methodRef.getName() +
", hashCode: "
+ hashCode);
if
(i +
1
< originalInstructions.size()) {
Instruction nextInstr = originalInstructions.get(i +
1
);
if
(nextInstr.getOpcode() == Opcode.MOVE_RESULT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_OBJECT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_WIDE) {
i++;
}
}
continue
;
}
}
}
}
}
}
}
}
}
newInstructions.add(instruction);
}
return
new
ImmutableMethodImplementation(
methodImpl.getRegisterCount(),
newInstructions,
methodImpl.getTryBlocks(),
methodImpl.getDebugItems()
);
}
};
}
@Override
public
Rewriter<MethodImplementation> getMethodImplementationRewriter(Rewriters rewriters) {
return
new
MethodImplementationRewriter(rewriters) {
@Override
public
MethodImplementation rewrite(MethodImplementation methodImpl) {
if
(methodImpl ==
null
) {
return
null
;
}
List<Instruction> originalInstructions =
new
ArrayList<>();
for
(Instruction instruction : methodImpl.getInstructions()) {
originalInstructions.add(instruction);
}
List<Instruction> newInstructions =
new
ArrayList<>();
for
(
int
i =
0
; i < originalInstructions.size(); i++) {
Instruction instruction = originalInstructions.get(i);
if
(instruction.getOpcode() == Opcode.INVOKE_STATIC) {
if
(instruction
instanceof
ReferenceInstruction) {
ReferenceInstruction refInstr = (ReferenceInstruction) instruction;
Reference reference = refInstr.getReference();
if
(reference
instanceof
MethodReference) {
MethodReference methodRef = (MethodReference) reference;
String methodKey = getMethodKey(methodRef);
Method calledMethod = methodMap.get(methodKey);
if
(calledMethod !=
null
&& calledMethod.getImplementation() !=
null
) {
if
(methodContainsHashCodeInvocation(calledMethod)) {
List<Integer> parameterRegisters = getInvokeInstructionParameterRegisters(instruction);
if
(parameterRegisters.size() >
0
) {
int
paramRegister = parameterRegisters.get(
0
);
String stringValue = findStringAssignedToRegister(paramRegister, originalInstructions, i);
if
(stringValue !=
null
) {
int
hashCode = stringValue.hashCode();
int
resultRegister = getResultRegister(originalInstructions, i);
if
(resultRegister != -
1
) {
Instruction constInstr = createConstInstruction(resultRegister, hashCode);
if
(constInstr !=
null
) {
newInstructions.add(constInstr);
System.out.println(
"Replaced invoke-static with const for method "
+ methodRef.getName() +
", hashCode: "
+ hashCode);
if
(i +
1
< originalInstructions.size()) {
Instruction nextInstr = originalInstructions.get(i +
1
);
if
(nextInstr.getOpcode() == Opcode.MOVE_RESULT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_OBJECT || nextInstr.getOpcode() == Opcode.MOVE_RESULT_WIDE) {
i++;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课