某颜色软件加解密函数逆向分析【初尝go语言逆向】

前言

在上网时偶然间看到一款颜色软件,激起了我分析的兴趣,在逆向分析的过程中,发现native层是使用go语言编写的,此前没接触过go语言的逆向,因此也借此机会学习了go语言逆向的相关知识。我记录了分析这款软件的主要流程,这里分享给大家。

java层分析

直接使用httpcanary抓包,在众多包中我选择url中有userinfo的包,这一看就是获取用户数据的

在这里插入图片描述

查看它的请求体和响应体,发现键名为"data"的值是加密的数据,因此我的目标是找到data数据加解密的位置,并还原出它们的算法。

请求体:
{"data":"5156526f6a634c4a744765506f684c746be324ec3592da9430b9ac8d07dab8c9183d56117c55bbb9001ded438ac2cd7776c25e4c57902ade91cbcd254747f54d2e0b91c47c54116ce1606ac59e355334d30cbf1f2794a64ef137d9593fbf03951044b189b716e7315af62e248195436b0a00ea7036626667eb03abe7ad73c417d3c6add6c2eaa502ea30f84fdb07cd70df8f10813bfcdab98d216704ba012ec30bbaffa3cdf2595e0cd8ce2d1f0c09d8d5d3803bcc8c2ac4c4ca0786b18bc96eb457f602b0724207ecca5ea0ff05e2e53ed777a1eb4805be967a490a30822b2942c3afb242865687ad3d1d07acc7ad90e75110f2f089747a541ba482f5ae6cb5c6f8a2ad2d2ceaab610c5b086364c9deec9b4c38f83f47c74f1644f5bc73204f6f048722b9ac976ed218cce50545fce2c5ceb9716970dd26f13efe9f83050bc7bb2a09e72360be358f5b17bf646b589cb3dc8e8f04806c9ffb9d9ca244141b73f2bca84dc361d12cf8fe197009592018bf174e86aa68b3f0eec6c823a217498d8a2bd57c495b2dc798f99caa601d69259fcd0004e8da0dd854d59daed89238fd2bc1ac2b2c69d8d14a639555c3497947641bb8076c69c8555eed606aac6eb742a2ca061c130bb4b1d034bb9a0b6afebe13e39cbe010343cd49971c76f5b83a6e1b69f45726a148c541da76eae511df3712dfbe26b119f3fd9fe104ada39e346c1025c110272863f5d55a3ab07accd38ed71f2fc46857d26177ebc0f2b7b82aa926c0c2a20efd9b0060a24b683e9beca8030561cc879b0d5a95fb6beec131b5e48f7b52fd6087fe8f2d927c2adad87f9aa2d50c857e6f3004729f7dfadc583918e7e4830f181bb185575254683e47bb3ab6bb829d5db789875160a2b6cce8e7f256e4ea08a80782dde53beaebf5f7720620a667649d3e44af","_ver":"v1","sign":"220676c5b98327c02d44c0b2252fd11a","timestamp":"1674027054"}
响应体:
{"errcode":0,"timestamp":1674026760,"data":"33F508647B1E3CFBBD09D0DEEAA5249F34BA0787663F026C278020170183093CEB9305656502A9E2D38054FA2C86773FD535EC53775A23713C7720BA47A7839E13AC9271E03C8E521F47E5EDF7BC0D8E6303761737FADE6BDF3D4DBBA2B618B2F7C311389CC6F371E41CC006D465206DB27B92841DD3D9D772CEE6A2345E8B58D224DA72B54597FE8223F189553F2E64A3F13384479DB64104786E6C0143ABB02632EEFB370787253DFC1FB8DA2A77F36AE3A1B1354D288C327D8E1D9622E2088C286B162AF045215B1630E58B9A0459D6EB351B713A35CD7AEF4A71956E4C6BFE19BCA9581B773936B412FAF0EFB204C052328C864C044AB49A716AAE33CDC127FA92A2B2475A351B4071C06D078AFF399C4195F99FC68CE222420CA2336D0756C7DD5F9E28C790BA884AFF28499A2A42DDDC92B7420BB17B7058CEB71B1A6810241FD54897F7B74954B027BED49AA6E332B633D13B0D30CF050D48F16FCDCAE92C7B0D6EA6B0427B1942D94D98B7C9C5E03E0875B8F4C6EFDA1338049ABA59F30FC1323B947A66B20DDF6546AFA8A4B4F1925E6891045F825E0AF32A27B6BDD56B52B7BBEDD070F5FFD8BC35F9473FB3246957B7CCBD4F823C90D6CB7B3A664393D4FE97D6323F07150A83CD31519C48D017E08E35E690C12A292461FD6E87E71249964A47F10EA45FAD61AE047CE860DC11E3AC86195FB0BE45762E4650EE0CA70ED1F4EABAAC56D26E0AB3C5C73561D95744E795121736B35A4CFAE4A62B20AC2A490373578719B3A3F2B8586D1CEA8A0924722721960F849F4909FCE0C9A6C684B9E2EE8AB0ABB99F756EC09D30E0F0A29897E5351E7CF10612A82954B662AFB10A95E14BCEEB7140CC805BBBF0C56BEFF5A609C4E80C379990956D6D4E9E8137F9CA6DA9E1D5BA6B88230AC0F4B5CDAD88E02F8CB45EBE23BEC53BF570C5B1F2D434256326ABCEF1C22FA5F738473545A8D924C01F96CF12775DD1F2715FE56E80737CE6F8771278CDE83C86DEF56B9C8736FEC2DC07308A4BE5F9019C433E83B4A3EBA69E2C5D993CAFD6C8B203F3BDE7CCD8EDCEA743865080FA061841AFABEAC7D543B3D4EBDBB2E7549DD1456C995D98ED7CB50303F70E48F850763D9F895969FB25EAF0A889696C4D73009E54800F28D5904B5593CF87210CC647A2DCB1113AEA0F73DCFDADC28BF7AD4FD6FCCE0D4A3F3B4242CCB29726A3781FF2D6C5D755E63F38E2F6424E7D69C67EA979322F7C9BB4A521EF566C46D31370BDBD8B26A95D82B24D0B340DA7A643647B7870A1C3507AB4FA459B225055A184BBEF0564CB052B02CC6C41D08EE03F907EC82A2A6C90F2437C5E1F7551CFD1D31E18AE065005C808E1991C0A9D3F6E86598C1333D9C4AE110060233DFE97622F8354DAAB0185313DFE15D3CCC99F89131B8DAC3DD0416C47693BBB55A5D707CCBC9B61E9594FCC4A0F626A828A55F0A3CB67ACCF30613D90342060FD7869A2EFB7149300D246716F1AB34D9DAA8F415657B754EFB80240646DE4C5264601BF959611252F4C1AAD1AFDD86FBB52D37C1A9DAF343FC9A791314BB18E3FE6B81B7555621EEB2A8CA408BF86D6261A2CFF6E5BB8873275923020ABEE93D56D3A4D195BBC4E40E652EFDA56D757A41ACA13BE26EF170674A9A6B113163B754C52592B691A969699D785B63E7A482A74FDC401CBA0FFD2AA7E81B47489CFE4D68B3AE7723A97F5EF421CD8FE9D64E2BE2F2A73F93828503D3175A4708E55961F6393826E4B56EDEF13AE1668066B90717AFDD33BF5385A34A6B847A717346874F2717A0F9DE5DF01D66C39E38194A3A441DAB4476881ED9B3206E3D5C8DFEF9EDA3793E53368819E09BF4F7A5E494EEFABED0EAEDDF5674B5182D327296B96D3195B152A1940D29DA9B8A81963EFE1031C20BCFFF6C9CEB1FD8CF04E8CA6DA45AA15FF8086C243A6782093","sign":"e32f75381d198788d65ecd7d23559f2f"}

