【wp】2020ChaMd5圣诞题


圣诞莫得安排于是跑去做了一下ChaMd5的圣诞题,不得不说让我这个逆向手很有做misc的成就感哈哈哈(误)。既然活动都结束了就来放一下wp吧,顺便整理一下批量处理文件的方法(pyinstaller批量解包+批量补文件头转格式+python和命令行的批量自动交互)。

题目指路:快打开你的2020圣诞礼物! - ChaMd5安全团队

So...begin!

下载题目压缩包,解压可以看到有59个exe。

(好家伙,这是考批量处理文件吧= =不得不说图标还挺好看的,费心了

不过看到自定义图标,逆向人第一个就想到了这些是用pyinstaller打包的exe啦。虽然也有可能是MFC,不过这里明显不是。

随便拖一个exe进ida,查看字符串看到有很多“py”字样,很明显就是pyinstaller这条路。

所以果断走流程解包(单个文件处理可以看->RE套路 - 关于pyinstaller打包文件的复原 | c10udlnk_Log,这里不细讲了,重点在多文件的批量处理)。

bat批量应用python脚本

这里写bat脚本

@echo off
for %%i in (*) do (
    echo %%i
    python2 pyinstxtractor.py %%i
)

(*)指当前目录,%%i就像平时用的for(int i=0;......)的那个i一样是个变量,echo是为了看处理到哪个文件了。

处理结束后得到59个解压文件包

python批量提取

然后继续写脚本(这次用python,因为bat实在是不熟)把文件夹里的无后缀文件提取出来,复制到manyPYC文件夹下,方便后续处理。

import os
import os.path
import re
import shutil

src='./'
dst='../manyPYC/'
allFile=os.listdir(src)
pattern=r'^[0-9]+$'
for fileName in allFile:
    if os.path.isdir(fileName):
        for file in os.listdir(fileName):
            if re.match(pattern,file)!=None:
                num=file
                shutil.copy(src+fileName+'/'+file,dst+num)
            if file=='struct':
                shutil.copy(src+fileName+'/'+file,dst+num+'_struct')

一般是两个有用的,一个是struct,另一个文件名在entry point里会提到,一般是跟exe同名的,不过这里不一样(是数字。

另外这里线性扫描一定是先扫到以数字命名的文件再到同目录下的struct文件,所以不用担心num未定义的问题。

python批量补头修复pyc

根据uncompyle的反编译规则,批量添加magic number,并输出.pyc。

import os

src='.'
allFile=os.listdir(src)
for file in allFile:
    if 'struct' in file:
        with open(file,'rb') as f:
            magic=f.read(8)
        with open(file[:-7],'rb') as f:
            context=f.read()
        with open(file[:-7]+'.pyc','wb') as f:
            f.write(magic)
            f.write(context)

提取struct的前八个字节补到同名文件里去,并改名为.pyc。

注意:这里提前用010Editor看了一下以数字命名的文件和struct是差八个字节,有点特殊。

bat批量应用指令

然后写bat脚本执行:

@echo off
for %%i in (*) do (
    echo %%i
    uncompyle6 %%i > %%i.py
)

得到59个python文件。

这里其实是出来像1.pyc.py这样的文件,有强迫症的朋友(比如我)可以写个脚本把中间的.pyc去掉hhh。

python和命令行的批量自动交互

随便查看一个(比如1.py

import hashlib, base64
from Crypto.Cipher import AES
from Crypto import Random

def decrypt(data, password):
    bs = AES.block_size
    if len(data) <= bs:
        return data
    unpad = lambda s: s[0:-ord(s[(-1)])]
    iv = data[:bs]
    cipher = AES.new(password, AES.MODE_CBC, iv)
    data = unpad(cipher.decrypt(data[bs:]))
    return data

encrypt_data = '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'
md5 = hashlib.md5()
fn = raw_input()
md5.update(fn)
if '4a47a0db6e60853dedfcfdf08a5ca249' == md5.hexdigest():
    print 'Are you robot?'
    print '25 + 3 = ?'
    ans = int(raw_input())
    if int(ans) == 28:
        try:
            md5 = hashlib.md5()
            inp = str(int(ans))
            md5.update(inp)
            password = md5.hexdigest()
            encrypt_data = base64.b64decode(encrypt_data)
            decrypt_data = decrypt(encrypt_data, password)
            f = open(fn, 'wb')
            f.write(decrypt_data)
            f.close()
        except:
            pass

可以看到,逻辑是先输入一个字符串,第一个check是md5值等于一个已给值(通过chamd5的网站查到该md5值为1.png,突然扣题?hhh),第二个check是简单的人机测试,都通过的话就会输出一张1.png的图片。

这个地方的批量处理是我卡得最最最最最最久的一个地方 TvT

因为bat完全是现学,不会写,于是想到用python进行跟命令行的交互。

一开始查到的资料是用subprocess,但是读输出的时候死活读不进去(因为要过人机测试就得把式子读进去),去查subprocess文档没什么头绪,往后查又说是什么输出流阻塞balabala,总之没太看懂(tcl

后来好不容易看到有一篇提到说用pexpect。然后查了一些文档,诶,居然交互成功了!暴风哭泣!

理论上说前面用bat写的地方都可以改成用pexpect写,不过这不是之前还不知道嘛(。)

以及,多亏这里搞通了,后面xctf刚好遇到一道逆向是可以用这种命令行交互爆破的(不是pwntools能连上的那种特殊架构文件),直接拿脚本改改就能用,省了几个小时的搜索时间hhh。

于是开始写脚本进行多次交互啦,逻辑很简单:

import pexpect

for i in range(1,60):
    r=pexpect.spawn("python2",[str(i)+".py"])
    r.sendline(str(i)+".png")
    r.readline()
    r.readline()
    exper=r.readline()[:-6]
    r.sendline(str(eval(exper)))
    r.wait()

得到59个png。

python连接png

当时做的时候因为懒得搞图像识别,所以直接手动记录这些图片上的字符。后来受到官方上发的wp的启发,感觉可以把这些png用PIL库连起来,就不用一张张点着看了。

这里我是把每个图片最左边6px提取出来横向拼接成一张图片,方便照着打hhh,调整了一下让字符大概居中排列(因为原图背景是透明的,所以这里提取出来就成黑的了= =。

png连接脚本:

from PIL import Image

cnt=0
base=6
colorData=[[] for _ in range(base*59)]
for x in range(1,60):
    img=Image.open(str(x)+'.png')
    rgb_img=img.convert('RGB')
    width,height=img.size
    for i in range(base):
        col=i+cnt*base
        for j in range(height):
            colorData[col].append(rgb_img.getpixel((i,j)))
    cnt+=1

widthALL=len(colorData)
heightALL=max([len(arr) for arr in colorData])
imgALL=Image.new("RGB",(widthALL,heightALL))
for i in range(widthALL):
    h=len(colorData[i])
    for j in range(h):
        imgALL.putpixel([i,(heightALL-h)//2+j],colorData[i][j])
imgALL.save("res.png")

通过观察可以发现,有用的字符都为红/绿/蓝的纯色,于是在脚本上做一些改进,把符合条件的像素点置黑,其余置白,更方便观察。

最后改进的脚本:

from PIL import Image

cnt=0
base=6
RED=(255,0,0)
GREEN=(0,255,0)
BLUE=(0,0,255)
BLACK=(0,0,0)
WHITE=(255,255,255)
colorData=[[] for _ in range(base*59)]
for x in range(1,60):
    img=Image.open(str(x)+'.png')
    rgb_img=img.convert('RGB')
    width,height=img.size
    for i in range(base):
        col=i+cnt*base
        for j in range(height):
            if rgb_img.getpixel((i,j)) in [RED,GREEN,BLUE]:
                colorData[col].append(BLACK)
            else:
                colorData[col].append(WHITE)
    cnt+=1

widthALL=len(colorData)
heightALL=max([len(arr) for arr in colorData])
imgALL=Image.new("RGB",(widthALL,heightALL))
for i in range(widthALL):
    h=len(colorData[i])
    for j in range(h):
        imgALL.putpixel([i,(heightALL-h)//2+j],colorData[i][j])
imgALL.save("res.png")

轻松记录下59个字符:https://www.chamd5.org/e1f10acc44f611ebb3780242ac130002.png

打开有

get flag

flag{ChaMd5_Ch33r_Up}

没想到这么晚交还水了个第三哈哈哈,溜了溜了(