首页
社区
课程
招聘
[原创]记一次unicorn半自动化逆向——还原某东sign算法
2021-3-9 18:41 36172

[原创]记一次unicorn半自动化逆向——还原某东sign算法

2021-3-9 18:41
36172

前言

关于题目。

 

题目是帖子快写完时候想到的,unicorn节省了极大的劳动力,当然也可以使用其他的虚拟执行引擎,但是使用unicorn你只需要pip install unicorn,然后建一个py文件就可以快乐地写代码跑了。

 

为什么是半自动化逆向?

 

因为需要自己写代码去控制unicorn怎么执行。

 

为什么会想起来还原某东sign算法?

 

因为已经有很多文章来分析怎么直接调用某东的sign算法了,比如使用frida rpc调的:某东直播弹幕实时抓取 https://www.52pojie.cn/thread-1332545-1-1.html。

 

让我尤其佩服的是一个老哥把它当作了练手项目玩出花了...

 

虽然老哥的文章有点散,但是入门极其友好,大家感兴趣可以去学习下,给老哥捧下场。

 

然后刚好最近想提升自己的汇编分析水平,于是就上手分析了。

 

http://91fans.com.cn/

 

image-20210309181744859

 

image-20210309181824749

 

image-20210309181902240

 

image-20210309181940242

分析准备

集成so文件

为了方便分析和调试,我选择了主动集成生成sign的so文件到自己写的apk中,然后主动调用。

 

可能是gradle版本的问题,搜索到的文章大都无效,踩坑十分多。

 

最后配置成功主要是根据两篇文章:http://www.sorgs.cn/post/7510/,https://blog.csdn.net/u011106915/article/details/106543464。

 

其实配置很简单,在src/main/目录下建立jniLibs/armeabi-v7a/目录,把so文件放在里面。

 

然后配置好build.gradleCMakeLists.txt就行了,当然,不同的gradle版本会有不同的问题,自己多搜索折腾下就好了。

 

image-20210309113547443

 

image-20210309113739384

 

image-20210309113815561

 

以及so文件中会有两处简单的校验,直接nop两条跳转指令就解决了,十分简单,就不多说了。

总加密流程

总的加密函数执行流程:

 

sub_127E4——>sub_126AC——>sub_18B8——>sub_227C

 

核心的加密算法都在sub_126AC中,传入待加密字符串,待加密字符串长度和两个随机值,

 

image-20210309105834591

 

image-20210309105806646

 

sub_126AC会返回加密后的字节,然后sub_18B8进行base64加密,最后sub_227C会进行标准的md5加密。

sub_126AC——加密选择

sub_126AC只是加密的入口,会根据传入的两个随机数选择加密方式和相应的key。

 

image-20210309110537742

 

image-20210309110812935

 

我们定义三种加密方式为version0,version1和version2,其中version0和version1加密流程全部一样,只不过里面函数传入的常量version0是32,version1是16,我们在分析过程中以Version0为例。

Version0 加密

执行流程

加密函数执行流程:

 

sub_10E4C——>sub_125F0——>sub_12580——>sub_10EA4——>sub_10D70

 

待加密字符串会被每8个字节分为一组传进sub_10EA4进行加密,如果最后还有字节剩余,会单独进入sub_10D70加密。

sub_10EA4比特位初始化

首先看下sub_10EA4函数的流程图,先进行初始化,接着有一个循环,循环次数是传入的key0的长度。

 

image-20210308171518031

 

最上面的两个长框框就是初始化过程,做了什么呢,IDA进行f5后比较简洁,我们截取一部分看下。

 

image-20210308173258806

 

image-20210308173335943

 

我们注意到分别传入的8个字节分别和0x80(b'1000 0000'),0x40(b'0100 0000'),0x20(b'0010 0000'),0x10(b'0001 0000'),0x8(b’0000 1000‘),0x4(b'0000 0100'),0x2(b'0000 0010'),0x1(b'0000 0001')进行与操作,然后赋值给一些变量,我们观察这些与操作的对象,会发现其实很有规律,这些变量正是输入的8个字节的64个比特位,后面会进行打乱比特位然后还原。

sub_10EA4打乱比特位

然后我们来看流程图中很有规律的十六个小框框,

 

image-20210308174216954

 

截取一些IDA进行f5后的片段进行分析。

 

image-20210308174746290

 

image-20210308174524110

 

其中key0_i是key0的第i个字符,在循环中key0_i也会和0x80(b'1000 0000'),0x40(b'0100 0000'),0x20(b'0010 0000'),0x10(b'0001 0000'),0x8(b’0000 1000‘),0x4(b'0000 0100'),0x2(b'0000 0010'),0x1(b'0000 0001')进行与操作,结果作为条件判断,也就是判断key0_i二进制的对应比特为是0还是1。然后会进入对应的分支交换初始化过程中得到的变量,也就是打乱比特位。

sub_10EA4比特位复原

在函数最后进行的是比特位复原,四轮循环,每轮还原两个字节。

 

image-20210308175646222

 

image-20210308175711322

 

如有不清楚的地方,可以自行调试观察。

使用unicorn还原sub_10EA4算法

我们怎么还原sub_10EA4中的算法呢,过程不难就是量多,难道真的要一点点对比着IDA的f5反编译手动写出一样的逻辑吗?当然不是,这里有更简单的方法,我们可以借助unicorn来快速还原。

 

我们已经分析出来sub_10EA4算法做的不过是打乱传入八个字节的比特位,生成新的八个字节,也就说总的比特位只是顺便变了,并没有被改变值,那么我们可不可以找到每个比特位在打乱前和被打乱后的映射关系呢?当然可以。

 

