menu Alkaid #二进制初学者 / 网络安全 / 大龄CTF退役选手
GKCTF 2020 Reverse Writeup
305 浏览 | 2020-05-28 | 分类:Reverse,CTF | 标签:

RE

0x1 Check_1n

源项目:https://github.com/404name/winter

在B站上看到一个大佬的C语言项目,他在gihub开源了代码,感觉挺有意思的。偷偷拿来魔改了一下当签到题,拿到题目后,如果是按照常规思路走,那就是先找到开机密码:

IDA打开搜索关键字符串就可以拿到开机密码:HelloWorld

然后,里面有个虚假的flag,base64接出来是Why don't you try the magic brick game,明示让玩打砖块游戏。

进入游戏,没过一会就当场去世以后,得到flag,如下图所示:

当然有些师傅直接看到了base58编码的flag,直接拿去解密速度就更快了。

0x2 Chelly's ldentity

输入 input 拷贝到 instr,并依次进行将 instr 传入三个函数

第一个函数用于限制输入长度为16字符。

第二个函数是加密函数。

首先生成了向量v10,紧接着用一个循环遍历输入,向量中元素小于instr中元素时累加向量的值,之后instr元素与这个值异或

加密函数中向量生成方式如下:

从2至len(此处为128),若满足if判断则加入向量中。其中if判断是一个判断是否为质数的函数,也就是说,向量是生成len之内的质数

简言之,加密函数将输入的每个字符对一个值进行异或,这个值是向量v10中低于该字符的元素之和。

第三个函数是一个check输入经过加密后是否正确的函数。

与一个数组进行判等。

因此,只需要生成一个128内的质数数组,用判等数组进行爆破即可。

#生成质数数组
def is_prime_num(num):
    #判断num是否为质数
    for i in range(2, num):
        if num % i == 0:
            return False
    return True
def create_table(n):
    #生成n之内的质数列表
    table = []
    for num in range(2, n):
        if is_prime_num(num):
            table.append(num)
    return table
def de_anwser(_key):
    #根据key,爆破
    table = create_table(128)
    flag = ''

    for k in _key:
        for ch in range(128):
            count = 0
            i = 0
            while table[i] < ch:
                count += table[i]
                i += 1
            tmp = ch ^ count
            if tmp == k:
                flag += chr(ch)
    return flag

key = [438,1176,1089,377,377,1600,924,377,1610,924,637,639,376,566,836,830 ]
flag = de_anwser(key)
print(flag) #flag{Che11y_1s_EG0IST}

0x3 BabyDriver

题目思路来源:参考2016 HCTF Reverse题目seven

一看源码,你就知道这是一个打着驱动幌子的简单迷宫,师傅们可能卡在了驱动对应的键盘码这块,我将上下左右设置为为IKJL,键盘过滤驱动捕获点击以后达成条件会输出成功,但不知道为啥卸载驱动以后老是蓝屏。。。

进到驱动程序的入口点DriverEntry里面,可以看到Driverunload,和分发函数,主题人想用KdDisableDebugger:

因为是键盘过滤驱动,主要看下IRP读操作的回调函数,也就是CompletionRoutine的位置:

很快在函数内找到迷宫的地图和行走逻辑,与普通迷宫不同的是,是由键盘过滤驱动获取键盘扫描码来控制上下左右:

根据键盘过滤驱动获得的key值为,我们可以知道IKJL为上下左右:

然后在虚拟机里加载下驱动,键盘依次敲击LKKKLLKLKKKLLLKKKLLLLLL,在有节奏感的敲击后,就可以获得success!

然后我虚拟机就蓝了。。。。

0x4 WannaReverse

题目思路来源:WannaCry的加密原理

这道题目被apeng师傅非预期了,膜就完事,首先讲下我认为的常规做法,我们拿到四个文件,主要需要逆向WannaReverse.exe这个文件,libcrypto-1_1.dll是openssl库用来提供rsa的相关加密函数,clickme.exe是仿照wannacry病毒的界面程序(自带换壁纸功能),flag.txt.Encry是被WannaReverse.exe加密的flag。

题目加密流程和WannaCry的加密原理差不多,也是RSA2048 + AES随机密钥,但是为了大大降低题目难度,也不在加密算法这里留坑了,直接把私钥给了,在clickme.exe里点Decrypt按钮就有私钥(模仿WannaRen直接给密钥)

main函数功能如下图所示:

