# 前言
最近分析的 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
函数执行完后的预期
然而当我们尝试用的 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
, 发现都是空值
在这个地方纠结了好几天之后我就在想,究竟是什么原因导致 hook 这个对象的返回值失败了呢?难道是 frida 有 bug, 所以我就去看了 frida art hook 的源码和原理,感觉也没啥问题呀,还是说我的 frida 的打开方式有问题呢
那来看看这个对象有哪些属性吧
console.log('arr_object has those properties->', Object.getOwnPropertyNames(arr_object)); |
欸打印看看这个 $w
, 没想到竟然可以成功的打印出 Object
来?!
这个 $w
是什么东西,为什么 arr_object
用 $w
属性就可以拿到正确的对象,但是打印原始的 arr_object
却一直是空?
进到 frida-java-bridge 里面去看看是什么情况
首先找到 $w
定义的地方,这个 this[Symbol.for('w')]
是什么呢?
向下翻翻源码后我们发现,没想到 $w
是 classWrapper
, 拿到这个 classWrapper
之后才算是拿到了最初的值,amazing~
# 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 Activity
的 attachBaseContext
, 然后通过 Thread.sleep(9999);
让进程永久的暂停下去,虽然 Thread 已经 sleep 了,但是我们的 RPC 主动调用解密函数这个过程依然可以正常进行
# flask 搭个本地服务器
接下来我们用 flask 去搭一个本地的接口,来让 jeb 脚本可以访问到啦,这里我使用的是 flask
, 由于 flask 支持的是 wsgi
协议,而 uvicorn
只能用 asgi
协议,所以我们需要用 asgiref
中的 WsgiToAsgi
将 asgi
转换成 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 |