我们只要借助unicorn,控制sub_10EA4函数输入的八个字节的64个二进制比特位,如果64个比特中只有一个是1,那么结果的比特位中会有几个1呢?也是只有一个,然后计算前和计算后比特位的1的索引位置就是要找的映射关系,我们进行64次计算,然后每次计算1的索引位置不同,最后就能得到全部比特位的计算前和计算后的映射关系了。

 

篇幅所限,unicorn的使用就不多说了。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from unicorn import *
from unicorn.arm_const import *
 
table = []
 
def bytes2bin(bytes):
    arr = []
    for v in [m for m in bytes]:
        arr.append(
            [(v & 128) >> 7, (v & 64) >> 6, (v & 32) >> 5, (v & 16) >> 4, (v & 8) >> 3, (v & 4) >> 2, (v & 2) >> 1,
             v & 1])
    return [i for j in arr for i in j]
 
 
def bin2bytes(arr):
    length = len(arr) // 8
    arr1 = [0 for _ in range(length)]
    for j in range(length):
        arr1[j] = arr[j * 8] << 7 | arr[j * 8 + 1] << 6 | arr[j * 8 + 2] << 5 | arr[j * 8 + 3] << 4 | arr[
            j * 8 + 4] << 3 | arr[j * 8 + 5] << 2 | arr[j * 8 + 6] << 1 | arr[j * 8 + 7]
    return bytes(arr1)
 
 
def read(name):
    with open(name, 'rb') as f:
        return f.read()
 
 
def hook_code(mu, address, size, user_data):
    if address == BASE + 0x119cc:
        arr2 = []
        for byte in mu.mem_read(PLAINTEXT_ADDR, 8):
            arr2.append(byte)
        table.append([user_data.index(1), bytes2bin(arr2).index(1)])
 
 
if __name__ == "__main__":
    key0 = b'44e715a6e322ccb7d028f7a42fa55040'
    mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
    BASE = 0x400000
    STACK_ADDR = 0x0
    STACK_SIZE = 1024
    PLAINTEXT_ADDR = 1024 * 2
    PLAINTEXT_SIZE = 1024
    KEY_ADDR = 1024 * 3
    KEY_SIZE = 1024
    mu.mem_map(BASE, 1024 * 1024)
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    mu.mem_map(PLAINTEXT_ADDR, PLAINTEXT_SIZE)
    mu.mem_map(KEY_ADDR, KEY_SIZE)
    mu.reg_write(UC_ARM_REG_SP, STACK_ADDR + STACK_SIZE - 1)
    # mu.mem_write(BASE, read("F:\\Code\\Pycharm\\JDSign\\libjdbitmapkit.so"))
    mu.mem_write(BASE, read("./libjdbitmapkit.so"))
    mu.mem_write(KEY_ADDR, key0)
 
    for i in range(64):
        arr1 = [0 for j in range(64)]
        arr1[i] = 1
        h = mu.hook_add(UC_HOOK_CODE, hook_code, arr1)
        mu.mem_write(PLAINTEXT_ADDR, bin2bytes(arr1))
        mu.reg_write(UC_ARM_REG_R1, KEY_ADDR)
        mu.reg_write(UC_ARM_REG_R2, 32)
        mu.reg_write(UC_ARM_REG_R3, PLAINTEXT_ADDR)
        mu.emu_start(BASE + 0x00010EA4 + 1, BASE + 0x000119D0)
        mu.hook_del(h)
    print(table)

可以得到映射表,例如[1,4]表示64个比特位在打乱后第1个位置的比特位会到第4个位置。

1
[[0, 0], [1, 4], [2, 61], [3, 15], [4, 56], [5, 40], [6, 6], [7, 59], [8, 62], [9, 58], [10, 17], [11, 2], [12, 12], [13, 8], [14, 32], [15, 60], [16, 13], [17, 45], [18, 34], [19, 14], [20, 36], [21, 21], [22, 22], [23, 39], [24, 23], [25, 25], [26, 26], [27, 20], [28, 1], [29, 33], [30, 46], [31, 55], [32, 35], [33, 24], [34, 57], [35, 19], [36, 53], [37, 37], [38, 38], [39, 5], [40, 30], [41, 41], [42, 42], [43, 18], [44, 47], [45, 27], [46, 9], [47, 44], [48, 51], [49, 7], [50, 49], [51, 63], [52, 28], [53, 43], [54, 54], [55, 52], [56, 31], [57, 10], [58, 29], [59, 11], [60, 3], [61, 16], [62, 50], [63, 48]]

然后可轻松还原sub_10EA4算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
def sub_10EA4(input):
    table = [[0, 0], [1, 4], [2, 61], [3, 15], [4, 56], [5, 40], [6, 6], [7, 59], [8, 62], [9, 58], [10, 17], [11, 2],
             [12, 12], [13, 8], [14, 32], [15, 60], [16, 13], [17, 45], [18, 34], [19, 14], [20, 36], [21, 21],
             [22, 22], [23, 39], [24, 23], [25, 25], [26, 26], [27, 20], [28, 1], [29, 33], [30, 46], [31, 55],
             [32, 35], [33, 24], [34, 57], [35, 19], [36, 53], [37, 37], [38, 38], [39, 5], [40, 30], [41, 41],
             [42, 42], [43, 18], [44, 47], [45, 27], [46, 9], [47, 44], [48, 51], [49, 7], [50, 49], [51, 63], [52, 28],
             [53, 43], [54, 54], [55, 52], [56, 31], [57, 10], [58, 29], [59, 11], [60, 3], [61, 16], [62, 50],
             [63, 48]]
    arr = bytes2bin(input)
    arr1 = [0 for i in range(len(arr))]
    for i in range(len(table)):
        arr1[table[i][1]] = arr[table[i][0]]
    return bin2bytes(arr1)

sub_10D70函数分析

sub_10D70函数在IDA反编译也比较清晰,我们来看下。

 

image-20210308212310967

 

