SO逆向之最右sign分析

篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

抓包

最右5.7.3.apk 启动charles,开始socks抓包

image-20211111144633352

postern配置代理

image-20211111144533053

postern配置规则

image-20211111144552678

开启vpn开始抓包

image-20211111144749349

Frida

jadx反编译搜索sign=

image-20211111144857121

主动调用a方法,构造字符串和字节数组参数,打印返回结果

1
2
3
4
5
6
7
function callSign(){
Java.perform(function () {
var plainTextBytes = Java.use("java.lang.String").$new("onejane").getBytes("UTF-8");
var result = Java.use("com.izuiyou.network.NetCrypto").a("12345", plainTextBytes);
console.log(result);
});
}

堆栈跟踪

1
2
objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins  
android hooking watch class_method com.izuiyou.network.NetCrypto.a --dump-args --dump-backtrace --dump-return

image-20211111210717876

除了使用objection,也可以通过frida脚本打印堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
setImmediate(function(){

Java.perform(function () {
var threadef = Java.use('java.lang.Thread');
var threadinstance = threadef.$new();
function Where(stack){
var at = ""
for(var i = 0; i < stack.length; ++i){
at += stack[i].toString() + "\n"
}
return at
}

var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
ZYNetCrypto.sign.implementation = function(arg1, arg2){
console.log("\nNetCrypto.sign\n");
// 打印入参信息
// console.log("str=" + arg1 + "\nbuf=" + printBytes(arg2) + "\nbuf2=" + Bytes2HexString(arg2) + "\njson=" + JSON.stringify(arg2));

var result = this.sign(arg1, arg2);
console.log("\nOut Rc=" + result);

var stack = threadinstance.currentThread().getStackTrace();
console.log("Full call stack:" + Where(stack));

return result;
}
});
});

image-20211111211530402

通过objection和frida堆栈跟踪,jeb打开搜索k46

image-20211111211843839

byte[] v1_2 = NetCrypto.encodeAES(v3_4.toString().getBytes(Charset.forName("UTF-8")));

接下来hook NetCrypto.encodeAES并打印出入参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setImmediate(function () {
Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
ZYNetCrypto.encodeAES.implementation = function (arg1) {
// console.log("\nNetCrypto.encodeAES " + arg1.length + "\n");
// console.log("encodeAES >>>>>>>" + JSON.stringify(arg1));
var strIn = Java.use('java.lang.String');
var outStr = strIn.$new(arg1);
// console.log(outStr);
var result = this.encodeAES(arg1);
console.log(outStr + " = " + printBytes(result));
return result;
}
});
});

image-20211111212255775

图中圈出的字符串即为V1_2,拿到了协议的明文,主要逻辑就是通过aes加密后的字符串结合请求地址利用so中的sign函数完成签名校验。

rpc调用

1
2
3
byte[] encodeAES = NetCrypto.encodeAES(jSONObject.toString().getBytes(Charset.forName("UTF-8")));
String a2 = NetCrypto.a(build.toString(), encodeAES);
newBuilder.url(a2).addHeader("X-Xc-Proto-Req", NetCrypto.getProtocolKey()).post(RequestBody.create(b(), encodeAES));

以上需要关注的函数有NetCrypto.encodeAESNetCrypto.aNetCrypto.getProtocolKey,在NetCrypto.a还有NetCrypto.sign,返回结果是加密的需要NetCrypto.decodeAES解密

zuiyouSign.js 对NetCrypto.sign进行hook打印出入参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hookSign() {
Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");

ZYNetCrypto.sign.implementation = function (arg1, arg2) {
console.log("\nNetCrypto.sign\n");
console.log("str=" + arg1 + "\nbuf=" + printBytes(arg2) + "\nbuf2=" + byteToHexString(arg2) + "\njson=" + JSON.stringify(arg2));

var result = this.sign(arg1, arg2);
console.log("\nOut Rc=" + result);
return result;
}
})
}
setImmediate(hookSign)

frida -UF cn.xiaochuankeji.tieba -l zuiyouSign.js 根据返回结果可以得知同一个请求每次返回的sign是一样的,第一个参数为请求地址。

image-20211111192138509

callSignFunZy.js 主动调用入参中的的byte数组返回sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function callSignFunZy(str, buf) {
var result = 'null';

console.log(str);
var arr = HexString2Bytes(buf);

Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
var res = ZYNetCrypto.sign(str, arr);
result = res;
});

return result;

}


rpc.exports = {
callzyfun: callSignFunZy, // 导出名不可以有大写字母和下划线
};

hookSign.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from flask import Flask, jsonify
import frida

app = Flask(__name__)
global gScript

device = frida.get_usb_device()
pid=device.get_frontmost_application().pid
session = device.attach(pid)
with open("agent/zuiyouSign.js") as f:
gScript = session.create_script(f.read())
gScript.load()

@app.route('/sign')
def zy_test():

str = 'https://api.izuiyou.com/my/profile'
buf = bytearray([0xf6, 0x01, 0xc4, 0xd4, 0xe3, 0xf3, 0x03, 0x13, 0x13, 0xa2, 0xd1, ...])
res = gScript.exports.callzyfun(str, buf.hex())
return jsonify(res)

if __name__ == '__main__':
app.run(host='0.0.0.0', port=1234, debug=True)

python hookSign.py 发起请求127.0.0.1:1234/sign获取sign

image-20211111194557932

完整实现

Frida结合 Magisk Hide报错:try disabling Magisk Hide in case it is active

