# 前言

最近分析的 app 发现加了 DexGuard , 所以在 java 层遇到了非常强力的字符串混淆,甚至 jeb 这么强大的 java 反混淆工具都不能把字符串解密出来,所以本文是对 java 字符串反混淆的过程记录,通过 frida 的 RPC 调用,利用 python flask 搭建一个主动调用 java 字符串解密函数的 GET 接口,随后编写 jeb 脚本访问这个本地端口,并传入字符串解密函数的参数,获取字符串解密后的结果后替换 Element, 实现基于 frida RPC 主动调用的 java 字符串的反混淆

# 神奇的防 hook 手段

DexGuard 有一个非常神奇的地方就是这个混淆的字符串解密函数竟然没有办法 hook 到返回值?!

将核心的部分提取出来之后写个 demo, 其 Java 代码如下,逻辑很简单,就是在调用 get_helloworld 获取拼接的字符串之前,先定义了一个 arr_object 对象数组并作为参数传入 get_helloworld 中,随后在 get_helloworld 中经过一顿拼接操作,将最终的返回值赋值给传入参数中的 arr_object , 随后调用 Toast.makeText 打印拼接后的字符串

private static void get_helloworld(String hello,String world,Object[]){
    String ret_value = hello+world+"get!";
    arr_object[0]=ret_value;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Object[] arr_object = new Object[1];
    get_helloworld("hello","world",arr_object);
    Toast.makeText(global_context, (String)arr_object[0], Toast.LENGTH_SHORT).show();
}

通过这种方式进行函数返回值的传递,被证实确实是可行的,我们看到弹窗弹出的内容也符合 get_helloworld 函数执行完后的预期

image-20240903163315465

然而当我们尝试用的 frida 代码去 hook get_helloworld , 想要读取通过对象数组传递的返回值之后却发现

function hook(){
    console.log('start hook');
    Java.perform(() =>{
        var app = Java.use("com.example.mytest.MainActivity");
        app.get_helloworld.implementation = function (str1,str2,arr_object) {
            console.log('before call get_helloworld: str1=',str1,'str2=',str2,'arr_object=',arr_object);
            var ret = this.get_helloworld(str1,str2,arr_object);
            console.log('after call get_helloworld:  str1=',str1,'str2=',str2,'arr_object=',arr_object,',ret=',ret);
            return ret;
        }
    })
}
setImmediate(hook)

nothing happened… 打印调用原函数前后的 arr_object , 发现都是空值

image-20240903163916328

在这个地方纠结了好几天之后我就在想,究竟是什么原因导致 hook 这个对象的返回值失败了呢?难道是 frida 有 bug, 所以我就去看了 frida art hook 的源码和原理,感觉也没啥问题呀,还是说我的 frida 的打开方式有问题呢

那来看看这个对象有哪些属性吧

console.log('arr_object has those properties->', Object.getOwnPropertyNames(arr_object));

image-20240905224052091

欸打印看看这个 $w , 没想到竟然可以成功的打印出 Object 来?!

image-20240905230310808

这个 $w 是什么东西,为什么 arr_object$w 属性就可以拿到正确的对象,但是打印原始的 arr_object 却一直是空?

进到 frida-java-bridge 里面去看看是什么情况

首先找到 $w 定义的地方,这个 this[Symbol.for('w')] 是什么呢?

image-20240905230602898

向下翻翻源码后我们发现,没想到 $wclassWrapper , 拿到这个 classWrapper 之后才算是拿到了最初的值,amazing~

image-20240905230659556

# frida RPC 调用

在字符串解密函数的返回值可以被成功的 hook 到之后,接下来我们便开始正式的字符串反混淆,这里需要用到 frida RPC 来主动调用字符串的解密函数,因为对于字符串混淆来说,不论解密的过程多么的复杂,我们只需要调用一下解密函数拿到解密后的结果就足够了

frida 的 rpc 调用代码如下,需要注意的是函数传入的是什么类型的数组,我们在构造函数的入参的时候也要用 Java.array 转换成对应类型的数组

function string_decrypt(a1,a2,a3,a4){
    var result;
    Java.perform(function (){
        var app = Java.use("com.test.Activity");
        if(a1==null){
            a1 = Java.array('int',[]);
        }
        else{
            a1 = Java.array('int',[a1]);
        }
        if(a2==null){
            a2 = Java.array('char',[]);
        }
        else{
            a2 = Java.array('char',[a2]);
        }
        a4 = Java.array('byte',a4);
        var ret;
        var obj_cls = Java.use('[Ljava.lang.Object;');
        ret = Java.array('Ljava.lang.Object;',[null]);
        app.bc(a1,a2,a3,a4,ret);
        var objArr = Java.cast(ret.$w, obj_cls);
        var ArrayClz = Java.use("java.lang.reflect.Array");
        var len = ArrayClz.getLength(objArr);
        var arr =[]
        for(let i=0;i!==len;i++){
            arr.push(ArrayClz.get(objArr,i).toString());
        }
        console.log('hook return value123->',arr[0]);
        result = arr[0];
        })
    return result;
}
rpc.exports = {
    // 如果 js 导出函数中包含驼峰命名,则 python 需要将大写替换成_小写,如 getUser => get_user
    stringdecrypt:string_decrypt,
};

同时因为 dexguard 是有 frida 检测的,一旦检测到 frida 进程就会闪退,所以我们可以在 frida 的检测点之前找个点去 hook, 例如可以 hook ActivityattachBaseContext , 然后通过 Thread.sleep(9999); 让进程永久的暂停下去,虽然 Thread 已经 sleep 了,但是我们的 RPC 主动调用解密函数这个过程依然可以正常进行

