JEB2插件教程之一JEB2AutoRenameByTypeInfo.py

JEB2发布有段时间了,相比于JEB1来说功能强大了很多,但是API也发生了巨大的变化,不仅是定义上也包括架构上,这给我们移植或者是新编写插件带来了一定不便, 目前也还没看到详细一些的API分析教程。本文以一个具体的应用分析为例,解释如何编写一个JEB2中处理混淆应用的插件,来实现自动识别和重命名。

案例

我们的样例APK是一个采用了比较剑走偏锋混淆的东西,其中绝大部分类名、函数名、field名都被替换成了包含lIi的字符串,如下截图所示:

obfuscated1

obfuscated2

这种给人工分析时追踪函数调用带来了不便,因为这些字符串字母长的都比较像,所以我们需要写一个JEB脚本来自动化重命名这些item。我们的逻辑如下:

  • 对于类:递归寻找它的父类和实现的接口。如果父类和接口包含了有意义的名字:例如SDK类Activity、不能混淆的类名MainActivity,以此为基础进行重命名
  • 对于Field:根据该Field的类型,重命名其名字
  • 对于函数:根据该函数的参数类型,重命名其名字

JEB2的API架构

由于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效果.

重命名后的效果如下:

deobfuscated

可以看到我们恢复出了较多可读信息. 完整代码: https://gist.github.com/flankerhqd/ca92b42f1f796763e5d1f8cd73247a30

# -*- coding: utf-8 -*-  
  
""" 
Sample client script for PNF Software's JEB2. 
 
More samples are available on our website and within the scripts/ folder. 
 
Refer to SCRIPTS.TXT for more information. 
"""  
  
import string  
import re,collections  
from com.pnfsoftware.jeb.client.api import IScript  
from com.pnfsoftware.jeb.client.api import IScript, IGraphicalClientContext  
from com.pnfsoftware.jeb.core import RuntimeProjectUtil  
from com.pnfsoftware.jeb.core.actions import Actions, ActionContext, ActionXrefsData  
from com.pnfsoftware.jeb.core.events import JebEvent, J  
from com.pnfsoftware.jeb.core.output import AbstractUnitRepresentation, UnitRepresentationAdapter  
from com.pnfsoftware.jeb.core.units.code import ICodeUnit, ICodeItem  
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit, IJavaStaticField, IJavaNewArray, IJavaConstant, IJavaCall, IJavaField, IJavaMethod, IJavaClass  
from com.pnfsoftware.jeb.core.actions import ActionTypeHierarchyData  
from com.pnfsoftware.jeb.core.actions import ActionRenameData  
from com.pnfsoftware.jeb.core.util import DecompilerHelper  
from com.pnfsoftware.jeb.core.output.text import ITextDocument  
from com.pnfsoftware.jeb.core.units.code.android import IDexUnit  
from java.lang import Runnable
  
class JEB2AutoRenameByTypeInfo(IScript):  
  def run(self, ctx):
    ctx.executeAsync("Running name detection...", JEB2AutoRename(ctx))
    print('Done')

class JEB2AutoRename(Runnable):  
  def __init__(self, ctx):
    self.ctx = ctx
  def run(self):
    ctx = self.ctx  
    engctx = ctx.getEnginesContext()  
    if not engctx:  
      print('Back-end engines not initialized')  
      return  
  
    projects = engctx.getProjects()  
    if not projects:  
      print('There is no opened project')  
      return  
  
    # 逻辑开始  
    prj = projects[0]  
  
    self.codeUnit = RuntimeProjectUtil.findUnitsByType(prj, ICodeUnit, False)
    self.curIdx = 0  
    bcUnits = []
    for unit in self.codeUnit:
      classes = unit.getClasses()
      if classes and unit.getName().lower() == "bytecode":
        bcUnits.append(unit)
    targetUnit = bcUnits[0]
    units = RuntimeProjectUtil.findUnitsByType(prj, IJavaSourceUnit, False)
    self.targetUnit = targetUnit
    #print(targetUnit.getClass(javaclz.getSupertype().getSignature()))
        #this is a single classes.dex item

    fuckingClasses = []
    cnt = 0
    for clz in targetUnit.getClasses():
      #the name maybe renamed
      #print(clz.getSignature(False))#False is for original Name
      if isFuckingName(clz.getName(False)):
        determinedName = self.tryDetermineGoodName(clz)
        if determinedName is None:
          determinedName = genNameFromIdx(cnt)
        else:
          determinedName = genNameFromIdx(cnt) + determinedName.split('/')[-1][:-1]
        self.commenceRename(clz.getSignature(False), determinedName, 0)
        print("cnt is " + str(cnt) +  "determined name is " + str(determinedName))
        cnt += 1
      
    #rename all fields
    cnt = 0
    for field in targetUnit.getFields():
      if isFuckingName(field.getName(False)):
        #get field type(renamed Type)
        fieldType = field.getFieldType().getName(True)
        newName = genNameFromIdx(cnt) + fieldType
        self.commenceRename(field.getAddress(), newName, 1)
        print("cnt is " + str(cnt) +  "determined name is " + str(newName))
        cnt += 1
    
    #rename all functions
    cnt = 0
    for mtd in targetUnit.getMethods():
      if isFuckingName(mtd.getName(False)):
        print(mtd.getName(False))
        #get method arguments
        #new mtd name is paramTypeJoin
        newName = genNameFromIdx(cnt) + ''.join(map(lambda x: x.getName(True), mtd.getParameterTypes()))
        self.commenceRename(mtd.getAddress(), newName, 2)
        print("cnt is " + str(cnt) +  "determined name is " + str(newName))
        cnt += 1
        



  def commenceRename(self, originName, newName, isClass):
    if isClass == 0:
      clz = self.targetUnit.getClass(originName)
    elif isClass == 1:
      clz = self.targetUnit.getField(originName)
    else:
      clz = self.targetUnit.getMethod(originName)
    actCntx = ActionContext(self.targetUnit, Actions.RENAME, clz.getItemId(), clz.getAddress())  
    actData = ActionRenameData()  
    actData.setNewName(newName)  

    if(self.targetUnit.prepareExecution(actCntx, actData)):  
      # 执行重命名动作  
      try:  
        bRlt = self.targetUnit.executeAction(actCntx, actData)  
        if(not bRlt):  
          print(u'executeAction fail!')  
      except Exception,e:  
        print Exception,":",e

  def tryDetermineGoodName(self, clzElement):
    decomp = DecompilerHelper.getDecompiler(self.targetUnit)
    javaunit = decomp.decompile(clzElement.getAddress())
    clzElement = javaunit.getClassElement()

    if not isFuckingName(clzElement.getName()):
      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;":
        continue
      if not isFuckingName(sig):
        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
    return None
    
def isFuckingName(s):
  if s.find('/') != -1:
    s = s.split('/')[-1][:-1]
  elif s[-1] == ';':
    s = s[1:-1]
  return set(list(s.lower())) == set(list('li'))

def genNameFromIdx(idx):
  ret = ''
  while idx / 26 != 0:
    ret += chr(ord('a') + idx % 26)
    idx = idx /26
  ret += chr(ord('a') + idx % 26)
  return ret

总结

JEB2的API相对于JEB1组织层次更多, 也就没那么直观. 但有了初步了解之后, 也可以很快掌握使用方法. 测试版本: JEB2 2.3.4

Ref:

  1. http://blog.csdn.net/weixin_37556843/article/details/66476295
  2. https://www.pnfsoftware.com/jeb2/apidoc/reference/packages.html
  3. https://groups.google.com/forum/#!topic/jeb-decompiler
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值