pwnable.kr-login

1.常规checksec一下,开了canary和NX,然后IDA打开分析漏洞。发现auth函数中可能存在栈溢出:

1
2
3
4
5
#注释头

int v4; // [esp+20h] [ebp-8h]
------------------------------------
memcpy(&v4, &input, a1);

如果a1大于8h,而我们可以控制input,那么就可以造成栈溢出。再往上翻一下,发现就是将我们的输入通过一系列操作给到input,然后a1是input的长度。

实际情况是将我们的输入给s,进行Base64解码,然后给v4,长度给v6。v4又给input,v6传值到达auth函数赋值给a1。这里input是全局变量,所以auth函数中的input中的内容其实就是我们输入经过base64解码的内容。

1
2
3
4
5
6
7
8
9
#注释头

_isoc99_scanf("%30s", &s);
--------------------------------------------------
v6 = Base64Decode((int)&s, &v4);
--------------------------------------------------
memcpy(&input, v4, v6);
------------------------------------------------------
auth(v6) == 1

▲题外话:最开始Base64搞不懂哪个是输入,哪个是输出,直接经过调试就可以判断。况且最开始的v4是0,总不能程序永远都将0进行base64解码然给到我们的输入地址中吧。但是调试的时候发现,每次输入相同的值,但是解码后得到的v4的值却是不一样的。这就纳闷了,为什么一样的输入四个AAAA得到的解码值不一样呢,难道程序还有个随机变量不成。之后再仔细调试发现这个base64decode有点不一样,虽然传入的两个参数都是地址,但是第一个参数的操作却是从该地址直接取值进行解码,然后对于第二个参数的操作却并不是将解码结果给到第二个参数,而是再开辟一块堆内存,之后将该堆内存的地址给到第二个参数。所以每次解码后第二个参数,也就是栈上的一个值,总是不一样,因为这里保存的是一个随机生成的堆地址,而不是解码后的值。同样之后观察main函数中的memcpy也可以发现:memcpy(&input, v4, v6);而memcpy的原型是:

1
2
3
#注释头

void *memcpy(void *dest, const void *src, size_t n)

前两个参数类型都应该是地址才对,而这里却直接将v4的值给传进去,那不就说明v4的值是一个地址吗。然后再跳转到汇编代码分析一波:

1
2
3
4
5
6
7
8
9
#注释头

.text:080493B3 call _memset
.text:080493B8 mov dword ptr [esp+18h], 0
.text:080493C0 lea eax, [esp+18h]
.text:080493C4 mov [esp+4], eax
.text:080493C8 lea eax, [esp+1Eh]
.text:080493CC mov [esp], eax
.text:080493CF call Base64Decode

同样Base64Decode函数的两个参数也都是地址,这里是直接取栈地址给到eax,然后再将eax的值给相应esp指向的栈内存。所以可以看到Base64Decode取值应该是从栈上取两个地址才对,分别位于main函数栈的是esp+4和esp。所以如果这里有个格式化字符串那么就完全可以泄露处出栈地址,之后就完全可控,可惜没有。还是回到正轨分析吧。

2.所以经过前面分析,程序要求我们输入一个base64编码过的字符串,随后会进行解码并且复制到位于bss段的全局变量input中,最后使用auth函数进行验证,通过后进入带有后门的correct()打开shell。并且由于长度有限制:所以我们的输入base64解码后最多只能有12个字节。

1
2
3
4
5
6
#注释头

if ( v6 > 12 )
{
puts("Wrong Length");
}

3.汇总一下,程序存在栈溢出,但只能溢出4个字节,也就是一个地址,也就是最多只能覆盖到ebp,然后存在后门函数。由于没办法直接覆盖返回地址,所以这里就在ebp上做文章,使用栈劫持技术。之前的栈劫持可以用rop,但是这里没办法,因为无法进行返回地址覆盖。但是还有一个地方,就是我们的输入最后会被解码赋值给input,这个input是个全局变量,不受到ASLR影响,而又可以控制12个字节,如果可以把栈挪移到这个地方,那么就是可控了。

栈模型如下:

imgimg

可控栈如下:

img

4.总体思路应该是:

①劫持auth函数的栈底到input_addr,那么auth函数在退出到main函数时,main函数栈的栈底就不会回到之前的main函数栈栈底,而是会挪移到我们input_addr,也就是payload3的值。

②开始执行auth函数中的退出操作,到leave时,执行操作leave的第一步汇编操作mov esp ebp,将栈顶指向ebp指向的内容,此时ebp已经被修改成了payload3,而payload3会被赋值成Input_addr,也就是esp会指向input_addr。

②执行leave第二步汇编指令pop ebp,将当前栈顶的值赋值给ebp,也就是ebp的值会变成payload1,(这里的payload1没什么作用,可以随便填)之后esp由于pop,esp+0x4,会往栈底移动一个地址,移动到指向我们输入的payload2处。

img

④之后retn执行,实际指令为pop eip,也就是将当前栈顶数据给eip,也就是eip被赋值为我们payload中的payload2。

img

⑤最后执行retn的第二条实际指令:jmp eip,此时eip就已经是payload2的值,所以将该payload2设置为correct函数地址或者是system(“/bin/sh”);就可以getshell。

总的来说,就是利用leave和retn两个操作来劫持eip的值,使其直接指向后门函数,一步getshell。

5.创建payload,组成应该是:

1
2
3
4
5
#注释头

#首先确定地址:
correct_addr = 0x08049278
input_addr = 0x0811eb40

之后确定payload的组成:

payload = padding + eip + input_addr。

1
2
3
4
5
6
#注释头

payload = "aaaa" #padding
payload += p32(0x08049284)
#system("/bin/sh")地址,整个payload被复制到bss上,栈劫持后retn时栈顶在这里
payload += p32(0x0811eb40) #新的eip地址

最后得注意发送的是base64编码之后的payload。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157