image-20240906132356027

# flask 搭个本地服务器

接下来我们用 flask 去搭一个本地的接口,来让 jeb 脚本可以访问到啦,这里我使用的是 flask , 由于 flask 支持的是 wsgi 协议,而 uvicorn 只能用 asgi 协议,所以我们需要用 asgiref 中的 WsgiToAsgiasgi 转换成 wsgi , 代码如下

import codecs
import frida
from flask import Flask, request
from asgiref.wsgi import WsgiToAsgi
from time import sleep
import json
import uvicorn
def onMessage(message, data):
    #print(message)
    if message["type"] == 'send':
        print(u"[*] {0}".format(message['payload']))
    else:
        print(message)
app = Flask(import_name=__name__)
asgi_app = WsgiToAsgi(app)
device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
pkg = "com.test.activ"  #包名
pid = device.spawn([pkg])
session = device.attach(pid)
with codecs.open('./str_dec.js', 'r', encoding='utf-8') as f:
    source = f.read()
script = session.create_script(source)
script.on('message', onMessage)
script.load()#注入脚本
device.resume(pid)
sleep(1)#这里得 sleep 1 秒,来让脚本加载完成
rpc = script.exports_sync
# 通过 methods 设置 GET 请求
@app.route('/strdec', methods=["GET"])
def json_request():
    # 接收处理 json 数据请求
    data = eval(request.args.get("body"))
    print(f'[*] receive {data}')
    print(type(data))
    method = data["method"]
    result = None
    if method == "bc":#处理函数名为 bc 的字符串加密函数
        argv1 = data['argv1']
        argv2 = data['argv2']
        argv3 = data['argv3']
        argv4 = data['argv4']
        result = rpc.stringDecrypt(argv1,argv2,argv3,argv4)
    return result
uvicorn.run(asgi_app,host='127.0.0.1', port=8080)

# Jeb Script 反混淆

这里字符串混淆的代码和之前用过的差不多,只是字符串解密函数 bc_decryptstring 这里,需要注意用 jeb 自带的网络库 Net().query 和先前 flask 创建的接口交互,到了这里基于主动调用的字符串反混淆就基本上完成了

# coding=utf-8
from com.pnfsoftware.jeb.client.api import IScript, IconType, ButtonGroupType
from com.pnfsoftware.jeb.core import RuntimeProjectUtil
from com.pnfsoftware.jeb.core.units.code.java import IJavaSourceUnit
from com.pnfsoftware.jeb.core.units.code import ICodeUnit, ICodeItem
from com.pnfsoftware.jeb.core.output.text import ITextDocument
from com.pnfsoftware.jeb.core.units.code.java import IJavaAssignment, IJavaSourceUnit, IJavaStaticField, IJavaNewArray, IJavaConstant, IJavaCall, IJavaField, IJavaMethod, IJavaClass
from com.pnfsoftware.jeb.core.events import JebEvent, J
from com.pnfsoftware.jeb.core.util import DecompilerHelper
from com.pnfsoftware.jeb.util.net import Net
import json
# 解密字符串函数的类名以及方法名
methodName = ['Lcom/test/Activity;', 'bc']
class dec_str_test(IScript):
    def run(self, ctx):
        print('start deal with strings')
        self.ctx = 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
        units = RuntimeProjectUtil.findUnitsByType(projects[0], IJavaSourceUnit, False)
        for unit in units:
            javaClass = unit.getClassElement()
            print('[+] decrypt:' + javaClass.getName())
            self.cstbuilder = unit.getFactories().getConstantFactory()
            #self.elebuilder = unit.getFactories().getElementFactory()
            self.processClass(javaClass)
            unit.notifyListeners(JebEvent(J.UnitChange))
        print('Done.')
    def processClass(self, javaClass):
        # if javaClass.getName() == methodName[0]:
        #     return
        for method in javaClass.getMethods():
            block = method.getBody()
            i = 0
            while i < block.size():
                stm = block.get(i)
                self.checkElement(block, stm)
                i += 1
    def checkElement(self, parent, e):
        try:
            if isinstance(e, IJavaCall):
                mmethod = e.getMethod()
                mname = mmethod.getName()
                msig = mmethod.getSignature()
                if mname == methodName[1] and methodName[0] in msig:
                    v = []
                    args = e.getArguments();
                    decstr = self.bc_decryptstring(args[0],args[1],args[2],args[3])
                    if decstr:
                        print(e)
                        print("decrypt bc->",decstr)
                        parent.replaceSubElement(e, self.cstbuilder.createString(decstr))
            for subelt in e.getSubElements():
                if isinstance(subelt, IJavaClass) or isinstance(subelt, IJavaField) or isinstance(subelt, IJavaMethod):
                    continue
                self.checkElement(e, subelt)
        except Exception,e:
            print(e)
            
    def read_array(self,arr):
        if arr.getType()==None:
            return None
        res = []
        arr = arr.getInitialValues()
        for i in range(len(arr)):
            res.append(arr[i].getByte())
        return res
    
    def bc_decryptstring(self, args1,args2,args3,args4):
        argv1= self.read_array(args1)
        argv2 = self.read_array(args2)
        argv3 = args3.getInt()
        argv4 = self.read_array(args4)
        body = {
            'method':'bc',
            'argv1':argv1,
            'argv2':argv2,
            'argv3':argv3,
            'argv4':argv4
        }
        r = Net().query("http://127.0.0.1:8080/strdec", {"body":str(body)})
        return r
更新于 阅读次数