JEB2发布有段时间了,相比于JEB1来说功能强大了很多,但是API也发生了巨大的变化,不仅是定义上也包括架构上,这给我们移植或者是新编写插件带来了一定不便, 目前也还没看到详细一些的API分析教程。本文以一个具体的应用分析为例,解释如何编写一个JEB2中处理混淆应用的插件,来实现自动识别和重命名。
我们的样例APK是一个采用了比较剑走偏锋混淆的东西,其中绝大部分类名、函数名、field名都被替换成了包含lIi的字符串,如下截图所示:
这种给人工分析时追踪函数调用带来了不便,因为这些字符串字母长的都比较像,所以我们需要写一个JEB脚本来自动化重命名这些item。我们的逻辑如下:
由于JEB2试图成为像IDA那样的universal disassembler,其架构多了很多包装层。对于APK分析来说,关键的部分关系如下所示:
IProjectUnit -> ICodeUnit -> IJavaSourceUnit
IProjectUnit代表了整个workspace,一般我们只会使用project[0]
>>> engctx.getProjects()
[Project:{/xxx/xxx.apk.jdb2}]
ICodeUnit则代表了一个project中所有的可解析部分,如下面我们提到的,因为JEB2为各种架构都提供了统一包装层,ICodeUnit不再仅仅是dex或者jar,而还会包括了libraries中的各种native Library。
>>> units = RuntimeProjectUtil.findUnitsByType(prj, ICodeUnit, False)
>>> map(lambda x: print(x.name), units)
[u'Bytecode', u'arm64 image', u'arm image', u'arm image', u'mips image', u'x86 image', u'x86_64 image']
其中Bytecode
项是对应的dex体. 其对应的ICodeUnit代表了整个dex, 已经提供了基本的类型信息,例如Class, Type, Method, Field, Package 使用者可以通过ICodeUnit.getClass/getMethod/getField获取到对应的ICodeClass/ICodeMethod/ICodeField. 但是这个层级的unit并没有提供class hierchy信息和具体的源代码AST信息,故我们还需要IJavaSourceUnit.
IJavaSourceUnit代表的是执行过反编译之后生成的Java源代码体,提供了更加丰富和细节的Java代码信息供使用. 其对应的AST元素为IJavaClass/IJavaMethod等等. 通过decompiler.decompile(icodeclass.getAddress())获取IJavaSourceUnit, 通过IJavaSourceUnit.getClassElement获取IJavaClass.
需要强调的是, ICodeUnit对应的是整个dex, 而IJavaSourceUnit对应的是单个反编译出的类.
在JEB2中,用户操作(自定义操作)被统一包装在ActionContext类之下,类似于transaction的形势.API使用者提交各种ActionContext,并检查返回值是否成功.一个典型的重命名操作如下:
>>> actCntx = ActionContext(self.targetUnit, Actions.RENAME, clz.getItemId(), clz.getAddress())
actData = ActionRenameData()
actData.setNewName(newName)
if codeUnit.prepareExecution(actCntx, actData):
codeUnit.executeAction(actCntx, actData)
值的注意的是,这里的clz对象均为ICodeUnit调用getClass所查询出的ICodeClass类,而不是IJavaSourceUnit对应的IJavaClass. ActionContext作用的对象也是代表整个dex的ICodeUnit.
除了重命名操作之外, ActionContext还包括了COMMENT, CONVERT, CREATE_PACKAGE, DELETE, MOVE_TO_PACKAGE, QUERY_OVERRIDES, QUERY_TYPE_HIER, QUERY_XREFS, RENAME等操作, 其实就是我们在UI中右键所能执行的操作. 读者可能要问, 像QUEYR_TYPE_HIER这种操作, 通过IJavaSource解析AST不是也可以做? 我认为确实是这样, 这里可能还是为了给不同语言提供一个统一的抽象接口. 当然QUERY_XREFS顾名思义是获取到对应的引用, 这方便我们做一些callgraph的查询.
如文章开头所示, 我们的目的是根据被混淆item的基类信息和类型信息/参数信息对其重命名. 主要逻辑如下:
for clz in codeunit.getClasses():
if isObfuscated(clz):
name = determineNameFromHierchy(clz) --->1
rename(clz, name)
for field in codeUnit.getFields():
if isObfuscated(field):
name = determineNameByFieldType(field)
rename(field, name)
for mtd in codeUnit.getMethods():
if isObfuscated(mtd):
name = determineNameByArgsType(field)
rename(field, name)
例如, class IiIiIiIi是继承于class iIiIiIiI, 而iIiIiIiI又继承于Activity/实现了onClickListener, 那么我们就可以使用Activity/onClickListener作为基准重命名两个被混淆的类. 这里的关键在于一个递归获取基类的函数, 如下所示:
'''
clzElement is ICodeClass retrieved from ICodeUnit.getClass()
'''
def tryDetermineGodeName(self, clzElement):
javaunit = self.decomp.decompile(clzElement.getAddress())
clzElement = javaunit.getClassElement()
#now clzElement is a IJavaClass
if not isFuckingName(clzElement.getName()):
#this is a non-obfuscated name, just return it
return clzElement.getName()
ssupers = clzElement.getImplementedInterfaces()
supers = []
supers.extend(ssupers)
# do not directly append on returned list!
superSig = clzElement.getSupertype().getSignature()
supers.append(clzElement.getSupertype())
for superItem in supers:
sig = superItem.getSignature()
if sig == "Ljava/lang/Object;":
#extend from java/lang/Object gives us zero info
#so try next
continue
if not isFuckingName(sig):
#return first non-obfuscated name
return sig
resolvedType = self.targetUnit.getClass(sig)
if resolvedType:
#this is a concret class
guessedName = self.tryDetermineGoodName(resolvedType)
if guessedName:
return guessedName
else:
#this is a SDK class
return sig
#cannot determine name from its supers, return None
return None
相对来讲, method和field的重命名就简单了很多, 如附代码所示, 在此不再赘述.
这里还有一个小细节, 因为需要操作的类比较多, 我们将插件定义为后台运行, 这样可以不阻塞UI, 同时获得更好的log效果.
重命名后的效果如下:
可以看到我们恢复出了较多可读信息. 完整代码: https://gist.github.com/flankerhqd/ca92b42f1f796763e5d1f8cd73247a30
JEB2的API相对于JEB1组织层次更多, 也就没那么直观. 但有了初步了解之后, 也可以很快掌握使用方法. 测试版本: JEB2 2.3.4