Back

CISCN2021 RE writeup

glass

安卓逆向,使用jeb打开,发现在java层仅进行了简单的输入,然后进入so层判断

用ida打开so层,直接搜索java,进入判断函数

下面的字符串应该是密钥

qmemcpy(v6, "12345678", sizeof(v6));

然后调用了三个函数

sub_FFC(v7, v6, v4);
sub_1088(v7, flag, 39);
sub_10D4(flag, 39, v6, v4);

进入查看,第一个是RC4密钥初始化,第二个是RC4加密,第三个是对密文进行简单的运算

从字符串里拿密文,写脚本进行求解,先对简单运算进行反向运算,然后找个RC4密码的脚本,跑一下就可以找到flag

cipher = [0xA3, 0x1A, 0xE3, 0x69, 0x2F, 0xBB, 0x1A, 0x84, 0x65, 0xC2, 0xAD, 0xAD, 0x9E, 0x96, 5, 2, 0x1F, 0x8E, 0x36, 0x4F, 0xE1, 0xEB, 0xAF, 0xF0, 0xEA, 0xC4, 0xA8, 0x2D, 0x42, 0xC7, 0x6E, 0x3F, 0xB0, 0xD3, 0xCC, 0x78, 0xF9, 0x98, 0x3F, 0]
key = [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]

def __rc4_init(key):
    keylength = len(key)
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % keylength]) % 256
        S[i], S[j] = S[j], S[i]
    return S

def rc4_crypt(key, data):
    S = __rc4_init(key)
    i = j = 0
    result = b''
    for a in data:
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        k = (a ^ S[(S[i] + S[j]) % 256]).to_bytes(1, 'big')
        result += k
    return result

def convert(k):
    ret = []
    while k > 0:
        ret.append(k & 0xff)
        k >>= 8
    return ret[::-1]

from libnum import n2s, s2n

for j in range(39):
    cipher[j] ^= key[j % 8]

for j in range(0, 39, 3):
    cipher[j], cipher[j + 1], cipher[j + 2] = cipher[j + 1] ^ cipher[j + 2], cipher[j + 1] ^ cipher[j], cipher[j] ^ cipher[j + 1] ^ cipher[j + 2]

print (rc4_crypt(key, cipher))

# b'CISCN{6654d84617f627c88846c172e0f4d46c}\xec'

baby_bc

不知道bc文件是什么,用 file 命令查看,发现是 LLVM ir bitcode 文件,上网搜索,使用 clang -o baby_bc baby.bc 搞成 elf 文件

拖入ida进行查看

首先对输入进行判断,分析输入的应该是长度是25的字符串,每个字符都在 0-5 之间

要拿到flag需要通过两个验证函数

第一个函数是将输入填入到map中,map中非零位输入应该为0,零位的输入不能为0

第二个函数是对map进行验证,分析后发现总共进行了如下验证:

  • 每行的数字不能相同
  • 每列的数字不能相同
  • 每行相邻两个数字的大小关系需要符合row矩阵的要求
  • 每列相邻两个数字的大小关系需要符合col矩阵的要求

知道要求后直接上z3约束求解器拿flag

from z3 import *

s = Solver()

flag = [Int("flag_%i" % i) for i in range(25)]

for i in range(25):
    s.add(flag[i] > 0)
    s.add(flag[i] < 6)

s.add(flag[12] == 4)
s.add(flag[18] == 3)

for i in range(5):
    add_row = 0
    add_col = 0
    for j in range(5):
        add_row += flag[i * 5 + j]
        add_col += flag[j * 5 + i]
    s.add(add_row == 15)
    s.add(add_col == 15)

s.add(flag[5] > flag[6])
s.add(flag[20] > flag[21])
s.add(flag[3] > flag[4])
s.add(flag[13] > flag[14])
s.add(flag[22] > flag[23])

s.add(flag[10] < flag[11])

s.add(flag[2] > flag[7])
s.add(flag[4] > flag[9])

s.add(flag[13] < flag[18])
s.add(flag[16] < flag[21])
s.add(flag[19] < flag[24])

for i in range(5):
    for j in range(5):
        for k in range(5):
            if j == k:
                continue
            s.add(flag[5 * i + j] != flag[5 * i + k])
            s.add(flag[5 * j + i] != flag[5 * k + i])