想要找到data值的拼接位置我有下面的几种思路:

  1. 在jadx中直接搜索"data"字符串,结合frida进行验证从而定位data的拼接位置
  2. 通过r0capture定位软件收发包的位置,再通过回溯找到参数拼接的位置
  3. 通过hook一些常用的参数拼接函数,再结合调用栈的方法定位到参数拼接的位置

对于该软件,上面的三种思路都是可以得到data值的拼接位置的,我在这里只讲一下第三种方法,对于其它两种方法读者可以自行尝试。

我主要hook了三个常用的类,并通过"data"对hook的结果进行过滤,脚本代码如下:

function hook_tools() {
    
    
    let JSONObject = Java.use("com.alibaba.fastjson.JSONObject");
    JSONObject["put"].overload('java.lang.String', 'java.lang.Object').implementation = function () {
    
    
        if (arguments[0] == "data") {
    
    
            var value = arguments[1]
            value = Java.cast(value, Java.use("java.lang.String"))
            console.log("JSONObject put data --> " + value)
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }
        let ret = this.put(arguments[0], arguments[1]);
        return ret;
    };

    let HashMap = Java.use("java.util.HashMap")
    HashMap["put"].overload('java.lang.Object', 'java.lang.Object').implementation = function () {
    
    
        if (arguments[0] == "data") {
    
    
            var value = arguments[1]
            value = Java.cast(value, Java.use("java.lang.String"))
            console.log("HashMap put data --> " + value)
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }
        let ret = this.put(arguments[0], arguments[1]);
        return ret;
    }

    let org_JSONObject = Java.use("org.json.JSONObject")
    org_JSONObject["put"].overload('java.lang.String', 'java.lang.Object').implementation = function () {
    
    
        if (arguments[0] == "data") {
    
    
            var value = arguments[1]
            value = Java.cast(value, Java.use("java.lang.String"))
            console.log("org_JSONObject put data --> " + value)
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        }
        let ret = this.put(arguments[0], arguments[1]);
        return ret;
    };
}

