逆向中常见的Hash算法和对称加密算法的分析

逆向中常常出现一些加密算法,如果我们能对这些加密算法进行快速识别则会大大减少我们逆向的难度,虽然IDA已有密码分析神器Findcrypt,但掌握手动分析方法能帮助我们应对更多的情况。这篇文章将介绍逆向中常见的单项散列算法和对称加密算法的识别方法。

0xFF. 前言

在很长一段时间里我经常发现自己面对复杂的加密算法无从下手,可能是因为还没有系统学过密码学吧orz,总之这个问题困扰了我很久。于是最近我花了一些时间来解决自己在密码学这块的薄弱点,写下这篇文章的目的之一也是为了巩固所学知识。
加密算法的部分没有涉及公钥加密算法(因为我不会hh)。每个算法都有一个例子,这些例子出自《加密与解密(第4版)》第6章的随书附带文件和各大比赛中的题目。

0x00. 目录

单向散列算法:

  1. MD5
  2. SHA

对称加密算法:

  1. RC4
  2. TEA
  3. DES
  4. AES
  5. SM4
  6. ChaCha20

0X01. MD5

MD5(Message Digest Algorithm)消息摘要算法对输入的任意长度的消息进行运算,产生一个128位的消息摘要。
MD5的特征是会出现下图中 A,B,C,D 这四个常量。
 


这里我们直接用《加密与解密(第4版)》随书文件MD5KeyGenMe.exe来分析。
 


在导入表中找到GetDlgItemTextA函数定位关键代码,两次GetDlgItemTextA函数读取的应该分别是Name和Serial Number:
 


sub_4012B0函数我们点进去看看发现了MD5的几个特征常量:
 


还原一下符号,需要注意的一点是连续调用两次MD5_Update相当于把两次的输入拼接后调用一次MD5_Update的结果:
 


写出注册机:

1

2

3

4

5

6

7

8

9

from hashlib import md5

name = b'pediy'

digest = md5(name + b'www.pediy.com').digest()

a2345 = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'

serial_number = ''

for in digest:

    serial_number += a2345[b % 32]

print(f'Serial Number('{name}')={serial_number[0:4]}-{serial_number[4:8]}-{serial_number[8:12]}-{serial_number[12:16]}')


Findcrypt插件能帮助我们快速找到这些常量,不过这里还是着重讲手动分析的方法。

0x02. SHA

安全散列算法(Secure Hash Algorithm,SHA)包括SHA-1、SHA-256、SHA-384和SHA-512,分别产生160位、256位、384位和512位的散列值。
类似于MD5,SHA算法使用了一系列的常数:
 


 


还是用随书文件SHA1KeyGenMe.exe来分析。
还是先通过导入表定位关键代码:
 


发现sub_401000函数中出现了SHA1算法用到的常量:
 


还原下符号:
 


写出注册机:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

from hashlib import sha1

name = b'pediy'

digest = sha1(name).digest()

aPEDIY = b'PEDIY Forum'

apediy = b'pediy.com'

key = bytearray(digest)

for in range(11):

    key[i] ^= aPEDIY[i]

for in range(12,17):

    key[i] ^= key[i - 12]

for in range(17,20):

    key[i] ^= apediy[i - 17]

serial_number = ''

for in range(10):

    serial_number += hex(key[i] ^ key[i + 10])[2:].zfill(2).upper()

print(serial_number)

0x03. RC4

RC4是一种比较简单的流密码,该算法虽然没有用到特征常量,但是特征也比较容易识别。
 


分析RC4 Sample.exe文件。
还是通过导入表找到关键代码:
 


sub_401000函数明显具有RC4密钥调度算法(KSA)的特征:
 


sub_401070函数是RC4算法的第二步:

0x04. TEA

TEA算法是分组密码,分组长度为64位,密钥长度为128位,采用Feistel网络。
其加密过程也非常简单,下面的代码摘自Wikipedia:

1

2

3

4

5

6

7

8

9

10

11

12

13

#include <stdint.h>