Magisk Manager > Settings >Magisk > Magisk Hide,用来隐藏ROOT,避免部分app检测ROOT

zuiyouSign.js 实现加密解密签名等函数的定义和导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 加密 encodeAES函数的参数和返回值都是byte数组,转成hex字符串
// byte[] encodeAES = NetCrypto.encodeAES(jSONObject.toString().getBytes(Charset.forName("UTF-8")));
function callEncodeAes(strData){
var result = 'null';

Java.perform(function() {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
// 入参转成字节数组 jSONObject.toString().getBytes(Charset.forName("UTF-8"))
var strIn = Java.use('java.lang.String');
var byteIn = strIn.$new(strData).getBytes();
// 开始加密
var res = ZYNetCrypto.encodeAES(byteIn);
result = Bytes2HexString(res);

});

return result;
}

// 解密
// NetCrypto.decodeAES(v7, true)
function callDecodeAes(dataBuf){
var rc = 'null';

var arr = HexString2Bytes(dataBuf);
Java.perform(function() {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
var res = ZYNetCrypto.decodeAES(arr,true);
rc = Bytes2HexString(res);
});

return rc;
}

// 签名
function callSignFun(str,buf){
var result = 'null';

var arr = HexString2Bytes(buf);

Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
var res = ZYNetCrypto.sign(str,arr) ;
result = res;
});

return result;

}


// 当前使用的key
function callGetProtocolKey(){
var result = 'null';
Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
var res = ZYNetCrypto.getProtocolKey() ;
result = res;
});

return result;
}

// 设置当前使用的key
function callSetProtocolKey(strKey){
var result = 'null';

Java.perform(function () {
var ZYNetCrypto = Java.use("com.izuiyou.network.NetCrypto");
ZYNetCrypto.setProtocolKey(strKey) ;
});

return result;

}

rpc.exports = {
callgetkey : callGetProtocolKey,
callsetkey : callSetProtocolKey,
calldecaes : callDecodeAes,
callencaes : callEncodeAes,
callsignfun : callSignFun
};

python hookSign.py hook 通过frida rpc方式调用js中的hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
# Python 2.x & 3.x compatible
# from distutils.log import warn as print

import sys
import frida
import codecs
import threading
import time
import re
import base64
import json

from flask import Flask, jsonify, request

if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf-8')


global gScript

HOOK_JS = './agent/zuiyouSign.js'

ZY_NAME = u'最右'
ZY_FRONTBOARD = 'cn.xiaochuankeji.tieba'

def bytesToHexString(bs):
return ''.join(['%02X ' % b for b in bs])

#系统标准输出,支持grep
def outWrite(text):
sys.stdout.write((text.encode('utf8') + b'\n' ).decode());

#带颜色打印输出
def colorPrint(color, s):
return "%s[31;%dm%s%s[0m" % (chr(27), color, s , chr(27))

#获取第一个USB连接的设备
def get_usb_iphone():
Type = 'usb'
if int(frida.__version__.split('.')[0]) < 12:
Type = 'tether'

device_manager = frida.get_device_manager()
changed = threading.Event()

def on_changed():
changed.set()

device_manager.on('changed', on_changed)

device = None
while device is None:
devices = [dev for dev in device_manager.enumerate_devices() if dev.type == Type]
if len(devices) == 0:
print ('Waiting for USB device...')
changed.wait()
else:
device = devices[0]

device_manager.off('changed', on_changed)

return device

#枚举运行进程信息
def listRunningProcess():
device = get_usb_iphone();
processes = device.enumerate_processes();
processes.sort(key = lambda item : item.pid)
outWrite('%-10s\t%s' % ('pid', 'name'))
for process in processes:
outWrite('%-10s\t%s' % (str(process.pid),process.name))

#枚举某个进程的所有模块信息
def listModulesoOfProcess(session):
moduels = session.enumerate_modules()
moduels.sort(key = lambda item : item.base_address)
for module in moduels:
outWrite('%-40s\t%-10s\t%-10s\t%s' % (module.name, hex(module.base_address), hex(module.size), module.path))
session.detach()

#从JS接受信息
def on_message(message, data):
print(message)
if message.has_key('payload'):
payload = message['payload']
if isinstance(payload, dict):
deal_message(payload)
else:
print (payload)

#加载JS文件脚本
def loadJsFile(session, filename):
source = ''
with codecs.open(filename, 'r', 'utf-8') as f:
source = source + f.read()
script = session.create_script(source)
script.on('message', on_message)
#接收js脚本的消息
script.load()
return script


app = Flask(__name__)

@app.route('/aesenc', methods=['POST']) # 数据加密
def zy_aesenc():
global gScript
data = request.get_data()
res = gScript.exports.callencaes(data.decode("utf-8"))
return res

@app.route('/aesdec', methods=['POST']) # 数据解密
def zy_aesdec():
global gScript

data = request.get_data()
res = gScript.exports.calldecaes(data.decode("utf-8"))
return res

@app.route('/sign', methods=['POST']) # 数据签名
def zy_sign():
global gScript

data = request.get_data()
print(data.decode("utf-8"))

res = gScript.exports.callsignfun('http://api.izuiyou.com/',data.decode("utf-8"))
return res

@app.route('/setkey', methods=['POST']) # 设置key
def zy_setKey():
global gScript

data = request.get_data()
res = gScript.exports.callsetkey(data);
return res

@app.route('/getkey') # 获取当前key
def zy_getKey():
global gScript

res = gScript.exports.callgetkey()
return res

def main():
global session
global appname
global gScript

