App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第1张图片

这是「进击的Coder」的第 652 篇技术分享

作者:Python 进阶者

来源:Python 爬虫与数据挖掘

阅读本文大概需要 13 分钟。

PS:本教程只用于学习探讨,不允许任何人使用技术进行违法操作,阅读教程即表示同意

为什么要学习加密算法

在搞逆向进行抓包的时候,可以经常发现一些莫名其妙的字符串,可能是81dc9bdb52d04dc20036dbd8313ed055等之类的一长串字符,这些是怎么生成呢?

这些其实就是加密,加密算法主要分为两大类

  • 标准加密算法

  • 非标准加密算法

标准加密算法任何语言中的实现,结果都是一样的。

应该是一样的,也可能不一样,如果不一样,说明更改了标准算法的某些变量,但是这种情况比较少。

非标准算法那就是自己写的了,这就具有很大的不确定性了,全靠程序员发挥!

注意:

  1. 在安卓逆向中,加密算法通常出现在 Java 层和 C++ 中!

  2. 在 Java 层标准算法是有固定名字的,即使再混淆,固定名字是不能混淆的所以比较好处理!

  3. 在 C++ 层标准加密算法是没有固定名字的,那就只能根据算法特征去识别了!

常用标准算法有哪些?

  1. 消息摘要算法(散列函数、哈希函数) MD5、SHA、MAC

  2. 对称加密算法 DES、3DES、AES

  3. 非对称加密算法 RSA

  4. 数字签名算法 MD5withRSA、SHA1withRSA、SHA256withRSA

因为本次主要是安卓逆向,所以就将常用的标准加密算法使用 Android 来复现一下!

我的环境

AndroidStudio 2020.3.1版本
Jdk 8版本

项目

CryptologyDemo.zip

Hex 和 Base64

Hex 和 Base64 不是加密,它是一种编码!!!

Hex 和 Base64 编码是加密算法中最常用的编码,任何加密算法最终都要选择它的表现形式,而 Hex 和 Base64 是最常用的!

添加依赖

api 'com.squareup.okhttp3:okhttp:3.10.0'

记得点击 Sync Now

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第2张图片

Hex

Hex 编码是一种用 16 个字符 (0-9 a-f) 表示任意二进制数据的方法!

它是一种编码,而非加密!

Hex 主要应用在 MD5 等加密表现形式上。

代码

//从字符串到hex
byte[] bytes = "zhangsan".getBytes(StandardCharsets.UTF_8);
ByteString of = ByteString.of(bytes);
String hex = of.hex();
Log.d(TAG, "hex:" + hex);

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第3张图片

Base64

Base64 是一种用 64 个字符 (A-Z a-z 0-9 + / =) 表示任意二进制数据的方法。

它是一种编码,而非加密。

相比较之下,Base64 应用就广泛的很多,像图片,,长密文甚至文件,都采用 Base64,因为可承载的数据很多!

代码

//从字符串到base64
byte[] bytes = "zhangsan".getBytes(StandardCharsets.UTF_8);
ByteString of = ByteString.of(bytes);
 //方式一
String base64 = of.base64();
Log.d(TAG, "base64_1:" + base64);
//方式二
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    String s = Base64.getEncoder().encodeToString("zhangsan".getBytes(StandardCharsets.UTF_8));
    byte[] encode = Base64.getEncoder().encode("zhangsan".getBytes(StandardCharsets.UTF_8));
    Log.d(TAG, "base64_2:" + s);
    Log.d(TAG, "base64_2:" + new String(encode));
}
//方式三
String s = android.util.Base64.encodeToString("zhangsan".getBytes(StandardCharsets.UTF_8),0);
Log.d(TAG, "base64_3:" + new String(s));

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第4张图片

消息摘要算法

消息摘要算法最主要的特征!

密文是不可逆的!

  • 就是说,我在客户端把密码通过 md5 加密了,服务端也得采用相同的方式加密,进行比较。

不定长度输入,固定长度输出

  • 就是说,不管是 123,还是 123456... 经过加密,加密的结果都是固定的长度!

加密结果唯一!

MD5

这就是最常用的 md5 加密,在 update 时压入数据,通过 digest 获得加密结果,md5 一般通过 hex 展示加密结果!

MD 系列算法