function hook_java(){
    
    
    Java.perform(function(){
    
    
        hook_tools()
    })
}

function main(){
    
    
    hook_java()
}

setImmediate(main)

软件注入frida脚本后,运行结果如下:

在这里插入图片描述

扫描二维码关注公众号,回复: 15297359 查看本文章

通过与抓包结果对比,确定frida hook结果是正确的,我又结合调用栈,将从com.tencent.mm.network.d.q2函数开始分析

在这里插入图片描述

根据调用栈可知,q2函数会调用JSON.parseObject函数,所以上图中用红框圈起来的是重点,parseObject函数的参数中应该包含data,点进o2函数观察一下

在这里插入图片描述

该函数通过k函数生成一个字符串作为返回值,所以要进入k函数看一下

在这里插入图片描述

在k函数中可以看到data的赋值位置,是e(encryptData)的返回值,我使用frida hook e函数验证一下

function observe_program(){
    
    
    let c = Java.use("com.szcx.lib.encrypt.c");
    c["e"].implementation = function (plainText) {
    
    
        console.log('e is called' + ', ' + 'plainText: ' + plainText);
        let ret = this.e(plainText);
        console.log('e ret value is ' + ret);
        return ret;
    };
}

function hook_java(){
    
    
    Java.perform(function(){
    
    
        observe_program()
    })
}

function main(){
    
    
    hook_java()
}

setImmediate(main)

在这里插入图片描述

经过验证,e函数就是data值的生成位置,同时该函数传入的参数是明文,返回值是密文,我进入e函数观察一下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

e函数最终调用了native层的encrypt函数,它的第一个参数是要加密的明文,第二个参数应该就是密钥了,同时可以发现encrypt函数所属的类EncryptUtil中有许多decrypt函数,从名字就可以看出,这些应该是解密函数。所以可以猜测EncryptUtil这个类就是这个软件核心的加解密的工具类,因此只要hook了这些函数就能实现对此软件的抓包了。

在写frida脚本时遇到如果同时hook这些native函数软件会卡住的情况,但如果hook它们的调用方就能解决这个问题,我只hook了encrypt和decrypt这两个函数,其它的函数应该与图片和视频有关,我并不关注,抓包脚本如下:

function capture(){
    
    
    let c = Java.use("com.szcx.lib.encrypt.c");
    c["f"].implementation = function (plainText, encryptKey) {
    
    
        console.log('encrypt is called' + ', ' + 'plainText: ' + plainText + ', ' + 'encryptKey: ' + encryptKey);
        let ret = this.f(plainText, encryptKey);
        console.log('encrypt ret value is ' + ret);
        console.log("===========================================")
        console.log("===========================================")
        return ret;
    };

    c["b"].implementation = function (encrypted, encryptKey) {
    
    
        console.log('decrypt is called' + ', ' + 'encrypted: ' + encrypted + ', ' + 'encryptKey: ' + encryptKey);
        let ret = this.b(encrypted, encryptKey);
        console.log('decrypt ret value is ' + ret);
        console.log("===========================================")
        console.log("===========================================")
        return ret;
    };
}