然后在encfile函数里进行了关键的操作,最后将文件头+加密的AES密钥+AES加密的文件写入flag.txt.Encry文件:

标准的流程就这些,动调一下应该可以直接梳理清除,

解题脚本:

#coding:utf-8
from Crypto.Cipher import AES
 from binascii import b2a_hex, a2b_hex
 import rsa
 import base64
def aes_decrypt(text,key):
key = key.encode('utf-8')
mode = AES.MODE_ECB
cryptor = AES.new(key, mode)
plain_text = cryptor.decrypt(a2b_hex(text))
return plain_text.replace('\x00','')
def decrypt(crypt_text): # 用私钥解密
with open('rsa_private_key.pem', 'r') as privatefile:
p = privatefile.read()
privkey = rsa.PrivateKey.load_pkcs1(p)
lase_text = rsa.decrypt(crypt_text, privkey)
return lase_text
if name == 'main':
with open("flag.txt.Encry",'rb') as f:
res = f.read()[0xd:0x165]
print res
res = base64.b64decode(res)
key = decrypt(res)
with open("flag.txt.Encry",'rb') as f:
res = f.read()[0x165:]
    
print (res.encode('hex'))
print (aes_decrypt(res.encode('hex'),key))

但是这里必须贴一下我失误的伪随机用法,apeng师傅的非预期解法,学习到了:

师傅的脚本:

from Crypto.Cipher import AES
from base64 import *
def gen_key(seed):
    k = b""
    for i in range(32):
        seed = seed * 214013 + 2531011
        k+=bytes([0x30+(((seed>>16) & 0x7fff)%10)])
    return k
t = 1590310000
# t = 1589530000
while True:
    key = gen_key(t)
    # print(key)
    aes = AES.new(key, mode=AES.MODE_ECB)
    cipher = b"\\\xbc\xea\x89\xba+\x18\'y?\x13\n\x8a\x97\xb4\x9b\xcdx\x9b\xd85\x92\x05EL\"\xa5i7\xebn+\x0e\xbd\x84\x0f\x91a8\xf6\xf1\xba\x99\x19Ar\x07\x91\xf0&h\x06a&\\ 5\xdd\xcf\xfcwWT\x81\xf2\xf2\xe4\xaf\xbf\xa2\x1d)\xael\x08;v\x1bf\xb8\xfer\xcb\xd6\x94\xc3\xd5j\xe7\x0cz(\xdc\xbc\xac\x80"
    if aes.decrypt(cipher).endswith(b"\x00\x00\x00"):
        print(t)
        print(aes.decrypt(cipher).decode("utf-16"))
        exit()
    t-=1
    if t%10000 == 0:
        print(t)

我这样的勒索病毒,就是在白给。。。。

0x5 EzMachine

如果没有限制的话,会存在多解的情况,因此后来更新了题目描述,flag的组成仅包括:字母、大括号、大括号回头和下划线。

首先打开IDA,来到main函数,发现有混淆干扰了反汇编,手动patch为nop

然后创建函数并查看伪代码,发现是VM结构,下面开始分析。

off_4448F4处保存各个opcode对应函数的地址,4449A0处为虚拟机代码起始地址,dword_445BD8为ip,off_4427FC处保存有四个寄存器的地址,dword_445BAC为栈的起始地址,dword_445BC8为esp,opcode中有对栈的操作、跳转操作、取输入的操作以及加减乘除异或五种运算,分析各个opcode的作用后,可以写脚本生成伪汇编代码:

