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)