function hook_java(){
    
    
    Java.perform(function(){
    
    
        capture()
    })
}

抓包效果也是很直观的,下图是请求userinfo时对响应体中的data值的解密数据

在这里插入图片描述

红框中圈起来的是用户的会员信息,只要把false修改为true就可以获得会员权限了,读者请自行尝试。

找到java层加解密的函数后还不算完,下面将分析encrypt和decrypt这两个native函数,还原它们的算法并脱机执行。

native层逆向分析

在分析encrypt函数之前,先写一个主动调用函数,明文传入"123456",密钥是hook得到的"BwcnBzRjN2U/MmZhYjRmND4xPjI+NWQwZWU0YmI2MWQ3YjAzKw8cEywsIS4BIg==",经过多次调用发现在输入相同的条件下,加密结果是发生变化的。主动调用脚本如下:

function call(src) {
    
    
    Java.perform(function () {
    
    
        let EncryptUtil = Java.use("com.qq.lib.EncryptUtil");
        let pwd = 'BwcnBzRjN2U/MmZhYjRmND4xPjI+NWQwZWU0YmI2MWQ3YjAzKw8cEywsIS4BIg=='
        let ret = EncryptUtil.encrypt(src, pwd);
        console.log("encrypt ret --> " + ret)
    })
}

在这里插入图片描述

使用ida解析libsojm.so文件,在导出表中搜索encrypt,可以判断它是一个静态注册的函数

在这里插入图片描述

看一下Java_com_qq_lib_EncryptUtil_encrypt函数的伪代码,反编译的效果很糟心,参数啥的都没识别出来

在这里插入图片描述

这是因为go语言的函数调用约定不同导致的,arm平台使用的是ATPCS函数调用约定,参数1~参数4分别保存到 R0~R3 寄存器中,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 R0 中。而go语言是采用栈传递参数,返回值存在最后一个参数的下面,看一下汇编指令

在这里插入图片描述

在Java_com_qq_lib_EncryptUtil_encrypt函数中遵循的还是ATPCS函数调用约定,调用的crosscall2函数会恢复Golang运行时所需的环境,第一个参数是一个指向函数接口的指针,第二个参数是参数地址,第三个参数是参数的大小。函数指针指向了_cgoexp_17c794619cba_Java_com_qq_lib_EncryptUtil_encrypt,我在这里下断点开始调试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

上面需要注意的是go语言中字符串不以\0作为终止符,它是一个结构体,在内存中上面是字符串的首地址,下面是字符串的长度,其定义如下:

type StringHeader struct {
    
    
    Data uintptr            // 字符串首地址
    Len  int                // 字符串长度
}

下面进入到上一步分析出的加密函数中继续调试

在这里插入图片描述

C3E3FD80处调用的函数传入了pwd,返回了两个字符串,分别是一个长度为0xA的字符串"mIZUjjghGd",另一个是长度为0x20的字符串"4c7e?2fab4f4>1>2>5d0ee4bb61d7b03",这里需要注意的是两个字符串在内存中是连续的,但它们并不是一个字符串,因为go语言的字符串不以\0作为终止符,而是有指明长度的

在这里插入图片描述

继续向下单步调试,发现一个函数传入了input以及上面得到的两个字符串,调用之后返回了加密后的密文

在这里插入图片描述

进入到这个函数继续分析,在该函数中首先有一个for循环做了一波运算

在这里插入图片描述

经过调试得知该循环是对字符串"4c7e?2fab4f4>1>2>5d0ee4bb61d7b03"进行操作,运算后得到了新的长度为0x20的字符串"3d0b85afe3a3969592c7bb3ee16c0e74"

在这里插入图片描述

继续向下调试

在这里插入图片描述

上图中的函数传入了两个字符串,经过处理后返回了新的字符串"fc9e9a75e633ee4d317b08520b6c3ba9",继续向下调试

在这里插入图片描述

