menu 牢记自己是菜
2020空指针Nu1l 复现
2337 浏览 | 2020-10-26 | 阅读时间: 约 5 分钟 | 分类: 乱七八糟的小比赛 | 标签:
请注意,本文编写于 1249 天前,最后修改于 1216 天前,其中某些信息可能已经过时。

0x1 前言

哦吼!完蛋,之前写的没保存,现在就要从新写。之前的前言咋写的来着?好像是说我最近太懒了,咕咕的厉害,导致复现越堆越多。现在要开始行动了。先是Nu1L的复现,然后在复现一下CUMT2020的校赛。有一说一,身为一个RE手,进行了4天比赛,连一个flag的影子都没有见到,这边建议当场退役哦!之后还有一道题目让我很难受,就是“全国工业互联网安全技术技能大赛”的re的第一题,5G的加密方式,感觉已经要出来了,但是最后的随机数没有办法绕过,难受!等一波WP,之后在复现一下吧。


0x2 olfo

经典签到题不会做,本题主要考点:多线程,花指令。
这里主要做一下本题目的复现,然后在稍微说一下一种保护:双进程保护(虽然和这道题没啥关系)
首先我们先查找字符串,交叉引用一下,发现没有地方引用。我们基本上就可以断定程序应该是有花代码,导致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子进程等,增加分析难度。我们要想调试子进程,我们必须断开子进程与父进程之间的联系,但是没有父进程处理子进程的异常,会导致子进程无法正确运行。
这里是有一道例题的,我还没有研究明白,研究明白后会补在这里。

0x3 Vss

这是一道密码学的题目,也是我比赛的时候研究时间最长的一道题目。看完题解之后,整个人都不好了,有些时候做不出来题目,并不是自己的能力不行而是自己的工具有些许的拉跨。
本题知识点:计算机视觉,二维码,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进行图片处理,处理过程大致如下:

  1. 由于图片为黑白相间的的二维码,所以提取到的像素只有0于255。生成两张中间图片,一张为share1(题目未给出)一张为share2(题目给出)。这两张图片的大小是原图片的两倍。随机生成一串二进制数列。
  2. 对生成的二进制数列进行处理,初始化两个变量分别为color0与color1。color0与color1与随机二进制数列的每一位相关,但始终相反,代表0与255(白色像素与黑色像素)。每次循环重置。
  3. 提取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)
    
  4. 存储两张图片,两张图片相与生成result(题目未给出),生成了一张模糊的二维码。

比赛的时候一直想要绕过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源码

0x4

发表评论

email
web

全部评论 (共 4 条评论)

    2020-10-31 21:43
    过来学习vss (☆ω☆)
      2020-11-02 15:59
      @iyzyi爆破,就狠狠的爆破 ୧(๑•̀⌄•́๑)૭
    atao
    2020-10-30 14:33
    师傅,想问一下re入门的话要从哪些内容看起来呢,汇编,计组,还有别的吗
      2020-11-03 00:03
      @atao推荐《加密与解密》 看看就有方向了