算法 摘要长度 实现
MD2 128 Java6
MD5 128 Java6
MD5 128 Bouncy Castle

代码

//md5
public static String md5(String plainText) throws Exception {
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    //1. md5加密的数据可以直接放在digest中
    //2. digest是加密之后的数据,但是有不可见字符,不要使用hex或base64来展示
    md5.update(plainText.getBytes(StandardCharsets.UTF_8));
    byte[] digest = md5.digest();

    //1. 效果完全同上,update可以压入数据,区别是digest是一次性压入,update可以分批次压入
    //byte[] digest = md5.digest(plainText.getBytes(StandardCharsets.UTF_8));

    //使用hex和base64来表示加密之后的数据,因为直接加密的有不可见字符
    ByteString of = ByteString.of(digest);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第5张图片

SHA

常用的是 sha-1 算法,所以本次演示的是 sha-1 算法。

sha-1 算法,甚至来说消息摘要算法基本上 api 都是通用的。

只需要换一个algorithm即可,所以就不废话了。

SHA 系列算法

算法 摘要长度 实现
SHA-1 160 Java6
SHA-256 256 Java6
SHA-384 384 Java6
SHA-512 512 Java6
SHA224 224 Bouncy Castle

代码

//SHA-1
public static String sha_1(String plainText) throws Exception {
    MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
    sha1.update(plainText.getBytes(StandardCharsets.UTF_8));
    byte[] digest = sha1.digest();

    ByteString of = ByteString.of(digest);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第6张图片

MAC

mac 这个名字听着挺牛逼的,其实就是比 md5 和 sha 算法多了个密钥而已,不必大惊小怪。

MAC系列算法

算法 消息摘要 实现
HmacMD5 128 Java6
HmacSHA1 160 Java6
HmacSHA256 256 Java6
HmacSHA384 384 Java6
HmacSHA512 512 Java6
HmacMD2 128 Java6
HmacMD4 128 Bouncy Castle
HmacSHA224 224 Bouncy Castle

代码

public static String mac(String plainText) throws Exception {
    //生成密钥
    SecretKeySpec hmacMD5 = new SecretKeySpec("123".getBytes(StandardCharsets.UTF_8), "HmacMD5");

    //hmacMD5.getAlgorithm()表示获取算法,此时获取的就是HmacMD5
    Mac instance = Mac.getInstance(hmacMD5.getAlgorithm());
    //同上
    //Mac instance = Mac.getInstance("HmacMD5");

    //初始化
    instance.init(hmacMD5);
    //压入数据
    instance.update(plainText.getBytes(StandardCharsets.UTF_8));
    byte[] doFinal = instance.doFinal();

    //同上
    //byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    ByteString of = ByteString.of(doFinal);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第7张图片

对称加密算法

虽然 md5 也叫加密算法,但是他是无法解密的,但是对称加密算法是可以进行加密和解密的,这就 nice 很多了。

因为加密和解密使用的密钥相同,所以叫做对称加密算法,那不同的,就是非对称咯!

注意了啊,对称加密算法的密钥是可以随便给的,但是有长度要求的,不是乱给的,但是加密的内容无限制。

各算法密钥长度

  • RC4 密钥长度 1~256 字节

  • DES 密钥长度 8 字节

  • 3DES/DESede/TripleDES 密钥长度 24 字节

  • AES 密钥长度 16,24,32 字节

根据密钥长度不同 AES 又分为 AES-128,AES-192,AES-256

DES

ECB 和 CBC 模式主要区别在于 CBC 模式需要一个 iv 向量!

DES 算法

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第8张图片

ECB 模式加解密

代码
//DES ECB 加密 Cipher
public static String des_encrypt_ECB(String plainText) throws Exception {
    //生成des所需要的key
    SecretKeySpec desKey = new SecretKeySpec("12345678".getBytes(StandardCharsets.UTF_8), "DES");
    //默认工作模式就是ECB,填充模式PKCS5Padding,
    //Cipher instance = Cipher.getInstance("DES");
    //也可以写全
    Cipher instance = Cipher.getInstance("DES/ECB/PKCS5Padding");
    //初始化,指定是加密模式还是解密模式和密钥
    instance.init(Cipher.ENCRYPT_MODE, desKey);
    //关于Cipher的update似乎有些问题,所以用doFinal的多
    //加密内容,返回结果
    byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    ByteString of = ByteString.of(doFinal);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

//DES ECB 解密
public static String des_decrypt_ECB(byte[] cipherBytes) throws Exception {
    //生成des所需要的key
    SecretKeySpec desKey = new SecretKeySpec("12345678".getBytes(StandardCharsets.UTF_8), "DES");
    Cipher instance = Cipher.getInstance("DES/ECB/PKCS5Padding");
    instance.init(Cipher.DECRYPT_MODE, desKey);
    byte[] doFinal = instance.doFinal(cipherBytes);
    return new String(doFinal);
}
//ECB
String des_encrypt_ECP = des_encrypt_ECB("zhangsan");
Log.d(TAG, "des加密,ECP模式:" + des_encrypt_ECP);
//加密拿到的des加密,ECP模式base结果为:AtLfLL8jc1n+uVm31GQvyw==
byte[] bytes1 = ByteString.decodeBase64("AtLfLL8jc1n+uVm31GQvyw==").toByteArray();
String s1 = des_decrypt_ECB(bytes1);
Log.d(TAG, "des解密,ECP模式:" + s1);
示例
App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第9张图片

CBC 模式加解密

CBC 模式就比 ECB 多了个 iv 向量而已,其他用法一样。

代码
//DES CBC,需要iv向量
public static String des_encrypt_CBC(String plainText) throws Exception {
    SecretKeySpec desKey = new SecretKeySpec("12345678".getBytes(StandardCharsets.UTF_8), "DES");
    Cipher instance = Cipher.getInstance("DES/CBC/PKCS5Padding");
    //CBC需要iv向量
    IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    //初始化时添加上iv向量
    instance.init(Cipher.ENCRYPT_MODE, desKey,ivParameterSpec);
    byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    ByteString of = ByteString.of(doFinal);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

//DES CBC 解密
public static String des_decrypt_CBC(byte[] cipherBytes) throws Exception {
    //生成des所需要的key
    SecretKeySpec desKey = new SecretKeySpec("12345678".getBytes(StandardCharsets.UTF_8), "DES");
    Cipher instance = Cipher.getInstance("DES/CBC/PKCS5Padding");
    IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    instance.init(Cipher.DECRYPT_MODE, desKey,ivParameterSpec);
    byte[] doFinal = instance.doFinal(cipherBytes);
    return new String(doFinal);
}
示例
App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第10张图片

DESede(3DES/TripleDES)

DESede 也分 CBC 和 ECB,使用方法同上,这里将他们合二为一!

DESede 算法

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第11张图片

代码

//DESede
public static String DESede_encrypt(String plainText) throws Exception {
    SecretKeySpec desKey = new SecretKeySpec("123456781234567812345678".getBytes(), "DESede");
    //ECB模式
    //        Cipher instance = Cipher.getInstance("DESede/ECB/PKCS5Padding");
    //        instance.init(Cipher.ENCRYPT_MODE, desKey);
    //        byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    //CBC模式需要iv向量
    Cipher instance = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    //初始化时添加上iv向量
    instance.init(Cipher.ENCRYPT_MODE, desKey, ivParameterSpec);
    byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    ByteString of = ByteString.of(doFinal);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}
//DESede
public static String DESede_decrypt(byte[] cipherBytes) throws Exception {
    //生成des所需要的key
    SecretKeySpec desKey = new SecretKeySpec("123456781234567812345678".getBytes(StandardCharsets.UTF_8), "DESede");
    //ECB模式
    //        Cipher instance = Cipher.getInstance("DESede/ECB/PKCS5Padding");
    //        instance.init(Cipher.DECRYPT_MODE, desKey);
    //        byte[] doFinal = instance.doFinal(cipherBytes);

    //CBC模式
    Cipher instance = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    instance.init(Cipher.DECRYPT_MODE, desKey,ivParameterSpec);
    byte[] doFinal = instance.doFinal(cipherBytes);
    return new String(doFinal);
}
//DESede
String deSede_encrypt = DESede_encrypt("zhangsan");
Log.d(TAG, "DESede加密:" + deSede_encrypt);
//AtLfLL8jc1n+uVm31GQvyw==
byte[] bytes3 = ByteString.decodeBase64("3M7YukhZweaysZBNnqYLBw==").toByteArray();
String s3 = DESede_decrypt(bytes3);
Log.d(TAG, "DESede解密:" + s3);

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第12张图片

AES

AES 算法是对称加密算法中最常用的算法!

AES 也分 CBC 和 ECB,这里也合二为一的!

AES算法

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第13张图片

代码

public static String AES_encrypt(String plainText) throws Exception {
    SecretKeySpec secretKeySpec = new SecretKeySpec("0123456789abcdef".getBytes(), "AES");
    //ECB模式
    Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
    instance.init(Cipher.ENCRYPT_MODE, secretKeySpec);
    byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    //CBC模式需要iv向量
    //        Cipher instance = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    //        IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    //        //初始化时添加上iv向量
    //        instance.init(Cipher.ENCRYPT_MODE, desKey, ivParameterSpec);
    //        byte[] doFinal = instance.doFinal(plainText.getBytes(StandardCharsets.UTF_8));

    ByteString of = ByteString.of(doFinal);
    String hex = of.hex();
    String base64 = of.base64();
    return hex + "||" + base64;
}

public static String AES_decrypt(byte[] cipherBytes) throws Exception {
    //生成des所需要的key
    SecretKeySpec secretKeySpec = new SecretKeySpec("0123456789abcdef".getBytes(), "AES");
    //ECB模式
    Cipher instance = Cipher.getInstance("AES/ECB/PKCS5Padding");
    instance.init(Cipher.DECRYPT_MODE, secretKeySpec);
    byte[] doFinal = instance.doFinal(cipherBytes);

    //CBC模式
    //        Cipher instance = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    //        IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
    //        instance.init(Cipher.DECRYPT_MODE, desKey, ivParameterSpec);
    //        byte[] doFinal = instance.doFinal(cipherBytes);

    return new String(doFinal);
}

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第14张图片

非堆成加密算法

非堆成加密算法中,最常用最典型的加密算法就是 RSA。原来说过,对称加密算法是因为加密解密用的是同一个密钥,但是非对称就不是了。

它需要一堆,称为公钥和私钥,当然,密钥不是随便写的!

在线密钥生成网站:http://web.chacuo.net/netrsakeypair

  • 公钥加密,私钥解密。

  • 私钥加密,公钥解密。

  • 一般公钥是公开的,私钥保密,私钥包含公钥。

  • 加密安全,但是性能差,加密长度有限制。

  • RSA 可以用于加密解密,也可以用来数据签名。

  • Java 中的私钥必须是 pkcs8 格式。

RSA

RSA 算法

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第15张图片

代码

//RSA
//解析公钥key并返回
public static PublicKey generatePublic(String publicKeyBase64) throws Exception {
    byte[] bytes = ByteString.decodeBase64(publicKeyBase64).toByteArray();
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(bytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    return keyFactory.generatePublic(x509EncodedKeySpec);
}

public static PrivateKey generatePrivate(String privateKeyBase64) throws Exception {
    byte[] bytes = ByteString.decodeBase64(privateKeyBase64).toByteArray();
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(bytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    return keyFactory.generatePrivate(pkcs8EncodedKeySpec);
}
//RSA 这里使用私钥解密
public static String RSAPrivateDecrypt(byte[] cipherBytes) throws Exception {
    String BEGIN_PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMOVkFb2U8aOxLZr\n" +
        "v/R/Vq/8+vB1fp4GnLLmBhH/g343Q5J6/9AVqbflgf9DRgzP/zBUoauRQnvfsUBt\n" +
        "6NXKv3t2bkkAkA4ulCqk6+pxW/Zy03LyyADUtkBrDrTfGHqaw6vJSp0qjT56u563\n" +
        "V0nOoUboUmj+AIZRrzNEcwAKa7B1AgMBAAECgYB4oflDCe+mGkzOTys4PIpVRe3o\n" +
        "/i84fM+NsD6yPyz1XlS5NlAuIg5qNI63yOCd6nR1dN26mn+tM8159dCUfNcY1W3F\n" +
        "JaTvBZKD5+6fDUKQ5UfHhlrd4rVxWKK+kuhdYe67/Y6twrMzL/TE+OXmn7jdxuq2\n" +
        "Au93oa2kxraM6pGJCQJBAN/P+ckCGRl26UraqzP3XwrVPq+yGQUMb8y627MXwVJJ\n" +
        "LsE3c9vuoDkm79rYN8jCXbxSkUbBpxopHYfdSxT/Dt8CQQDftlI8PZXDzJLlJAmm\n" +
        "LynoC7OO52sdC+PoqndJ04DDjo1rg6fcWaaIXFmOL/WTn5HJt8pa4r7vi54DChZ7\n" +
        "ju8rAkBUBUSVdGctyxk7k6mv4Y7Zh0J4PNjtr0SNTBzMN//IP1cBDCs/hm655ecn\n" +
        "dgJDKMx9tVV6hZqQ1JyUc7wLDtFrAkB1s6ZmvXw7jTnIR4KwJeZliSqKyGVJ3gSm\n" +
        "WHH0rMv1l93+MEG0JJMC8ZvIvKD3b6Azwng8A0q0HAAh1z/m+FgLAkEA0PahyHnX\n" +
        "ZCzB5ic4QvkiKCqZ+SyibYXOGxBGyCXkuirCwqrtaEorrFxgNEssdpHcEmk71+nv\n" +
        "gvrL5QkvgcLvMA==";
    PrivateKey privateKey = generatePrivate(BEGIN_PRIVATE_KEY);
    Cipher instance = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    instance.init(Cipher.DECRYPT_MODE,privateKey);
    byte[] doFinal = instance.doFinal(cipherBytes);

    return new String(doFinal);
}
//RSA 使用公钥加密
public static String RSAPublicEncrypt(String plainText) throws Exception {
    String BEGIN_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDlZBW9lPGjsS2a7/0f1av/Prw\n" +
        "dX6eBpyy5gYR/4N+N0OSev/QFam35YH/Q0YMz/8wVKGrkUJ737FAbejVyr97dm5J\n" +
        "AJAOLpQqpOvqcVv2ctNy8sgA1LZAaw603xh6msOryUqdKo0+eruet1dJzqFG6FJo\n" +
        "/gCGUa8zRHMACmuwdQIDAQAB";
    PublicKey publicKey = generatePublic(BEGIN_PUBLIC_KEY);
    Cipher instance = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    instance.init(Cipher.ENCRYPT_MODE,publicKey);
    byte[] doFinal = instance.doFinal(plainText.getBytes());

    ByteString of = ByteString.of(doFinal);
    return of.base64();
}
 //RSA
//加密
String rsaPublicEncrypt = RSAPublicEncrypt("zhangsan");
Log.d(TAG, "RSA加密:" + rsaPublicEncrypt);
//解密
byte[] bytes5 = ByteString.decodeBase64(rsaPublicEncrypt).toByteArray();
String rsaPrivateDecrypt = RSAPrivateDecrypt(bytes5);
Log.d(TAG, "RSA解密:" + rsaPrivateDecrypt);

示例

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第16张图片

总结

本文讲述的加密算法主要分为三大类,也是最常用的几个加密算法。

  • 消息摘要算法 (MD5,SHA1,MAC)

  • 对称加密算法 (DES,DESede,AES)

  • 非堆成加密算法 (RSA)

经过比较发现,在 Java 中加密算法有几大特点

  • 通过MessageDigest类生成的算法有 MD5,SHA1

  • 通过Mac类生成的算法有 MAC

  • 通过Cipher生成的算法有 DES,DESede,AES,RSA

嗯,似乎你不太懂什么意思,意思就是可以通过类反推算法。

这样就可以完成自吐算法了,什么算法直接都一把梭哈了,后面再讲!

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第17张图片

End

崔庆才的新书《Python3网络爬虫开发实战(第二版)》已经正式上市了!书中详细介绍了零基础用 Python 开发爬虫的各方面知识,同时相比第一版新增了 JavaScript 逆向、Android 逆向、异步爬虫、深度学习、Kubernetes 相关内容,‍同时本书已经获得 Python 之父 Guido 的推荐,目前本书正在七折促销中!

内容介绍:《Python3网络爬虫开发实战(第二版)》内容介绍

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第18张图片

扫码购买

App 爬虫逆向必知!盘点 App 逆向中常见的加密算法!_第19张图片

f903f1e9bbe51784e9779ff43b8dd91e.png

点个在看你最好看

outside_default.png

你可能感兴趣的:(算法,java,python,机器学习,加密解密)