opcode = {0x00:'nop',0x01:'mov',0x02:'pushi',0x03:'pushr',0x04:'pop',0x05:'print',0x06:'add',0x07:'sub',0x08:'mul',0x09:'div',0x0A:'xor',0x0B:'jmp',0x0C:'cmp',0x0D:'je',0x0E:'jn',0x0F:'jg',0x10:'jl',0x11:'input',0x12:'clrstr',0x13:'LoadStack',0x14:'LoadString',0xFF:'quit'}
operand = {'nop':0,'mov':2,'pushi':1,'pushr':1,'pop':1,'print':0,'add':2,'sub':2,'mul':2,'div':2,'xor':2,'jmp':1,'cmp':2,'je':1,'jn':1,'jg':1,'jl':1,'input':0,'clrstr':0,'LoadStack':2,'LoadString':2,'quit':0}
code = [
    0x01, 3,3,
    0x05, 0x00, 0x00,
    0x11, 0x00, 0x00,
    0x01, 1,17,
    0x0C, 0,1,
    0x0D, 10, 0x00,
    0x01, 3,1,
    0x05, 0x00, 0x00,
    0xFF, 0x00, 0x00,
    0x01, 2, 0,
    0x01, 0, 17,
    0x0C, 0, 2,
    0x0D, 43, 0x00,
    0x14, 0, 2,
    0x01, 1, 97,
    0x0C, 0, 1,
    0x10, 26, 0x00,
    0x01, 1, 122,
    0x0C, 0, 1,
    0x0F, 26, 0x00,
    0x01, 1, 71,
    0x0A, 0, 1,
    0x01, 1, 1,
    0x06, 0, 1,
    0x0B, 36, 0x00,
    0x01, 1, 65,
    0x0C, 0, 1,
    0x10, 36, 0x00,
    0x01, 1, 90,
    0x0C, 0, 1,
    0x0F, 36, 0x00,
    0x01, 1, 75,
    0x0A, 0, 1,
    0x01, 1, 1,
    0x07, 0, 1,
    0x01, 1, 16,
    0x09, 0, 1,
    0x03, 1, 0x00,
    0x03, 0, 0x00,
    0x01, 1, 1,
    0x06, 2, 1,
    0x0B, 11, 0x00,
    0x02, 0x7, 0x00,
    0x02, 0xD, 0x00,
    0x02, 0x0, 0x00,
    0x02, 0x5, 0x00,
    0x02, 0x1, 0x00,
    0x02, 0xC, 0x00,
    0x02, 0x1, 0x00,
    0x02, 0x0, 0x00,
    0x02, 0x0, 0x00,
    0x02, 0xD, 0x00,
    0x02, 0x5, 0x00,
    0x02, 0xF, 0x00,
    0x02, 0x0, 0x00,
    0x02, 0x9, 0x00,
    0x02, 0x5, 0x00,
    0x02, 0xF, 0x00,
    0x02, 0x3, 0x00,
    0x02, 0x0, 0x00,
    0x02, 0x2, 0x00,
    0x02, 0x5, 0x00,
    0x02, 0x3, 0x00,
    0x02, 0x3, 0x00,
    0x02, 0x1, 0x00,
    0x02, 0x7, 0x00,
    0x02, 0x7, 0x00,
    0x02, 0xB, 0x00,
    0x02, 0x2, 0x00,
    0x02, 0x1, 0x00,
    0x02, 0x2, 0x00,
    0x02, 0x7, 0x00,
    0x02, 0x2, 0x00,
    0x02, 0xC, 0x00,
    0x02, 0x2, 0x00,
    0x02, 0x2, 0x00,
    0x01, 2, 1,
    0x13, 1, 2,
    0x04, 0, 0x00,
    0x0C, 0, 1,
    0x0E, 91, 0x00,
    0x01, 1, 34,
    0x0C, 2, 1,
    0x0D, 89, 0x00,
    0x01, 1, 1,
    0x06, 2, 1,
    0x0B, 78, 0x00,
    0x01, 3, 0,
    0x05, 0x00, 0x00,
    0xFF, 0x00, 0x00,
    0x01, 3, 1,
    0x05, 0x00, 0x00,
    0xFF, 0x00, 0x00,
]
for i in range(0,len(code),3):
    if not code[i] in opcode.keys():
        print('unknow')
        continue
    op = opcode[code[i]]
    print(op, end = ' ')
    if operand[op] != 0:
        print(code[i+1:i+1+operand[op]])
    else:
        print()

得到伪汇编代码:

