@TOC
题目大纲
题号 |
题目 |
一 |
java层加密 |
二 |
so层加密 |
三 |
so层加密带混淆 |
四 |
grpc |
五 |
双向认证 |
六 |
设备指纹加密 |
七 |
quic |
八 |
upx |
九 |
tcp |
十 |
tls |
第一题:java层加密
不管三七二十一,上来先抓个包看看
这是一个post请求,并且附带三个参数,其中一个是sign,那么第一题要分析的应该就是这个sign了,用jadx反编译apk,尝试搜索一下请求地址【/app1】
只有一个结果,就是你了。选中对应的调用方法,右键选择【查找用例】
这次有两个结果,随便点其中一个
这里看到了sign的关键词,猜测这个就是生成sign的函数,尝试使用frida hook一下对比结果
var Sign = Java.use("com.yuanrenxue.match2022.security.Sign");
Sign.sign.implementation = function(a){
console.log('on sign');
send('a');
send(a);
var sign = this.sign(a);
send('sign');
send(sign);
console.log('le sign');
return sign;
};
抓包结果
page=1
sign=e17b4693d730d180b6f6c523a427c08e
t=1652709242
hook结果
on sign
a
长 度:16
字节集:[112, 97, 103, 101, 61, 49, 49, 54, 53, 50, 55, 48, 57, 50, 52, 50]
字符串:b'page=11652709242'
Base64:cGFnZT0xMTY1MjcwOTI0Mg==
HEX :706167653d3131363532373039323432
sign
e17b4693d730d180b6f6c523a427c08e
le sign
可以看到sign的结果是对的上的,入参就是页数加上时间戳,与请求的参数也是可以对上的,那么sign的计算就是这个函数。
如果把这个函数改成python,那是比较麻烦的,那这了就直接用java运行了,把代码复制到新的文件,复制后【OooO00o.OooO00o(-592855683721616730L)】这个地方会报错,hook一下很容易就知道是【"%02x%02x%02x%02x"】,后面这个相关的就都跳过了。
然后编写一个测试的main方法
运行的结果是【b94231f3390145e577ab667354a2395d】,神了个奇了,居然与hook的结果不一样。代码里面都是正常的运算代码,也没有设置到时间和随机数这些变量,怎么会不一样呢?
这里就要考虑有没有可能是反编译出了问题,那这里函数不多,那就以函数为单位测试一下哪里出错了
第一步运行的是padding函数,那么就hook一下看看
Sign.padding.implementation = function(bArr){
console.log('on padding');
let ret = this.padding(bArr);
console.log(ret);
console.log('le padding');
return ret;
};
可以得到
on sign
a
长 度:16
字节集:[112, 97, 103, 101, 61, 49, 49, 54, 53, 50, 55, 56, 56, 52, 48, 50]
字符串:b'page=11652788402'
Base64:cGFnZT0xMTY1Mjc4ODQwMg==
HEX :706167653d3131363532373838343032
on padding
[112, 97, 103, 101, 61, 49, 49, 54, 53, 50, 55, 56, 56, 52, 48, 50, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0]
le padding
sign
8fd84d9c105338c8b4872a67b2cca259
le sign
然后在java中也打印一下这个结果
发现结果已经不一致了
这个函数看名字应该是一个填充的函数,在整个函数体下来看应该是一个哈希算法的填充
这里的最后8个字节就是来自于这个循环,实际就是内容长度的序列化数据,但是java代码这里指定的数据类型是int,但是8个字节的应该是long类型,所以这里序列化的时候用了两个int来填充,导致数据不一样,所以把【length】的类型从int修改为long
再次运行后发现,运行结果就与hook结果一致了。代码就扣出来,那么要怎么给python进行调用呢?
在某帮m3u8解密算法分析这篇文章的回复中,我提到了如何在python中使用jpype库调用java代码,所以这里也用相同的方法调用
import java.util.ArrayList;
public class Sign {
public static void main(String[] args) {
System.out.println(new Sign().sign("page=11652788402".getBytes()));
}
private static final int A = 1732584193;
private static final int B = -271733879;
private static final int C = -1732584194;
private static final int D = 271733878;
private static int f(int i, int i2, int i3) {
return ((~i) & i3) | (i2 & i);
}
private static int ff(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + f(i2, i3, i4) + i5, i6);
}
private static int g(int i, int i2, int i3) {
return (i & i3) | (i & i2) | (i2 & i3);
}
private static int gg(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + g(i2, i3, i4) + i5 + 1518565785, i6);
}
private static int h(int i, int i2, int i3) {
return (i ^ i2) ^ i3;
}
private static int hh(int i, int i2, int i3, int i4, int i5, int i6) {
return rotateLeft(i + h(i2, i3, i4) + i5 + 1859775393, i6);
}
private ArrayList<Integer> padding(byte[] bArr) {
long length = bArr.length * 8;
ArrayList<Integer> arrayList = new ArrayList<>();
for (byte b : bArr) {
arrayList.add(Integer.valueOf(b));
}
arrayList.add(128);
while (((arrayList.size() * 8) + 64) % 512 != 0) {
arrayList.add(0);
}
for (int i = 0; i < 8; i++) {
arrayList.add(Integer.valueOf((int) ((length >>> (i * 8)) & 255)));
}
return arrayList;
}
private static int rotateLeft(int i, int i2) {
return (i >>> (32 - i2)) | (i << i2);
}
public String sign(byte[] bArr) {
ArrayList<Integer> padding = padding(bArr);
int i = A;
int i2 = B;
int i3 = C;
int i4 = D;
for (int i5 = 0; i5 < padding.size() / 64; i5++) {
int[] iArr = new int[16];
for (int i6 = 0; i6 < 16; i6++) {
int i7 = (i5 * 64) + (i6 * 4);
iArr[i6] = (padding.get(i7 + 3).intValue() << 24) | padding.get(i7).intValue() | (padding.get(i7 + 1).intValue() << 8) | (padding.get(i7 + 2).intValue() << 16);
}
int[] iArr2 = {0, 4, 8, 12};
int i8 = i;
int i9 = i2;
int i10 = i3;
int i11 = i4;
int i12 = 0;
while (i12 < 4) {
int i13 = iArr2[i12];
i8 = ff(i8, i9, i10, i11, iArr[i13], 3);
int ff = ff(i11, i8, i9, i10, iArr[i13 + 1], 7);
i10 = ff(i10, ff, i8, i9, iArr[i13 + 2], 11);
i9 = ff(i9, i10, ff, i8, iArr[i13 + 3], 19);
i12++;
i11 = ff;
}
int[] iArr3 = {0, 1, 2, 3};
int i14 = i8;
int i15 = i11;
for (int i16 = 0; i16 < 4; i16++) {
int i17 = iArr3[i16];
i14 = gg(i14, i9, i10, i15, iArr[i17], 3);
i15 = gg(i15, i14, i9, i10, iArr[i17 + 4], 5);
i10 = gg(i10, i15, i14, i9, iArr[i17 + 8], 9);
i9 = gg(i9, i10, i15, i14, iArr[i17 + 12], 13);
}
int[] iArr4 = {0, 2, 1, 3};
int i18 = i14;
int i19 = 0;
while (i19 < 4) {
int i20 = iArr4[i19];
int hh = hh(i18, i9, i10, i15, iArr[i20], 3);
i15 = hh(i15, hh, i9, i10, iArr[i20 + 8], 9);
i10 = hh(i10, i15, hh, i9, iArr[i20 + 4], 11);
i9 = hh(i9, i10, i15, hh, iArr[i20 + 12], 15);
i19++;
i18 = hh;
}
i += i18;
i2 += i9;
i3 += i10;
i4 += i15;
}
return String.format("%02x%02x%02x%02x", Integer.valueOf(i), Integer.valueOf(i2), Integer.valueOf(i3), Integer.valueOf(i4));
}
}
把上面代码保存到Sign.java
javac Sign.java
jar cvf Sign.jar Sign.class
运行上面两个命令行后,可以得到Sign.jar,复制到python同目录下,使用前需要先安装依赖
pip install JPype1
import jpype
import requests
import time
def main():
jpype.startJVM(jpype.getDefaultJVMPath(), "-ea", "-Djava.class.path=Sign.jar") # 启动java虚拟机
jclass = jpype.JClass("Sign") # 获取java类
Sign = jclass() # 实例化java对象
url = 'https://appmatch.yuanrenxue.com/app1'
for page in range(1, 6):
data = {
"page": page,
"t": int(time.time())
}
data["sign"] = str(Sign.sign(f'page={data["page"]}{data["t"]}'.encode()))
response = requests.post(url, data=data).json()
print(response)
jpype.shutdownJVM()
if __name__ == '__main__':
main()
第二题:so层加密
通过第一题可以知道,大部分的题目窗口都在【com.yuanrenxue.match2022.fragment.challenge】这个类下面
第二题中加载了so,并且只有一个sign函数是native函数,那么就hook这个函数,同时抓包查看
on sign
str
1:1652965963
sign
IB/wL5GD01zlBS3MvRTLjw==
le sign
可以看到请求体与hook的数据是一致的,但是接着就不分析so了,直接上unidbg工具,从https://github.com/zhkl0228/unidbg拉取项目到本地,拉取完成后可以尝试运行一下看雪so的测试类【com.kanxue.test2.MainActivity】
没有出现报错,并且出现【Found: XuE】的字样,表示环境正常,开始编写我们自己的类【com.yuanrenxue.match2022.fragment.challenge.ChallengeTwoFragment】
package com.yuanrenxue.match2022.fragment.challenge;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
public class ChallengeTwoFragment {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass ChallengeTwoFragment;
private final boolean logging;
public ChallengeTwoFragment(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.yuanrenxue.match2022").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("../yuanrenxuem106.apk"));
vm.setVerbose(logging);
DalvikModule dm = vm.loadLibrary("match02", true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
ChallengeTwoFragment = vm.resolveClass("com/yuanrenxue/match2022/fragment/challenge/ChallengeTwoFragment");
}
public void destroy() throws IOException {
emulator.close();
}
public static void main(String[] args) throws Exception {
ChallengeTwoFragment challengeTwoFragment = new ChallengeTwoFragment(false);
System.out.println(challengeTwoFragment.sign("1:1652965963"));
challengeTwoFragment.destroy();
}
public String sign(String str) {
StringObject sign = ChallengeTwoFragment.newObject(null).callJniMethodObject(
emulator,
"sign(Ljava/lang/String;)Ljava/lang/String;",
vm.addLocalObject(new StringObject(vm, str))
);
return sign.getValue();
}
}
测试结果与hook结果是一致的,大功告成,接下在就是怎么给到python调用了。一种是可以在java起一个本地服务端,然后通过http调用。另一种是像第一题一样,打包成jar给到python调用,那么我这里用的是后后面一种方法。
根据【JAVA】使用intellij IDEA将项目打包为jar包的提示,按照步骤把【unidbg-android】这整个项目打包成jar
打包后文件数量比较多,是正常现象,最后一个就是我们打包出来的jar,把全部文件都复制到python的目录
我的目录结构如上图,那么接着就是根据上一题一样,直接调用生成sign就可以,没有什么特别的
import jpype
import requests
import time
def main():
jpype.startJVM(jpype.getDefaultJVMPath(), "-ea", "-Djava.class.path=../unidbg-android.jar")
jclass = jpype.JPackage("com.yuanrenxue.match2022.fragment.challenge").ChallengeTwoFragment
ChallengeTwoFragment = jclass(False)
url = 'https://appmatch.yuanrenxue.com/app2'
for page in range(1, 6):
data = {
"page": page,
"ts": int(time.time())
}
data["sign"] = str(ChallengeTwoFragment.sign(f'{data["page"]}:{data["ts"]}'.encode()))
response = requests.post(url, data=data).json()
print(response)
jpype.shutdownJVM()
if __name__ == '__main__':
main()
第三题:so层加密带混淆
第三题和第二题基本相似,hook native方法,然后抓包
on crypto
str
0011652967032000
j
1652967032000
crypto
177f3b597937c04a0f4fccc208de9fce0ce39e5764c4115754b1cd643d528898
le crypto
hook结果与抓包结果也是对的上的,接下来一样是用unidbg生成签名
package com.yuanrenxue.match2022.fragment.challenge;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
public class ChallengeThreeFragment {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass ChallengeThreeFragment;
private final boolean logging;
public ChallengeThreeFragment(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.yuanrenxue.match2022").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("../yuanrenxuem106.apk"));
vm.setVerbose(logging);
DalvikModule dm = vm.loadLibrary("match03", true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
ChallengeThreeFragment = vm.resolveClass("com/yuanrenxue/match2022/fragment/challenge/ChallengeThreeFragment");
}
public void destroy() throws IOException {
emulator.close();
}
public static void main(String[] args) throws Exception {
ChallengeThreeFragment ChallengeThreeFragment = new ChallengeThreeFragment(true);
System.out.println(ChallengeThreeFragment.sign("0011652967032000", "1652967032000"));;
ChallengeThreeFragment.destroy();
}
public String sign(String str, String j) {
StringObject sign = ChallengeThreeFragment.newObject(null).callJniMethodObject(
emulator,
"crypto(Ljava/lang/String;J)Ljava/lang/String;",
vm.addLocalObject(new StringObject(vm, str)),
Long.parseLong(j)
);
return sign.getValue();
}
}
虽然每次运行的结果都不一样,可能是有随机数造成的。打包过去试试请求
import jpype
import requests
import time
def main():
jpype.startJVM(jpype.getDefaultJVMPath(), "-ea", "-Djava.class.path=../unidbg-android.jar")
jclass = jpype.JPackage("com.yuanrenxue.match2022.fragment.challenge").ChallengeThreeFragment
ChallengeThreeFragment = jclass(False)
url = 'https://appmatch.yuanrenxue.com/app3'
for page in range(1, 6):
tim = str(int(time.time() * 1000))
data = {
"page": page
}
data["m"] = str(ChallengeThreeFragment.sign(str(data['page']).zfill(3) + tim, tim))
response = requests.post(url, data=data).json()
print(response)
jpype.shutdownJVM()
if __name__ == '__main__':
main()
可以请求成功,说明也是没有问题的
第四题:grpc
第四题是grpc,那么grpc是什么呢?
更多人可能接触的更多的是基于REST的通信。我们已经看到,REST 是一种灵活的体系结构样式,它定义了对实体资源的基于CRUD的操作。 客户端使用请求/响应通信模型跨 HTTP 与资源进行交互。尽管 REST 是广泛实现的,但一种较新的通信技术gRPC已在各个生态中获得巨大的动力。
gRPC,其实就是RPC框架的一种,前面带了一个g,代表是RPC中的大哥,龙头老大的意思,另外g也有global的意思,意思是全球化比较fashion,是一个高性能、开源和通用的 RPC 框架,基于ProtoBuf(Protocol Buffers) 序列化协议开发,且支持众多开发语言。面向服务端和移动端,基于 HTTP/2 设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
既然说到protobuf,就不得不说以前的一篇文章【JS逆向系列】某方数据获取,proto入门,我做了这道题才知道,原来这就是grpc呀,那么某方数据用的就是grpc,没有看前面文章的建议先看看再回来看这道题
接下来一样是抓包和hook
on sign
str
1:1652969194488
j
1652969194488
sign
8faf8d3081d431db
le sign
因为pbf是序列化后的数据,但是依稀能够看到最后一段签名的对的上的,首先第一步就是调用so生成sign
package com.yuanrenxue.match2022.fragment.challenge;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;
public class ChallengeFourFragment {
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final DvmClass ChallengeFourFragment;
private final boolean logging;
public ChallengeFourFragment(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.yuanrenxue.match2022").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("../yuanrenxuem106.apk"));
vm.setVerbose(logging);
DalvikModule dm = vm.loadLibrary("match04", true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
ChallengeFourFragment = vm.resolveClass("com/yuanrenxue/match2022/fragment/challenge/ChallengeFourFragment");
}
public void destroy() throws IOException {
emulator.close();
}
public static void main(String[] args) throws Exception {
ChallengeFourFragment ChallengeFourFragment = new ChallengeFourFragment(true);
System.out.println(ChallengeFourFragment.sign("1:1652969194488", "1652969194488"));;
ChallengeFourFragment.destroy();
}
public String sign(String str, String j) {
StringObject sign = ChallengeFourFragment.newObject(null).callJniMethodObject(
emulator,
"sign(Ljava/lang/String;J)Ljava/lang/String;",
vm.addLocalObject(new StringObject(vm, str)),
Long.parseLong(j)
);
return sign.getValue();
}
}
这次运行结果就是一致的,然后就可以打包给python使用了。第二步就是编写proto文件,因为java层的代码是被混淆的,不好找出原本的键名,不过proro对键名是不敏感的,那么就以简单为主,按照前面的参数来自定义键名,首先尝试编写请求体参数
需要先安装依赖
pip install grpcio
pip install protobuf
pip install grpcio-tools
根据抓包的链接【http://180.76.60.244:9901/challenge.Challenge/SayHello】创建proto文件
这三个地方需要注意,要与抓包的参数对应
syntax = "proto2";
// http://180.76.60.244:9901/challenge.Challenge/SayHello
// python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. app_proto2.proto
package challenge;
message RequestMessage {
optional int32 page = 1;
optional int64 t = 2;
optional string sign = 3;
}
message ResponseMessage {
repeated Item data = 1;
}
message Item {
optional string value = 1;
}
service Challenge{
rpc SayHello (RequestMessage) returns (ResponseMessage) {}
}
上面文件保存为【app_proto2.proto】,放到python文件同目录,然后执行下面指令
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. app_proto2.proto
然后就可以得到【app_proto2_pb2.py】和【app_proto2_pb2_grpc.py】
根据其他的文章说明,新建Stub之前,需要先创建Channel
channel = grpc.insecure_channel('180.76.60.244:9901')
然后创建client用于真正的PRC请求
client = app_proto2_pb2_grpc.ChallengeStub(channel=channel)
测试发出请求
import app_proto2_pb2
import app_proto2_pb2_grpc
import jpype
import time
import grpc
def main():
channel = grpc.insecure_channel('180.76.60.244:9901')
client = app_proto2_pb2_grpc.ChallengeStub(channel=channel)
jpype.startJVM(jpype.getDefaultJVMPath(), "-ea", "-Djava.class.path=../unidbg-android.jar")
jclass = jpype.JPackage("com.yuanrenxue.match2022.fragment.challenge").ChallengeFourFragment
ChallengeFourFragment = jclass(False)
requests_data = app_proto2_pb2.RequestMessage()
requests_data.page = 1
requests_data.t = int(time.time() * 1000)
requests_data.sign = str(ChallengeFourFragment.sign(str(requests_data.page) + ':' + str(requests_data.t), str(requests_data.t)))
print(requests_data)
response = client.SayHello(requests_data)
print(response)
jpype.shutdownJVM()
if __name__ == '__main__':
main()
请求数据正常,完成
本题参考文献
1.【后台技术】gRPC 基础概念详解
2.gRPC传输协议使用(python教程)
3.猿人学-app逆向比赛第四题grpc题解
第五题:双向认证
既然题目已经告我我们是双向认证了,那么直接抓包肯定是不用想了,必然抓不到,这里我就直接用r0ysue的r0capture,这个frida脚本同时可以把客户端证书dump出来,项目地址:https://github.com/r0ysue/r0capture
这里项目拉取下来后,我修改了小小东西,我把证书dump出来的目录改到了对应app的目录下,避免一些权限问题
可以看到上面证书已经被dump出来,dump后的密码为【r0ysue】,同时也抓到包了,可以看到请求地址,请求头和请求体
把dump到手机的证书传到电脑上,我顺便改名为【yuanrenxue.p12】接着使用【OpenSSL】库来提取出key和cert
from OpenSSL import crypto
p12 = crypto.load_pkcs12(open("yuanrenxue.p12", 'rb').read(), b'r0ysue')
with open('client.key', 'wb') as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey()))
with open('client.cert', 'wb') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, p12.get_certificate()))
然后可以获得【client.key】和【client.cert】,接着试试发送请求
from OpenSSL import crypto
import requests
def main():
# p12 = crypto.load_pkcs12(open("yuanrenxue.p12", 'rb').read(), b'r0ysue')
# with open('client.key', 'wb') as f:
# f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey()))
# with open('client.cert', 'wb') as f:
# f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, p12.get_certificate()))
url = 'https://180.76.60.244:18443/api/app5'
for page in range(1, 6):
data = {
'page': page
}
response = requests.post(url, data=data, verify=False, cert=('client.cert', 'client.key'))
print(response.text)
if __name__ == '__main__':
main()
可以看到已经正确获取数据了,到这里就结束了。
前面不是说有十道题,怎么一半就没了呢?主要是因为菜,后面的做不出来