前面有说,待加密字符串会被每8个字节分为一组传进sub_10EA4进行加密,sub_10D70会加密最后余出来的几个字节,这6个case就是加密余出来的1到7个字节。case 0到case 6对应的六个函数都是一个模板,我们以case 0为例来分析,即sub_4B7C。

sub_4B7C初始化

sub_4B87只会加密处理一个字节,首先我们来看下sub_4B7C的流程图。

 

image-20210308212954647

 

可以看到密密麻麻,其他五个case也都长这样,看到后首先第一个反应就是,这是ollvm吗?在经过仔细的分析以及动态调试之后,我判断这个并不是ollvm,没有看到控制流平坦化会带有的标志性大量的常量,也没有找到不可到达的分支,虚假控制流以及指令替换的痕迹,当然也可能是我水平太低了,没有认出来这种ollvm,总之,我还是铁着头把整个流程从头到尾看了一遍。

 

来看下初始化,

 

image-20210308214035243

 

看样子有点像sub_10EA4,但仔细一看又很多不一样,上半部分取了传入的一个字节的比特位,还做了些其他的计算操作,赋值给变量。

 

而下半部分呢,则是取一些变量的地址,然后放到另一些变量地址加偏移处,这些说起来可能很模糊,但是如果看一下sub_4B7C的栈空间就会清晰很多。

 

为了方便理解,我们把这样连着五个变量在一起的当作一个数组,这样的数组往下拉可以看到是有八个,每个数组的前四个位置都存放着其他数组首的地址,而第五个位置则存放着前面说的输入字节的比特位经过计算后的值,其实分析后面的case就会发现,有多少输入的比特位,就会有多少个这样的数组。

 

image-20210308214910555

 

初始化就这么多,而后面就开始根据这些东西进行大量的循环计算。

sub_4B7C计算分析

image-20210308215648511

 

整个循环是根据key0的每个字节的每个比特位作为判断条件来选择分支,然后进行循环的,所有的计算类型只有两种,就是下面的两个大方框,是两个小循环。

 