void encrypt (uint32_t v[2], const uint32_t k[4]) {

    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */

    uint32_t delta=0x9E3779B9;                     /* a key schedule constant */

    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */

    for (i=0; i<32; i++) {                         /* basic cycle start */

        sum += delta;

        v0 += ((v1<<4+ k0) ^ (v1 + sum) ^ ((v1>>5+ k1);

        v1 += ((v0<<4+ k2) ^ (v0 + sum) ^ ((v0>>5+ k3);

    }                                              /* end cycle */

    v[0]=v0; v[1]=v1;

}

其中特征常量delta是由黄金分割点得来的,delta = 0x9E377989。TEA的变体XTEA和XXTEA都用到了这个常量,但是加密过程不同,在识别算法时需要注意。
在加密轮数方面,作者推荐的加密轮数是64轮,即循环32次,也可以采用其他加密轮数,比如32轮或者128轮,在分析的时候也需要注意。
补充Wikipedia上的一些资料:

分析TEAKeyGenMe.exe
 


sub_401380函数中出现了MD5特征常量:
 


sub_401000函数中出现了TEA特征常量,明显为TEA算法:
 


整个加密过程是先取MD5(name)的前8个字节作为密钥对序列号进行TEA加密,加密的结果再与与MD5(name)的前8个字节异或,异或的结果与MD5的后8个字节比较。
写出注册机:

1

2

3

4

5

6

7

8

9

10

11

from hashlib import md5

from binascii import b2a_hex

from pytea import TEA

name = b'pediy'

md5_digest = md5(name).digest()

buf = bytearray(md5_digest[0:8])

for in range(8):

    buf[i] ^= md5_digest[i + 8]

tea = TEA(md5_digest)

print(f'Serial Number("{name.decode()}"): {b2a_hex(tea.Decrypt(buf)).decode().upper()}')

我在网上找了半天也没找到个好用的TEA的Python实现,干脆自己写了个:已上传GitHub

0x05. DES

DES全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的分组算法。
DES同前面的TEA一样,都采用了Feistel网络,其加密过程可以用以下两个图表示:
Figure 1— The overall Feistel structure of DES
 


Figure 2—The Feistel function (F-function) of DES
 


图一中的IPFP分别代表初始置换(Initial Permutation)和末尾置换(Final Permutation),图二中的S1S8是8个置换盒(Substitution-Box),这些都可以作为识别DES算法的特征。
随便在GitHub上扒一份源码,就能找到这些常量:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

int S1[] = {14,  413,  1,  21511,  8,  310,  612,  5,  9,  0,  7,

             015,  7,  414,  213,  110,  61211,  9,  5,  3,  8,

             4,  114,  813,  6,  2111512,  9,  7,  310,  5,  0,

            1512,  8,  2,  4,  9,  1,  7,  511,  31410,  0,  613};

int S2[] = {15,  1,  814,  611,  3,  4,  9,  7,  21312,  0,  510,

             313,  4,  715,  2,  81412,  0,  110,  6,  911,  5,

             014,  71110,  413,  1,  5,  812,  6,  9,  3,  215,

            13,  810,  1,  315,  4,  211,  6,  712,  0,  514,  9};

int S3[] = {10,  0,  914,  6,  315,  5,  11312,  711,  4,  2,  8,

            13,  7,  0,  9,  3,  4,  610,  2,  8,  514121115,  1,

            13,  6,  4,  9,  815,  3,  011,  1,  212,  51014,  7,

             11013,  0,  6,  9,  8,  7,  41514,  311,  5,  212};

int S4[] = 71314,  3,  0,  6,  910,  1,  2,  8,  51112,  415,

            13,  811,  5,  615,  0,  3,  4,  7,  212,  11014,  9,

            10,  6,  9,  01211,  71315,  1,  314,  5,  2,  8,  4,

             315,  0,  610,  113,  8,  9,  4,  51112,  7,  214};

int S5[] = 212,  4,  1,  71011,  6,  8,  5,  31513,  014,  9,

            1411,  212,  4,  713,  1,  5,  01510,  3,  9,  8,  6,

             4,  2,  1111013,  7,  815,  912,  5,  6,  3,  014,

            11,  812,  7,  114,  213,  615,  0,  910,  4,  5,  3};

int S6[] = {12,  11015,  9,  2,  6,  8,  013,  3,  414,  7,  511,

            1015,  4,  2,  712,  9,  5,  6,  11314,  011,  3,  8,

             91415,  5,  2,  812,  3,  7,  0,  410,  11311,  6,

             4,  3,  212,  9,  515101114,  1,  7,  6,  0,  813};

int S7[] = 411,  21415,  0,  813,  312,  9,  7,  510,  6,  1,

            13,  011,  7,  4,  9,  11014,  3,  512,  215,  8,  6,

             1,  4111312,  3,  7141015,  6,  8,  0,  5,  9,  2,

             61113,  8,  1,  410,  7,  9,  5,  01514,  2,  312};

int S8[] = {13,  2,  8,  4,  61511,  110,  9,  314,  5,  012,  7,

             11513,  810,  3,  7,  412,  5,  611,  014,  9,  2,

             711,  4,  1,  91214,  2,  0,  6101315,  3,  5,  8,

             2,  114,  7,  410,  8131512,  9,  0,  3,  5,  611};

在2020祥云杯的某道APK逆向里,Findcrypt插件失效(可能是Findcrypt分析不了ARM框架下的文件),所以我们只能靠手动分析找到DES的特征(以下是S1到S8):
 


还有一些别的特征,都可以帮助我们快速识别DES算法:

0x06. AES

AES(Advanced Encryption Standard,高级加密标准)是用于代替DES的新一代加密标准。AES具有128比特的分组长度,支持128比特、192比特和256比特的密钥长度。
AES的加密过程:
 


SubBytes函数:
 


我们识别AES的方法就是找到AES的SubBytes函数中使用的这个S-box。
在GitHub上扒一份源码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

/*

 * S-box transformation table

 */

static uint8_t s_box[256= {

    // 0     1     2     3     4     5     6     7     8     9     a     b     c     d     e     f

    0x630x7c0x770x7b0xf20x6b0x6f0xc50x300x010x670x2b0xfe0xd70xab0x76// 0

    0xca0x820xc90x7d0xfa0x590x470xf00xad0xd40xa20xaf0x9c0xa40x720xc0// 1

    0xb70xfd0x930x260x360x3f0xf70xcc0x340xa50xe50xf10x710xd80x310x15// 2

    0x040xc70x230xc30x180x960x050x9a0x070x120x800xe20xeb0x270xb20x75// 3

    0x090x830x2c0x1a0x1b0x6e0x5a0xa00x520x3b0xd60xb30x290xe30x2f0x84// 4

    0x530xd10x000xed0x200xfc0xb10x5b0x6a0xcb0xbe0x390x4a0x4c0x580xcf// 5

    0xd00xef0xaa0xfb0x430x4d0x330x850x450xf90x020x7f0x500x3c0x9f0xa8// 6

    0x510xa30x400x8f0x920x9d0x380xf50xbc0xb60xda0x210x100xff0xf30xd2// 7

    0xcd0x0c0x130xec0x5f0x970x440x170xc40xa70x7e0x3d0x640x5d0x190x73// 8

    0x600x810x4f0xdc0x220x2a0x900x880x460xee0xb80x140xde0x5e0x0b0xdb// 9

    0xe00x320x3a0x0a0x490x060x240x5c0xc20xd30xac0x620x910x950xe40x79// a

    0xe70xc80x370x6d0x8d0xd50x4e0xa90x6c0x560xf40xea0x650x7a0xae0x08// b

    0xba0x780x250x2e0x1c0xa60xb40xc60xe80xdd0x740x1f0x4b0xbd0x8b0x8a// c

    0x700x3e0xb50x660x480x030xf60x0e0x610x350x570xb90x860xc10x1d0x9e// d

    0xe10xf80x980x110x690xd90x8e0x940x9b0x1e0x870xe90xce0x550x280xdf// e

    0x8c0xa10x890x0d0xbf0xe60x420x680x410x990x2d0x0f0xb00x540xbb0x16};// f

/*

 * Inverse S-box transformation table

 */

static uint8_t inv_s_box[256= {

    // 0     1     2     3     4     5     6     7     8     9     a     b     c     d     e     f

    0x520x090x6a0xd50x300x360xa50x380xbf0x400xa30x9e0x810xf30xd70xfb// 0

    0x7c0xe30x390x820x9b0x2f0xff0x870x340x8e0x430x440xc40xde0xe90xcb// 1

    0x540x7b0x940x320xa60xc20x230x3d0xee0x4c0x950x0b0x420xfa0xc30x4e// 2

    0x080x2e0xa10x660x280xd90x240xb20x760x5b0xa20x490x6d0x8b0xd10x25// 3

    0x720xf80xf60x640x860x680x980x160xd40xa40x5c0xcc0x5d0x650xb60x92// 4

    0x6c0x700x480x500xfd0xed0xb90xda0x5e0x150x460x570xa70x8d0x9d0x84// 5

    0x900xd80xab0x000x8c0xbc0xd30x0a0xf70xe40x580x050xb80xb30x450x06// 6

    0xd00x2c0x1e0x8f0xca0x3f0x0f0x020xc10xaf0xbd0x030x010x130x8a0x6b// 7

    0x3a0x910x110x410x4f0x670xdc0xea0x970xf20xcf0xce0xf00xb40xe60x73// 8

    0x960xac0x740x220xe70xad0x350x850xe20xf90x370xe80x1c0x750xdf0x6e// 9

    0x470xf10x1a0x710x1d0x290xc50x890x6f0xb70x620x0e0xaa0x180xbe0x1b// a

    0xfc0x560x3e0x4b0xc60xd20x790x200x9a0xdb0xc00xfe0x780xcd0x5a0xf4// b

    0x1f0xdd0xa80x330x880x070xc70x310xb10x120x100x590x270x800xec0x5f// c

    0x600x510x7f0xa90x190xb50x4a0x0d0x2d0xe50x7a0x9f0x930xc90x9c0xef// d

    0xa00xe00x3b0x4d0xae0x2a0xf50xb00xc80xeb0xbb0x3c0x830x530x990x61// e

    0x170x2b0x040x7e0xba0x770xd60x260xe10x690x140x630x550x210x0c0x7d};// f

下面分析AESKeyGenMe.exe文件。
初步分析关键代码:
 


在sub_401EC0函数中找到AES的S_box和S_box的逆,基本确定是AES加密,没有找到iv,推测是ECB模式:
 


密钥:
 


写出注册机:

1

2

3

4

5

6

7

8

9

from hashlib import md5

from Crypto.Cipher import AES

from binascii import b2a_hex

name = b'pediy'

md5_digest = md5(name).digest()

aes = AES.new(key=b'\x2B\x7E\x15\x16\x28\xAE\xD2\xA6\xAB\xF7\x15\x88\x09\xCF\x4F\x3C',mode=AES.MODE_ECB)

dec = aes.decrypt(md5_digest)

print(f'Serial Number("{name.decode()}")={b2a_hex(dec).decode().upper()}')

0x07. SM4

SM4是国密算法,由国家密码局发布。SM4是一个分组算法,分组长度为128比特,密钥长度为128比特,其结构是Fesitel网络的一个变体。
我们识别SM4算法的方法同样是找到SM4的S-box(在GitHub上找的源码):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

static const unsigned char SboxTable[16][16=

{

    {0xd60x900xe90xfe0xcc0xe10x3d0xb70x160xb60x140xc20x280xfb0x2c0x05},

    {0x2b0x670x9a0x760x2a0xbe0x040xc30xaa0x440x130x260x490x860x060x99},

    {0x9c0x420x500xf40x910xef0x980x7a0x330x540x0b0x430xed0xcf0xac0x62},

    {0xe40xb30x1c0xa90xc90x080xe80x950x800xdf0x940xfa0x750x8f0x3f0xa6},

    {0x470x070xa70xfc0xf30x730x170xba0x830x590x3c0x190xe60x850x4f0xa8},

    {0x680x6b0x810xb20x710x640xda0x8b0xf80xeb0x0f0x4b0x700x560x9d0x35},

    {0x1e0x240x0e0x5e0x630x580xd10xa20x250x220x7c0x3b0x010x210x780x87},

    {0xd40x000x460x570x9f0xd30x270x520x4c0x360x020xe70xa00xc40xc80x9e},

    {0xea0xbf0x8a0xd20x400xc70x380xb50xa30xf70xf20xce0xf90x610x150xa1},

    {0xe00xae0x5d0xa40x9b0x340x1a0x550xad0x930x320x300xf50x8c0xb10xe3},

    {0x1d0xf60xe20x2e0x820x660xca0x600xc00x290x230xab0x0d0x530x4e0x6f},

    {0xd50xdb0x370x450xde0xfd0x8e0x2f0x030xff0x6a0x720x6d0x6c0x5b0x51},

    {0x8d0x1b0xaf0x920xbb0xdd0xbc0x7f0x110xd90x5c0x410x1f0x100x5a0xd8},

    {0x0a0xc10x310x880xa50xcd0x7b0xbd0x2d0x740xd00x120xb80xe50xb40xb0},

    {0x890x690x970x4a0x0c0x960x770x7e0x650xb90xf10x090xc50x6e0xc60x84},

    {0x180xf00x7d0xec0x3a0xdc0x4d0x200x790xee0x5f0x3e0xd70xcb0x390x48}

};

拿2020纵横杯的第一道逆向题friendlyRE举例。
我们直接找到关键代码,这里是比较了Str1和Str2,通过交叉引用可以确定Str2加密过程,Str2为"2NI5JKCBI5Hyva+8AZa3mq!!":
 


再去找Str1的加密过程,发现有个地方用到了Base64表:
 


并且表是变换过的,第一次是大小写互换,第二次相当于是把表的前32位和后32位互换:
 


 


继续找找到了SM4的S-box:
 


找到key:
 


整个过程大概就是把输入经过SM4加密之后再经过一个变表的BASE64再与"2NI5JKCBI5Hyva+8AZa3mq!!"比较,写出exp:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import base64

from binascii import b2a_hex, a2b_hex

from gmssl.sm4 import CryptSM4, SM4_DECRYPT

BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

BASE64_change = (BASE64[32:] + BASE64[:32]).swapcase()

table = str.maketrans(BASE64_change,BASE64)

Str2 = 'N25IKJBC5IyHav8+ZA3aqm!!'

target = ''

for in range(0,len(Str2),2):

    target += Str2[i + 1]

    target += Str2[i]

target = target.translate(table).replace('!','=')

base64_decode = base64.b64decode(target)

print(b2a_hex(base64_decode))

crypt_sm4 = CryptSM4()

crypt_sm4.set_key(b'Thisisinsteresth',SM4_DECRYPT)

flag = crypt_sm4.crypt_ecb(base64_decode)

print(b2a_hex(flag))

理论上是可以这么写,但是gmssl库的SM4的padding方式跟题目里SM4的padding方式不一致,导致无法解密。
所以换工具解密:
 


Hex2Str,得到最后的flag为:DoyouKnowVEHSEH!

0x08. ChaCha20

ChaCha系列流密码,作为Salsa密码的改良版,具有更强的抵抗密码分析攻击的特性,“20”表示该算法有20轮的加密计算。
ChaCha20有一个初始矩阵,矩阵的输入为一个256位的密钥、64位随机数、64位计数器值以及4×32位的常数,它们均填充在32位整型数组中作为初始矩阵。排列方式如下:
 


四个常数0x61707865 0x3320646e 0x79622d32 0x6b206574按小端存储转为ASCII字符是"expand 32-byte k",这是我们用来识别ChaCha20算法的主要特征。
分析今年年初*CTF的一道题Favourite Architecure flag0,riscv架构的文件只能用Ghirda分析。由于未知原因main函数的反汇编失效了,只能手撸汇编。
 


我们直接从打印错误的代码开始分析,有两个大跳转跳到了这里,往回看能找到能找到两个地方调用的是同一个函数,估计是对加密结果进行比较:
 


 

图片描述


从第一次比较分析,先把用来比较的数据dump下来:

1

enc1 = b'\x88\xE7\x03\xB4\x36\xCD\x97\xAB\x5A\xA5\xA6\x0B\xDF\xCE\x08\x3B\x9D\x90\x32\x3C\x4E\x15\x14\xBD\x8D\x38\x38\xB0\xEE\x2A\xBC\x4B\xF9\xAA\x24\x26\x76\xA3\xA5\x75\x5E'

从比较函数往前找,找到了这个很诡异的地方:
 

图片描述


百度搜索关键词expand 32-byte k找到了GitHub上的一处代码:
 

图片描述


一比对发现是几乎一模一样(实际上并不一样),锁定第一个加密算法是ChaCha20:
 

图片描述


顺藤摸瓜找到密钥:
 

图片描述


然而用了各种Python库和在线网站都解密不了,一度怀疑人生...
后来分析发现了题目里用到的ChaCha20算法貌似跟正常的ChaCha20算法不太一样,一般来说ChaCha20算法输入的Nonce(随机数)是8字节,题目中的ChaCha20算法却给了12个字节的Nonce,于是改变思路去GitHub上翻了几个ChaCha20的C语言实现,找到了题目用到的源码:
 

图片描述


 

图片描述


 

图片描述

图片描述


比对下来发现是完全一样,依葫芦画瓢写出exp的第一部分:

1

2

3

4

5

6

7

8

9

10

11

12

13

#include "chacha20.h"

#include <stdio.h>

#include <stdlib.h>

int main(){

    char enc[100= "\x88\xE7\x03\xB4\x36\xCD\x97\xAB\x5A\xA5\xA6\x0B\xDF\xCE\x08\x3B\x9D\x90\x32\x3C\x4E\x15\x14\xBD\x8D\x38\x38\xB0\xEE\x2A\xBC\x4B\xF9\xAA\x24\x26\x76\xA3\xA5\x75\x5E";

    char key[100= "tzgkwukglbslrmfjsrwimtwyyrkejqzo";

    char nonce[100= "oaeqjfhclrqk";

    struct chacha20_context *ctx = (struct chacha20_context*)malloc(sizeof(struct chacha20_context));

    chacha20_init_context(ctx,(uint8_t*)key,(uint8_t*)nonce,0x80);

    chacha20_xor(ctx,(uint8_t*)enc,41);

    printf(enc);

}

flag{have_you_tried_ghidra9.2_decompiler_

其实更好的方法是直接动态调试dump密钥流异或,无奈riscv的动态调试环境没搭起来。
然而这种ChaCha20实现到底是变体还是翻车就不知道了...
第二部分则是一个很明显的TEA:
 


写出第二部分的exp:

1

2

3

4

5

6

from pytea import TEA

key = b'\xBB\xA0\x68\x13\x1E\xCE\x0A\x19\x57\xA3\xD8\x35\x61\x2C\xBF\x26'

enc = b'\xF9\x87\x50\xC4\xB2\xF2\x03\x07\x3C\xF4\x74\x69\x59\xBB\xB4\xED\x2A\xB0\xF0\x0F\xF2\x20\x85\x00\xDD\x23\xCD\xFD\x75\x48\x02\x35\xD3\xB6\xD7\xF1\xE1\x1B\xF2\x74\x12\xBF\x2D\xCB\xF6\x53\xB4\xA4'

cipher = TEA(key)

print(cipher.Decrypt(enc,16).decode())

if_you_have_hexriscv_plz_share_it_with_me_thx:P}

完整的flag:

flag{have_you_tried_ghidra9.2_decompiler_if_you_have_hexriscv_plz_share_it_with_me_thx:P}

0x09. 总结

最后再总结一下识别单向散列算法和对称加密算法的方法:

  1. 直接用IDA的插件Findcrpyt
  2. RC4、TEA这些流程比较简单的算法可以直接通过加密过程识别
  3. 一些用到了常量的算法,可以通过它的特征常量识别。比如TEA的delta、ChaCha20的sigma、DES和AES以及SM4的S-Box等等
  4. 善用GitHub,作为老程序员,这种复杂的加密算法一般都是在GitHub上抄的(至少我不会自己写hh)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值