# 1. 获取USB设备
device = get_usb_iphone()
if len(sys.argv)>2:
appname = sys.argv[2]
else:
appname = ZY_NAME

print('设备信息:' + str(device))
if sys.argv[1]=='ps':
# 枚举运行进程信息
print ('ps')
listRunningProcess()
elif sys.argv[1]=='hook':
# 动态Hook
print ('zyhook')

pid = device.spawn(ZY_FRONTBOARD)
session = device.attach(pid)
print("[*] Attach Application id:",pid)

device.resume(pid)
print("[*] Application onResume")

gScript = loadJsFile(session, HOOK_JS)
# sys.stdin.read()

# 启动web服务
app.run()


if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
if session:
session.detach()
sys.exit()
else:
pass
finally:
pass

image-20211113182156691

在源码中encodeAES的参数中jSONObject的值由mt8.Z返回,所以我们通过objection内存搜索打印返回即可

1
2
3
objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins 
android hooking list class_methods mt8
android hooking watch class_method mt8.Z --dump-args --dump-backtrace --dump-return

image-20211113182358081

python run.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from __future__ import print_function
# Python 2.x & 3.x compatible
# from distutils.log import warn as print

import sys
import requests
from requests import RequestException
import hashlib
import time
import json

if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf-8')

def get_frida_rpc(url,msg):
try:
r = requests.post(url,data=msg.encode('utf-8'))
# 这里可以做一些返回值的错误处理
return r.text
except RequestException as e:
print(e)

# 首页推荐内容解析
def dataAesEnc(msg):
return get_frida_rpc('http://127.0.0.1:5000/aesenc',msg)

def dataAesDec(msg):
return get_frida_rpc('http://127.0.0.1:5000/aesdec',msg)

def dataSign(msg):
return get_frida_rpc('http://127.0.0.1:5000/sign',msg)

def dataGetKey():
r = requests.get('http://127.0.0.1:5000/getkey')
return r.text

def dataSetKey(msg):
return get_frida_rpc('http://127.0.0.1:5000/setkey',msg)


# 获取数据
def get_data(uri, msg):
postData = dataAesEnc(msg)
# print(postData)
sign = dataSign(postData)
# print(sign)
protocolKey = dataGetKey()
# print(protocolKey)

url = uri + str(sign)
headers = { 'ZYP': 'mid=239631186',
'X-Xc-Agent' : 'av=5.5.11,dt=0',
'User-Agent': 'okhttp/3.12.2 Zuiyou/5.5.11 (Android/27)',
'X-Xc-Proto-Req' : protocolKey,
'Request-Type' : 'text/json',
'Content-Type' : 'application/xcp',
}
# 将十六进制字符串转字节数组 b'/\x07\xc5\xf4\x143\xc2\xe1\x01...
byte_data = bytes.fromhex(postData)
# proxies = {'http': '127.0.0.1:8888',
# 'https': '127.0.0.1:8888'
# }

try:
# ,proxies=proxies
r = requests.post(url, headers=headers, data=byte_data, verify=False,stream=True,timeout=15)

# print(r.headers['X-Xc-Proto-Res'])
# key是 cat开头的就需要把返回包里面的duck key设置进去,
if protocolKey.find('cat') == 0:
print(protocolKey)
print(r.headers['X-Xc-Proto-Res'])
dataSetKey(r.headers['X-Xc-Proto-Res'])
# 这里要做一些错误处理
bufRc = r.raw.read();
rcStr = dataAesDec(bufRc.hex())
rc = bytes.fromhex(rcStr).decode("utf-8")
return rc

except RequestException as e:
print(e)

def main():
# 手机端首页推荐地址
uri = 'http://api.izuiyou.com/index/recommend?sign='
msg = '{"filter":"all","auto":0,"tab":"推荐","direction":"down","c_types":[1,2,11,15,16,51,17,52,53,40,50,41,22,25,27],"sdk_ver":{"tt":"3.1.0.3","tx":"4.211.1081","bd":"5.86","mimo":"5.0.3","tt_aid":"5004095","tx_aid":"1106701465","bd_aid":"c8655095","mimo_aid":"2882303761518470184"},"ad_wakeup":1,"h_ua":"Mozilla\/5.0 (Linux; Android 8.1.0; Redmi 6A Build\/O11019; wv) AppleWebKit\/537.36 (KHTML, like Gecko) Version\/4.0 Chrome\/62.0.3202.84 Mobile Safari\/537.36","manufacturer":"Xiaomi","h_av":"5.5.11","h_dt":0,"h_os":27,"h_app":"zuiyou","h_model":"Redmi 6A","h_did":"866655030396869","h_nt":1,"h_m":239631186,"h_ch":"xiaomi","h_ts":1603179121590,"token":"T7K4Nnqg98_aFV9JwkfuiZtvPrRJ02EXxbnm7TXr3qiIWWaT1vjNNNCpcUu112TDw_VXu","android_id":"57b9b8465c2e440b","h_ids":{"imei2":"878739042239784","meid":"98001184062989","imei1":"878739042239776","imei":"98001184062989"},"h_os_type":"miui"}'

items = get_data(uri, msg)
print(items)

if __name__ == '__main__':
j = 1
while True:
print("第{}次加载的段子\n".format(j))
main()
j += 1

# 测试程序,加载3次
if j >1 :
break

print("加载完毕")

image-20211113182515066

Xposed

Android Studio新建Empty Activity–OneJaneXposed,删除默认MainActivity和AndroidManifest.xml中的activity

AndroidManifest.xml

