某免费小说APP sign参数逆向分析与实现

查看 102|回复 9
作者:S11ence   
Overview
闲来无事,随便下载了个免费小说软件,对其中的登录时的参数进行了分析
Java层分析
抓包定位函数
在登录发送验证码时使用Charles抓包


sendSms_packet.png (16.21 KB, 下载次数: 0)
下载附件
sendSms_packet
2022-12-8 10:30 上传

根据关键词在Java层搜索


sendSms_search.png (11.63 KB, 下载次数: 0)
下载附件
sendSms_search
2022-12-8 10:30 上传

查找用例,定位到函数


sendSms_h.png (40.91 KB, 下载次数: 0)
下载附件
sendSms_h
2022-12-8 10:30 上传

可以看到将一些参数存入ArrayMap中传入j.addSign()函数中,这里的addSign是我自己手动修改的名字
传入的Key值对照数据包中的参数也都能够对的上


sendSms_content.png (15.56 KB, 下载次数: 0)
下载附件
sendSms_content
2022-12-8 10:30 上传

跟进分析
继续跟进addSign函数中


addSign.png (12.43 KB, 下载次数: 0)
下载附件
addSign
2022-12-8 10:29 上传

又添加了一个时间戳键值对后,经过getSortedParamStr后传给hash得到sign值


getSortedParamStr.png (32.91 KB, 下载次数: 0)
下载附件
2022-12-8 10:29 上传

getSortedParamStr将键值对按键进行排序后,转成字符串返回。不同键值对之间用&连接,键与值之间用=连接
比如,对如下键值对进行转换
arraymap = {
    "flag": "1",
    "channelId": "1240202",
    "imei": "____7548",
    "device": "Nexus 5X",
}
channelId=1240202&device=Nxus 5X&flag=1&imei=___7548
Security的hash函数调用了JNISecurity的hash2函数,参数有Signature的SHA1WithRSA、KeyFactory的RSA,字符串转成的字节


Security.hash.png (23.92 KB, 下载次数: 0)
下载附件
Security_hash
2022-12-8 10:30 上传

JNISecurity里的hash2是一个native函数,可以看到类里加载了UiControl库,大概率就在libUiControl.so里


JNISecurity.hash2.png (22.25 KB, 下载次数: 0)
下载附件
JNISecurity_hash2
2022-12-8 10:29 上传

So层分析
定位到动态注册函数
解压后在lib文件夹里找到libUiControl.so
先在Function搜索hash2,不出所料没有结果,所以这是动态注册的函数


search1.png (11.42 KB, 下载次数: 0)
下载附件
search1
2022-12-8 10:29 上传

那么应该分析JNI_OnLoad函数


JNI_OnLoad1.png (28.99 KB, 下载次数: 0)
下载附件
2022-12-8 10:29 上传

由GetEnv得知参数v26类型为JNIEnv*,另外看下面有调用固定常数偏移函数大概率都是JNIEnv*参数,重设类型后就能分析出JNI函数了


JNI_OnLoad2.png (19.29 KB, 下载次数: 0)
下载附件
2022-12-8 10:29 上传

往下分析,在sub_7A5A8函数中找到了调用了RegisterNatives函数,可能是我们要找的hash函数


sub_7A5A8.png (19.48 KB, 下载次数: 0)
下载附件
sub_7A5A8
2022-12-8 10:30 上传



sub_87890.png (42.73 KB, 下载次数: 0)
下载附件
2022-12-8 10:30 上传

由RegisterNatives的定义可知,
第一个参数clazz是注册的函数所在的类
第二个参数methods是JNINativeMethod类型,里面包含了函数名、函数签名以及函数指针
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
typedef struct {
    const char* name;
    const char* signature;
    void* fnPtr;
} JNINativeMethod;
不过这里的各个参数当前都是空的数据,需要先经过sub_78F54解密才能得到原本的数据。第二个参数是我们的结果


sub_78F54.png (47.29 KB, 下载次数: 0)
下载附件
2022-12-8 10:30 上传

sub_78F54函数的逻辑也比较简单,主要运算的部分也就只有中间这一句而已


2F5008.png (17.38 KB, 下载次数: 0)
下载附件
2F5008
2022-12-8 10:29 上传

