nice 登錄協(xié)議分析

環(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)過加密惠昔。

image-20211111172637175.png

sign

jadx搜索"sign"优构,

image-20211111152744072.png

從搜索結(jié)果來看疗琉,第一個(gè)比較符合,幾個(gè)參數(shù)名都能對(duì)得上,打開看看

image-20211111152927825.png

可以看到谬晕,a3 = dgb.a(sb)碘裕,繼續(xù)追蹤

image-20211111153346593.png

然后依次點(diǎn)進(jìn)faw.a(str).c()攒钳,.i()函數(shù)看看大概干了什么:

faw.a(str)

image-20211111153557598.png

.c()

image-20211111153640630.png

.i()

image-20211111153718022.png

大致可以看出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ò)了不撑。文兢。

image-20211111154618974.png

既然這樣,改用objection焕檬,執(zhí)行命令objection -g com.nice.main explore姆坚,然后執(zhí)行android hooking search classes dgb

image-20211111154459766.png

修改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;
    }
})
image-20211111172942180.png

可以看出輸入?yún)?shù)就是將body里面的參數(shù)按key排序实愚,然后進(jìn)行urlencode兼呵。

試了之后發(fā)現(xiàn)是標(biāo)準(zhǔn)MD5實(shí)現(xiàn):

image-20211111173116046.png

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"

image-20211111160137851.png

這次打開下面的代碼:

image-20211111160159877.png

沒什么信息萍程,繼續(xù)往下走,點(diǎn)開NiceSignUtils.a

image-20211111160253622.png

NiceSignUtils.a函數(shù)無法查看兔仰,不過上面的native函數(shù)getSignCodegetSignRequest都比較可疑茫负,使用objection監(jiān)控一下,看下是否有調(diào)用:

android hooking watch class com.nice.main.helpers.utils.NiceSignUtils --dump-args --dump-backtrace
image-20211111160749772.png

可以看到乎赴,最后是調(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;
    }
})
image-20211111173522485.png
image-20211111173801253.png

可以看到str就是body那個(gè)字典,arr1str的字節(jié)數(shù)組榕吼,arr2did饿序,arr3不太清楚。

so層

ida打開libsalt.so羹蚣,打開Java_com_nice_main_helpers_utils_NiceSignUtils_getSignRequest函數(shù)原探,可以看到熟悉的字符串:

image-20211111162116039.png
image-20211111162151475.png

有個(gè)看起來很重要的函數(shù),進(jìn)去看看:

image-20211111162247138.png

繼續(xù)點(diǎn)進(jìn)去:

image-20211111162354430.png

看起來這就是加密的主體了顽素,先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());
        }
    })
})
image-20211111174919228.png

a1是body的字典咽弦,a2dida3是body攜帶的key

然后就是分析nice_sign_v3函數(shù)的代碼實(shí)現(xiàn)了胁出,

j_swap_char -> swap_char

通過分析代碼(也可以通過hook輸入型型,輸出)得出,它是交換字符串的前半段和后半段

image-20211111163510194.png
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)闹蒜。

image-20211111164813344.png

然后是j_cJSON_Parsesub_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());
        }
    })
})
image-20211111175207852.png

看起來是對(duì)字典序列化姥闪,將字典按key排序,然后拼接起來砌烁,繼續(xù)往下面分析


image-20211111165548258.png

框里面是一個(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"

image-20211111170257449.png

點(diǎn)進(jìn)去,查找引用:

image-20211111170532134.png

奇怪的字符串函似,進(jìn)去看看:

image-20211111170621543.png

繼續(xù)看看bas.a

image-20211111170658258.png

所以槐脏,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:其中keydid都是可以使用隨機(jī)生成的值

測(cè)試

image-20211111175959650.png
image-20211111180021019.png

以上代碼僅供把玩撇寞,由于我沒有注冊(cè)nice的賬號(hào)顿天,所以login的時(shí)候是看返回碼來判斷代碼請(qǐng)求是否和app行為一致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蔑担,一起剝皮案震驚了整個(gè)濱河市牌废,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌啤握,老刑警劉巖鸟缕,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異排抬,居然都是意外死亡懂从,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門蹲蒲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來番甩,“玉大人,你說我怎么就攤上這事届搁≡笛Γ” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵卡睦,是天一觀的道長(zhǎng)宴胧。 經(jīng)常有香客問我,道長(zhǎng)么翰,這世上最難降的妖魔是什么牺汤? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮浩嫌,結(jié)果婚禮上檐迟,老公的妹妹穿的比我還像新娘。我一直安慰自己码耐,他們只是感情好追迟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骚腥,像睡著了一般敦间。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上束铭,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天廓块,我揣著相機(jī)與錄音,去河邊找鬼契沫。 笑死带猴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懈万。 我是一名探鬼主播拴清,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼会通!你這毒婦竟也來了口予?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤涕侈,失蹤者是張志新(化名)和其女友劉穎沪停,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裳涛,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡木张,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了调违。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窟哺。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖技肩,靈堂內(nèi)的尸體忽然破棺而出且轨,到底是詐尸還是另有隱情,我是刑警寧澤虚婿,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布旋奢,位于F島的核電站,受9級(jí)特大地震影響然痊,放射性物質(zhì)發(fā)生泄漏至朗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一剧浸、第九天 我趴在偏房一處隱蔽的房頂上張望锹引。 院中可真熱鬧矗钟,春花似錦、人聲如沸嫌变。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腾啥。三九已至东涡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倘待,已是汗流浹背疮跑。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留凸舵,地道東北人祖娘。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像贞间,于是被迫代替她去往敵國和親贿条。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容