前几天的XCTF最后一场终于打完了,三场比赛下来对逆向部分的大概感觉是从第一场的啥都不会做(一道lua+一道apk)到后来的终于能有参与度,至少后两场的题目都是pc逆向,虽然特殊架构但好歹能做(tcl。
本文是三场XCTF所有逆向题目的wp+复现整理。赛中拿到了7/10的flag,很多是跟着队里的大佬做出来的(tqltql),这边就试着独立复现一下,至少打完应该长长记性(
P.S. 不会的题暂时标了【TODO】,等wp出了回来填坑= =
比赛官网:XCTF高校网络安全专题挑战赛
[12.20] 华为云专场
Weird_lua【TODO】
【TODO】
lua是第一次接触,.lua文件没法反编译,估计虚拟机被魔改了
divination【TODO】
【TODO】
apk逻辑没看出来,废了废了
[12.23] 鲲鹏计算专场
mips
mips架构。
ida反编译以后可以看到
v4是我们输入的字符串,很明显是迷宫逻辑,上下左右用wasd走,迷宫存在dword_100111F0里。
sub_10000744()这个初始函数是用来找起点用的(就是迷宫中3所在的地方,在后面可以看到3其实表示的是当前位置)。
这里也可以看到应该有多个迷宫(dword_10011D10是用来表示第几个迷宫的,且<=2,一个迷宫有225个数)+一个迷宫宽为15=三个迷宫,每个迷宫为15*15。
然后就是下面的四个函数,随便挑一个出来(比如sub_10000D28())可以看到
很明显是个往右走的函数,3表示当前位置,并把上一个当前位置标为1(可走路径)。并且可以看到终点是4,就是说我们要把每个迷宫从3走到4。
dump迷宫数组,写脚本打印迷宫:
aMap=[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 3, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0]
for i in range(45):
for j in range(15):
if aMap[i*15+j]==0:
tmp='*'
elif aMap[i*15+j]==1:
tmp='.'
elif aMap[i*15+j]==3:
tmp='@'
else:
tmp='#'
print(tmp,end='')
print()
if i==14 or i==29:
print()
可以看到打印出了三个迷宫,为了看得清楚所以选用几个特定字符打印。
.....**********
.....*@*.******
.....*.*.******
.....*.*.******
.....*.*.....**
.....*.*****.**
.....*.*****.**
.....*.*****..*
.....*........*
.....********#*
...............
...............
...............
...............
...............
#sssssssddddddds
..*************
..*@*....******
..*.****.******
..*.****.******
..*..***.....**
..*..*******.**
..*..*******.**
..*..*****....*
..*..*****.**.*
..*..*****.****
..*......*.*..*
..*...........*
..***********#*
...............
...............
#ssssssssssdddddddddds
***************
*@..***********
***.*...*******
***...*.*******
****.**.*******
*..*.**.*******
**...**.*******
*******.*******
*******....****
**********.****
**********.****
**********.****
**********....*
*************.*
*************#*
#ddssddwddssssssdddssssdddss
走迷宫,然后把路径拼起来,根据提示转md5,get flag。
(有个疑惑哈,第二个迷宫理论上说就算是最短路也有多解?是题目出锅了还是我哪里看漏了= =
(再补一句,题目似乎甚至没要求最短路???神奇.jpg
import hashlib
s=b"sssssssdddddddsssssssssssddddddddddsddssddwddssssssdddssssdddss"
print("flag{%s}"%hashlib.md5(s).hexdigest())
。
所以用ChmDecompiler打开re.chm,解压缩,可以看到目录下出现一个包含四个文件的文件夹(其实源文件只有三个,.hhp是ChmDecompiler自动生成的)。
一个一个翻可以看到doc.htm里有一段奇怪的Item1。
大概可以看到是powershell的语法?(感觉像win后门,这么多no的参数
查了一下其实就是把后面那大段进行base64解码而已,用wsl解一下base64有
然后得到了一段.NET代码(白字)。
通过查微软文档可以知道,这里是把base64解码以后的字符进行Deflate解压的过程,所以用脚本把中间那段base64解码,并整理输出。
import base64
import zlib
def deflate(data):
try:
return zlib.decompress(data, -zlib.MAX_WBITS)
except zlib.error:
return zlib.decompress(data)
code='TY5BC4IwGIbvgv9hjB2McJhEhNChJMGTkN2qg7qvFHQT/bL575vpoV2/53n2skJJBInkQG5xwqOqhkcQXCATx7q+gkaHsvYj7kIVvCgburItVgm9MTxbVB5LATp5OlQvb6IMV0LdQvdPpu+8x66SL2eOrMl+Ck7naUA69ggND5UcoEOzI+pUc8p62G3TRZubv34K6IbLespADoGR27vv+R7HpqXzt8Q9y0IJI5N8RLCtLw=='
de_code=deflate(base64.b64decode(code)).decode()
for x in de_code.split('\r\n'):
print(x)
很明显的逻辑了,把doc.chm(应该是原来的re.chm)中"xxxxxxxx"后面的部分提取出来,还是用base64解码得到文件。
把这后面的内容手动复制出来到cont.txt里,进行base64解码,最后存在theFile中。
base64 -d cont.txt > theFile
查看theFile可以猜测是exe(毕竟最开始给的就是有powershell指令的base64),把文件头补上,并改后缀名(即theFile.exe)。
用ida打开,通过FindCrypt插件可以看到AES,跟过去能看到AES加密时的S盒(其实这里前两个都是S盒,第三个是逆S盒),猜测用到了AES加密。
往上回溯找到主函数
显然,这里是AES加密过程,sub_180001100()是密钥拓展过程,sub_1800015B0()是AES加密。
看了一下感觉是原装无魔改的AES,密文密钥都给了,那就直接写脚本解密。
注意这里是以整数形式给出的,别忘了小端序。
from Crypto.Cipher import AES
from binascii import *
arr=[0x16157E2B,0xA6D2AE28,0x8815F7AB,0x3C4FCF09]
key=""
for i in range(4):
key=hex(arr[i])[2:]+key
key=unhexlify(key)[::-1] #注意小端序的问题
tmp=0x46C42084AA2A1B56E799D643453FF4B5
cipher=unhexlify(hex(tmp)[2:])[::-1]
enc=AES.new(key,AES.MODE_ECB)
print(enc.decrypt(cipher))
这里实际上是求最短能得到的路径(15步),懒得想了,直接去网上抓了个现成代码下来改了改。
#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#define maxState 10000
#define N 3
using namespace std;
bool isEqual(int a[N][N][maxState],int b[N][N],int n){
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
if(a[i][j][n] != b[i][j]) return false;
}
}
return true;
}
bool isEqual(int a[N][N],int b[N][N]){
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
if(a[i][j] != b[i][j]) return false;
}
}
return true;
}
int evalute(int state[N][N],int target[N][N]){
int num = 0;
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++)
if(state[i][j] != target[i][j]) num ++;
}
return num;
}
void findBrack(int a[N][N],int x,int y){
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
if(a[i][j] == 0) {
x = i;y = j;return;
}
}
}
}
bool move(int a[N][N],int b[N][N],int dir){
//1 up 2 down 3 left 4 right
int x = 0,y = 0;
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
b[i][j] = a[i][j];
if(a[i][j] == 0) {
x = i;y = j;
}
}
}
if(x == 0 && dir == 1) return false;
if(x == N-1 && dir == 2) return false;
if(y == 0 && dir == 3) return false;
if(y == N-1 && dir == 4) return false;
if(dir == 1){b[x-1][y] = 0;b[x][y] = a[x-1][y];}
else if(dir == 2){b[x+1][y] = 0;b[x][y] = a[x+1][y];}
else if(dir == 3){b[x][y-1] = 0;b[x][y] = a[x][y-1];}
else if(dir == 4){b[x][y+1] = 0;b[x][y] = a[x][y+1];}
else return false;
return true;
}
void statecpy(int a[N][N][maxState],int b[N][N],int n){
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
a[i][j][n] = b[i][j];
}
}
}
void getState(int a[N][N][maxState],int b[N][N],int n){
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
b[i][j] = a[i][j][n];
}
}
}
void statecpy(int a[N][N],int b[N][N]){
for(int i = 0;i < N;i++){
for(int j = 0;j < N;j++)
a[i][j] = b[i][j];
}
}
int checkAdd(int a[N][N][maxState],int b[N][N],int n){
for(int i = 0;i < n;i ++){
if(isEqual(a,b,i)) return i;
}
return -1;
}
int Astar(int a[N][N][maxState],int start[N][N],int target[N][N],int path[maxState]){
bool visited[maxState] = {false};
int fitness[maxState] = {0};
int passLen[maxState] = {0};
int curpos[N][N];
statecpy(curpos,start);
int id = 0,Curid = 0;
fitness[id] = evalute(curpos,target);
statecpy(a,start,id++);
while(!isEqual(curpos,target)){
for(int i = 1;i < 5;i ++){//向四周找方向
int tmp[N][N] = {0};
if(move(curpos,tmp,i)){
int state = checkAdd(a,tmp,id);
if(state == -1){//not add
path[id] = Curid;
passLen[id] = passLen[Curid] + 1;
fitness[id] = evalute(tmp,target) + passLen[id];
statecpy(a,tmp,id++);
}else{//add
int len = passLen[Curid] + 1,fit = evalute(tmp,target) + len;
if(fit < fitness[state]){
path[state] = Curid;
passLen[state] = len;
fitness[state] = fit;
visited[state] = false;
}
}
}
}
visited[Curid] = true;
//找到适应度最小的最为下一个带搜索节点
int minCur = -1;
for(int i = 0;i < id;i ++)
if(!visited[i] && (minCur == -1 || fitness[i] < fitness[minCur])) minCur = i;
Curid = minCur;
getState(a,curpos,Curid);
if(id == maxState) return -1;
}
return Curid;
}
void show(int a[N][N][maxState],int n){
cout << "-------------------------------\n";
for(int i = 0;i < N;i ++){
for(int j =0;j < N;j ++){
cout << a[i][j][n] << " ";
}
cout << endl;
}
cout << "-------------------------------\n";
}
int calDe(int a[N][N]){
int sum = 0;
for(int i = 0;i < N*N;i ++){
for(int j = i+1;j < N*N;j ++){
int m,n,c,d;
m = i/N;n = i%N;
c = j/N;d = j%N;
if(a[c][d] == 0) continue;
if(a[m][n] > a[c][d]) sum ++;
}
}
return sum;
}
void autoGenerate(int a[N][N]){
int maxMove = 50;
srand((unsigned)time(NULL));
int tmp[N][N];
while(maxMove --){
int dir = rand()%4 + 1;
if(move(a,tmp,dir)) statecpy(a,tmp);
}
}
int main(){
int a[N][N][maxState] = {0};
// int start[N][N] = {1,2,3,4,5,6,7,8,0};
// autoGenerate(start);
// cout << start[0][0] << start[1][1];
int start[N][N] = {4,0,3,7,2,6,8,1,5};
int target[N][N] = {1,2,3,4,5,6,7,8,0};
if(!(calDe(start)%2 == calDe(target)%2)){
cout << "无解\n";
return 0;
}
int path[maxState] = {0};
int res = Astar(a,start,target,path);
if(res == -1){
cout << "达到最大搜索能力\n";
return 0;
}
int shortest[maxState] = {0},j = 0;
while(res != 0){
shortest[j++] = res;
res = path[res];
}
cout << "第 0 步\n";
show(a,0);
for(int i = j - 1;i >= 0;i --){
cout << "第 " << j-i << " 步\n";
show(a,shortest[i]);
}
return 0;
}
得到每一步的情况,进而根据switch写出路径。
第 0 步
-------------------------------
4 0 3
7 2 6
8 1 5
-------------------------------
第 1 步
-------------------------------
4 2 3
7 0 6
8 1 5
-------------------------------
第 2 步
-------------------------------
4 2 3
7 1 6
8 0 5
-------------------------------
第 3 步
-------------------------------
4 2 3
7 1 6
8 5 0
-------------------------------
第 4 步
-------------------------------
4 2 3
7 1 0
8 5 6
-------------------------------
第 5 步
-------------------------------
4 2 0
7 1 3
8 5 6
-------------------------------
第 6 步
-------------------------------
4 0 2
7 1 3
8 5 6
-------------------------------
第 7 步
-------------------------------
4 1 2
7 0 3
8 5 6
-------------------------------
第 8 步
-------------------------------
4 1 2
7 5 3
8 0 6
-------------------------------
第 9 步
-------------------------------
4 1 2
7 5 3
0 8 6
-------------------------------
第 10 步
-------------------------------
4 1 2
0 5 3
7 8 6
-------------------------------
第 11 步
-------------------------------
0 1 2
4 5 3
7 8 6
-------------------------------
第 12 步
-------------------------------
1 0 2
4 5 3
7 8 6
-------------------------------
第 13 步
-------------------------------
1 2 0
4 5 3
7 8 6
-------------------------------
第 14 步
-------------------------------
1 2 3
4 5 0
7 8 6
-------------------------------
第 15 步
-------------------------------
1 2 3
4 5 6
7 8 0
-------------------------------
6 左
2 上
4 右
8 下
// 884226886224488
路径为“884226886224488”。
接下来看主函数里check上面的部分,看到sub_409070()实际上是一个scanf,而dword_4A1B60是我们的输入,也就是最后的flag,中间对输入进行处理以后才得到“884226886224488”这个字符串。
在里面翻可以翻到一个sub_400B58(),猜测是base64换表编码。
于是尝试写脚本编码。
import base64
b64table="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
mytable=""
offset=-18
for i in range(len(b64table)):
mytable+=b64table[(i+offset)%len(b64table)]
text="884226886224488".encode()
cipher=base64.b64encode(text).decode().translate(str.maketrans(b64table,mytable))
print(cipher)
试试能不能过check。
wsl运行:(要装qemu才能执行,毕竟特殊架构。
cp $(which qemu-mips) .
./qemu-mips -L . ./puzzle
执行mips程序,输入脚本中解出的字符串,发现成功了,get flag。
然后翻到了一个符合这个特点的密码,Playfair Cipher:
不同的是密码表是直接给出的,不过加密流程再对回ida里的反编译感觉挺像的,于是果断试试。
按照Playfair Cipher的加解密流程写出脚本:
def getIndex(c):
for i in range(len(key)):
if key[i].find(c)!=-1:
return i,key[i].find(c)
letter_list="ABCDEFGHJKLMNOPQRSTUVWXYZ"
key=["CREIH","TQGNU","AOVXL","DZKYM","PBWFS"]
cipher="KIMLXDWRZXTHXTHQTXTXHZWC"
text=""
for i in range(0,len(cipher),2):
j=i+1
x1,y1=getIndex(cipher[i])
x2,y2=getIndex(cipher[j])
if x1==x2:
text+=key[x1][(y1+1)%5]+key[x2][(y2+1)%5]
elif y1==y2:
text+=key[(x1+1)%5][y1]+key[(x2+1)%5][y2]
else:
text+=key[x1][y2]+key[x2][y1]
i+=2
print(text)
走一遍脚本解密可以得到:
YES MAYBE YOU CAN RUN AN ARM PE
看起来能读的通,成功get flag。