1
2
3
4
5
6
7
8
9
10
11
<meta-data
android:name="xposedmodule"
android:value="true" />

<meta-data
android:name="xposeddescription"
android:value="zy hook" />

<meta-data
android:name="xposedminversion"
android:value="53" />

app\build.gradle

1
2
3
4
dependencies {
compileOnly 'de.robv.android.xposed:api:82'
compileOnly 'de.robv.android.xposed:api:82:sources'
}

app\src\main\java\com\example\onejanexposed\HookLoader.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HookLoader implements IXposedHookLoadPackage {
private final static String TAG = "onejane";
public static void log(String s) {
Log.i(TAG, s);
}

public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {

log("Im comming Frist"+loadPackageParam.packageName);
XposedBridge.log("zzx zy has Hooked Frist!");

if (loadPackageParam.packageName.equals("cn.xiaochuankeji.tieba")) {
log(TAG+"Im comming zy 3");
}
}
}

app\src\main\assets\xposed_init 配置xposed入口类

1
com.example.onejanexposed.HookLoader

在xposed中启动该模块即可

image-20211121125229618

完整xposed实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
public class HookLoader implements IXposedHookLoadPackage {
private final static String TAG = "onejane";
public static void log(String s) {
Log.i(TAG, s);
}

public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {

log("Im comming "+loadPackageParam.packageName);
XposedBridge.log("xposed log Hooked Frist!");

if (loadPackageParam.packageName.equals("cn.xiaochuankeji.tieba")) {
log("Im comming zy 3");


// http server
class myHttpServer extends NanoHTTPD {
private static final String REQUEST_ROOT = "/";

public myHttpServer() throws IOException {
// 端口是8088,也就是说要通过http://127.0.0.1:8088来访当问
super(8888);
start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
log("---fenfei Server---");
}

@Override
public Response serve(IHTTPSession session) {
// log("serve");
//这个就是之前分析,重写父类的一个参数的方法,
//这里边已经把所有的解析操作已经在这里执行了
return super.serve(session);
}

@Override
public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
// log("serve xxx");
//这就是上边的serve方法最后一行调用的那个过时的方法,这里简单的做个判断就好了
// if (!method.equals(Method.POST)) {//判断请求方式是否争取
// return newFixedLengthResponse("the request method is incoorect");
// }

log(uri);

for (Map.Entry<String, String> entry : files.entrySet()) {
log("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}


Class<?> clazzZy = null;
try {
clazzZy = loadPackageParam.classLoader.loadClass("com.izuiyou.network.NetCrypto");
log("load class:" + clazzZy);
} catch (Exception e) {
log("load class err:" + Log.getStackTraceString(e));
return newFixedLengthResponse("load class is null");
}

if (StringUtils.containsIgnoreCase(uri, "getkey")) {//判断uri是否正确
return getKey(clazzZy);
}

if (StringUtils.containsIgnoreCase(uri, "setkey")) {//判断uri是否正确
String postData = files.get("postData");
if (!StringUtils.isEmpty(postData)) {//判断post过来的数据是否正确
return setkey(clazzZy,postData);
}else{
return newFixedLengthResponse("postData is null");
}
}

if (StringUtils.containsIgnoreCase(uri, "sign")) {
String postData = files.get("postData");
if (!StringUtils.isEmpty(postData)) {
return sign(clazzZy,postData);
}else{
return newFixedLengthResponse("postData is null");
}
}

if (StringUtils.containsIgnoreCase(uri, "aesenc")) {
String postData = files.get("postData");
if (!StringUtils.isEmpty(postData)) {
return aesenc(clazzZy,postData);
}else{
return newFixedLengthResponse("postData is null");
}
}


if (StringUtils.containsIgnoreCase(uri, "aesdec")) {//判断uri是否正确
String postData = files.get("postData");
if (!StringUtils.isEmpty(postData)) {//判断post过来的数据是否正确
return aesdec(clazzZy,postData);
}else{
return newFixedLengthResponse("postData is null");
}
}


//判断完了开始解析数据,如果是你想要的数据,那么你就给返回一个正确的格式就好了
//举个栗子:return newFixedLengthResponse("{\"result\":0,\"success\":true}");
return super.serve(uri, method, headers, parms, files);
}

public Response sign(Class<?> clazzUse,String strData){
byte[] inBuf = Hex.hex2Byte(strData);
String rc = (String) XposedHelpers.callStaticMethod(clazzUse, "sign","http://api.izuiyou.com/",inBuf);
log("sign = "+rc);
return newFixedLengthResponse(rc);

}

public Response aesenc(Class<?> clazzUse,String strData){
byte[] inBuf = strData.getBytes() ;
byte[] rc = (byte[])XposedHelpers.callStaticMethod(clazzUse, "encodeAES",inBuf);
String rcStr = Hex.byte2Hex(rc);
log("aesenc = "+ rcStr);
return newFixedLengthResponse(rcStr);

}

public Response aesdec(Class<?> clazzUse,String strData){
byte[] inBuf = Hex.hex2Byte(strData);
byte[] rc = (byte[])XposedHelpers.callStaticMethod(clazzUse, "decodeAES",inBuf,true);
String rcStr = Hex.byte2Hex(rc);
log("aesdec = "+ rcStr);
return newFixedLengthResponse(rcStr);

}


public Response setkey(Class<?> clazzUse,String strKey){
XposedHelpers.callStaticMethod(clazzUse, "setProtocolKey",strKey);
log("setkey = "+strKey);
return newFixedLengthResponse("set key ok");
}

public Response getKey(Class<?> clazzUse){
String rc = (String) XposedHelpers.callStaticMethod(clazzUse, "getProtocolKey");
log("getkey = "+rc);
return newFixedLengthResponse(rc);
}
}
new myHttpServer();
}
}
}

build.gradle

1
2
implementation 'org.apache.commons:commons-lang3:3.7'
implementation 'org.nanohttpd:nanohttpd:2.3.1'

结合run.py修改请求地址为172.20.103.67:8888,以上只是通过objection内存漫游拿到的参数借助xposed或者frida主动调用so层函数,实现在python的rpc调用sign参数的加密。

Unidbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ZuiYouSign extends AbstractJni {
public static void main(String[] args) throws IOException {
// 1、需要调用的Apk文件所在路径
String apkFilePath = "right573.apk";
// 2、需要调用函数所在的Java类完整路径,比如a/b/c/d等等,注意需要用/代替.
String classPath = "com/izuiyou/network/NetCrypto";
ZuiYouSign runZyObj = new ZuiYouSign(apkFilePath, classPath);
runZyObj.destroy();
}

// ARM模拟器
private final ARMEmulator emulator;
// vm
private final VM vm;
// 载入的模块
private final Module module;

private final DvmClass TTEncryptUtils;

public ZuiYouSign(String apkFilePath, String classPath) throws IOException {
// 创建app进程,包名可任意写
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.onejane.RunZy").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
// 作者支持19和23两个sdk
memory.setLibraryResolver(new AndroidResolver(23));

// 创建DalvikVM,利用apk本身,可以为null
vm = ((AndroidARMEmulator) emulator).createDalvikVM(new File(apkFilePath));
vm.setVerbose(true);
vm.setJni(this);
new AndroidModule(emulator, vm).register(memory);

// (关键处1)加载so,填写so的文件路径 或者vm.loadLibrary(new File("net_crypto.so"), true)
DalvikModule dm = vm.loadLibrary("net_crypto", false);

// 调用jni
dm.callJNI_OnLoad(emulator);
module = dm.getModule();

// (关键处2)加载so文件中的哪个类,填写完整的类路径
TTEncryptUtils = vm.resolveClass(classPath);
}

/**
* 关闭模拟器
* @throws IOException
*/
private void destroy() throws IOException {
emulator.close();
System.out.println("emulator destroy...");
}
}

image-20211124174951985

在JNIOnLoad时对函数进行了动态注册,当loadLibrary时不执行init相关函数,即第二个参数为false时输出结果乱码,因为so对字符串做了混淆加密,一般在Init array节或者JNIOnLoad,总之在字符串使用前的任何一个时机点都有可能。IDA打开libnet_crypto.so,shift+F7找到节区中的init_array,解密逻辑就在图中的函数中,如果模拟执行时加载so不执行init相关函数,导致so字符串没有被解密,输出乱码。

image-20211124180120859

public static {
    we5.a(ContextProvider.get(), "net_crypto");
    NetCrypto.native_init();
}

加载完net_crypto后,执行native_init()

1
2
3
4
5
runZyObj.initCall();
private void initCall() {
// jeb反编译的smali指令 .method public static native native_init()V
TTEncryptUtils.callStaticJniMethod(emulator, "native_init()V");
}

或者通过地址调用

1
2
3
4
5
6
7
8
9
10
DalvikModule dm = vm.loadLibrary(new File("libnet_crypto.so"), true); // 加载so到虚拟内存
module = dm.getModule(); //获取本SO模块的句柄

public void native_init(){
// 0x4a069 是native_init动态注册的地址
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
module.callFunction(emulator, 0x4a069, list.toArray());
}

报错如下:

java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:426)