mov [3, 3]
print
input
mov [1, 17] //flag长度
cmp [0, 1]
je [10]
mov [3, 1]
print
quit
mov [2, 0]
mov [0, 17]
cmp [0, 2]
je [43]
LoadString [0, 2]
mov [1, 97]
cmp [0, 1]
jl [26]  //if input[i] < 'a': jmp
mov [1, 122]
cmp [0, 1]
jg [26]  //if input[i] > 'z': jmp
zmov [1, 71]
xor [0, 1]
mov [1, 1] 
add [0, 1]
jmp [36]
mov [1, 65]
cmp [0, 1]
jl [36]  //if input[i] < 'A': jmp
mov [1, 90]
cmp [0, 1]
jg [36]  //if input[i] > 'Z': jmp
mov [1, 75]
xor [0, 1]
mov [1, 1]
sub [0, 1]
mov [1, 16]
div [0, 1] 
pushr [1]
pushr [0]  
mov [1, 1]
add [2, 1]
jmp [11]
pushi [7]
pushi [13]
pushi [0]
pushi [5]
pushi [1]
pushi [12]
pushi [1]
pushi [0]
pushi [0]
pushi [13]
pushi [5]
pushi [15]
pushi [0]
pushi [9]
pushi [5]
pushi [15]
pushi [3]
pushi [0]
pushi [2]
pushi [5]
pushi [3]
pushi [3]
pushi [1]
pushi [7]
pushi [7]
pushi [11]
pushi [2]
pushi [1]
pushi [2]
pushi [7]
pushi [2]
pushi [12]
pushi [2]
pushi [2]
mov [2, 1]
LoadStack [1, 2]
pop [0]
cmp [0, 1]
jn [91]
mov [1, 34]
cmp [2, 1]
je [89]
mov [1, 1]
add [2, 1]
jmp [78]
mov [3, 0]
print
quit
mov [3, 1]
print
quit

伪代码中,类似于mov、add、sub等指令后面的操作数为寄存器编号,例如 mov [1,2]代表将2号寄存器的内容放入1号寄存器。接下来分析伪汇编代码,发现是对输入的值进行处理,小写字母异或71后+1,大写字母异或75后-1,非字母不处理,处理得到的结果除以16,得到的商和余数分别进栈,并在最后部分比较,因此可以写出exp计算flag:

array = [0x7,0xd,0x0,0x5,0x1,0xc,0x1,0x0,0x0,0xd,0x5,0xf,0x0,0x9,0x5,0xf,0x3,0x0,0x2,0x5,0x3,0x3,0x1,0x7,0x7,0xb,0x2,0x1,0x2,0x7,0x2,0xc,0x2,0x2,]
array = array[::-1]
for i in range(0, len(array), 2):
    c = array[i] + array[i+1]*16
    tmp = (c-1) ^ 71
    if tmp >= ord('a') and tmp <= ord('z'):
        print(chr(tmp), end = "")
        continue
    tmp = (c+1) ^ 75
    if tmp >= ord('A') and tmp <= ord('Z'):
        print(chr(tmp), end = "")
        continue
    print(chr(c), end = "")

flag{Such_A_EZVM}

0x6 DbgIsFun

首先检查tls回调,出现了smc,解密方法为第i位与i异或,解密后创建了运行该函数的线程

main函数的开头可以看到安装了seh函数

安装后进行输入,然后对输入的长度减了一个0x1C,并随后触发int3断点来到seh函数。

SEH中的判断如图所示。在main函数中对flag长度做的sub操作会影响EFlags的值,当输入长度等于0x1C时,ZFlag置0,SEH函数由此判断输入的长度是否正确,如果错误会将程序的流程引向一段假的flag解密函数。长度正确则对标志位置1。

dword_41A8E0这个标志位将在子线程的函数中被循环检查,静态下该函数还未解密,所以需要进行动态调试等待代码段解密。解密后来到子线程函数:

当dword_41A8E0处的标志位被置1后,函数跳出Sleep死循环开始运行,首先会取41A8DE起始的前0x8C个字节计算字节码之和,如果该段代码被patch或者存在断点,字节码之和就会改变,从而影响后续的计算结果。

计算完字节码之和后与所输入字符串进行异或,再往后就是将异或后的结果进行RC4加密并校验,密钥为GKCTF

如图所示,校验时的数据存放在代码段,因此调试时的反汇编结果可能出现错误,需要手动矫正。从4014AA开始即为校验数据,将这段数据进行RC4解密,密钥GKCTF,再与前面计算得到的字节码之和进行异或即可得到flag

from Crypto.Cipher import ARC4
key = b"GKCTF"
hexstr = "2DD40FD054EE75D0E03096E1798AE0FE183A27E72F86C9FE6643A775"
newstr = b""
for i in range(0, len(hexstr), 2):
    newstr += int(hexstr[i:i+2],16).to_bytes(1,'little')
rc4 = ARC4.new(key)
flag = rc4.decrypt(newstr)
for i in flag:
    print(chr(i^0xC9), end = "")

flag{5tay4wayFr0m8reakp0int}

温柔正确的人总是难以生存,因为这世界既不温柔,也不正确

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!