它们的汇编如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:00005BE2 loc_5BE2                                ; CODE XREF: sub_4B7C+104↑j
.text:00005BE2                 CMP             R1, R2
.text:00005BE4                 MOV             R12, R1
.text:00005BE6                 BEQ             loc_5C00
.text:00005BE8                 MOV             R6, R1
.text:00005BEA                 MOV             R8, R2
.text:00005BEC                 B               loc_5BF0
.text:00005BEE ; ---------------------------------------------------------------------------
.text:00005BEE
.text:00005BEE loc_5BEE                                ; CODE XREF: sub_4B7C+1082↓j
.text:00005BEE                 MOV             R12, R6
.text:00005BF0
.text:00005BF0 loc_5BF0                                ; CODE XREF: sub_4B7C+1070↑j
.text:00005BF0                 LDRB            R6, [R6,#0x10]
.text:00005BF2                 STRB.W          R6, [R8,#0x10]
.text:00005BF6                 MOV             R8, R12
.text:00005BF8                 LDR.W           R6, [R12,#0xC]
.text:00005BFC                 CMP             R6, R2
.text:00005BFE                 BNE             loc_5BEE
.text:00005C00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
text:00005C00                 CMP             R11, R2
.text:00005C02                 STRB.W          R4, [R12,#0x10]
.text:00005C06                 MOV             R4, R11
.text:00005C08                 LDRB.W          R8, [SP,#0xE0+var_98]
.text:00005C0C                 IT NE
.text:00005C0E                 MOVNE           R12, R2
.text:00005C10                 BNE             loc_5C16
.text:00005C12                 B               loc_5C2C
.text:00005C14 ; ---------------------------------------------------------------------------
.text:00005C14
.text:00005C14 loc_5C14                                ; CODE XREF: sub_4B7C+10AE↓j
.text:00005C14                 MOV             R4, R6
.text:00005C16
.text:00005C16 loc_5C16                                ; CODE XREF: sub_4B7C+1094↑j
.text:00005C16                 LDRB            R6, [R4,#0x10]
.text:00005C18                 RSBS.W          R6, R6, #1
.text:00005C1C                 IT CC
.text:00005C1E                 MOVCC           R6, #0
.text:00005C20                 STRB.W          R6, [R12,#0x10]
.text:00005C24                 LDR             R6, [R4,#4]
.text:00005C26                 MOV             R12, R4
.text:00005C28                 CMP             R6, R2
.text:00005C2A                 BNE             loc_5C14

这两个小循环做了什么呢?其实十分简单,就是不断地查刚才我们定义的数组的前四个位置存放的变量地址值,判断是否等于另一个变量的地址值,只不过判断的过程中会不断地移动这些数组的第五个位置的值。

使用unicorn还原case0——sub_4B7C算法

这个算法似乎很难搞,确实很难搞,不同于sub_10EA4的对输入字节的比特位进行简单的交换,在获取了比特位后又进行了很多计算。

 

该怎么办呢,我们来看下sub_4B7C算法的最后部分。

 

image-20210308220953273

 

可以看到和sub_10EA4函数最后的比特位还原部分几乎是一样的,这时候我们进行下大胆的猜测,sub_4B7C函数其实也是打乱输入字节的比特位进行了还原,只不过比特位在打乱后还进行了计算,而且每个比特位进行计算的规则都是一样的。即输入的字节第x个比特位是0的话,打乱计算还原后,第y个比特位会是m;如果输入的字节第x个比特位是1的话,第y个比特位则会是n,(x, 0)——>(y,m)(x,1)——>(y,n)。后面经过实践,证明了这样的猜想是正确的。

 

然后我们还是可以借助unicorn来找到所有的映射关系,还原sub_4B7C算法。思路我们进行下改变,我们可以unicorn控制输入字节的比特位,用八个比特位只有一个1的计算结果和八个比特位全是0的计算结果进行对比从而得到所有的映射关系。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from unicorn import *
from unicorn.arm_const import *
 
table = []
table1 = []
 
def bytes2bin(bytes):
    arr = []
    for v in [m for m in bytes]:
        arr.append(
            [(v & 128) >> 7, (v & 64) >> 6, (v & 32) >> 5, (v & 16) >> 4, (v & 8) >> 3, (v & 4) >> 2, (v & 2) >> 1,
             v & 1])
    return [i for j in arr for i in j]
 
 
def bin2bytes(arr):
    length = len(arr) // 8
    arr1 = [0 for _ in range(length)]
    for j in range(length):
        arr1[j] = arr[j * 8] << 7 | arr[j * 8 + 1] << 6 | arr[j * 8 + 2] << 5 | arr[j * 8 + 3] << 4 | arr[
            j * 8 + 4] << 3 | arr[j * 8 + 5] << 2 | arr[j * 8 + 6] << 1 | arr[j * 8 + 7]
    return bytes(arr1)
 
 
def read(name):
    with open(name, 'rb') as f:
        return f.read()
 
 
def hook_code(mu, address, size, user_data):
    if address == BASE + 0x5284:
        table.append(bytes2bin(mu.mem_read(PLAINTEXT_ADDR, 1)))
 
 
if __name__ == "__main__":
    key0 = b'44e715a6e322ccb7d028f7a42fa55040'
    mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
    BASE = 0x400000
    STACK_ADDR = 0x0
    STACK_SIZE = 1024
    PLAINTEXT_ADDR = 1024 * 2
    PLAINTEXT_SIZE = 1024
    KEY_ADDR = 1024 * 3
    KEY_SIZE = 1024
    mu.mem_map(BASE, 1024 * 1024)
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    mu.mem_map(PLAINTEXT_ADDR, PLAINTEXT_SIZE)
    mu.mem_map(KEY_ADDR, KEY_SIZE)
    mu.reg_write(UC_ARM_REG_SP, STACK_ADDR + STACK_SIZE - 1)
    # mu.mem_write(BASE, read("F:\\Code\\Pycharm\\JDSign\\libjdbitmapkit.so"))
    mu.mem_write(BASE, read("./libjdbitmapkit.so"))
    mu.mem_write(KEY_ADDR, key0)
 
    for i in range(9):
        arr1 = [0 for j in range(8)]
        if i != 0:
            arr1[i-1] = 1
        h = mu.hook_add(UC_HOOK_CODE, hook_code, arr1)
        mu.mem_write(PLAINTEXT_ADDR, bin2bytes(arr1))
        mu.reg_write(UC_ARM_REG_R0, KEY_ADDR)
        mu.reg_write(UC_ARM_REG_R1, 32)
        mu.reg_write(UC_ARM_REG_R2, 1)
        mu.reg_write(UC_ARM_REG_R3, PLAINTEXT_ADDR)
        mu.emu_start(BASE + 0x0004B7C + 1, BASE + 0x0005288)
        mu.hook_del(h)
 
    for i in range(8):
        for j in range(8):
            arr3 = []
            if table[0][j] != table[i+1][j]:
                table1.append([i, j, table[0][j], table[i+1][j]])
    print(table1)

就结果为

1
[[0, 6, 0, 1], [1, 4, 1, 0], [2, 5, 0, 1], [3, 0, 0, 1], [4, 2, 0, 1], [5, 3, 0, 1], [6, 1, 1, 0], [7, 7, 0, 1]]

我们可以得到为[[0, 6, 0, 1], [1, 4, 1, 0], [2, 5, 0, 1], [3, 0, 0, 1], [4, 2, 0, 1], [5, 3, 0, 1], [6, 1, 1, 0], [7, 7, 0, 1]],里面的列表每个即为[x,y,m,n],比如[6, 1, 0, 1]意味,第6个比特位在打乱计算后会放到第1个比特位上,如果第6个比特位是0,则在打乱计算后会放到第6个比特位上是0,反之是1。

 

然后可轻松还原sub_4B7C算法。

1
2
3
4
5
6
7
8
9
10
11
def sub_4B7C(input):
    table = [[0, 6, 0, 1], [1, 4, 1, 0], [2, 5, 0, 1], [3, 0, 0, 1], [4, 2, 0, 1], [5, 3, 0, 1], [6, 1, 1, 0],
             [7, 7, 0, 1]]
    arr = bytes2bin(input)
    arr1 = [0 for i in range(8)]
    for i in range(8):
        if arr[i] == 0:
            arr1[table[i][1]] = table[i][2]
        else:
            arr1[table[i][1]] = table[i][3]
    return bin2bytes(arr1)

使用unicorn一步求出所有case的映射关系

好了,现在我们已经还原出第一个case,我们再会过头来看下sub_10D70,可以看到一共有七个case,分别对应的七个函数不同的只有函数起始结束地址,以及要处理的字节数,开拓下思维,这时候我们完全可以all in one,一次得到所有case的映射关系。

 

image-20210308212310967

 

代码如下:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from unicorn import *
from unicorn.arm_const import *
 
table = []
table1 = []
 
 
def bytes2bin(bytes):
    arr = []
    for v in [m for m in bytes]:
        arr.append(
            [(v & 128) >> 7, (v & 64) >> 6, (v & 32) >> 5, (v & 16) >> 4, (v & 8) >> 3, (v & 4) >> 2, (v & 2) >> 1,
             v & 1])
    return [i for j in arr for i in j]
 
 
def bin2bytes(arr):
    length = len(arr) // 8
    arr1 = [0 for _ in range(length)]
    for j in range(length):
        arr1[j] = arr[j * 8] << 7 | arr[j * 8 + 1] << 6 | arr[j * 8 + 2] << 5 | arr[j * 8 + 3] << 4 | arr[
            j * 8 + 4] << 3 | arr[j * 8 + 5] << 2 | arr[j * 8 + 6] << 1 | arr[j * 8 + 7]
    return bytes(arr1)
 
 
def read(name):
    with open(name, 'rb') as f:
        return f.read()
 
 
def hook_code(mu, address, size, user_data):
    if address == BASE + user_data[2] -4:
        table.append(bytes2bin(mu.mem_read(PLAINTEXT_ADDR, m[0])))
 
 
if __name__ == "__main__":
    key0 = b'44e715a6e322ccb7d028f7a42fa55040'
    mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
    BASE = 0x400000
    STACK_ADDR = 0x0
    STACK_SIZE = 1024 * 10
    PLAINTEXT_ADDR = 1024 * 10
    PLAINTEXT_SIZE = 1024
    KEY_ADDR = 1024 * 11
    KEY_SIZE = 1024
    mu.mem_map(BASE, 1024 * 1024)
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    mu.mem_map(PLAINTEXT_ADDR, PLAINTEXT_SIZE)
    mu.mem_map(KEY_ADDR, KEY_SIZE)
    mu.reg_write(UC_ARM_REG_SP, STACK_ADDR + STACK_SIZE - 1)
    # mu.mem_write(BASE, read("F:\\Code\\Pycharm\\JDSign\\libjdbitmapkit.so"))
    mu.mem_write(BASE, read("./libjdbitmapkit.so"))
    mu.mem_write(KEY_ADDR, key0)
 
    arr = [[1, 0x0004B7C, 0x0005288], [2, 0x00061A0, 0x0006AE8], [3, 0x0007994, 0x000084A6], [4, 0x000091AC, 0x00009DF4],
           [5, 0x0000ABF8, 0x0000BA8C], [6, 0x0000C8C0, 0x0000D9A0], [7, 0x0000E7FC, 0x0000FC1C]]
    for m in arr:
        for i in range(m[0]*8+1):
            arr1 = [0 for j in range(m[0]*8)]
            if i != 0:
                arr1[i - 1] = 1
            h = mu.hook_add(UC_HOOK_CODE, hook_code, m)
            mu.mem_write(PLAINTEXT_ADDR, bin2bytes(arr1))
            mu.reg_write(UC_ARM_REG_R0, KEY_ADDR)
            mu.reg_write(UC_ARM_REG_R1, 32)
            mu.reg_write(UC_ARM_REG_R2, 1)
            mu.reg_write(UC_ARM_REG_R3, PLAINTEXT_ADDR)
            mu.emu_start(BASE + m[1] + 1, BASE + m[2])
            mu.hook_del(h)
 
        for i in range(m[0]*8):
            for j in range(m[0]*8):
                arr3 = []
                if table[0][j] != table[i + 1][j]:
                    table1.append([i, j, table[0][j], table[i + 1][j]])
        print("case %s 映射关系:"%(m[0]-1))
        print(table1)
        table.clear()
        table1.clear()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0 映射关系:
[[0, 6, 0, 1], [1, 4, 1, 0], [2, 5, 0, 1], [3, 0, 0, 1], [4, 2, 0, 1], [5, 3, 0, 1], [6, 1, 1, 0], [7, 7, 0, 1]]
case 1 映射关系:
[[0, 5, 0, 1], [1, 9, 0, 1], [2, 0, 1, 0], [3, 7, 1, 0], [4, 10, 0, 1], [5, 6, 0, 1], [6, 13, 1, 0], [7, 1, 0, 1], [8, 4, 0, 1], [9, 11, 0, 1], [10, 14, 1, 0], [11, 3, 1, 0], [12, 12, 0, 1], [13, 15, 1, 0], [14, 8, 0, 1], [15, 2, 0, 1]]
case 2 映射关系:
[[0, 17, 0, 1], [1, 7, 0, 1], [2, 5, 0, 1], [3, 19, 1, 0], [4, 18, 0, 1], [5, 15, 1, 0], [6, 22, 0, 1], [7, 21, 0, 1], [8, 16, 0, 1], [9, 4, 0, 1], [10, 12, 0, 1], [11, 2, 1, 0], [12, 10, 1, 0], [13, 13, 1, 0], [14, 20, 1, 0], [15, 8, 1, 0], [16, 9, 0, 1], [17, 23, 0, 1], [18, 11, 1, 0], [19, 6, 0, 1], [20, 1, 0, 1], [21, 3, 1, 0], [22, 0, 1, 0], [23, 14, 0, 1]]
case 3 映射关系:
[[0, 25, 1, 0], [1, 4, 0, 1], [2, 29, 0, 1], [3, 1, 0, 1], [4, 27, 1, 0], [5, 18, 1, 0], [6, 23, 1, 0], [7, 14, 1, 0], [8, 28, 1, 0], [9, 11, 0, 1], [10, 9, 1, 0], [11, 13, 0, 1], [12, 24, 1, 0], [13, 0, 1, 0], [14, 5, 0, 1], [15, 2, 1, 0], [16, 26, 0, 1], [17, 12, 0, 1], [18, 31, 1, 0], [19, 16, 1, 0], [20, 30, 0, 1], [21, 15, 0, 1], [22, 10, 0, 1], [23, 22, 1, 0], [24, 7, 1, 0], [25, 21, 0, 1], [26, 6, 1, 0], [27, 3, 1, 0], [28, 8, 1, 0], [29, 20, 0, 1], [30, 19, 1, 0], [31, 17, 0, 1]]
case 4 映射关系:
[[0, 11, 0, 1], [1, 12, 0, 1], [2, 28, 1, 0], [3, 30, 0, 1], [4, 13, 1, 0], [5, 24, 0, 1], [6, 22, 1, 0], [7, 25, 1, 0], [8, 23, 1, 0], [9, 3, 0, 1], [10, 16, 0, 1], [11, 8, 1, 0], [12, 34, 0, 1], [13, 2, 0, 1], [14, 5, 0, 1], [15, 7, 1, 0], [16, 4, 0, 1], [17, 14, 0, 1], [18, 39, 1, 0], [19, 33, 0, 1], [20, 15, 0, 1], [21, 0, 0, 1], [22, 31, 0, 1], [23, 9, 1, 0], [24, 29, 0, 1], [25, 26, 1, 0], [26, 19, 0, 1], [27, 6, 1, 0], [28, 27, 1, 0], [29, 10, 1, 0], [30, 37, 0, 1], [31, 38, 1, 0], [32, 20, 0, 1], [33, 21, 1, 0], [34, 1, 0, 1], [35, 36, 0, 1], [36, 32, 0, 1], [37, 17, 0, 1], [38, 18, 0, 1], [39, 35, 1, 0]]
case 5 映射关系:
[[0, 11, 0, 1], [1, 45, 0, 1], [2, 15, 1, 0], [3, 22, 0, 1], [4, 10, 0, 1], [5, 7, 0, 1], [6, 3, 0, 1], [7, 42, 0, 1], [8, 17, 1, 0], [9, 21, 0, 1], [10, 4, 0, 1], [11, 8, 1, 0], [12, 19, 1, 0], [13, 32, 0, 1], [14, 28, 1, 0], [15, 31, 1, 0], [16, 29, 0, 1], [17, 14, 1, 0], [18, 39, 1, 0], [19, 27, 1, 0], [20, 2, 1, 0], [21, 24, 0, 1], [22, 26, 1, 0], [23, 9, 1, 0], [24, 41, 0, 1], [25, 1, 1, 0], [26, 47, 0, 1], [27, 44, 0, 1], [28, 23, 1, 0], [29, 0, 1, 0], [30, 12, 1, 0], [31, 18, 0, 1], [32, 33, 0, 1], [33, 36, 0, 1], [34, 40, 1, 0], [35, 34, 0, 1], [36, 25, 0, 1], [37, 16, 1, 0], [38, 5, 1, 0], [39, 35, 0, 1], [40, 38, 0, 1], [41, 37, 1, 0], [42, 13, 0, 1], [43, 20, 1, 0], [44, 6, 0, 1], [45, 43, 0, 1], [46, 30, 0, 1], [47, 46, 1, 0]]
case 6 映射关系:
[[0, 7, 1, 0], [1, 9, 0, 1], [2, 53, 1, 0], [3, 19, 1, 0], [4, 15, 1, 0], [5, 8, 0, 1], [6, 3, 0, 1], [7, 24, 1, 0], [8, 18, 0, 1], [9, 51, 0, 1], [10, 42, 1, 0], [11, 39, 0, 1], [12, 20, 0, 1], [13, 12, 0, 1], [14, 28, 1, 0], [15, 27, 1, 0], [16, 23, 0, 1], [17, 49, 0, 1], [18, 10, 1, 0], [19, 55, 1, 0], [20, 52, 1, 0], [21, 17, 0, 1], [22, 48, 0, 1], [23, 14, 1, 0], [24, 33, 0, 1], [25, 25, 1, 0], [26, 4, 1, 0], [27, 11, 0, 1], [28, 47, 1, 0], [29, 0, 0, 1], [30, 21, 1, 0], [31, 44, 0, 1], [32, 16, 0, 1], [33, 41, 0, 1], [34, 29, 0, 1], [35, 1, 0, 1], [36, 46, 0, 1], [37, 5, 0, 1], [38, 30, 0, 1], [39, 45, 0, 1], [40, 31, 1, 0], [41, 43, 1, 0], [42, 36, 1, 0], [43, 26, 0, 1], [44, 34, 0, 1], [45, 2, 0, 1], [46, 6, 0, 1], [47, 50, 1, 0], [48, 13, 1, 0], [49, 37, 1, 0], [50, 32, 0, 1], [51, 40, 0, 1], [52, 35, 0, 1], [53, 38, 0, 1], [54, 54, 0, 1], [55, 22, 0, 1]]

Version2 加密

执行流程

加密执行函数流程:sub_10DE4——>sub_12FF0——>sub_12ECC——>sub_130D0,其中sub_12ECC是重点,我们直接看sub_12ECC。

sub_12ECC初始化

进入函数sub_12ECC,先看下开始处的汇编片段。r1寄存器存放着key2字符串地址,r2寄存器存放着常量1,r3寄存器存放着待加密字符串地址,在开辟栈空间后SP,#0x48+arg_0地址存放着是待加密字节的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00012ECC                 LDR.W           R12, =(__stack_chk_guard_ptr - 0x12ED8)
.text:00012ED0                 PUSH.W          {R4-R11,LR}
.text:00012ED4                 ADD             R12, PC ; __stack_chk_guard_ptr
.text:00012ED6                 LDR.W           R12, [R12] ; __stack_chk_guard
.text:00012EDA                 SUB             SP, SP, #0x24
.text:00012EDC                 MOV             R10, R3
.text:00012EDE                 LDR.W           R3, [R12]
.text:00012EE2                 MOV             R7, R1
.text:00012EE4                 LDR.W           R8, [SP,#0x48+arg_0]
.text:00012EE8                 MOV             R9, R2
.text:00012EEA                 STR             R3, [SP,#0x48+var_2C]
.text:00012EEC                 CMP.W           R8, #0
.text:00012EF0                 BNE             loc_12F02

我们注意到下面这四句汇编指令,是把待加密字符串存放的地址放到r10寄存器中,key2字符串地址放到r7寄存器中,常量1放到r9寄存器,从SP,#0x48+arg_0地址取出待加密字符串长度后放到r8寄存器中。

1
2
3
4
5
6
7
.text:00012EDC                 MOV             R10, R3
 
.text:00012EE2                 MOV             R7, R1
 
.text:00012EE8                 MOV             R9, R2
 
.text:00012EE4                 LDR.W           R8, [SP,#0x48+arg_0]

sub_12ECC加密流程

然后我们在函数sub_12ECC中往下走,找到0x12F68地址处开始的关键汇编片段,即sign算法加密部分,这里是一个循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:00012F66                 MOVS            R3, #0
.text:00012F68
.text:00012F68 loc_12F68                              
.text:00012F68                 AND.W           R2, R3, #0xF
.text:00012F6C                 ADD             R0, SP, #0x48+var_28
.text:00012F6E                 ADD             R2, R0
.text:00012F70                 AND.W           R1, R3, #7
.text:00012F74                 LDRB.W          R0, [R10]
.text:00012F78                 ADDS            R3, #1
.text:00012F7A                 LDRB.W          R2, [R2,#-0x14]
.text:00012F7E                 CMP             R3, R8
.text:00012F80                 LDRB            R4, [R7,R1]
.text:00012F82                 EOR.W           R0, R2, R0
.text:00012F86                 EOR.W           R0, R0, R4
.text:00012F8A                 ADD             R0, R2
.text:00012F8C                 EOR.W           R2, R2, R0
.text:00012F90                 UXTB            R2, R2
.text:00012F92                 STRB.W          R2, [R10],#1
.text:00012F96                 LDRB            R1, [R7,R1]
.text:00012F98                 EOR.W           R2, R2, R1
.text:00012F9C                 STRB.W          R2, [R10,#-1]
.text:00012FA0                 BNE             loc_12F68

r10寄存器

 

我们抓重点,看待加密字符串是怎么被加密的,待加密字符串地址放在了r10寄存器中,涉及r10寄存器的汇编指令有三条。

 

00012F74处的LDRB.W R0, [R10]是取出待加密字符串的一个字节放到r0寄存器中随后会进行计算操作;

 

00012F92处的STRB.W R2, [R10],#1,这条指令首先会把放在r2寄存器中的计算结果值放到r10寄存器当前存储的地址上,这个并不重要,因为还没有计算完成,后面的指令才是把最后的计算值存放值到这个地址上,重要的是这条指令随后r10寄存器中的地址值会加一,因为这是一个基于索引后置修改取址模式;

 

00012F9200012F9C之间,我们可以看到取了r7寄存器存放的地址值的一个偏移地址的值(偏移值为r1寄存器中的值)放到了r1寄存器中,随后把r1r2寄存器中的值进行亦或,结果存放在r2寄存器中。

 

最后是涉及到r10寄存器的第三条指令00012F9C处,STRB.W R2, [R10,#-1],把计算结果值放到R10,#-1地址处,注意到r10寄存器中的地址值刚才已经加一,所以现在减一后还是刚才存储的地址,所以这里才是计算结果最后存放的指令。

 

r3寄存器

 

好了,我们已经知道计算结果是放到了r2寄存器中,我们从00012F66开始看,一步步看是怎么计算出来的。

 

可以看到,r3作为计数器,每轮循环会在.text:00012F78 ADDS R3, #1处加一,并在.text:00012F7E CMP R3, R8处和r8寄存器值即加密字符串长度进行比较。

 

r2寄存器

 

在开始处,r3首先和0xf进行与操作,结果放在r2寄存器中,随后将与操作的结果值和SP, #0x48+var_28地址值进行相加,相加结果仍放在r2寄存器中,随后会进行.text:00012F7A LDRB.W R2, [R2,#-0x14],也就是说现在r2寄存器中存放的是SP, #0x48+var_28地址加上一个偏移值(即(i &0xf) - 0x14)这个地址上存放的值。

 

r4寄存器

 

然后在.text:00012F70 AND.W R1, R3, #7处把r3寄存器中的值和7进行与操作,结果放在r1寄存器中,然后在.text:00012F80 LDRB R4, [R7,R1]处把r1寄存器中的地址值作为偏移加上r7寄存器中的值,取出这个地址中的值放在r4寄存器中,r7寄存器前面我们说了放的是key2,所以放在r4寄存器中这个的值即是key2[i&7]

 

计算部分

 

随后便是计算部分:

1
2
3
4
5
6
7
8
.text:00012F82                 EOR.W           R0, R2, R0
.text:00012F86                 EOR.W           R0, R0, R4
.text:00012F8A                 ADD             R0, R2
.text:00012F8C                 EOR.W           R2, R2, R0
.text:00012F90                 UXTB            R2, R2
 
.text:00012F96                 LDRB            R1, [R7,R1]
.text:00012F98                 EOR.W           R2, R2, R1

此时,涉及计算的r0 r2 r4我们都已经知道是什么了,r0寄存器中的值我们在提r10寄存器时候有知道它是待加密字符串取出的一个字节。

 

IDA F5

 

这部分使用IDA进行f5的效果是这样的。

 

image-20210308163440724

sub_12ECC算法还原

现在我们想还原这个加密过程该怎么办,要看我们缺什么。待加密字符串,key2和加密计算过程我们都有了,只有一个SP, #0x48+var_28地址加上一个偏移 -0x14,这个地址存放有一个数组,每轮加密循环会取出一个值(数组偏移(i & 0xf)处)放在r2寄存器中然后进行计算,我们通过静态分析无法知道这个数组值是什么,所以进行ida动态调试,可以得到这个地方存放的数组值为[0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A]。

 

image-20210309140639735

 

随后可python写出sub_12ECC函数中的加密计算过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def sub_12ECC(input):
    arr = [0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A]
    key2 = b"80306f4370b39fd5630ad0529f77adb6"
    arr1 = [0 for _ in range(len(input))]
    for i in range(len(input)):
        r0 = int(input[i])
        r2 = arr[i & 0xf]
        r4 = int(key2[i & 7])
        r0 = r2 ^ r0
        r0 = r0 ^ r4
        r0 = r0 + r2
        r2 = r2 ^ r0
        r1 = int(key2[i & 7])
        r2 = r2 ^ r1
        arr1[i] = r2 & 0xff
    return bytes(arr1)

sign算法全流程还原

好了,到现在所有的关键函数已经被我们分析出来了,想还原出来所有的sign加密流程已经成了一个时间问题的体力劳动了,本来想放个半成品给大家参考下,经评论区老哥友情提醒,分析流程已经足够了,这部分就先和谐掉吧...

 

以及文章目的是学习交流的,请勿不正确使用,用于违法行为。

后记

最近经常会在想自己大部分时候只会用frida去hook十分像一个脚本小子(逃,迫切需要提升汇编分析水平,分析到这里对我自己来说进步不小,但是要学习的东西还很多,如果有什么希望的话,就是希望能早一点摆脱对IDA f5的依赖,可以手撕汇编:)

 

最后,如果你有学到有用的东西,请不要忘记点赞,不枉我写了这么多。


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2021-3-14 11:16 被0x指纹编辑 ,原因:
上传的附件:
收藏
点赞46
打赏
分享
打赏 + 40.00雪花
打赏次数 4 雪花 + 40.00
 
赞赏  wx_小毛球   +10.00 2021/03/13 精品文章~大佬请回复一下我,谢谢。qq911657507
赞赏  Gwyn9   +10.00 2021/03/11 精品文章~
赞赏  _air   +10.00 2021/03/10 感谢分享~
赞赏  又见飞刀z   +10.00 2021/03/10 感谢分享~
最新回复 (52)
雪    币: 493
活跃值: (860)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
_air 2021-3-9 19:36
2
0
前排学习
雪    币: 211
活跃值: (511)
能力值: ( LV9,RANK:172 )
在线值:
发帖
回帖
粉丝
vmtest 2021-3-9 20:52
3
0

以前完整还原过某东算法, 上面的sv格式是 1XX (X在[0, 2]随机取值), 不同的值最后一个快padding&算法不一样,分析了其中2种。

最后于 2021-3-10 10:12 被vmtest编辑 ,原因:
雪    币: 4257
活跃值: (1886)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
cqzhou 2021-3-10 09:24
4
0
插眼 
雪    币: 295
活跃值: (835)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
darbra 2021-3-10 11:11
5
0

指纹大佬优秀 还以为某东那个某是“精”

最后于 2021-3-10 11:12 被darbra编辑 ,原因:
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-10 11:24
6
0
vmtest 以前完整还原过某东算法,&nbsp;上面的sv格式是 1XX (X在[0, 2]随机取值), 不同的值最后一个快padding&amp;算法不一样,分析了其中2种。
不知道分析的是不是一个版本,不同的sv会选择三种加密流程中的一种,最后的几个字节处理分析起来确实比较头疼
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-10 11:24
7
0
darbra 指纹大佬优秀&nbsp;还以为某东那个某是“精”
看完文章就知道是哪个某了
雪    币: 5348
活跃值: (5349)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
GitRoy 3 2021-3-10 16:26
8
0
前排给指纹大佬点烟!
雪    币: 17842
活跃值: (59828)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2021-3-10 16:41
9
0
感谢分享!赞!
雪    币: 423
活跃值: (1978)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
fenfei331 2021-3-10 18:24
10
1
感谢分享,必须赞一个
雪    币: 128
活跃值: (111)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Gwyn9 2021-3-11 22:44
11
0
感谢分享,学习学习
雪    币: 6517
活跃值: (8420)
能力值: ( LV17,RANK:787 )
在线值:
发帖
回帖
粉丝
无名侠 12 2021-3-11 23:36
12
0
太肝了,我什么时候才能这么有耐心
雪    币: 6569
活跃值: (3823)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
LowRebSwrd 4 2021-3-12 10:29
13
0
感谢分享,强悍! 
雪    币: 224
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
感谢你曾来过 2021-3-12 10:59
14
0
我想知道这一套流程分析下来花了多久时间
雪    币: 530
活跃值: (1116)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
青丝梦 2021-3-12 12:08
15
0
色流大佬 转战电商了?
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-12 12:14
16
0
fenfei331 感谢分享,必须赞一个[em_28]
把sign玩出花的老哥 你出现了
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-12 12:15
17
0
无名侠 太肝了,我什么时候才能这么有耐心
大师傅就不来要笑我了
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-12 12:19
18
0
LowRebSwrd 感谢分享,强悍!
感谢斑竹,斑竹辛苦了
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-12 12:20
19
0
感谢你曾来过 我想知道这一套流程分析下来花了多久时间
如果能看出来,两三天就够了,我是开始没看出来,分析了很久,后面才看出来
雪    币: 3494
活跃值: (17944)
能力值: ( LV12,RANK:277 )
在线值:
发帖
回帖
粉丝
0x指纹 5 2021-3-12 12:21
20
0
青丝梦 色流大佬 转战电商了?
转战汇编
雪    币: 986
活跃值: (6012)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
Ssssone 2 2021-3-12 16:22
21
0
膜!
雪    币: 3124
活跃值: (573)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小hanger 2021-3-12 18:45
22
0
插眼
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_小毛球 2021-3-13 23:25
23
0
大佬为什么我用你的代码post请求总是signature verification failed,但是get请求却成功了,可不可以给我解答一下?qq911657507
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_小毛球 2021-3-13 23:33
24
0
大佬急着等你回复呢
雪    币: 3862
活跃值: (1139)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
braintrust 2021-3-13 23:43
25
1

不错的文章。。跟后面重现了下流程,完美成功。

最后于 2021-3-14 00:25 被braintrust编辑 ,原因:
游客
登录 | 注册 方可回帖
返回