tujia民宿X-TJH及请求数据unidbg逆向

tujia民宿X-TJH及请求数据unidbg逆向

X-TJH逆向

ida跳转0x36a9

image-20220114150250593
image-20220114150319100
image-20220115091717394

由于X-TJH的长度是40,猜测是SHA1。可以看出v58就是最后的加密结果,而v70[57]就是它原始的字节数组结果。往前看到j_tjget,很自然猜测它是类似于SHA1的Final操作,进而猜测j_tjreset是Update,j_tjcreate是Init。

先看看j_tjcreate

image-20220115092337653

可以看到SHA1的常量和K值。

再看看j_tjreset

image-20220115093812958

unidbg下断点看看。

emulator.attach().addBreakPoint(module.base + 0x2c94+1);
image-20220115094322702

看看r1的数据

image-20220115094428950

尝试作为SHA1的输入,在cyberchef看看结果

image-20220115094613695

和结果对不上。

image-20220115094708445

看到tjreset函数里有个异或操作,补上看看

image-20220115095008961

和正确结果对上了。

看看异或0x21之后的结果

image-20220115095110918

似乎是base64,加个base64解密看看

image-20220115095244535

看到了原始数据的样子,只不过是逆序的,将数据再逆序看看

image-20220115095417920

只有两个部分不太好直接确定,多次更换输入后,发现第一个字符串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)
image-20220115100351765

请求数据逆向

image-20220115100607758

j_tj_crypt应该是加密的地方,j_tjtxtutf8应该是base64编码的地方。

下个断点看看j_tjtxtutf8的输入

emulator.attach().addBreakPoint(module.base + 0x291c+1);
image-20220115101216301
image-20220115101237773

在cyberchef验证一下

image-20220115101401581

和unidbg的加密结果一致。

base64的输入就是j_tj_crypt的输出,接下来就是看看j_tj_crypt

image-20220115102023721

可以看到第1个参数有3种取值,代表着不同的加密模式,取值不同时,加密的结果也不同。

ver = "1"

image-20220115102320479

ver = "2"

image-20220115102441469

ver = "3"

image-20220115102525150
加密模式1
image-20220115103114058

ver = "1"时,v27 = v18 = 4, v19 = 0, v13 = a8 = = 23

hook看看j_CCCrypt的输入

emulator.attach().addBreakPoint(module.base + 0x4480+1);
image-20220115104414211
image-20220115104437991

根据ARM ATPCS调用约定,当参数个数小于等于4个的时候,子程序间通过R0~R3来传递参数(即R0-R3代表参数1-参数4),如果参数个数大于4个,余下的参数通过sp所指向的数据栈进行参数传递。而函数的返回值总是通过R0传递回来。

msp看看其余参数的数据栈

image-20220115104957086

看看a7

image-20220115105027534

输入大概知道了,接下来分析函数本身

image-20220115105537507

主要有4个函数,不过有3个函数是动态的

image-20220115110430223

鼠标移到左括号前,按Tab切换到汇编代码

image-20220115110522934

0x44ca下个断点,然后看看r6的值

emulator.attach().addBreakPoint(module.base + 0x44ca);
image-20220115110707782

跳转到0x4eeb看看

image-20220115110808622

看看j_CCCryptorCreate

image-20220115111619907

先看第一个

image-20220115112022134

0x42ac下断点

image-20220115112132559

跳转到0x4e7d

image-20220115112214118

第二个

image-20220115112341102
image-20220115112444703

跳转到0x4e89

image-20220115112551870

终于来了个有东西的函数了,由此推测ver = "1"时使用了RC4加密。

进去看看

image-20220115113044318
image-20220115113102702

参数个数对不上

返回F5一下

image-20220115113200294
image-20220115113144471

hook一下CC_RC4_set_key

emulator.attach().addBreakPoint(module.base + 0xc244+1);
image-20220115113330175

r1应该是长度

image-20220115113441695

这个其实就是j_CCCrypta4的前16个字节

RC4的输入则是POST的body,cyberchef验证一下

image-20220115113819761

和unidbg一致,接下来就是看key是怎么来的。

image-20220115114018014

已经知道它是sub_302C的输出,接下来分析这个函数

image-20220115114111655

emm,有个Hmac映入眼帘,先hook看看

emulator.attach().addBreakPoint(module.base + 0x47b4+1);
image-20220115114829728
image-20220115114847743
image-20220115114903591
image-20220115114922879

blr在函数返回处下断点,c执行到函数返回处,看看0x4020d030的值

image-20220115115027935

看长度应该是HMAC-SHA1,cyberchef验证一下

image-20220115115327488

接下来就是sub_33E4这个函数

image-20220115115424095

一个字符替换,主要功能就是把字节数组的前半部分和后半部分的逆序进行一个穿插,重写一下即可。

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
image-20220115151550676

ver = "2"时,v27 = v18 = 0, v19 = 0, v13 = (23 + 16) & 0xFFFFFFF0 = 32

和模式1一样,在0x4304下断点

image-20220115153929707
image-20220115154000325

跳转到0x4829

image-20220115155430643

sub_4DE0

image-20220115154539766

sub_4E0C

image-20220115154610960

又调了一个函数,hook看看函数地址

image-20220115154702740
image-20220115154756748

跳转到0x11a51

image-20220115154859190

函数名很明显了,这里设置了AES加密的iv,hook看看iv。

image-20220115155114445

所以iv是'\x00' * 16

回过头看看sub_4828的动态函数

image-20220115155529844
image-20220115155556811
image-20220115155652637

跳转到0x119e9

image-20220115155733078

hook看看

image-20220115160127343
image-20220115160003749

就是之前sub_302C的结果

要素齐全了,cyberchef验证一下。

image-20220115160338382
加密模式3
image-20220115162657527

ver = "3"时,v27=0, a13 = (23 + 16) & 0xFFFFFFF0 = 32v19则是sub_302C的结果。这个函数在模式1中已经分析了,它的输入也知道了,a5 = salt, v30 = "dGpjcnlwdG8K",hook看看结果。

image-20220115164354301

和之前一样,在0x4304下断点

image-20220115153929707
image-20220115163243130

和模式2一样,也是跳转到0x4829,那说明也是AES-CBC,同样在设置key和iv的地方下断点。

key -> 0x119e9

image-20220115163633509
image-20220115163719284

熟悉的字节数组

iv -> 0x11a51

image-20220115163950733
image-20220115163937486

同样是sub_302C的前16个字节

cyberchef验证一下

image-20220115170007288

实现

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)

你可能感兴趣的:(tujia民宿X-TJH及请求数据unidbg逆向)