if s.check() == sat:
    model = s.model()
    for i in range(25):
        print (model[flag[i]].as_long().real, end='')

print ('\nfinish')
# 1425353142354212153442315

最后把两个地方改成 0 就行了

little_evil

基本分析

直接用ida直接打开会看到一个叫做"squashfs",而且和ruby有关,但比赛的时候没有多想,然后就走远了

放一张珍贵截图

后来得知正确方法需要先用binwalk分解一下,这里有个坑,需要自己手动装一个"squashfs"的插件

顺便补充一下什么是"squashfs":基于Linux内核使用的压缩只读文件系统。难怪要用binwalk,沉思

利用输出去混淆

分解后翻一下目录,可以找到一个 out.rb 的文件

打开后发现是一个被严重混淆的脚本,大概长下面这样

$l1Il="";
$l1lI="";
def llIl() $lI1lll=$lI1lll|7; end;
def l1lll() $lI1lll=10; end;
def llI1l() $lI1lll=$lI1lll|4; end;
def lIlI() $lI1lll=$lI1lll+3; end;
def l111() $lI1lll=$lI1lll%3; end;
def lI1IlI() $lI1lll=$lI1lll|3; end;
def ll1l1() $lI1lll=$lI1lll*8; end;
def l1lI() $lI1lll=$lI1lll-3; end;
def lI1lII() $lI1lll=$lI1lll%1; end;
def lIlIl() $lI1lll=$lI1lll&10; end;
def lIll() $lI1lll=$lI1lll-4; end;
def lII1() $lI1lll=$lI1lll%2; end;
def l1III() $lI1lll=$lI1lll|1; end;
def l1l111() $lI1lll=$lI1lll|5; end;
def l1IIII() $lI1lll=$lI1lll%10; end;
def l11I() $l1Il=$l1Il+$lI1lll.chr; end;
def lIlll() $lI1lll=$lI1lll*9; end;
def l11IlI() $lI1lll=$lI1lll-8; end;
def lI1I1() $lI1lll=$lI1lll+5; end;
def ll11lI() $lI1lll=$lI1lll&9; end;
def lII1l1()
    #send($l1Il[0,4], $l1Il[4,$l1Il.length]);
    aFile=File.new("out2.rb", "w");
    aFile.syswrite($l1Il);
    aFile.close;
end;

最后一个函数里本来只有一个 send 方法,这个方法是执行第一个参数的函数,后面的参数都是这个函数的变量

这里跟着学长学习了一个针对解释性语言混淆的办法,就是直接输出这个send中的变量

输出之后还是一个相似的脚本,简单换一下行,长这样:

# eval
$llll="";
$llII="";
def l1llI()$l1lI1l=$l1lI1l|7; end; 
def ll1III()$l1lI1l=$l1lI1l%7; end; 
def lllI()$l1lI1l=$l1lI1l/4; end; 
def lIl1l()$l1lI1l=$l1lI1l-3; end; 
def l1lll()$l1lI1l=$l1lI1l|10; end; 
def l11I1I()$l1lI1l=10; end; 
def l1l1()$l1lI1l=$l1lI1l&7; end; 
def l1II()$l1lI1l=$l1lI1l%8; end; 
def ll1I()$l1lI1l=$l1lI1l|8; end; 
def ll11()$l1lI1l=$l1lI1l^6; end; 
def ll1l1I()$l1lI1l=$l1lI1l|1; end; 
def lI1Il()$l1lI1l=$l1lI1l|3; end; 
def llI1I()$l1lI1l=$l1lI1l+6; end; 
def llIl1()$l1lI1l=$l1lI1l*4; end; 
def lI1ll()$l1lI1l=$l1lI1l*5; end; 
def l1111()$l1lI1l=$l1lI1l^7; end; 
def l1lII()$l1lI1l=$l1lI1l^4; end; 
def lIIl()$l1lI1l=$l1lI1l%5; end; 
def lII11()$l1lI1l=$l1lI1l+9; end; 
def lI11I()$llll=$llll+$l1lI1l.chr; end; 
def l1IlI()send($llll[0,4], $llll[4,$llll.length]); end; 

