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 = <body length> = 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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容