这个函数只传入了一个参数0x10,返回了一个长度为0x10的字符串,经过多次调试发现,这个字符串是随机生成的,这也就是为什么在输入相同的条件下加密后的结果不同的原因

继续向下调试

在这里插入图片描述

这个函数传入了之前生成的字符串"fc9e9a75e633ee4d317b08520b6c3ba9",返回了长度为0x20的字节数组,提前说了,这就是之后加密算法的密钥,经过多次调试确认这是不会变化的,因此结果可以直接拿来使用。

在这里插入图片描述

BLX R0指令跳转的函数是一个加密函数,传入了明文"123456",返回了加密后的结果,进入其中发现其中的逻辑还是比较复杂的,我使用了Findcrypt这个插件,尝试查找一下这个so中用了哪些加密算法,下图是Findcrypt的运行结果

在这里插入图片描述

看到是有AES加密算法的特征的,这里该怎么验证呢?sub_CC7BC函数是AES几个数组使用者,因此可以在上面的BLX R0以及sub_CC7BC函数打上断点,如果在BLX R0处的断点触发后,sub_CC7BC函数处的断点接着被触发,那么就能确定这个加密算法极大可能是AES加密算法。经过调试,验证了上面的假设是成立的,因此下面就是确定具体使用了AES加密算法的哪一种模式。

前面的调试过程中得到了一个0x20长度的字节数组和一个随机的字符串,字节数组可能是密钥,随机字符串可能是iv,在cyberchef中进行尝试.

在这里插入图片描述

经过尝试可以确定,这个加密算法是标准的AES-256-CFB。

把明文进行AES加密后还没有完,接着BLX R0继续向下调试

在这里插入图片描述

这个函数的作用是把AES加密后的结果与随机生成的字符串拼接在一起。

接着向下调试,遇到了一个循环运算,伪代码如下图:

在这里插入图片描述

这个循环运算所做的事情也很简单,就是把前面拼接在一起的字符串的十六进制形式转化成字符串。

经过上面循环运算得到的字符串就是最终的结果,至此所有的加密过程都已经分析完毕。总结一下,先把传入的pwd经过一系列的运算得到长度为0x20的字节数组,然后生成一个长度为0x10的随机字符串,把字节数组作为密钥、随机字符串作为iv对明文做AES-256-CFB的加密运算,再把随机字符串和AES加密后的结果拼接在一起,最后把它们的十六进制转成字符串就完成了整个加密过程。

同理,解密过程应该是加密过程的逆操作,首先我们可以把密文的每一个字节作为一个十六进制,每两个十六进制就是一个字节,前0x10个字节就是iv,我们又已知密钥,只需要对密文做AES的解密操作就可以拿到明文了。

脱机执行

我使用python还原了encrypt和decrypt算法,并使用requests库发送网络请求,下面给出完整代码

import json
import random
from Cryptodome.Cipher import AES
import time
import hashlib
import requests

def generate_random_str(len):
    table = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    arr = []
    for i in range(0x10):
        index = random.randint(0, 51)
        arr.append(table[index])
    return str(bytes(arr), encoding = "utf-8")

def aes_encrypt(text, iv):
    key = [0xA7, 0x91, 0x4B, 0x68, 0x21, 0x10, 0x16, 0x5D, 0xDB, 0x0B, 0xA7, 0xCF, 0x4E, 0xAC, 0xBE, 0xFC, 0x1C, 0xC2, 0x1F, 0x56, 0xF3, 0x41, 0xB8, 0x2A, 0x89, 0x0A, 0xCA, 0xED, 0x2C, 0xC3, 0x4A, 0x75]
    key = bytes(key)
    mode = AES.MODE_CFB
    cryptos = AES.new(key, mode, iv.encode(),segment_size=128)
    cipher_text = cryptos.encrypt(text.encode())
    return list(cipher_text)

def custom_encrypt(text,random_str,aes_encrypt_list):
    abc_table = b"0123456789abcdef"
    v20 = 0
    v21 = 0
    v31 = 0x10 + len(text)
    arr = []
    for i in range(v31 * 2):
        arr.append(0)
    table = random_str.encode('utf-8') + bytes(aes_encrypt_list)
    while (v20 < v31):
        v23 = table[v20]
        arr[v21] = abc_table[v23 >> 4]
        arr[v21 + 1] = abc_table[v23 & 0xF]
        v20 = v20 + 1
        v21 = v21 + 2
    return str(bytes(arr), encoding="utf-8")

