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
查看一下从 getchar
到 while
之间的代码,把重复出现的 ++
都合并一下
这里以第一次 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成 1
到 0x20
,运行一下。
得到如下结果
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}')
和最后的正确结果做个对比,发现一样。