環(huán)境
python: 3.8
frida: 12.8.0
objection: 1.8.4
app version: 5.0.0
從圖可以看出杭煎,在url里面有個(gè)sign谦秧,然后post的body里面還有個(gè)sign_v1
,此外password經(jīng)過加密惠昔。
sign
jadx搜索"sign"
优构,
從搜索結(jié)果來看疗琉,第一個(gè)比較符合,幾個(gè)參數(shù)名都能對(duì)得上,打開看看
可以看到谬晕,a3 = dgb.a(sb)
碘裕,繼續(xù)追蹤
然后依次點(diǎn)進(jìn)faw.a(str)
,.c()
攒钳,.i()
函數(shù)看看大概干了什么:
faw.a(str)
.c()
.i()
大致可以看出dgb.a
應(yīng)該是對(duì)str
做了個(gè)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;
}
})
然后執(zhí)行frida -U com.nice.main -l nice_sign.js
,發(fā)現(xiàn)報(bào)錯(cuò)了不撑。文兢。
既然這樣,改用objection焕檬,執(zhí)行命令objection -g com.nice.main explore
姆坚,然后執(zhí)行android hooking search classes dgb
:
修改nice_sign.js
,并執(zhí)行
// 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;
}
})
可以看出輸入?yún)?shù)就是將body里面的參數(shù)按key排序实愚,然后進(jìn)行urlencode兼呵。
試了之后發(fā)現(xiàn)是標(biāo)準(zhǔn)MD5實(shí)現(xiàn):
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
,發(fā)現(xiàn)沒有結(jié)果腊敲,轉(zhuǎn)而繼續(xù)搜索"sign"
:
這次打開下面的代碼:
沒什么信息萍程,繼續(xù)往下走,點(diǎn)開NiceSignUtils.a
:
NiceSignUtils.a
函數(shù)無法查看兔仰,不過上面的native函數(shù)getSignCode
和getSignRequest
都比較可疑茫负,使用objection監(jiān)控一下,看下是否有調(diào)用:
android hooking watch class com.nice.main.helpers.utils.NiceSignUtils --dump-args --dump-backtrace
可以看到乎赴,最后是調(diào)用了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那個(gè)字典,arr1
是str
的字節(jié)數(shù)組榕吼,arr2
是did
饿序,arr3
不太清楚。
so層
ida打開libsalt.so羹蚣,打開Java_com_nice_main_helpers_utils_NiceSignUtils_getSignRequest
函數(shù)原探,可以看到熟悉的字符串:
有個(gè)看起來很重要的函數(shù),進(jìn)去看看:
繼續(xù)點(diǎn)進(jìn)去:
看起來這就是加密的主體了顽素,先hook看看傳入?yún)?shù)
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
函數(shù)的代碼實(shí)現(xiàn)了胁出,
j_swap_char -> swap_char
通過分析代碼(也可以通過hook輸入型型,輸出)得出,它是交換字符串的前半段和后半段
def swap(x):
n = len(x) // 2
return x[n:] + x[:n]
j_nice_md5 -> nice_md5
可以通過hook它的輸入和輸出發(fā)現(xiàn)全蝶,這是一個(gè)標(biāo)準(zhǔn)的MD5實(shí)現(xiàn)闹蒜。
然后是j_cJSON_Parse
和sub_2790
函數(shù)寺枉,里面的代碼看起來比較復(fù)雜,我們直接hook函數(shù)sub_2790
看看它的輸出是什么绷落,是否方便構(gòu)造
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());
}
})
})
看起來是對(duì)字典序列化姥闪,將字典按key排序,然后拼接起來砌烁,繼續(xù)往下面分析
框里面是一個(gè)非標(biāo)準(zhǔn)的方法:每2個(gè)字符筐喳,取第一個(gè)字符的高位,取第二個(gè)字符的低位往弓,然后取或操作疏唾。
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"
:
點(diǎn)進(jìn)去,查找引用:
奇怪的字符串函似,進(jìn)去看看:
繼續(xù)看看bas.a
所以槐脏,password用的是RSA加密,其中公鑰是"MIGfMA0GCSqG...AQAB"
import base64
import hashlib
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
# 請(qǐng)自行查找
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
總體實(shí)現(xiàn)
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
# 請(qǐng)自行查找
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
都是可以使用隨機(jī)生成的值
測(cè)試
以上代碼僅供把玩撇寞,由于我沒有注冊(cè)nice的賬號(hào)顿天,所以login的時(shí)候是看返回碼來判斷代碼請(qǐng)求是否和app行為一致。