tujia民宿X-TJH及請求數據unidbg逆向
X-TJH逆向
ida跳轉0x36a9
由于X-TJH
的長度是40,猜測是SHA1。可以看出v58
就是最后的加密結果,而v70[57]
就是它原始的字節數組結果。往前看到j_tjget
,很自然猜測它是類似于SHA1的Final操作,進而猜測j_tjreset
是Update,j_tjcreate
是Init。
先看看j_tjcreate
。
可以看到SHA1的常量和K值。
再看看j_tjreset
unidbg下斷點看看。
emulator.attach().addBreakPoint(module.base + 0x2c94+1);
看看r1
的數據
嘗試作為SHA1的輸入,在cyberchef看看結果
和結果對不上。
看到tjreset
函數里有個異或操作,補上看看
和正確結果對上了。
看看異或0x21之后的結果
似乎是base64,加個base64解密看看
看到了原始數據的樣子,只不過是逆序的,將數據再逆序看看
只有兩個部分不太好直接確定,多次更換輸入后,發現第一個字符串dGpoY2hr
是固定的。而第二個字符串則有個很明顯的特點,那就是它的字符串是按字符排序的,而這個字符的輸入很顯然是POST的body。
實現
def calc_tjh(params, ua, app_client, body, ts):
if isinstance(params, (dict, list, tuple)):
if hasattr(params, "items"):
params = params.items()
params = urlencode(params)
params = params or ''
if isinstance(body, dict):
body = json.dumps(body, separators=(',', ':'))
body = re.sub('[^a-zA-Z0-9]', '', body)
body = ''.join(sorted(body))
print(body)
data = '#'.join((params, ua, str(ts), 'dGpoY2hr', app_client, body))
print(data)
data2 = bytes(reversed(data.encode()))
print(data2)
data3 = base64.b64encode(data2)
print(data3)
tjh = hashlib.sha1(data3).hexdigest()
return tjh
def test_tjh():
params = 'key=nodeApiConfig'
ua = 'Mozilla/5.0'
app_client = 'LON=null;LAT=null;'
body = '{\"code\":\"hello everhu\"}'
ts = 1642084337
tjh = calc_tjh(params, ua, app_client, body, ts)
print(tjh)
請求數據逆向
j_tj_crypt
應該是加密的地方,j_tjtxtutf8
應該是base64編碼的地方。
下個斷點看看j_tjtxtutf8
的輸入
emulator.attach().addBreakPoint(module.base + 0x291c+1);
在cyberchef驗證一下
和unidbg的加密結果一致。
base64的輸入就是j_tj_crypt
的輸出,接下來就是看看j_tj_crypt
可以看到第1個參數有3種取值,代表著不同的加密模式,取值不同時,加密的結果也不同。
ver = "1"
ver = "2"
ver = "3"
加密模式1
當ver = "1"
時,v27 = v18 = 4, v19 = 0, v13 = a8 = <body length> = 23
hook看看j_CCCrypt
的輸入
emulator.attach().addBreakPoint(module.base + 0x4480+1);
根據ARM ATPCS調用約定,當參數個數小于等于4個的時候,子程序間通過R0~R3來傳遞參數(即R0-R3代表參數1-參數4),如果參數個數大于4個,余下的參數通過sp所指向的數據棧進行參數傳遞。而函數的返回值總是通過R0傳遞回來。
msp
看看其余參數的數據棧
看看a7
輸入大概知道了,接下來分析函數本身
主要有4個函數,不過有3個函數是動態的
鼠標移到左括號前,按Tab
切換到匯編代碼
在0x44ca
下個斷點,然后看看r6
的值
emulator.attach().addBreakPoint(module.base + 0x44ca);
跳轉到0x4eeb
看看
看看j_CCCryptorCreate
先看第一個
0x42ac
下斷點
跳轉到0x4e7d
第二個
跳轉到0x4e89
終于來了個有東西的函數了,由此推測ver = "1"
時使用了RC4加密。
進去看看
參數個數對不上
返回F5
一下
hook一下CC_RC4_set_key
emulator.attach().addBreakPoint(module.base + 0xc244+1);
r1
應該是長度
這個其實就是j_CCCrypt
的a4
的前16個字節
RC4的輸入則是POST的body,cyberchef驗證一下
和unidbg一致,接下來就是看key
是怎么來的。
已經知道它是sub_302C
的輸出,接下來分析這個函數
emm,有個Hmac映入眼簾,先hook看看
emulator.attach().addBreakPoint(module.base + 0x47b4+1);
blr
在函數返回處下斷點,c
執行到函數返回處,看看0x4020d030
的值
看長度應該是HMAC-SHA1
,cyberchef驗證一下
接下來就是sub_33E4
這個函數
一個字符替換,主要功能就是把字節數組的前半部分和后半部分的逆序進行一個穿插,重寫一下即可。
def sub_33E4(data, a2=20):
"""
trans byte
"""
half = a2 // 2
s1 = b''.join(bytes(x) for x in zip(data[:half], data[half:][::-1]))
s2 = b''.join(bytes(x) for x in zip(s1[half:][::-1], s1[:half]))
return s2
加密模式2
ver = "2"
時,v27 = v18 = 0, v19 = 0, v13 = (23 + 16) & 0xFFFFFFF0 = 32
和模式1一樣,在0x4304
下斷點
跳轉到0x4829
sub_4DE0
sub_4E0C
又調了一個函數,hook看看函數地址
跳轉到0x11a51
函數名很明顯了,這里設置了AES加密的iv,hook看看iv。
所以iv是'\x00' * 16
回過頭看看sub_4828
的動態函數
跳轉到0x119e9
hook看看
就是之前sub_302C
的結果
要素齊全了,cyberchef驗證一下。
加密模式3
當ver = "3"
時,v27=0, a13 = (23 + 16) & 0xFFFFFFF0 = 32
,v19
則是sub_302C
的結果。這個函數在模式1中已經分析了,它的輸入也知道了,a5 = salt, v30 = "dGpjcnlwdG8K"
,hook看看結果。
和之前一樣,在0x4304
下斷點
和模式2一樣,也是跳轉到0x4829
,那說明也是AES-CBC
,同樣在設置key和iv的地方下斷點。
key -> 0x119e9
熟悉的字節數組
iv -> 0x11a51
同樣是sub_302C
的前16個字節
cyberchef驗證一下
實現
import base64
import hashlib
import hmac
import json
import re
from urllib.parse import urlencode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
_KEY = b'dGpjcnlwdG8K'
def calc_tjh(params, ua, app_client, body, ts):
if isinstance(params, (dict, list, tuple)):
if hasattr(params, "items"):
params = params.items()
params = urlencode(params)
params = params or ''
if isinstance(body, dict):
body = json.dumps(body, separators=(',', ':'))
body = re.sub('[^a-zA-Z0-9]', '', body)
body = ''.join(sorted(body))
print(body)
data = '#'.join((params, ua, str(ts), 'dGpoY2hr', app_client, body))
print(data)
data2 = bytes(reversed(data.encode()))
print(data2)
data3 = base64.b64encode(data2)
print(data3)
tjh = hashlib.sha1(data3).hexdigest()
return tjh
def encrypt_body(body, salt, ts, ver):
body = body.encode()
data = ''.join((salt, str(ts))).encode()
key = sub_302C(data, _KEY)
print(key)
if ver == '1':
result = rc4_crypt(body, key)
elif ver == '2':
result = aes_encrypt(body, key, b'\x00' * 16)
elif ver == '3':
iv = sub_302C(salt.encode(), _KEY)
result = aes_encrypt(body, key, iv)
output = base64.b64encode(result).decode()
return output
def sub_302C(data, key):
"""
make length-of-16 key with data and key
"""
data2 = hmac.new(key, data, hashlib.sha1).digest()
data3 = sub_33E4(data2)
key = data3[:16]
return key
def sub_33E4(data, a2=20):
"""
trans byte
"""
half = a2 // 2
s1 = b''.join(bytes(x) for x in zip(data[:half], data[half:][::-1]))
s2 = b''.join(bytes(x) for x in zip(s1[half:][::-1], s1[:half]))
return s2
def rc4_crypt(data, key):
"""
encrypt/decrypt data with key
Args:
data: data to encrypt/decrypt
key: rc4 key
"""
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xff
S[i], S[j] = S[j], S[i]
bucket = []
i = 0
j = 0
for c in data:
i = (i + 1) & 0xff
j = (j + S[i]) & 0xff
S[i], S[j] = S[j], S[i]
k = c ^ S[(S[i] + S[j]) & 0xff]
bucket.append(k)
return bytes(bucket)
def aes_encrypt(data, key, iv):
data = pad(data, AES.block_size)
cryptor = AES.new(key, AES.MODE_CBC, iv)
buf = cryptor.encrypt(data)
return buf
def test_tjh():
params = 'key=nodeApiConfig'
ua = 'Mozilla/5.0'
app_client = 'LON=null;LAT=null;'
body = '{"code":"hello everhu"}'
ts = 1642084337
tjh = calc_tjh(params, ua, app_client, body, ts)
print(tjh)
def test_body():
datasets = [
('1', '4cFS+3YZRDW7K0P9uvgEEPDnbI9xkeE='),
('2', 'kQgG94ZE/OvGHN+wXvtMS95kdddjHDQjLx+Zf+qAMTY='),
('3', '2Knmhtkoe8UFpFbBZRvaPI6+GcwtC0Py4rP1U777nKk='),
]
for ver, result in datasets:
ts = 1642126567
salt = 'everever'
body = '{"code":"hello everhu"}'
ret = encrypt_body(body, salt, ts, ver)
print(ret)