请注意,本文编写于 1546 天前,最后修改于 1513 天前,其中某些信息可能已经过时。
哦吼!完蛋,之前写的没保存,现在就要从新写。之前的前言咋写的来着?好像是说我最近太懒了,咕咕的厉害,导致复现越堆越多。现在要开始行动了。先是Nu1L的复现,然后在复现一下CUMT2020的校赛。有一说一,身为一个RE手,进行了4天比赛,连一个flag的影子都没有见到,这边建议当场退役哦!之后还有一道题目让我很难受,就是“全国工业互联网安全技术技能大赛”的re的第一题,5G的加密方式,感觉已经要出来了,但是最后的随机数没有办法绕过,难受!等一波WP,之后在复现一下吧。
经典签到题不会做,本题主要考点:多线程,花指令。
这里主要做一下本题目的复现,然后在稍微说一下一种保护:双进程保护(虽然和这道题没啥关系)
首先我们先查找字符串,交叉引用一下,发现没有地方引用。我们基本上就可以断定程序应该是有花代码,导致IDA分析错误,部分代码没有被反汇编。我们观察一下主函数:
我们发现了段花代码,一种是call结合ret的,一种是jmp加垃圾数据的。
由于是签到题,花代码的总量不是很多,我们手动nop一下就好了。结束之后我们就可以F5了。
观察程序,我们发现了一处patch。使用我们输入的头几个字符进行了几次异或,改变了10个字节的数据。搁以前我一定是先算出来,再在程序中进行相应的更改。但是IDApython还是要学一下的,这里来一个通用的patch脚本:
a='n1ctf'
for i in range(10):
PatchByte(0x400a69+i,Byte(0x400a69+i)^ord(a[i%5]))
在我们完成patch后,我们来到了加密函数。
程序将我们的输入和一个不知道是什么玩意的东西进行了异或,然后对比输出,这里就要说到fock()函数了。这里使用了fock()函数创建了一个子线程,我们跟过去看看子线程。
这里我们很清楚的看见了一个execve(),他调用了一个系统函数,打印输出了一串字符,然后使用PTRACE_PEEKDATA进行了14位的数据提取。根据逻辑写出脚本,提取flag。
a = [53, 45, 17, 26, 73, 125, 17, 20, 43, 59, 62, 61, 60, 95]
s = ''
for i in range(14):
s += chr((ord('Linux version '[i]) + 2) ^ a[i])
print('n1ctf' + s)
题目结束,说点题外话。最近在准备一道签到题(虽然被周学长血虐之后,我已经把他丢尽了回收站),我需要使用一个多线程,来完成我的相应操作,但是多线程的程序真的乱七八糟,不好控制,在我查资料的时候偶然的发现了一种保护。
双进程保护,他算是一个反动调的保护,具体实现原理如下:程序会存在两个进程,他们之间的关系是调试器与被调试器的关系,真正的程序通常位于子进程中,不过子进程已经被父进程调试,所以导致无法被Debug加载或者附加。父进程会处理子进程返回的异常,动态patch子进程等,增加分析难度。我们要想调试子进程,我们必须断开子进程与父进程之间的联系,但是没有父进程处理子进程的异常,会导致子进程无法正确运行。
这里是有一道例题的,我还没有研究明白,研究明白后会补在这里。
这是一道密码学的题目,也是我比赛的时候研究时间最长的一道题目。看完题解之后,整个人都不好了,有些时候做不出来题目,并不是自己的能力不行而是自己的工具有些许的拉跨。
本题知识点:计算机视觉,二维码,python,随机数预估脚本randcrack。
对,你们没有听错,随机数预测脚本,可以通过输出的随机数,基本精确的预测输入的seed。众所周知,计算机里面是没有随机数的,他都是通过特定的种子生成一串类似于随机数的数子,当种子相同的时候,随机生成的数值也是相同的(操作系统之间可能还有一点点差别)。举个例子,在MC中就有几个十分著名的种子文件,当你使用这个种子文件生成世界时,所有的村庄,地牢的坐标一定是与先前世界是相同的。这个脚本就是通过输出,对输入的种子进行了预测,据说在头624个比特内预测的值接近100%,前一千可以接近95%。我们来看一下这道题目。
首先题目给出了python的加密源码:
#!/usr/bin/python3
import qrcode # https://github.com/lincolnloop/python-qrcode
import random
import os
from PIL import Image
from flag import FLAG
def vss22_gen(img):
m, n = img.size
share1, share2 = Image.new("L", (2*m, 2*n)), Image.new("L", (2*m, 2*n))
image_data = img.getdata()
flipped_coins = [int(bit) for bit in bin(random.getrandbits(m*n))[2:].zfill(m*n)]
for idx, pixel in enumerate(image_data):
i, j = idx//n, idx % n
color0 = 0 if flipped_coins[idx] else 255
color1 = 255 if flipped_coins[idx] else 0
if pixel:
share1.putpixel((2*j, 2*i), color0)
share1.putpixel((2*j, 2*i+1), color0)
share1.putpixel((2*j+1, 2*i), color1)
share1.putpixel((2*j+1, 2*i+1), color1)
share2.putpixel((2*j, 2*i), color0)
share2.putpixel((2*j, 2*i+1), color0)
share2.putpixel((2*j+1, 2*i), color1)
share2.putpixel((2*j+1, 2*i+1), color1)
else:
share1.putpixel((2*j, 2*i), color0)
share1.putpixel((2*j, 2*i+1), color0)
share1.putpixel((2*j+1, 2*i), color1)
share1.putpixel((2*j+1, 2*i+1), color1)
share2.putpixel((2*j, 2*i), color1)
share2.putpixel((2*j, 2*i+1), color1)
share2.putpixel((2*j+1, 2*i), color0)
share2.putpixel((2*j+1, 2*i+1), color0)
share1.save('share1.png')
share2.save('share2.png')
def vss22_superposition():
share1 = Image.open('share1.png')
share2 = Image.open('share2.png')
res = Image.new("L", share1.size, 255)
share1_data = share1.getdata()
share2_data = share2.getdata()
res.putdata([p1 & p2 for p1, p2 in zip(share1_data, share2_data)])
res.save('result.png')
def main():
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=12,
border=4,
)
qr.add_data(FLAG)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
vss22_gen(img._img)
img.save('res.png')
vss22_superposition()
if __name__ == '__main__':
main()
具体逻辑就是加载了一串flag,使用qrcode将我们的flag变成了一张二维码。使用PIL进行图片处理,处理过程大致如下:
提取flag图片数据,生成一个2*2的像素。逻辑如下(不知道如何描述,程序不难理解):
if pixel:
share1.putpixel((2*j, 2*i), color0)
share1.putpixel((2*j, 2*i+1), color0)
share1.putpixel((2*j+1, 2*i), color1)
share1.putpixel((2*j+1, 2*i+1), color1)
share2.putpixel((2*j, 2*i), color0)
share2.putpixel((2*j, 2*i+1), color0)
share2.putpixel((2*j+1, 2*i), color1)
share2.putpixel((2*j+1, 2*i+1), color1)
else:
share1.putpixel((2*j, 2*i), color0)
share1.putpixel((2*j, 2*i+1), color0)
share1.putpixel((2*j+1, 2*i), color1)
share1.putpixel((2*j+1, 2*i+1), color1)
share2.putpixel((2*j, 2*i), color1)
share2.putpixel((2*j, 2*i+1), color1)
share2.putpixel((2*j+1, 2*i), color0)
share2.putpixel((2*j+1, 2*i+1), color0)
比赛的时候一直想要绕过rand,但是一直没有成功。题解则是选择通过输出强行"猜"出了rand。下面结合wp记录一下这个库的用法:
看懂代码的小伙伴就会问了,原图你不晓得,rand你也不晓得,你咋生成随机数的前62432个数列呢?看完题解我也才行然大悟,当我们尝试一下他的原版代码后,就会惊奇的发现:woc无论放什么字符串进去,都会存在一个白色的边框!当加密到此处时,rand就会被单独的暴露出来,我们只需要提取此处数据,生成62432个字符即可调用函数库,脚本代码如下(有注释版):
from PIL import Image
from randcrack import RandCrack
import random
share = Image.open('share2.png')
width = share.size[0]//2
res = Image.new('L', (width, width))
print(width)#width=444由于加密使得图片扩容,即一个像素生成2*2=4个像素
bits = ''#由于share2的构成的逻辑是当原图片是255像素
for idx in range(width*width-624*32, width*width):#取倒数624*32个数据
i,j = idx//width, idx % width#i,j分别代表行和列
print(i,end=" ")
print(j)
if share.getpixel((2*j, 2*i)) == 255:#取最小的一个像素点代表此处的方阵
bits += '0'
else:
bits += '1'#bits即为初始化的字符串
rc = RandCrack()
for i in range(len(bits), 0, -32):
rc.submit(int(bits[i-32:i], 2))#每次将32位放入初始化的rc一共放入624组,达到初始化最少数据
flipped_coins = [int(bit) for bit in bin(rc.predict_getrandbits(width*width-624*32))[2:].zfill(width*width-624*32)] + list(map(int, bits))#通过初始化数据与脚本猜测剩余随机数
data = []
for idx in range(width*width):
i, j = idx//width, idx % width
if share.getpixel((2*j, 2*i)) == 255:
data.append(0 if flipped_coins[idx] else 255)
else:
data.append(255 if flipped_coins[idx] else 0)
res.putdata(data)
res.save('ans.png')
主要解决两个问题,也是本库的关键用法:
rc.submit(int(bits[i-32:i], 2))
原型:rc.submit(int)
此处是初始化rc,每次放入一个int即可。由于我们使用random.getrandbits(444 * 444)生成了一个总长度为444 * 444的数字,一共生成了197136个位,大约是6161个32位,6161<10000所以脚本生成的可信度在95%以上。总结用法:放入连续的624个32位即可推测种子。
rc.predict_getrandbits(width*width-624*32)
原型:rc.predict_getrandbits(A,B)此处A,B为范围
此处的则是直接给出了长度(可能吧,这里有点没搞清楚),直接生成看了随机数列。
由于二维码是允许错误率在10%左右,所以有些点猜错了也无妨,最后还原图片即可。
randcrack源码
全部评论 (共 4 条评论)