nice 登录协议分析

环境

python: 3.8

frida: 12.8.0

objection: 1.8.4

app version: 5.0.0

从图可以看出,在url里面有个sign,然后post的body里面还有个sign_v1,此外password经过加密。

image-20211111172637175.png

sign

jadx搜索"sign"

image-20211111152744072.png

从搜索结果来看,第一个比较符合,几个参数名都能对得上,打开看看

image-20211111152927825.png

可以看到,a3 = dgb.a(sb),继续追踪

image-20211111153346593.png

然后依次点进faw.a(str).c().i()函数看看大概干了什么:

faw.a(str)

image-20211111153557598.png

.c()

image-20211111153640630.png

.i()

image-20211111153718022.png

大致可以看出dgb.a应该是对str做了个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;
    }
})

然后执行frida -U com.nice.main -l nice_sign.js,发现报错了。。

image-20211111154618974.png

既然这样,改用objection,执行命令objection -g com.nice.main explore,然后执行android hooking search classes dgb

image-20211111154459766.png

修改nice_sign.js,并执行

// 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

可以看出输入参数就是将body里面的参数按key排序,然后进行urlencode。

试了之后发现是标准MD5实现:

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,发现没有结果,转而继续搜索"sign"

image-20211111160137851.png

这次打开下面的代码:

image-20211111160159877.png

没什么信息,继续往下走,点开NiceSignUtils.a

image-20211111160253622.png

NiceSignUtils.a函数无法查看,不过上面的native函数getSignCodegetSignRequest都比较可疑,使用objection监控一下,看下是否有调用:

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

可以看到,最后是调用了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那个字典,arr1str的字节数组,arr2didarr3不太清楚。

so层

ida打开libsalt.so,打开Java_com_nice_main_helpers_utils_NiceSignUtils_getSignRequest函数,可以看到熟悉的字符串:

image-20211111162116039.png
image-20211111162151475.png

有个看起来很重要的函数,进去看看:

image-20211111162247138.png

继续点进去:

image-20211111162354430.png

看起来这就是加密的主体了,先hook看看传入参数

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函数的代码实现了,

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它的输入和输出发现,这是一个标准的MD5实现。

image-20211111164813344.png

然后是j_cJSON_Parsesub_2790函数,里面的代码看起来比较复杂,我们直接hook函数sub_2790看看它的输出是什么,是否方便构造

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

看起来是对字典序列化,将字典按key排序,然后拼接起来,继续往下面分析


image-20211111165548258.png

框里面是一个非标准的方法:每2个字符,取第一个字符的高位,取第二个字符的低位,然后取或操作。

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

点进去,查找引用:

image-20211111170532134.png

奇怪的字符串,进去看看:

image-20211111170621543.png

继续看看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

# 请自行查找
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

总体实现


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


# 请自行查找
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都是可以使用随机生成的值

测试

image-20211111175959650.png
image-20211111180021019.png

以上代码仅供把玩,由于我没有注册nice的账号,所以login的时候是看返回码来判断代码请求是否和app行为一致。

你可能感兴趣的:(nice 登录协议分析)