环境
python: 3.8
frida: 12.8.0
objection: 1.8.4
app version: 5.0.0
从图可以看出,在url里面有个sign,然后post的body里面还有个sign_v1
,此外password经过加密。
sign
jadx搜索"sign"
,
从搜索结果来看,第一个比较符合,几个参数名都能对得上,打开看看
可以看到,a3 = dgb.a(sb)
,继续追踪
然后依次点进faw.a(str)
,.c()
,.i()
函数看看大概干了什么:
faw.a(str)
.c()
.i()
大致可以看出dgb.a
应该是对str
做了个MD5,然后我们用frida来hook试试
// nice_sign.js
Java.perform(function(){
var dgb = Java.use("defpackage.dgb");
dgb.a.implementation = function(str) {
console.log("dgb.a-str", str);
var ret = this.a(str);
console.log("dgb.a-ret", ret);
return ret;
}
})
然后执行frida -U com.nice.main -l nice_sign.js
,发现报错了。。
既然这样,改用objection,执行命令objection -g com.nice.main explore
,然后执行android hooking search classes dgb
:
修改nice_sign.js
,并执行
// nice_sign.js
Java.perform(function(){
var dgb = Java.use("dgb"); // 修改class路径
dgb.a.implementation = function(str) {
console.log("dgb.a-str", str);
var ret = this.a(str);
console.log("dgb.a-ret", ret);
return ret;
}
})
可以看出输入参数就是将body里面的参数按key排序,然后进行urlencode。
试了之后发现是标准MD5实现:
python重写如下:
# cipher.py - part1
import hashlib
from urllib.parse import urlencode
def serialize(data, quote=True):
if isinstance(data, (list, tuple, dict)):
if hasattr(data, 'items'):
data = data.items()
data = sorted(data)
if quote:
data = urlencode(data)
else:
data = '&'.join(f'{k}={v}' for k, v in data)
return data
def calc_sign(data):
data = serialize(data)
return hashlib.md5(data.encode()).hexdigest()
sign-v1
Java层
jadx搜索sign-v1
,发现没有结果,转而继续搜索"sign"
:
这次打开下面的代码:
没什么信息,继续往下走,点开NiceSignUtils.a
:
NiceSignUtils.a
函数无法查看,不过上面的native函数getSignCode
和getSignRequest
都比较可疑,使用objection监控一下,看下是否有调用:
android hooking watch class com.nice.main.helpers.utils.NiceSignUtils --dump-args --dump-backtrace
可以看到,最后是调用了getSignRequest
方法,frida hook该方法
Java.perform(function() {
var NiceSignUtils = Java.use("com.nice.main.helpers.utils.NiceSignUtils");
NiceSignUtils.getSignRequest.implementation = function(str, arr1, arr2, arr3) {
console.log("getSignRequest-str", str);
console.log("getSignRequest-arr1", JSON.stringify(arr1));
console.log("getSignRequest-arr2", JSON.stringify(arr2));
console.log("getSignRequest-arr3", JSON.stringify(arr3));
var ret = this.getSignRequest(str, arr1, arr2, arr3);
console.log("getSignRequest-ret", ret);
return ret;
}
})
可以看到str
就是body那个字典,arr1
是str
的字节数组,arr2
是did
,arr3
不太清楚。
so层
ida打开libsalt.so,打开Java_com_nice_main_helpers_utils_NiceSignUtils_getSignRequest
函数,可以看到熟悉的字符串:
有个看起来很重要的函数,进去看看:
继续点进去:
看起来这就是加密的主体了,先hook看看传入参数
Java.perform(function(){
var sign_v3 = Module.findExportByName("libsalt.so", "nice_sign_v3");
Interceptor.attach(sign_v3, {
onEnter: function(args){
console.log("sign_v3-arg0", args[0].readCString());
console.log("sign_v3-arg1", args[1].readCString());
console.log("sign_v3-arg2", args[2].readCString());
}
})
})
a1
是body的字典,a2
是did
,a3
是body携带的key
然后就是分析nice_sign_v3
函数的代码实现了,
j_swap_char -> swap_char
通过分析代码(也可以通过hook输入,输出)得出,它是交换字符串的前半段和后半段
def swap(x):
n = len(x) // 2
return x[n:] + x[:n]
j_nice_md5 -> nice_md5
可以通过hook它的输入和输出发现,这是一个标准的MD5实现。
然后是j_cJSON_Parse
和sub_2790
函数,里面的代码看起来比较复杂,我们直接hook函数sub_2790
看看它的输出是什么,是否方便构造
Java.perform(function(){
var bptr = Module.findBaseAddress("libsalt.so");
var ptr_0x2790 = bptr.add(0x2790 + 1);
Interceptor.attach(ptr_0x2790, {
onEnter: function(args) {
this.arg0 = args[0];
},
onLeave: function(retval){
console.log("0x2790-retval", retval);
console.log("0x2790-retval", retval.readCString());
}
})
})
看起来是对字典序列化,将字典按key排序,然后拼接起来,继续往下面分析
框里面是一个非标准的方法:每2个字符,取第一个字符的高位,取第二个字符的低位,然后取或操作。
def bitop(s):
data = []
for i in range(len(s) // 2):
x = ord(s[2*i]) & 0xf0 | ord(s[2*i+1]) & 0xf
data.append(chr(x))
return ''.join(data)
然后就是将上面的部分拼接起来:
def calc_sign_v1(data, did, key):
new_did = swap(did)
did_sign = hashlib.md5(new_did.encode()).hexdigest()
s1 = key + did_sign + '8a5f746c1c9c99c0b458e1ed510845e5'
sign1 = hashlib.md5(s1.encode()).hexdigest()
new_sign1 = swap(sign1)
data = serialize(data, quote=False)
new_data = bitop(data)
s2 = new_data + new_sign1
sign2 = hashlib.sha1(s2.encode()).hexdigest()
new_sign2 = swap(sign2[8:])
return new_sign2
password
jadx搜索"password"
:
点进去,查找引用:
奇怪的字符串,进去看看:
继续看看bas.a
所以,password用的是RSA加密,其中公钥是"MIGfMA0GCSqG...AQAB"
import base64
import hashlib
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
# 请自行查找
PUB_KEY_B64 = 'MIGfMA0GCSqGSI...DYznGO9wIDAQAB'
PUB_KEY = base64.b64decode(PUB_KEY_B64)
def rsa_encrypt(msg):
cipher = PKCS1_v1_5.new(RSA.import_key(PUB_KEY))
content = cipher.encrypt(msg.encode())
content = base64.b64encode(content).decode('utf8')
return content
总体实现
import base64
import hashlib
import json
import random
import requests
import string
from urllib.parse import urlencode
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
# 请自行查找
PUB_KEY_B64 = 'MIGfMA0GCSqGSI...DYznGO9wIDAQAB'
PUB_KEY = base64.b64decode(PUB_KEY_B64)
def rsa_encrypt(msg):
cipher = PKCS1_v1_5.new(RSA.import_key(PUB_KEY))
content = cipher.encrypt(msg.encode())
content = base64.b64encode(content).decode('utf8')
return content
def serialize(data, quote=True):
if isinstance(data, (list, tuple, dict)):
if hasattr(data, 'items'):
data = data.items()
data = sorted(data)
if quote:
data = urlencode(data)
else:
data = '&'.join(f'{k}={v}' for k, v in data)
return data
def calc_sign(data):
data = serialize(data)
return hashlib.md5(data.encode()).hexdigest()
def swap(x):
n = len(x) // 2
return x[n:] + x[:n]
def bitop(s):
data = []
for i in range(len(s) // 2):
x = ord(s[2*i]) & 0xf0 | ord(s[2*i+1]) & 0xf
data.append(chr(x))
return ''.join(data)
def calc_sign_v1(data, did, key):
new_did = swap(did)
did_sign = hashlib.md5(new_did.encode()).hexdigest()
s1 = key + did_sign + '8a5f746c1c9c99c0b458e1ed510845e5'
sign1 = hashlib.md5(s1.encode()).hexdigest()
new_sign1 = swap(sign1)
data = serialize(data, quote=False)
new_data = bitop(data)
s2 = new_data + new_sign1
sign2 = hashlib.sha1(s2.encode()).hexdigest()
new_sign2 = swap(sign2[8:])
return new_sign2
def make_body(data, did, key=None):
key = key or ''.join(random.choice(string.ascii_lowercase) for _ in range(16))
sign_v1 = calc_sign_v1(data, did, key)
data = json.dumps(data, separators=(',', ':'))
body = f'nice-sign-v1://{sign_v1}:{key}/{data}'
return body
def login(username, password, did):
data = {
'mobile': username,
'country': '1',
'password': rsa_encrypt(password),
'platform': 'mobile',
}
sign = calc_sign(data)
body = make_body(data, did)
params = [
('sign', sign),
# ('token', ''),
('did', did),
('osn', 'android'),
# ('osv', '7.1.2'),
('appv', '5.0.0'),
('src', 'login'),
('tpid', 'login'),
]
headers = {
'Cache-Control': 'no-cache',
'Host': 'api.oneniceapp.com',
'Content-Type': 'application/json; charset=utf-8',
'Connection': 'Keep-Alive',
'Accept-Encoding': 'gzip',
'User-Agent': 'nice/5.0.0, Android/7.1.2, Google+Pixel, OkHttp',
}
resp = requests.post('https://api.oneniceapp.com/account/login',
params=params,
data=body,
headers=headers,
verify=False)
print(resp.json())
if __name__ == '__main__':
data = {"mobile":"13688888888","country":"1","password":"TAa6+bNY8Ld4..Zlt81BD3z6InUibp1JIjo4=","platform":"mobile"}
did = 'fa6f4a54e20e9..67886f3002bd8a'
key = 'jdkycwh..rqpxwb1'
sign = calc_sign(data)
print(sign)
sign_v1 = calc_sign_v1(data, did, key)
print(sign_v1)
# login('13688888888', '12345678', did)
PS:其中key
和did
都是可以使用随机生成的值
测试
以上代码仅供把玩,由于我没有注册nice的账号,所以login的时候是看返回码来判断代码请求是否和app行为一致。