v9是第一个参数,结合具体值分析可以得到是取三位一组然后转成十进制数,和v12异或
v12是字符串"8080"
后面的v8 - (v10 & 0xFFFFFFFC)其实就相当于v8&3,只取了最后两位
可以得到一个简单的idapython解密脚本
def decrypt(start,size=0):
    if size == 0:
        size = get_item_end(start)-start
    data = get_bytes(start,size)
    key = [56,48]
    out = []
    for i in range(0,len(data),3):
        tmp  = data[i:i+3].decode()
        if tmp.startswith('\x00'):
            return out
        t = int(tmp)
        tmp = t^key[i//3%2]
        out.append(chr(tmp))
        s = ''.join(out)
    return s
addrs = [0x2F5008,0x2F5084,0x2F5130,0x2F513D,0x2F51EC]
for i in range(len(addrs)):
    if i !=len(addrs)-1:
        size = addrs[i+1]-addrs
    else:
        size = 0x10
    s = decrypt(addrs)
    print(hex(addrs),''.join(s))
输出结果发现果然是我们想要找到的hash2函数


decrypt_output.png (12.53 KB, 下载次数: 0)
下载附件
2022-12-8 10:29 上传

重命名参数后,可以知道sub_877EC是hash函数的函数指针
sub_87324是hash2函数的函数指针


sub_87890_2.png (32.55 KB, 下载次数: 0)
下载附件
2022-12-8 10:30 上传

另外有个坑点,一开始分析的是arm64的so文件,可以看到反编译的结果不是很好分析,头脑有点迷糊没有对上哪个函数指针对应哪个函数
后面换了32位的so文件才发现反编译效果好的太多了,不仅methods数组各个参数排列的很整齐,甚至hash2函数名的符号表都还在:cry:


sub_49B8C.png (17.75 KB, 下载次数: 0)
下载附件
sub_49B8C
2022-12-8 10:30 上传

让我想到了之前打的一个比赛中,两个不同架构的so文件,arm的反编译有问题,反而x86反编译的结果非常清晰
分析hash2函数
点进来同样发现调用了指针加偏移的函数,估计也是JNI函数,不过还是跟进分析一下sub_78EEC函数


sub_87324_1.png (27.82 KB, 下载次数: 0)
下载附件
2022-12-8 10:30 上传

sub_78EEC也是这样的调用函数,惯性的改类型成JNIEnv*发现得到的是FindClass函数,显然不太对


sub_78EEC.png (18.95 KB, 下载次数: 0)
下载附件
sub_78EEC
2022-12-8 10:30 上传

FindClass显然得不到JNIEnv*类型的变量


sub_78EEC_2.png (17.61 KB, 下载次数: 0)
下载附件
sub_78EEC_2
2022-12-8 10:30 上传

交叉引用一下,发现在JNI_OnLoad里调用了这个变量,才发现原来是JavaVM*类型


qword_3E2860.png (53.03 KB, 下载次数: 0)
下载附件
qword_3E2860
2022-12-8 10:29 上传

改成JavaVM*就得到了GetEnv函数


sub_78EEC_3.png (17.78 KB, 下载次数: 0)
下载附件
2022-12-8 10:30 上传

将sub_87324里的各个env变量修正后,JNI函数就都能正常显示了,
将其中的几个字符串写在了旁边的注释中
函数逻辑也比较清晰了


sub_87324_2.png (60.94 KB, 下载次数: 0)
下载附件
sub_87324_2
2022-12-8 10:30 上传

还原算法
比较常见的JNI层调用Java算法的过程
[ol]
  • 先FindClass获取类对象
  • 用NewObjectV获取类的实例
  • 然后GetMethodID获取所要调用的method的ID
  • 调用CallObjectMethodV或CallVoidMethodV等函数来调用对应的方法
    [/ol]
    翻译成Java代码,一个比较常规的签名算法
    byte[] key = new byte[]{};
    byte[] bArr = str.getBytes(StandardCharsets.UTF_8);
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec =  new PKCS8EncodedKeySpec(key);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
    Signature signature = Signature.getInstance("SHA1WithRSA");
    signature.initSign(privateKey);
    signature.update(bArr);
    byte[] result = signature.sign();
    retrun result;
    type是hash2的第一个参数,传入的是固定的2,因此这里使用的私钥是off_3BFC50+1=unk_2F5477


    off_3BFC50.png (7.67 KB, 下载次数: 0)
    下载附件
    off_3BFC50
    2022-12-8 10:29 上传

    长度为0x279


    arr_size.png (1.28 KB, 下载次数: 0)
    下载附件
    2022-12-8 10:29 上传

    hash2签名后再Base64一下即为sign参数值
    代码验证
    使用python还原算法后验证,同时实现了发包请求
    class DeJian:
        def __init__(self, phone):
            self.phone = phone
                    self.logger = self.init_log()
        def init_log(self):
            # 创建logger实例
            logger = logging.getLogger('DeJian')
            # 设置日志级别
            logger.setLevel(logging.DEBUG)
            # 流处理器
            ch = logging.StreamHandler()
            ch.setLevel(logging.DEBUG)
            # 日志打印格式
            formatter = logging.Formatter('%(message)s')
            # 添加格式配置
            ch.setFormatter(formatter)
            # 添加日志配置
            logger.addHandler(ch)
            return logger
        def req(self, method, url, **kwargs):
            if kwargs.get("headers"):
                # 如果传递过来的请求有头信息,那么我们就在头信息中做追加
                kwargs["headers"].update = {
                    "content-type": "application/x-www-form-urlencoded",
                    "Host": "dj.palmestore.com",
                    "user-agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Nexus 5X Build/OPM1.171019.011)"
                }
            else:
                # 如果传递过来的请求没有header
                kwargs["headers"] = {
                    "content-type": "application/x-www-form-urlencoded",
                    "Host": "dj.palmestore.com",
                    "user-agent": "Dalvik/2.1.0 (Linux; U; Android 8.1.0; Nexus 5X Build/OPM1.171019.011)"
                }
            for k, v in kwargs["data"].items():
                kwargs["data"][k] = self.url_encode(v)
            kwargs["data"] = self.get_sorted_param_str(kwargs["data"])
            self.logger.debug(f"请求的参数为{method},url为{url}, 其他参数为{kwargs}")
            r = requests.request(method=method, url=url, **kwargs)
            self.logger.info(f"响应内容为{r.json()}")
            return r.json()
        def url_encode(self, s):
            r = ['+', '/', '=']
            res = s
            for i in r:
                res = res.replace(i, '%' + hex(ord(i))[2:].upper())
            return res
        def sign(self, content):
                 private_key = b""
            pri_key = RSA.importKey(private_key)
            signer = PKCS1_v1_5.new(pri_key)
            hash_obj = SHA1.new(content.encode())
            sig1 = signer.sign(hash_obj)
            signature = base64.b64encode(sig1).decode()
            res = self.url_encode(signature)
            return res
        def get_sorted_param_str(self, dic):
            content = ''.join([f'{k}={dic[k]}&' for k in sorted(dic)])[:-1]
            return content
        def sendSms(self):
            url = ''
            dic = {
                "versionId": "20005056",
                "device": "Nexus 5X",
                "flag": "1",
                "imei": "",
                "phone": self.phone,
                "times": "1",
                "sendType": "0",
                "channelId": "1240202",
                "timestamp": "1669439016670",
            }
            dic["sign"] = self.sign(self.get_sorted_param_str(dic))
            r = self.req('post', url, data=dic)
    可以看到计算得到的sign值和抓包得到的sign值是一致的


    sign_send.png (9.78 KB, 下载次数: 0)
    下载附件
    sign_send
    2022-12-8 10:30 上传

    同时返回包也是成功


    sms_response.png (4.88 KB, 下载次数: 0)
    下载附件
    sns_reponses
    2022-12-8 10:30 上传

    手机上也是成功收到了短信
    同样地该APP在登陆时的sign参数也是类似的逻辑


    login_packet.png (5.46 KB, 下载次数: 0)
    下载附件
    login_packet
    2022-12-8 10:29 上传



    login_content.png (13 KB, 下载次数: 0)
    下载附件
    login_content
    2022-12-8 10:29 上传

    将之前的代码添加了登录的功能,实现了从获取验证码到登录的过程


    output.png (44.79 KB, 下载次数: 0)
    下载附件
    output
    2022-12-8 10:29 上传

    下载次数, 函数

  • Aswind   

    感谢分享,学习了
    lysmbetter   

    厉害,感谢分享
    dailexing   

    感谢分享,学习了
    dzqlzy1212   

    谢谢分享,学习了。
    bad55   

    挺深入的,学习一下
    Heat   

    感谢楼主的分享
    wzg01   

    感谢分享,书龄十年,这个很需要
    一个板栗   

    谢谢分享思路~
    正义天下   

    感谢分享,学习了
    您需要登录后才可以回帖 登录 | 立即注册