This page looks best with JavaScript enabled

MRCTF_2020 Re 部分题解

 ·  ☕ 6 min read · 👀... views

这次比赛只做了RE(Pwn师傅们太猛了,飞速秒题),个人感觉RE没想象中那么简单(说好的新生赛、0基础呢 QAQ)

Start Time:3.27 18:00 UTC +8

End Time:3.29 22:00 UTC +8

撸啊撸

看到这个数组中的内容时真的感叹出题人的脑洞,C语言char数组里直接存上VB的代码(应该是VB吧…不是很确定)实在是tql
撸啊撸

这里主要要注意两个点,一个是这个数组的内容会在程序初始化的时候被更改,所以要动态后下断点看,然后就是代码中的 x~i 表示的意思是x异或i,这里当时猜了好久

总的来说只要看到这个数组的内容这题就很简单了

cmps=[83,80,73,80,76,125,61,96,107,85,62,63,121,122,101,33,123,82,101,114,54,100,101,97,85,111,39,97]
flag = []
for i in range(len(cmps)):
    if i%2 != 0:
        flag.append( chr(cmps[i]^(i+1)) )
    else:
        flag.append( chr(cmps[i]-6) )
print("".join(flag) )

Transform

这题没什么好说的,逻辑非常明显了

ciper = [
    0x67, 0x79, 0x7B, 0x7F, 0x75, 0x2B, 0x3C, 0x52, 0x53, 0x79, 
    0x57, 0x5E, 0x5D, 0x42, 0x7B, 0x2D, 0x2A, 0x66, 0x42, 0x7E, 
    0x4C, 0x57, 0x79, 0x41, 0x6B, 0x7E, 0x65, 0x3C, 0x5C, 0x45, 
    0x6F, 0x62, 0x4D, 0x00
]
box = [
    0x09, 0x0A, 0x0F, 0x17, 0x07, 0x18, 0x0C, 0x06, 0x01,
    0x10, 0x03, 0x11, 0x20, 0x1D, 0x0B, 0x1E, 0x1B, 0x16,
    0x04, 0x0D, 0x13, 0x14, 0x15, 0x02, 0x19, 0x05, 0x1F,
    0x08, 0x12, 0x1A, 0x1C, 0x0E, 0x00
]

flag = ['0' for i in range(33)]
for i in range(33):
    flag[box[i]] = chr( ciper[i]^box[i] )
    print("".join(flag) )

hello_world_go

中文搜索引擎启动 –> 搜索字符串“flag” –> 发现flag并提交

hello_world_go

junk

这题的关键逻辑部分被加了花,不过没有关系,没有反调试直接动调就可以了(也可以自己patch,就是几条db指令)

逻辑部分先是一个长度计算与比较,可以知道flag长度为0x2B

junk

过了长度check后走过一些花指令会在这里对flag逐位与3异或

junk

然后再对每个字节按位循环左移(ror)四位

junk

最后做了一个base64,不过这个base64的box是被换掉的

import base64
a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz)!@#$%^&*(+/"
b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
ciper = "%BUEdVSHlmfWhpZn!oaWZ(aGBsZ@ZpZn!oaWZ(aGBsZ@ZpZn!oYGxnZm%w.."
flag_base64 = ""
ciper_list = list(ciper)
for i in ciper_list:
    flag_base64 += b[a.find(i)]
print(flag_base64)
# 我们需要循环右移字位,即每个字节的高四位与第四位位置互换,即每个字节的两个16位数进行互换即可
ciper =  [ "0x" + str.zfill(hex(x).replace('0x', ''), 2)[::-1] for x in list(base64.b64decode(flag_base64.encode()) )]
print(ciper)
flag = ""
for i in ciper:
    flag += chr( eval(i)^3 )
print(flag)

PixelShooter

apk逆向 首先在模拟器中安装并试玩

PixelShooter

安卓unity游戏的核心逻辑一般位于assets\bin\Data\Managed\Assembly-CSharp.dll

ida载入时发现为.Net程序,换用dnSpy进行反编译,直接找GameOver函数

PixelShooter

PixelShooter

Shit

