0x00.绪论
mini-LCTF,前身是makerCTF,是西电校内享誉盛名(?)的CTF,作为菜鸡CTFer也尝试着参加了一手
因为咱主攻PWN的原因,所以这一篇应该只有PWN(
(不过主要还是因为我太菜了XD
注:所有题目在archive.lctf.online上都有部署
0x01.Sign in
Starting Point
点进页面就可以直接获得flag了
0x02.PWN
hello - ret2shellcode
证明了我真的是菜鸡的一道pwn题,搞了半天才明白XD
做题环境Manjaro-KDE
首先使用checksec
指令查看保护,可以发现保护基本都是关的,只有Partial RELRO
,那么基本上是可以为所欲为了wwww
拖进IDA
进行分析
可以发现在vul函数
存在明显的栈溢出
而程序的主函数中调用了vul
函数
那么程序漏洞很明显了:
- 使用
fgets
读入最大为72个字节的字符串,但是只分配给了48字节的空间,存在栈溢出
又有一个可疑的bd
函数,那么第一时间想到**ret2text
**——构造payload跳转到bd
但是很明显,bd
函数基本是是空的(悲)
然后我就在这里卡了半天,证明我真的菜XD,感谢cor1e大佬的耐心解答
那么我们该如何利用这个bd
呢?
可以看到在bd
中存在gadgetsjmp rsp
,那么其实我们可以利用这个指令跳转回栈上,执行我们放在栈上的shellcode
也就是说这其实是一道ret2shellcode的题
我们只需要利用这个gadgets将控制流跳转回栈上并执行我们放置在栈上的代码即可getshell
那么payload就很容易构造出来了:
注:第一次盲打payload居然错了,我还是太菜了Or2
1 | from pwn import * |
高阶解法:栈迁移
费力解法
假如说这道题并不存在供我们进行跳转的bd函数
,给我们的溢出长度就只有72-48=24个字节,但是仅仅是asm(sellcraft.sh())
就已经需要48个字节,那么我们该如何通过仅仅24字节的溢出完成pwn呢
答案是使用**栈迁移
**技术
我们可以将栈劫持到bss段
,再在上面放上我们的shellcode
故构造exp如下:
1 | from pwn import * |
如何将一个栈劫持到别的地方?栈迁移的运行原理究竟是?
做了那么多的pwn题,我们都知道在构造栈溢出的rop链时不仅要算上栈原本所分配的空间的大小,还要算上
ebp/rbp
寄存器所占用的4/8个字节,之后才到函数的返回地址,这是因为在一个函数启动时,会先将ebp/rbp的值压入栈中,之后将指向栈顶的esp/rsp寄存器的值赋给ebp/rbp寄存器,随后通过ebp/rbp寄存器来实现对栈的访问同样的,有压栈同样就会有出栈来恢复ebp/rbp与esp/rsp的值,我们都知道在函数运行到末尾时,会执行
leave
,retn
两条指令其中
leave
指令便是用来恢复栈帧的指令,它等价于如下指令:
1
2
3 ;ebp & esp in 32-bit
mov rsp, rbp
pop rbp由于对栈的访问都是通过ebp/rbp进行的,那么我们不难想到,只要我们在溢出时覆写掉栈内储存的原ebp/rbp的值,在retn指令返回后程序所判定的栈基址其实就变成了我们所覆写上的那个地址,之后程序在调用栈上的变量的时候其实都是基于我们所覆写上的新的栈基址进行操作的了,相当于把栈劫持到另一个地方,所以又叫栈劫持
注:赛期因为各种ddl导致只解出了这一道题XD 后面的题解是在赛后写的 部分参照了Lunatic和eqqie师傅的题解
ezsc - Alphanumeric Shellcode
首先是惯例的checksec
四舍五入保护全关,我们有极大的操作空间
程序分析
拖入IDA进行分析
v7是一个函数指针,并被分配到了0x1000
大小的内存空间
程序会从输入流逐字节读入最大4096个字节写入到v7所指向的空间上,且限制输入仅能为字母或数字否则终止输入
输入结束后尝试执行函数指针v7
那么我们直接输入一段shellcode即可getshell
但是限制了输入只能是数字和字母,常见的shellcode都包含一些其他字符
于是只好百度XD
得知一种叫做Alphanumeric Shellcode的东西www
Alphanumeric Shelllcode
具体参见:https://www.freebuf.com/articles/system/232280.html
首先从GitHub上随便找一个轮子
1 | $ git clone https://github.com/TaQini/alpha3.git |
然后编写生成我们的shellcode的脚本
1 | # sc.py |
输出重定向至文本
1 | $ python sc.py > sc |
使用轮子生成alphanumeric shellcode
1 | $ python ./ALPHA3.py x64 ascii mixedcase rax --input="sc" |
1 | Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a071N00 |
连接服务端,发送我们的alphanumeric shellcode,成功getshell
![MVEZ9F_H1__I_
_D`AQI_8.png](https://i.loli.net/2020/09/11/XolvfAEcPxyF6q5.png)
easycpp - UAF
首先是惯例的checksec
我们可以看到,和前面几题不同的是这个程序是一个32位的程序
说实话我也不知道为什么画风突变变成32位XD
程序分析
拖入IDA进行分析
用到了一个名为B
的类,我们先看看这个类都有些啥
IDA中类B有一个构造函数B()
和一个print()
函数
类B的构造函数如下:
类B的构造函数首先调用了类A的构造函数,然后将变量_vptr_A设置为一个函数指针
我们不难看到在0x80489E4位置上我们还需要再跳转一次到0x80488F2的位置,这个位置上有一个B::print()
函数,故可知这是一个二级函数指针
我们再来看看类A的构造函数,如下:
类A的构造函数也是将变量_vptr_A
设置为一个函数指针
位于off_80489F0上的函数为A::print()
接下来是main函数的简单分析:
v3、v4都是一个类型为类 B
的指针
首先setvbuf()
函数将程序设置为无缓冲输入
之后创建了一个类B的实例并将地址给到指针v3和v4
之后将该实例内的变量_vptr_A
的值设为0
之后使用delete释放掉之前分配给v3、v4的内存
之后从标准输入流读入1024个字节到buf(或许有操作空间?)
之后调用strbuf()
函数重新分配一段内存空间并将buf的值拷贝一份(当然最后并没有指针接收这一块内存,那么它会成为野内存吗?)
最后重新调用v4的函数指针变量所指向的函数
那么我们在这里就可以发现一个漏洞:
Use After Free
v3、v4所指向的内存空间被释放后v3、v4并没有被设置为NULL,在运行到strdup()函数时这一块内存空间又被重新分配给strdup()函数,而之后又通过v4再次对这一块内存空间进行调用,很明显存在UAF(Use After Free)漏洞
同时我们可以发现存在一个backdoor()
函数直接调用了system("/bin/sh")
,可以直接getshell
故我们只需要覆写掉函数指针所指的函数为backdoor()函数即可getshell
构造二级指针的结构应为
buf | buf+4 |
---|---|
右边那一块的地址 -> | 指向backdoor函数的地址 |
buf的地址如下:
故得exp如下
1 | from pwn import * |
向服务器发送payload,成功getsgell
可能是因为一些奇奇怪怪的原因,
cat flag
出来的flag不对
noleak - Integer Overflow + Heap Overflow + Fastbin Attack
首先是惯例的checksec
,发现开了NX保护和Canary
先简单运行看一下,有新建、编辑、删除的简单计划程序,这种十分规范的模式让人感觉的出来应该是一道堆题(说好的只出栈呢
拖入IDA进行分析:(以下部分函数及变量经重命名
该程序有着分配、修改、释放堆块的功能,一看就是一道很难的堆题
我们发现在用以读入输入的sub_400896()
函数中存在整数溢出漏洞:当我们输入的长度为0时,由于其会先减一再转换成unsigned int类型,发生整数溢出,导致我们可以输入较长的内容到堆块上
同时我们发现存在白给的后门函数`:
由于got表开启了读写权限,故考虑fastbin attack:伪造fake chunk改写got表中free的地址为后门函数地址,之后随意释放一个堆块即可getshell
由于堆块指针表位于bss段,故我们考虑将其中一个指针的地址改写为free@got后直接edit即可
构造exp如下:
1 | from pwn import * |
运行脚本即可getshell
heap_master - tcache double free(use after free) + orw
遭到了sad师傅的暴击Or2
(某种程度上算是minil的压轴题…?
虽然说当时我连看都没看(←摸鱼划水人士,小朋友别学他
惯例的checksec
,发现除了地址随机化以外都开上了
拖入IDA进行分析
程序本身有着分配堆块、释放堆块、输出堆块内容的功能
我们发现在delete()
函数中free()
后并没有将相应堆块指针置0,存在UAF
题目提示libc可能是2.23也可能是2.27,尝试直接进行double free,发现程序没有崩溃,故可知是没有double free检查的2.27的tcache
libc2.29后tcache加入double free检查
在main()
函数开头的init()
函数中调用了prctl()
函数,限制了我们不能够getshell
首先我们想到,我们可以先填满tcache,之后分配一个unsorted bin范围的chunk,通过打印该chunk的内容获取main_arena + 0x60的地址,进而获得libc的地址
libc2.27之前unsorted bin里的chunk的fd/bk似乎是指向main_arena+0x58的,现在变成main_arena+0x60了,等有时间得去看看libc2.27的源码了…
虽然我们不能够getshell,但是依然可以通过double free进行任意地址写,毕竟CTF题目的要求是得到flag,不一定要得到shell,故考虑通过environ变量泄漏出栈地址后在栈上构造rop链进行orw读出flag
tips: __environ是一个保存了栈上变量地址的系统变量
通过动态调试我们容易得到___environ
与new()
中的返回地址间距离为0x220,将rop链写到这个返回地址上即可接收到flag
构造payload如下:
1 | from pwn import * |
运行脚本即得flag
【感想】
不愧是能让sad师傅困扰一天的题目,也让我困扰了一天…同时也确实如同🐧师傅所说的,“这是十分值得复现的一道题”
比较精髓的一个点应该就是对于环境变量__environ的利用来在栈上构造rop链了
【踩坑*1】
一开始我在自己的manjaro(libc2.32)上调,计算出来的__environ到返回地址的距离是0x230…,然后一直打不通…冥思苦想半天只好选择装个ubuntu18(libc2.27)试一下,发现距离居然是0x220…然后就通了…
【未解决问题】
在第一次double free的时候的chunk size必须为0x60(0x71),第二次double free时的chunk size必须大于等于0xb0(0xc1),究竟为何我暂且蒙在古里…(希望有师傅能通过左侧的邮箱告诉我www)
jail - chroot越狱
惯例的checksec
,开了NX和canary
拖入IDA进行分析
不难看出该程序会fork()出子进程,子进程读入我们的输入并调用execveat
系统调用来尝试执行我们的程序
但是flag一般位于根目录下,而我们在子进程中的工作目录被转移到了/tmp/jail/下,无法直接读取根目录下的flag
注意到在开头的init()函数中会打开/tmp
和/
,文件描述符分别为3和4(0,1,2为stdin,stdout,stderr)
故直接考虑使用fchdir()
函数便可切换到根目录下读取flag
构造exp如下
C部分
1 |
|
Python部分
1 | from pwn import * |
运行即得flag