一开始的 eval 就是 send 中调用的函数,可以分析出来后面的东西就是要用来执行的,因为这是解释性语言,直接输出就拿到源代码了

和刚才进行同样的操作,拿到第三份脚本

begin $_=$$/$$;@_=$_+$_;$-_=$_-@_
$__=->_{_==[]||_==''?$.:$_+$__[_[$_..$-_]]}
@__=->_,&__{_==[]?[]:[__[_[$.]]]+@__[_[$_..$-_],&__]}
$_____=->_{@__[[*_],&->__{__[$.]}]}
@_____=->_{@__[[*_],&->__{__[$-_]}]}
$______=->_{___,______=$_____[_],@_____[_];_____=$__[___];____={};__=$.;(_=->{
  ____[______[__]]=___[__];(__+=$_)==_____ ?____:_[]})[]}
@______=->_,__{_=[*_]+[*__];____=$__[_];___={};__=$.;(_____=->{
  ___[_[__][$.]]=_[__][$_];(__+=$_)==____ ?___:_____[]})[]}
$_______=->_{$___=[];@___=$__[_];__=___=____=$.;$____,@____={},[]
(_____=->{
  _[____]=='5'?(@____<<____):$.
  _[____]=='6'?($____[@____[$-_]]=____;@____=@____[$...$.-@_]):$.
  (____+=$_)==@___?$.:_____[]})[]
$____=$____=={}?{}:@______[$____,$______[$____]]
(______=->{_[__]==
'0'?($___[___]||=$.;$___[___]+=$_):_[__]==
'1'?($___[___]||=$.;$___[___]-=$_):_[__]==
'2'?($___[___]||=$.;$___[___]=STDIN.getc.ord):_[__]==
'3'?(___+=$_):_[__]==
'4'?(___-=$_):_[__]==
'5'?(__=($___[___]||$.)==$.?$____[__]:__):_[__]==
'6'?(__=($___[___]||$.)!=$.?$____[__]:__):_[__]==
'7'?($><<(''<<$___[___])):$.
(__+=$_)==@___?_:______[]})[]}
$_______['33516351...44516644'];rescue Exception;end #中间部分省略了

这份脚本就很丑了,最后一长串的数字,让我自己来猜的话肯定会猜是一个虚拟机

然后一大堆 ? 一看就是 switch 语句,后来细看才发现全是三元运算符,但也是 switch 的作用

于是将指令部分翻译成 python(只是熟悉一点而已)

if _[tmp_2] == '0':
    global_3[tmp_3] ||= $.
    global_3[tmp_3] += global_1
if _[tmp_2] == '1':
    global_3[tmp_3] ||= $.
    global_3[tmp_3] -= global_1
if _[tmp_2] == '2':
    global_3[tmp_3] ||= $.
    global_3[tmp_3] = STDIN.getc.ord
if _[tmp_2] == '3':
    tmp_3 += global_1
if _[tmp_2] == '4':
    tmp_3 -= global_1
if _[tmp_2] == '5':
    if (global_3[tmp_3] or $.) == $.:
        tmp_2 = global_4[tmp_2]
if _[tmp_2] == '6':
    if (global_3[tmp_3] or $.) != $.:
        tmp_2 = global_4[tmp_2]
if _[tmp_2] == '7':
    global_0<<(''.append(global_3[tmp_3]))

因为是补题,所以提前知道是 brainfuck 语言,但还是尝试自己逆了一下

  • tmp_3 是指针,操作3和4对应了指针+1 -1(>和<)
  • global_3 是指针指向的字节,操作0和1对应了字节的+1 -1(+和-)
  • 操作2中含有获取输入,对应了获取输入操作(,)
  • 操作7中含有«,怀疑是输出,对应了输出操作(.)
  • 5和6对应了跳转,猜测5是[,6是]

之后就可以找个脚本翻译 brainfuck 了

我先用 python 将其转为了正常的 brainfuck 语言

finalop = ''
base = '+-,><[].'
for c in op: # 那一串数字
    finalop += (base[int(c)])
print (finalop)

然后找了个脚本,这是核心部分:

    int cur = 0;
    while ((c = getc(in)) != EOF) {
        switch (c) {
            case '>': 
                // fprintf(out, "\t\t++c;\n"); 
                cur++; 
                break;
            case '<': 
                // fprintf(out, "\t\t--c;\n"); 
                cur--; break;
            case '+': fprintf(out, "\t\t++a[%d];\n", cur); break;
            case '-': fprintf(out, "\t\t--a[%d];\n", cur); break;
            case '.': fprintf(out, "\t\tputchar(a[%d]);\n", cur); break;
            case ',': fprintf(out, "\t\ta[%d] = getchar();\n", cur); break;
            case '[': fprintf(out, "\twhile (a[%d]) {\n", cur); break;
            case ']': fprintf(out, "\t}\n"); break;
            default: break;
        }
    }

一开始随便找了个脚本就运行,然后尝试去看,但后来发现很多指针位置的变化,看着很累,于是让指针的变化在内部运行,对具体数做变化的时候直接打印指针的值就可以了

Brainfuck 代码阅读

接下来就是痛苦的 Brainfuck 代码阅读环节了,虽然代码已经有了最简单的美化,但看起来还是像混淆过的汇编。

自己做的时候是一点一点美化代码,然后阅读的。但最后找到验证函数才搞明白。

所以先去最下面找到验证函数,看到最下面有两个putchar,猜测就是通过验证了,于是找进入的条件

		a[2] = getchar();
        // several code
	while (a[2]) {
		// several code
		a[1] = 0;
        // several code
	}
		a[2] = 0;
		a[3] = 0;
	while (a[1]) {
		++a[2];
		++a[3];
		--a[1];
	}
	    // several code
	while (a[2]) {
		// several code
		putchar(a[4]);
		// several code
		putchar(a[4]);
		a[2] = 0;
	}

进入的条件是要 a[2] > 0,网上看就知道需要让 a[1] > 0,所以在编辑器里选中一下,就能找到所有 a[1] 出现的地方(这就体现出这种输出方法的优势了)

然后发现 a[1] 会在一开始赋值为 1,但一旦进入 while(a[2]) 这种大循环,就会出现 a[1]=0 的赋值,所以我们的目标就是在进入循环前让 a[2]==0

查看一下从 getcharwhile 之间的代码,把重复出现的 ++ 都合并一下

这里以第一次 getchar 的代码为例,(剩下几次形式几乎完全一致,就是参数有点小变化而已)

		a[2] = getchar();
	while (a[3]) {
		--a[3];
	}
	while (a[4]) {
		--a[4];
	}
		++a[4];
		++a[4];
		++a[4];
		++a[4];
		++a[4];
		++a[4];
		++a[4];
	while (a[4]) {
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		++a[3];
		--a[4];
	}
	while (a[3]) {
		--a[2];
		--a[3];
	}
	while (a[2]) {
	while (a[4]) {
		--a[4];
	}
	while (a[5]) {
		--a[5];
	}
	while (a[1]) {
		--a[1];
	}
	while (a[4]) {
		++a[5];
		++a[1];
		--a[4];
	}
	while (a[5]) {
		++a[4];
		--a[5];
	}
	while (a[2]) {
		--a[2];
	}
	}
	while (a[2]) {
		--a[2];
	}

美化一下:

		a[2] = getchar();
		a[3] = 0;
		a[4] = 7;
	while (a[4]) {
		a[3] += 11
		--a[4];
	} // a[3] = a[4] * 11 = 77
        a[2] -= a[3]
	while (a[2]) {
        a[4] = 0;
        a[5] = 0;
        a[1] = 0;
        a[2] = 0;
	}
    	a[2] = 0;

简单地说就是会生成一个数字,然后用 a[2] 去减,如果结果为 0,就通过验证了,对所有的输入都搞一次,就能拿到五个输入字符 M5Ya7

总结

做这道题的时候,最大的问题就是没有搜索足够的资料,如果第一步想出来的话的,以比赛的时间,应该还是有机会做出来这道题的,毕竟后续的工作都是体力活,一点一点做下去应该就差不多能出来了

不过不管怎么说,补题的过程还是学到了很多东西的,比如“病毒式”混淆可以直接用输出来解,brainfuck的小型解释器怎么看,以及最后直接输出索引地址,做题经验++

HMI

先说结论:屑题

参考了这篇博客:https://myts2.cn/2021/05/16/ciscn2021/

逆向分析

用 file 命令看一眼,发现全是 .NET,直接上 dnSpy

先搜索 CISCN 字符串,找到最后的验证和输出

checked
{
	while (!string.IsNullOrEmpty(AnalogValueDisplay.combined[num4]))
	{
		num4++;
		if (num4 > 7)
		{
			IL_1B9:
			if (num3 == 0)
			{
				string hash = AnalogValueDisplay.GetHash(string.Join(",", AnalogValueDisplay.combined));
				Console.WriteLine("Booooooooooooooooom!");
				if (Operators.CompareString(hash.Substring(0, 10), "F0B278CCB9", false) == 0)
				{
							Console.WriteLine("CISCN{" + hash + "}");
						}
					}
			return;
		}
	}
	num3 = 1;
	goto IL_1B9;
}

所以最后需要通过一个md5验证,然后往回找 combined 是什么,发现是从 text 赋值的

而具体赋值到哪里,则是由 num2 决定的, num2 是一串 41047 - 41054 的字符串

比赛的时候只知道这个是一个端口,但具体是什么没搞出来,疯狂往回找引用发现找不到东西,怀疑还是需要远程往里面打数据,因此尝试搭建GRFICS的平台(队内大佬找到的),最后熬不动放弃了

参考别人的wp之后发现需要使用 Modbus Slave 往里面打数据,开始补题

Modbus Slave调试

之前找到的 401** 原来就是 Modbus 的端口,所以只需要用 Modbus Slave 往相应端口添加数据就行

先直接运行找到粗略的范围(调试修改数据太慢了),目标就是让数字都变成白色

在粗查的时候就能发现小数点后有一些位置在 exe 界面是看不到的

明确范围后进 dnSpy 调试,总结出一个表格

min max dif combined i
$41046$ $52.8016$ $17312$ $52.8992$ $17344$ $0.00305$ $0.00305$ $2$
$41047$ $25.0002$ $1634$ $25.092$ $1640$ $0.0153$ $0.0153$ $1$
$41048$ $62.10105$ $20361$ $62.19865$ $20393$ $0.00305$ $0.00305$ $0$
$41049$ $406.6128$ $26576$ $406.6893$ $26581$ $0.0153$ $0.0153$ $3$
$41050$ $54.00025$ $17705$ $54.09785$ $17737$ $0.00305$ $0.00305$ $7$
$41051$ $158.0031$ $10327$ $158.0949$ $10333$ $0.0153$ $0.0153$ $6$
$41052$ $22.0027$ $7214$ $22.09725$ $7245$ $0.00305$ $0.00305$ $4$
$41053$ $13.1121$ $857$ $13.1886$ $862$ $0.0153$ $0.0153$ $5$

接下来在这一范围内进行爆破就好了

对范围做了个计算,我搞出来的是 2028571776,不知道为什么参考比我这个小一点

爆破

因为最后要算 md5,所以精度不能有问题,又因为爆破范围大概在 20 亿左右,所以速度也不能慢

于是决定先用python的Decimal来算小数,再用cpp求解

from decimal import Decimal

min = [52.8016, 25.0002, 62.10105, 406.6128, 54.00025, 158.0031, 22.0027, 13.1121]
max = [52.8992, 25.092, 62.19865, 406.6893, 54.09785, 158.0949, 22.09725, 13.1886]
dif = [0.00305, 0.0153, 0.00305, 0.0153, 0.00305, 0.0153, 0.00305, 0.0153]
round = [33, 7, 33, 6, 33, 7, 32, 6]
for i in range(8):
    min[i] = str(min[i])
    max[i] = str(max[i])
    dif[i] = str(dif[i])
    res = min[i]
    for _ in range(round[i]):
        print (res, end = ', ')
        res = Decimal(res) + Decimal(dif[i])
    print ()

最后算出来的结尾会有0,手动去除一下就行

然后用cpp进行爆破,这里写的比较懒

#pragma GCC optimize(3)
#include <bits/stdc++.h>
#include <openssl/md5.h>

using namespace std;

string combine[8][40] = {
    {"62.10105", "62.1041", "62.10715", "62.1102", "62.11325", "62.1163", "62.11935", "62.1224", "62.12545", "62.1285", "62.13155", "62.1346", "62.13765", "62.1407", "62.14375", "62.1468", "62.14985", "62.1529", "62.15595", "62.1590", "62.16205", "62.1651", "62.16815", "62.1712", "62.17425", "62.1773", "62.18035", "62.1834", "62.18645", "62.1895", "62.19255", "62.1956", "62.19865"},
    {"25.0002", "25.0155", "25.0308", "25.0461", "25.0614", "25.0767", "25.092"},
    {"52.8016", "52.80465", "52.8077", "52.81075", "52.8138", "52.81685", "52.8199", "52.82295", "52.8260", "52.82905", "52.8321", "52.83515", "52.8382", "52.84125", "52.8443", "52.84735", "52.8504", "52.85345", "52.8565", "52.85955", "52.8626", "52.86565", "52.8687", "52.87175", "52.8748", "52.87785", "52.8809", "52.88395", "52.8870", "52.89005", "52.8931", "52.89615", "52.8992"},
    {"406.6128", "406.6281", "406.6434", "406.6587", "406.674", "406.6893"},
    {"22.0027", "22.00575", "22.0088", "22.01185", "22.0149", "22.01795", "22.0210", "22.02405", "22.0271", "22.03015", "22.0332", "22.03625", "22.0393", "22.04235", "22.0454", "22.04845", "22.0515", "22.05455", "22.0576", "22.06065", "22.0637", "22.06675", "22.0698", "22.07285", "22.0759", "22.07895", "22.0820", "22.08505", "22.0881", "22.09115", "22.0942", "22.09725"},
    {"13.1121", "13.1274", "13.1427", "13.158", "13.1733", "13.1886"},
    {"158.0031", "158.0184", "158.0337", "158.049", "158.0643", "158.0796", "158.0949"},
    {"54.00025", "54.0033", "54.00635", "54.0094", "54.01245", "54.0155", "54.01855", "54.0216", "54.02465", "54.0277", "54.03075", "54.0338", "54.03685", "54.0399", "54.04295", "54.0460", "54.04905", "54.0521", "54.05515", "54.0582", "54.06125", "54.0643", "54.06735", "54.0704", "54.07345", "54.0765", "54.07955", "54.0826", "54.08565", "54.0887", "54.09175", "54.0948", "54.09785"},
};

int size[8] = {33, 7, 33, 6, 32, 6, 7, 33};

string MD5(const string& src )
{
    MD5_CTX ctx;

    string md5_string;
    unsigned char md[16] = { 0 };
    char tmp[33] = { 0 };

    MD5_Init( &ctx );
    MD5_Update( &ctx, src.c_str(), src.size() );
    MD5_Final( md, &ctx );

    for( int i = 0; i < 16; ++i )
    {   
        memset( tmp, 0x00, sizeof( tmp ) );
        sprintf( tmp, "%02X", md[i] );
        md5_string += tmp;
    }   
    return md5_string;
}

int main(){
    cout << MD5("62.10105,25.0002,52.8016,406.6128,22.0027,13.1121,158.0031,54.00025,") << endl;
    int cur[8] = {};
    time_t start = clock();
    for (long long i = 0; i < 2028571776; ++i){
        string in = "";
        for (int j = 0; j < 8; ++j){
            if (cur[j] >= size[j]){
                cur[j] = 0;
                ++cur[j + 1];
            }
            in += combine[j][cur[j]];
            in += ",";
        }
        string out = MD5(in);
        cur[0] += 1;
        // cout << in << endl;
        if (out[0] == 'F' && out[1] == '0' && out[2] == 'B' && out[3] == '2' && out[4] == '7' && out[5] == '8' && out[6] == 'C' && out[7] == 'C' && out[8] == 'B' && out[9] == '9'){
            cout << "in:" << in << endl;
            cout << "out:" << out << endl;
        }
        if (i % 5000000 == 0)
            cout << 100.0 * i / 2028571776 << "%" << endl;
    }
    time_t end = clock();
    printf("time=%fs\n", (double)(end - start)/CLOCKS_PER_SEC);
    return 0;
}

md5是直接上网抄的,来源:https://blog.csdn.net/u012063703/article/details/49178349

最后的结果

...
60.8803%
in:62.1834,25.0002,52.84735,406.6893,22.01795,13.1886,158.0031,54.06125,
out:F0B278CCB982F6132DD6A834C4827D0D
61.1268%
...
time=2639.569345s

爆破出答案大概花了 $60% \times 2640=26.4\min$

结论

这题难度不在于逆向,前期的基本分析以及后面需要打数据动调这些和逆向有关的操作,比赛的时候其实都想到了,但问题在于不知道还有 Modbus Slave 这种东西

所以全程都很迷茫,完全不知道该怎么做,官方的提示早上才放出来,那会都收工准备补觉了(一个小时的时间,找数据范围+写脚本+爆破,根本来不及好吧)

以及过程中的调数据就是无限二分,累的一批,这题说是 Misc 我都信

最后的爆破数据量也太大了,参考的博客用go跑了两小时,我这边用c++跑了半个小时,不过队友用c的多线程只跑了半分钟,看截图只爆破了 $2%$ 就出结果了,应该是划分的位置正好在答案边上,有时间学习一下多线程

综上:屑题

gift

新版本的GO对magic number以及一些结构上都做了修改,所以老版本的符号表修复脚本就不能用了,好在免费的ida7.6正好支持GO的符号表恢复,可以直接做了。

主函数主要部分如下

  main_CISCN6666666();
  main_CISCN66666666();
  main_CISCN6666666666();
  max_len_v2 = qword_928238; // 0x20
  index_v3 = 0LL;
  while ( (__int64)index_v3 < max_len_v2 )
  {
    qword_9720E8 = 0LL;
    if ( qword_928238 <= index_v3 )
      runtime_panicIndex();
    v14 = off_928230[index_v3];
    runtime_makeslice((__int64)"\b", v14, v14, v10);
    v5 = (__int64 *)v11;
    v19 = (__int64 *)v11;
    v4 = 1LL;
    while ( v4 <= 4 )
    {
      v12 = v4;
      main_wtf(0LL, v4, v5, v14, v14);
      v4 = v12 + 1;
      v5 = v19;
    }
    if ( (unsigned __int64)qword_9720E8 >= 0x11 )
      runtime_panicIndex();
    v6 = *((_BYTE *)&v16 + qword_9720E8);
    v22[0] = &unk_8765E0;                       // output_length
    v22[1] = &qword_9239C0[v6 ^ 0x66u];         // output
    v8 = qword_92EAB0;
    v10 = 2LL;
    v1 = v22;
    fmt_Fprintf(v0, (__int64)v22, (const char *)qword_9239C0);
    index_v3 += 1;
  }

开头的三个CISCN函数是简单的输出,中间生成空的slice然后扔到了wtf函数中,输出是根据索引,从qword_9239C0中选择一个字符。

尝试运行的时候发现运行时间很长,但在程序中没有看到延时的操作,那么这道题应该是一个耗时的算法。

观察发现 wtf 函数是一个递归函数,而 off_928230 中存的就是递归的深度。

尝试找规律,直接将深度patch成 10x20,运行一下。

得到如下结果

Welcome to CISCN 2021!
Here is our free flag for you as a gift:
CISCN{45b3247c45b3247c4

猜测最后的输出是有规律的

cur_time = [ 
    1, 3, 6, 9, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x14, 0x19, 0x1E,
    0x28, 0x42, 0x66, 0x0A0, 0x936, 0x3D21, 0x149A7, 0x243AC, 0x0CB5BE, 0x47DC61, 0x16C0F46, 
    0x262C432, 0x4ACE299, 0x10FBC92A, 0x329ECDFD, 0x370D7470
]
res = ['c', '4', '5', 'b', '3', '2', '4', '7']

print ('CISCN{', end = '')
for c in cur_time:
    print (res[c % len(res)], end = '')
print ('}')

print ('CISCN{4b445b3247c45344c54c44734445452c}')

和最后的正确结果做个对比,发现一样。

Built with Hugo
Theme Stack designed by Jimmy