这题真的魔鬼,反调一套一套的。首先根据字符串交叉引用可以看到sub_401640就是main函数,里面有一个非常明显的 IsDebuggerPresent 变量检测。然后动调运行却发现程序并没有跑到main函数就卡死了,然后可以注意到CPU在暴涨,那基本就可以判定在程序初始化过程中有地方检测了调试器并进入了类似于死循环的东西。然后我检测了各种初始化数组最终找到了罪魁祸首在sub_401010
Shit
赛后看到有些师傅是用先运行后attche的方式绕过这个检测的,真的tql…

然后接下来检测了输入长度后会来到这个地方
Shit

在sub_401460中会对时间进行检测,也就是说若是F7跟入研究这个函数就会被check到然后修改掉box,而且里面写的非常复杂,含花指令(比赛的时候就是死于这里,读了半天没读懂最后box错了跑不出flag QAQ)所以正确的做法应该是把断点下在后面的loc_4012F0处。这个loc_4012F0里面就是加密逻辑了,逻辑大致是这样的

0. 将输入的每四个字符进行分组,并按其asc码byte成双字的16进制数,如"flag"组装成 0x666c6167
1. 对0逻辑右移box位
2. 对0逻辑左移0x20-box
3. 1和2异或

4. 3逻辑右移0x10 后取反  最后逻辑与0xFFFF   (0xFFFF-3)
5. 3逻辑左移0x10

6. 4和5异或
7. 6与 (1<<box)异或
第一次7的值应为 0x8C2C133A,以此类推

每次要与上一次结果异或,第一次不需要异或

最后写出脚本就行了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ciper = [0x8C2C133A, 0xF74CB3F6, 0xFEDFA6F2, 0xAB293E3B, 0x26CF8A2A, 0x88A1F279]
sh_box = [0x3, 0x10, 0xd, 0x4, 0x13, 0xb]

for i in range(5, 0, -1):
	ciper[i] ^= ciper[i-1]
for i in range(6):
    ciper[i] ^= (1 << sh_box[i])
    ciper[i] = ((~(ciper[i] & 0xffff) << 16) | ((ciper[i] & 0xffff0000) >> 16)) & 0xffffffff
    ciper[i] = ((ciper[i] << (sh_box[i])) | (ciper[i] >> (0x20 - sh_box[i]))) & 0xffffffff
print(ciper)
print("".join([bytes.fromhex(hex(x).replace('0x','')).decode() for x in ciper]))

Virtual_Tree

主要有两次加密,第一次sub_EB1610是一个树结构的遍历与异或,遍历顺序按回溯深搜的方式

Virtual_Tree

这里处理完后在sub_EB16F0里面会做xor add div操作,这三种操作都加了花指令,patch掉看伪代码就很简单了

Virtual_Tree

这里主要有个问题是div操作会abs,一时想不到怎么逆,然后就手测了….手测发现全部是要进行取相反数运算的

ciper = [
    0x17, 0x63, 0x77, 0x03, 0x52, 0x2E, 0x4A, 0x28, 0x52, 0x1B, 
    0x17, 0x12, 0x3A, 0x0A, 0x6C, 0x62
]
box = [
    13, 12, 7, 16, 15, 11, 6, 3, 10, 5, 14, 9, 8, 4, 2, 1
]
def xor(a1, a2):
    ciper[a1] ^= ciper[a2]
def div(a1, a2):
    # if a1 == 12 or a1 == 9 or a1 == 6:
    #     ciper[a1] = ciper[a2] - ciper[a1]
    #     return 0
    # elif a1 == 10:
    #     ciper[a1] = ciper[a2] - ciper[a1]
    #     return 0
    # if a1 == 3:
    #     ciper[a1] = ciper[a2] - ciper[a1]  # r  x
    #     return 0
    result = ciper[a2] - ciper[a1]
    # if result>=0:
    #     print(a1, a2, ciper[a1], ciper[a2], result, ciper[a2] + ciper[a1])
    #     exit(0)
    # result = ciper[a2] + ciper[a1]
    ciper[a1] = result
def add(a1, a2):
    ciper[a1] -= a2
    if ciper[a1] < 0:
        ciper[a1] += 0x100