补环境重写 callStaticObjectMethodV

1
2
3
4
5
6
7
8
9
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":
return vm.resolveClass("android/content/Context", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(signature);
}

return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

image-20211121143238365

调用sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 3、jadx中smali  invoke-static {p0, p1}, Lcom/izuiyou/network/NetCrypto;->sign(Ljava/lang/String;[B)Ljava/lang/String;
String methodSign = "sign(Ljava/lang/String;[B)Ljava/lang/String;";
String InBuf = "50027f7...";
//输出方法调用结果
String ret = runZyObj.getSign(methodSign
, new StringObject(runZyObj.vm, "https://zyadapi.izuiyou.com/ad/popup_ad") //"https://api.izuiyou.com/index/recommend")
, hexStringToBytes(InBuf));

// Out Rc=v2-1ff7402d2b4fa9a4c39b3853262f18fd
System.out.printf("ret:%s\n", ret);

private String getSign(String methodSign, Object... args) {
// 使用jni调用传入的函数签名对应的方法()
Object value = TTEncryptUtils.callStaticJniMethodObject(emulator, methodSign, args).getValue();
return value.toString();
}

或者通过地址调用

1
2
3
4
5
6
7
8
9
10
11
private String callSign(){
// 准备入参 0x4a28D是sign动态注册的地址
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv()); // 第一个参数是env
list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
list.add(vm.addLocalObject(new StringObject(vm, "12345")));
ByteArray plainText = new ByteArray(vm, "onejane".getBytes(StandardCharsets.UTF_8));
list.add(vm.addLocalObject(plainText));
Number number = module.callFunction(emulator, 0x4a28D, list.toArray())[0];
return vm.getObject(number.intValue()).getValue().toString();
};

报错如下:

java.lang.UnsupportedOperationException: android/content/Context->getClass()Ljava/lang/Class;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)

补环境重写getClass

1
2
3
4
5
6
7
8
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":
return vm.resolveClass("java/lang/Class");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

报错如下:

java.lang.UnsupportedOperationException: java/lang/Class->getSimpleName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)

getSimpleName 是个类名,根据之前的报错逻辑,com/izuiyou/common/base/BaseApplication->getAppContext()获取到Context后,通过android/content/Context->getClass()获取到类,最终获取类名,之前补环境虽然只补了类型,只要最终返回的结果正确即可。根据objection内存漫游拿到完整类名就是cn.xiaochaunkeji.tieba.AppController,getSimpleName就是AppController

image-20211124204039287

或者通过frida主动调用静态函数获取getSimpleName的返回结果

1
2
3
4
5
6
7
8
9
10
setImmediate(function(){
Java.perform(function(){
var BaseApplication = Java.use("com.izuiyou.common.base.BaseApplication").getAppContext().getClass();
var methods = BaseApplication.class.getDeclaredMethods();
for(var j = 0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
}
});
})

遍历该类中的方法确实存在一个叫getSimpleName()

1
2
3
4
5
6
setImmediate(function(){
Java.perform(function(){
var BaseApplication = Java.use("com.izuiyou.common.base.BaseApplication").getAppContext().getClass();
console.log(BaseApplication.getSimpleName())
});
})

返回结果就是AppController

补环境

1
2
3
4
5
6
7
8
9
10
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":
return vm.resolveClass("java/lang/Class");
case "java/lang/Class->getSimpleName()Ljava/lang/String;":
return new StringObject(vm, "AppController");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

报错如下:

java.lang.UnsupportedOperationException: cn/xiaochuankeji/tieba/common/debug/AppLogReporter->reportAppRuntime(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:612)

reportAppRuntime没有返回值,直接return

1
2
3
4
5
6
7
8
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "cn/xiaochuankeji/tieba/common/debug/AppLogReporter->reportAppRuntime(Ljava/lang/String;Ljava/lang/String;)V":
return;
}
throw new UnsupportedOperationException(signature);
}

报错如下:

java.lang.UnsupportedOperationException: android/content/Context->getFilesDir()Ljava/io/File;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)

读取文件直接返回java/io/File

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":
return vm.resolveClass("java/lang/Class");
case "java/lang/Class->getSimpleName()Ljava/lang/String;":
return new StringObject(vm, "AppController");
case "android/content/Context->getFilesDir()Ljava/io/File;":
return vm.resolveClass("java/io/File");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

报错如下:

java.lang.UnsupportedOperationException: java/lang/Class->getAbsolutePath()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:373)

获取绝对路径,直接返回/sdcard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "android/content/Context->getClass()Ljava/lang/Class;":
return vm.resolveClass("java/lang/Class");
case "java/lang/Class->getSimpleName()Ljava/lang/String;":
return new StringObject(vm, "AppController");
case "android/content/Context->getFilesDir()Ljava/io/File;":
return vm.resolveClass("java/io/File");
case "java/lang/Class->getAbsolutePath()Ljava/lang/String;":
return new StringObject(vm, "/sdcard");
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

报错如下:

java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:169)

判断是否调试,直接return false

1
2
3
4
5
6
7
8
@Override
public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "android/os/Debug->isDebuggerConnected()Z":
return Boolean.FALSE;
}
return super.callStaticBooleanMethodV(vm,dvmClass,signature,vaList);
}

报错如下:

java.lang.UnsupportedOperationException: android/os/Process->myPid()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:189)

需要pid

1
2
3
4
5
6
7
8
9
@Override
public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "android/os/Process->myPid()I":
//return 123;
return emulator.getPid();
}
return super.callStaticIntMethodV(vm,dvmClass,signature,vaList);
}

image-20211124204809055

算法还原

Hook

由于返回值固定为32位,且输入不变输出不变,可能是哈希算法中的md5算法,不过libnet_crypto.so做了一定的OLLVM混淆,借助IDA-Edit-Plugins-FindHash对哈希算法进行正则匹配,对函数逐个反编译。

image-20211125102427529

1
2
3
4
5
6
7
PYTHON_CONFIGURE_OPTS="--disable-ipv6" proxychains pyenv install 3.9.1
pyenv local 3.9.1
proxychains4 pip install objection==1.11.0
adb push frida-server-15.1.12-android-arm64 /data/local/tmp
mv frida-server-15.1.12-android-arm64 fs15112arm64
chmod 777 fs15112arm64
./fs15112arm64

frida -UF cn.xiaochuankeji.tieba -l libnet_crypto_findhash_1637807043.js 刷新页面触发sign生成

image-20211125122227065

IDA快捷键 G 跳转到65540,F5跳转到伪C代码,

image-20211125122620830

尝试hook该函数,这三个参数不确定是指针还是数值,所以先全部做为数值处理,作为long类型看待,防止整数溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void hook65540(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
System.out.println(ctx.getR0Long());
System.out.println(ctx.getR1Long());
System.out.println(ctx.getR2Long());
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
});
}
public static void main(String[] args) throws Exception {
zuiyou test = new zuiyou();
test.hook65540();
test.native_init();
System.out.println(test.callSign());
}

打印入参

3221223188
7
3221223060

参数2应该是数组,参数1和3则像是地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void hook65540(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1");
System.out.println(ctx.getR1Long());
Inspector.inspect(ctx.getR2Pointer().getByteArray(0, 0x10), "Arg3");
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
}
});
}

打印地址所指向的内存,类似frida中的dexdump,第二个参数总是和入参的长度一致,默认作为长度。

image-20211125124133542

第一个参数就是我们传入的onejane,即public static native String sign(String str, byte[] bArr)第二个参数

HookZz在执行前,push保存参数,在后面再pop取出参数,我们观察下参数3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void hook65540(){
// 加载HookZz
IHookZz hookZz = HookZz.getInstance(emulator);

hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap导出函数
@Override
// 类似于 frida onEnter
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// 类似于Frida args[0]
Inspector.inspect(ctx.getR0Pointer().getByteArray(0, 0x10), "Arg1");
System.out.println(ctx.getR1Long());
Inspector.inspect(ctx.getR2Pointer().getByteArray(0, 0x10), "Arg3");
// push
ctx.push(ctx.getR2Pointer());
};

@Override
// 类似于 frida onLeave
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
// pop 取出
Pointer output = ctx.pop();
Inspector.inspect(output.getByteArray(0, 0x10), "Arg3 after function");
}
});
}

image-20211125124347564

md5有四个iv,该函数中确实也有四个数值,通过H键转成十六进制,和默认的iv值不一致,说明md5算法被魔改了。

image-20211125125021958

将md5.py中的四个iv改成样本中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# codeing=utf-8
# 引入math模块,因为要用到sin函数
import math

# 定义常量,用于初始化128位变量,注意字节顺序,文中的A=0x01234567,这里低值存放低字节,即01 23 45 67,所以运算时A=0x67452301,其他类似。
# 这里用字符串的形势,是为了和hex函数的输出统一,hex(10)输出为'0xA',注意结果为字符串。
A = '0x67552301' # '0x67452301'
B = '0xEDCDAB89' # '0xefcdab89'
C = '0x98BADEFE' # '0x98badcfe'
D = '0x16325476' # '0x10325476'
# 定义每轮中用到的函数。L为循环左移,注意左移之后可能会超过32位,所以要和0xffffffff做与运算,确保结果为32位。
F = lambda x, y, z: ((x & y) | ((~x) & z))
G = lambda x, y, z: ((x & z) | (y & (~z)))
H = lambda x, y, z: (x ^ y ^ z)
I = lambda x, y, z: (y ^ (x | (~z)))
L = lambda x, n: (((x << n) | (x >> (32 - n))) & (0xffffffff))

# 定义每轮中循环左移的位数,这里用4个元组表示,用元组是因为速度比列表快。
shi_1 = (7, 12, 17, 22) * 4
shi_2 = (5, 9, 14, 20) * 4
shi_3 = (4, 11, 16, 23) * 4
shi_4 = (6, 10, 15, 21) * 4

# 定义每轮中用到的M[i]次序。
m_1 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
m_2 = (1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12)
m_3 = (5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2)
m_4 = (0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9)


# 定义函数,用来产生常数T[i],常数有可能超过32位,同样需要&0xffffffff操作。注意返回的是十进制的数。
def T(i):
result = (int(4294967296 * abs(math.sin(i)))) & 0xffffffff
return result


# 定义函数,用来将列表中的元素循环右移。原因是在每轮操作中,先运算A的值,然后是D,C,B,16轮之后右恢复原来顺序,所以只要每次操作第一个元素即可。
def shift(shift_list):
shift_list = [shift_list[3], shift_list[0], shift_list[1], shift_list[2]]
return shift_list


# 定义主要的函数,参数为当做种子的列表,每轮用到的F,G,H,I,生成的M[],以及循环左移的位数。该函数完成一轮运算。
def fun(fun_list, f, m, shi):
count = 0
global Ti_count
# 引入全局变量,T(i)是从1到64循环的。
while count < 16:
xx = int(fun_list[0], 16) + f(int(fun_list[1], 16), int(fun_list[2], 16), int(fun_list[3], 16)) + int(m[count],
16) + T(
Ti_count)
xx = xx & 0xffffffff
ll = L(xx, shi[count])
# fun_list[0] = hex((int(fun_list[1],16) + ll)&(0xffffffff))[:-1]
fun_list[0] = hex((int(fun_list[1], 16) + ll) & (0xffffffff))
# 最后的[:-1]是为了去除类似'0x12345678L'最后的'L'
fun_list = shift(fun_list)
count += 1
Ti_count += 1
# print (fun_list)
return fun_list


# 该函数生成每轮需要的M[],最后的参数是为了当有很多分组时,进行偏移。
def genM16(order, ascii_list, f_offset):
ii = 0
m16 = [0] * 16
f_offset = f_offset * 64
for i in order:
i = i * 4
m16[ii] = '0x' + ''.join((ascii_list[i + f_offset] + ascii_list[i + 1 + f_offset] + ascii_list[
i + 2 + f_offset] + ascii_list[i + 3 + f_offset]).split('0x'))
ii += 1
for c in m16:
ind = m16.index(c)
m16[ind] = reverse_hex(c)
return m16


# 翻转十六进制数的顺序:'0x01234567' => '0x67452301'
def reverse_hex(hex_str):
hex_str = hex_str[2:]
hex_str_list = []
for i in range(0, len(hex_str), 2):
hex_str_list.append(hex_str[i:i + 2])
hex_str_list.reverse()
hex_str_result = '0x' + ''.join(hex_str_list)
return hex_str_result


# 显示结果函数,将最后运算的结果列表进行翻转,合并成字符串的操作。
def show_result(f_list):
result = ''
f_list1 = [0] * 4
for i in f_list:
f_list1[f_list.index(i)] = reverse_hex(i)[2:]
result = result + f_list1[f_list.index(i)]
return result


# 程序主循环
while True:
abcd_list = [A, B, C, D]
Ti_count = 1
# input_m = raw_input('msg>>>')
input_m = input('msg>>>')
# 对每一个输入先添加一个'0x80',即'10000000'
ascii_list = list((map(hex, map(ord, input_m))))
# print('ascii_list:',ascii_list)
msg_lenth = len(ascii_list) * 8
ascii_list.append('0x80')

# 补充0
while (len(ascii_list) * 8 + 64) % 512 != 0:
ascii_list.append('0x00')

# 最后64为存放消息长度,注意长度存放顺序低位在前。
# 例如,消息为'a',则长度为'0x0800000000000000'
msg_lenth_0x = hex(msg_lenth)[2:]
msg_lenth_0x = '0x' + msg_lenth_0x.rjust(16, '0')
msg_lenth_0x_big_order = reverse_hex(msg_lenth_0x)[2:]
msg_lenth_0x_list = []
for i in range(0, len(msg_lenth_0x_big_order), 2):
msg_lenth_0x_list.append('0x' + msg_lenth_0x_big_order[i:i + 2])
ascii_list.extend(msg_lenth_0x_list)
# print (ascii_list)

# 对每个分组进行4轮运算
for i in range(0, len(ascii_list) // 64):
# 将最初128位种子存放在变量中,
aa, bb, cc, dd = abcd_list
# 根据顺序产生每轮M[]列表
order_1 = genM16(m_1, ascii_list, i)
order_2 = genM16(m_2, ascii_list, i)
order_3 = genM16(m_3, ascii_list, i)
order_4 = genM16(m_4, ascii_list, i)
# 主要四轮运算,注意打印结果列表已经被进行过右移操作!
abcd_list = fun(abcd_list, F, order_1, shi_1)
# print ('--------------------------------------')
abcd_list = fun(abcd_list, G, order_2, shi_2)
# print ('--------------------------------------')
abcd_list = fun(abcd_list, H, order_3, shi_3)
# print ('--------------------------------------')
abcd_list = fun(abcd_list, I, order_4, shi_4)
# print ('--------------------------------------')
# 将最后输出与最初128位种子相加,注意,最初种子不能直接使用abcd_list[0]等,因为abcd_list已经被改变
output_a = hex((int(abcd_list[0], 16) + int(aa, 16)) & 0xffffffff)
output_b = hex((int(abcd_list[1], 16) + int(bb, 16)) & 0xffffffff)
output_c = hex((int(abcd_list[2], 16) + int(cc, 16)) & 0xffffffff)
output_d = hex((int(abcd_list[3], 16) + int(dd, 16)) & 0xffffffff)
# 将输出放到列表中,作为下一次128位种子
abcd_list = [output_a, output_b, output_c, output_d]
# 将全局变量Ti_count恢复,一遍开始下一个分组的操作。
Ti_count = 1
# 最后调用函数,格式化输出
print('md5>>>' + show_result(abcd_list))
# 0CC175B9C0F1B6A831C399E269772661
break

msg>>>onejane
md5>>>abb01439f7c91b4a4aa431a4dee8aba0

返回的结果和unidbg中的Arg3的结果一致,AB B0 14 39 F7 C9 1B 4A 4A A4 31 A4 DE E8 AB A0 ...9...JJ.1.....,因此可以断定,此处魔改确实只魔改了IV的md5算法。

主动调用

Unidbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void callMd5(){
List<Object> list = new ArrayList<>(10);

// arg1
String input = "onejane";
// malloc memory
MemoryBlock memoryBlock1 = emulator.getMemory().malloc(16, false);
// get memory pointer
UnidbgPointer input_ptr=memoryBlock1.getPointer();
// write plainText on it
input_ptr.write(input.getBytes(StandardCharsets.UTF_8));

// arg2
int input_length = input.length();

// arg3 -- buffer
MemoryBlock memoryBlock2 = emulator.getMemory().malloc(16, false);
UnidbgPointer output_buffer=memoryBlock2.getPointer();

// 填入参入
list.add(input_ptr);
list.add(input_length);
list.add(output_buffer);
// run
module.callFunction(emulator, 0x65540 + 1, list.toArray());

// print arg3
Inspector.inspect(output_buffer.getByteArray(0, 0x10), "output");
};

image-20211125130632061

Frida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function call_65540(base_addr){
// 函数在内存中的地址
var real_addr = base_addr.add(0x65541)
var md5_function = new NativeFunction(real_addr, "int", ["pointer", "int", "pointer"])
// 参数1 明文字符串的指针
var input = "onejane";
var arg1 = Memory.allocUtf8String(input);
// 参数2 明文长度
var arg2 = input.length;
// 参数3,存放结果的buffer
var arg3 = Memory.alloc(16);
md5_function(arg1, arg2, arg3);
console.log(hexdump(arg3,{length:0x10}));
}

function callMd5(){
// 确定SO 的基地址
var base_addr = Module.findBaseAddress("libnet_crypto.so");
call_65540(base_addr);
}

image-20211125130424025

总结

本文针对最右app的sign进行抽丝剥茧的分析,利用Frida的Hook及RPC主动调用,Xposed函数调用,Unidbg补环境模拟执行so等手段实现so逆向分析,最终得出核心算法通过魔改md5实现算法还原。

文章作者: J
文章链接: http://onejane.github.io/2021/11/11/SO逆向之最右sign分析/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