def decrypt(text):
    arr = []
    arr_reHex = []
    text_bytes = text.encode()
    for i in text_bytes:
        arr.append(int(str(chr(i)), 16))
    for i in range(0, len(arr) // 2):
        arr_reHex.append((arr[i * 2] << 4) | arr[i * 2 + 1])
    arr_iv = arr_reHex[0:0x10]
    arr_cipher = arr_reHex[0x10:]
    key = [0xA7, 0x91, 0x4B, 0x68, 0x21, 0x10, 0x16, 0x5D, 0xDB, 0x0B, 0xA7, 0xCF, 0x4E, 0xAC, 0xBE, 0xFC, 0x1C, 0xC2, 0x1F, 0x56, 0xF3, 0x41, 0xB8, 0x2A, 0x89, 0x0A, 0xCA, 0xED, 0x2C, 0xC3, 0x4A, 0x75]
    key = bytes(key)
    mode = AES.MODE_CFB
    cryptos = AES.new(key, mode, bytes(arr_iv), segment_size=128)
    plain_text = cryptos.decrypt(bytes(arr_cipher))
    return str(plain_text, encoding="utf-8")

def generate_sign(data):
    appKey = "81d7beac44a86f4337f534ec93328370"
    timestamp = str(int(time.time()))
    text = "_ver=v1&data=" + data + "&timestamp=" + timestamp + appKey
    text_sha = hashlib.sha256(text.encode('utf-8')).hexdigest()
    text_md5 = hashlib.md5(text_sha.encode('utf-8')).hexdigest()
    return text_md5

def get_userInfo(data, sign):
    headers = {
    
    
        'User-Agent': 'okhttp-okgo/jeasonlzy',
        'token': 'F39FED66EB36A4B94B308114826D0DBCAFDCAFA186F0938BD5CCF4117B28D586E6780DE54474D93BA0C8EC44B1FA1797F245AB0F648DFC62B60184916108DEDF2F0A490D5475059855ACEE4FCDCE2E91ED2BA128F68135896D8D8192DAC604BEDDE68FA864'
    }
    j = json.dumps({
    
    'data': data, '_ver': 'v1', "sign": sign, "timestamp": str(int(time.time()))})
    r = requests.post("http://api50.fiftymvapi.com:8080/api.php/api/user/userinfo",headers=headers , data=j)
    return r

if __name__ == '__main__':
    text = '{"system_build_id":"a1000","system_iid":"f7767b9f58a4ce56ba2b5ea559fbbdbc","app_status":"9001A7FD9DDFE91CDA376F4EB9DD0E2FC915ADA4:2","system_version":"5.7.0","system_build_aff":"","bundle_id":"jp.fihvv.vrdrrh","system_app_type":"local","new_player":"fx","system_oauth_id":"ee916e21e1eda4fa5bf5dc522e088f20","system_oauth_type":"android","system_token":"B8B8D7BF07295EF62A008193126FA3D0C34AFC437ED0E4DDE00E83D4F81647364884A6B1BCED057E77384252F5F7AFF92E243C7E6C266ED02D388212B291AC775F5F7857B9E51DF19A4E24E455AC2AA664141F3701D4F31A7F96B921C589BE83827EBB367C"}'
    random_str = generate_random_str(0x10)
    aes_encrypt_list = aes_encrypt(text, random_str)
    custom_encrypt_str = custom_encrypt(text,random_str,aes_encrypt_list)

    sign = generate_sign(custom_encrypt_str)

    r = get_userInfo(custom_encrypt_str, sign)

    if r.status_code == requests.codes.ok:
       print("请求userinfo成功")
       print("响应体:")
       print(r.text)
       print("data解密数据:")
       print(decrypt(r.json()["data"]))
    else:
        print("请求userinfo失败")

运行效果展示:

在这里插入图片描述

附录

软件链接:https://pan.baidu.com/s/16DZ3ReL2kMwjI4tAFE39qw?pwd=7xve

猜你喜欢

转载自blog.csdn.net/weixin_56039202/article/details/128743295
今日推荐