add(15, 2)
xor(14, 15)
div(12, 2)
xor(11, 12)
div(10, 7)
div(9, 8)
xor(8, 7)
add(7, 3)
div(6, 1)
xor(4, 5)
div(3, 7)
add(2, 7)
xor(1, 2)
add(0, 10)

flag = ""
for i in range(16):
    flag += chr( ciper[i]^(64+box[i]) )
print(flag)

Hard-to-go

go逆向 需要使用golang_loader_assist.py脚本修复符号表,有了符号表逻辑就非常清楚了,这里贴一下链接
https://github.com/strazzere/golang_loader_assist/releases/tag/IDA-7.3-and-Below
https://github.com/strazzere/golang_loader_assist

修复符号表后
Hard-to-go

可以很明显的看到是RC4算法,因为修复后会有些指针重传之类的问题,可以动调一下观察加密函数crypto_rc4__Cipher_XORKeyStream的参数就可以找到key为

MRCTF_GOGOGOMRCTF_GOGOGO

Hard-to-go

然后跟进比较函数internal_bytealg_Compare可以看到下面中的寄存器必然有一个保存着input_ciper和ciper,根据rc4加密算法的特点:密文长度等于原文长度,可以通过长度确定ciper在rdi中,而rsi保存的是input_ciper,所以可知结果的十六进制串为

7D306EC9CC03931E854D455FC546F4A8A03E11BE70751DA3CD7FFFBD8112

这里贴下解密脚本(算法是从csdn上找的代码)

 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
import binascii 
def rc4_crypt(PlainBytes:bytes, KeyBytes:bytes) -> str:
    '''[summary]
    rc4 crypt
    Arguments:
        PlainBytes {[bytes]} -- [plain bytes]
        KeyBytes {[bytes]} -- [key bytes]
    
    Returns:
        [string] -- [hex string]
    '''
    keystreamList = []
    cipherList = []
    keyLen = len(KeyBytes)
    plainLen = len(PlainBytes)
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + KeyBytes[i % keyLen]) % 256
        S[i], S[j] = S[j], S[i]
 
    i = 0
    j = 0
    for m in range(plainLen):
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) % 256]
        cipherList.append(k ^ PlainBytes[m])
 
    result_hexstr = ''.join(['%02x' % i for i in cipherList])
    return result_hexstr.upper()
 
if __name__ == "__main__":
    data = '7D306EC9CC03931E854D455FC546F4A8A03E11BE70751DA3CD7FFFBD8112'
    key = "MRCTF_GOGOGOMRCTF_GOGOGO"
    result = rc4_crypt(binascii.a2b_hex(data), key.encode())
    print(bytes.fromhex("435443465446435446434654435443465446544346435446435443465446"))

# CTCFTFCTFCFTCTCFTFTCFCTFCTCFTF

EasyCpp

  • Author: Bayerischen

输入分为9组,每组都是一个整数,每组分别异或1 然后进入depart()

EasyCpp

depart每当输入的9组数中的一组能被i除尽的时候就递归继续进行depart,并将当前递归层的v7赋值为1.运算结束后会进行字符替换,将对应数字替换成字母,最后和结果进行比较。

逆着逻辑写出脚本

s = ["=zqE=z=z=z","=lzzE","=ll=T=s=s=E","=zATT","=s=s=s=E=E=E","=EOll=E","=lE=T=E=E=E","=EsE=s=z","=AT=lE=ll"]
a = []
for i in s:
    i = i[1:]
    i = i.replace("O","0" ).replace("l","1" ).replace("z","2" ).replace("E","3" ).replace("A","4" ).replace("s","5" ).replace("G","6" ).replace("T","7" ).replace("B","8" ).replace("q","9" )#.replace("="," " )
    i = i.split("=")
    i[0] = int(i[0])
    for c in range(1, len(i)):
        i[0] *= int(i[c])
    a.append(i[0])
for b in a:
    print(b^1, end = " ")

得到2345 1222 5774 2476 3374 9032 2456 3531 6720
输入后得到:

EasyCpp

MRCTF{4367FB5F42C6E46B2AF79BF409FB84D3}

Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer