去年写了misc和web,今年确实没时间了,就只把逆向和密码写了写。
简单聊一下逆向吧,今年的逆向确实考点种类比去年要繁杂一点。有个魔改的SM4,我也有个自建的密码学库的题。还有很多很好玩的比如rust和cpython逆向,并且包括smc和反调试这些基础题型都有。总体来说还是比较全面的。
不过有一说一啊(我是没想到我rust解出数居然还没cpython多,可能是大家对动态调试不太熟悉的原因?我rust我是真连rust风格都没敢写,全是c的语句用rust写了,本来想用trait和impl去给enum,struct之类的做成类c++那种,想了想算了,就来了点基础的RC4。)
目录
ezre
这道题宛如暴风雨来临前的平静。。。
玩个花点的,本质上还是维吉尼亚,但是key有点小问题。
解密也行,我这儿直接无脑爆破了(乐
data = "QKEMK{7JB5_i5_W3SllD_3z_W3}"
key = "ISCTF"
def enc(word, key):
if 25 >= ord(word) - 65 >= 0:
return chr(65 + (ord(word) - 65 + ord(key) - 65) % 26)
else:
return word
for pos in range(len(data)):
for i in range(0x20, 0xff):
if enc(chr(i), key[pos % 5]) == data[pos]:
print(chr(i),end = "")
桀桀桀
进来之后通过string定位到主函数
交叉引用到主函数
将函数变得稍微好看点
因为每次put完之后都有个相同的函数,很容易想到等价于std::endl。
然后scanf进来了一个字符串,%5s表示的是个字符串,长度为5.也就是无论输入进来的是多长,都会补全为长度为5。
其实看到长度为5也能猜出来说是,毕竟就是ISCTF嘛,典中典。不过也可以z3求解一下。
from z3 import *
sol = Solver()
key = [Int(f"v{
i}") for i in range(5)]
sol.add((key[0] * key[1]) * 2**10 == 0x5eac00)
sol.add((key[1] + 1) * (key[1] - 1) == 0x1ae8)
sol.add(key[2] + key[3] == 151)
sol.add((key[2] * 2**11) - key[3] == 137132)
sol.add(sum(key) == 377)
for i in key:
sol.add(i < 0xff)
sol.add(i > 0)
sol.check()
result = sol.model()
print("".join([chr(result[i].as_long()) for i in key]))
# key = ISCTF
Seed = sum([ord(i) ^ 0xa1 for i in "ISCTF"])
print(Seed)
拿到key和Seed
然后是一个加密函数
在sub_4111B3,也就是图中的RC4(我也不知道为啥脑子懵逼了写的RC4,实际上这儿是个TEA加密)这个函数中,有点花指令,我们nop掉就行了
为了方便演示,我们重新打开一遍这个程序,并把数据库取消掉
双击进去
显然是花指令
按tab键进到汇编页面
一眼看到这儿有个花指令
nop掉就行了,或者快捷键 Ctrl + N
然后我们重建函数
从push ebp(就花指令上边能找到这个,上图也有,在loc_411990下边)
到 retn这个指令(往下找,遇见的第一个就是)
选中他们,然后按P键构造函数
构造完成后,就可以按F5查看汇编了
一眼TEA加密,然后稍微有点变种
这个无所谓,写个脚本解密
#include <iostream>
#include <stdint.h>
using namespace std;
void TEA_decode(uint32_t* v){
unsigned int sum = 8*0x114514 + 0x9E3779B9 * 24;
uint32_t key[4] = {
0x6fc6, 0x69d3, 0x68d5, 0x73cc};
uint32_t key1,key2,key3,key4;
key1 = key[2];
key2 = key[3];
key3 = key[0];
key4 = key[1];
uint32_t delta = 0x114514;
for(int i = 31; i >= 0; i--){
if(i == 23){
delta = 0x9e3779b9;
}
if(i == 15){
key1 = key[0];
key2 = key[1];
key3 = key[2];
key4 = key[3];
}
v[1] -= ((v[0] << 4) + key3) ^ (v[0] + sum) ^ ((v[0] >> 5) + key4);
v[0] -= ((v[1] << 4) + key1) ^ (v[1] + sum) ^ ((v[1] >> 5) + key2);
sum -= delta;
}
}
main函数还有别的内容,所以我们先继续往下看
TEA加密完了用前边的seed置随机数种子了,然后rand产生了个随机数,实际上给到v12了。
然后将加密之后的flag和随机数传到了这个函数
sub_7D128A
这个函数说白了也是个陷阱函数
我们进去,ida只能识别到有个strlen,而且这个strlen还有个坑人的地方,我们后边说
但是当我们进到汇编,就可以发现,后边还藏着一部分
我们来看这三行汇编
首先是将esp + 4,因为又要call函数了,所以这里esp+4
然后这里将eax对应的值给了 [ebp + var_20] 这个地址
其中var_20就是-0x20,我们一会儿还会见到它。
那这个eax的值是什么呢?
实际上就是我们strlen的值,也就是将strlen的值给到了ebp - 0x20这个地址
那接下来,我们就要进到动调的世界,来看看这个call的到底是何方神圣
给这儿下个断点
随便输入个flag值
然后按F7步入
先到这儿
跳到了这里,我们按C键解释为代码
又是call一个函数
我们继续F7
套到了这里
再按一下F7
就进到了实际的函数体了
但是这里ida没有识别出来是个函数
我们需要自己构造一下函数,依旧是跟上边构造函数一样
从loc_6F1BF7下边的mov开始,到retn结束,选中然后按P键
然后这个时候再按F5,就可以看到伪代码了
动调或者分析可知,这个所谓的 (a1 - 44),实际上就是个循环的值 i。
这个a1 - 32,实际上就是上边看到的ebp - 0x20,也就是strlen的值。
而这个a1 + 8,实际上就是经过上边TEA加密后的flag值了。
然后就是a1 - 8和a1 - 20,我们可以在内存里找到他们的值
先确定a1的地址是0xCFFBD4
那么a1 - 8就是0xCFFBCC
找到他的值,是0x3C
同理,a1 - 20是0xCFFBC0
是0x26
这样我们就把值找全了。
让我们把上边的伪代码重新写一下
int i = 0;
int v1 = 0;
while(i < strlen(flag)){
if(i % 2){
v1 = 0x3C ^ flag[i];
}
else{
v1 = 0x26 ^ flag[i];
}
flag[i] = v1;
i++;
}
OK,看似万事俱备只欠东风,直接解密就行了。
但是对于我们这个例子(测试的flag为 ISCTF{aaaaaaaaaaaaaaaaaaaaaaaaa} )
如果你去细心看strlen的值
会发现这里不是0x20,而是0x1A
如果你再换个例子,你会发现这里的值发生改变了。
但我们输入的flag长度就是32啊,为什么会出现问题呢?
实际上这是因为strlen被\x00 给截断了
箭头指的地方就是0x1A,也就是第26个字符。
所以后边的就没有发生加密
而如果我们要判断原flag有没有被截断,只需要看后边一直的那个加密后的数据,中间有没有产生 \x00 就行了。
我们返回出来提取这个v20,先给for打个断点,然后执行到这个断点处(F9)
然后提取v20的值即可
提取32字节,并且发现没有截断,所以循环长度是32
data = [0xB1, 0xD2, 0xF9, 0x7A, 0x83, 0x4C, 0x51, 0x23, 0xB7, 0xAD, 0xA9, 0xBE, 0xE8, 0xFA, 0x24, 0x16, 0x93, 0xFE, 0x42, 0xD7, 0xB0, 0x1F, 0x52, 0xF7, 0x5A, 0x7D, 0x80, 0xE8, 0x28, 0xFC, 0x41, 0x6F]
for i in range(32):
if i % 2 == 1:
data[i] ^= 0x3c
else:
data[i] ^= 0x26
#这里是在处理成标准格式,方便交到上边的TEA的c++程序去解密
result = ", ".join(["0x" + "".join([hex(j)[2:].rjust(2, "0") for j in data[i:i+4][::-1]]) for i in range(0, len(data), 4)])
print(result)
得到结果
0x46dfee97, 0x1f7770a5, 0x828f9191, 0x2a02c6ce, 0xeb64c2b5, 0xcb742396, 0xd4a6417c, 0x5367c00e
然后让我们补齐c++程序的main函数,将上边的值填到data里边
int main(void){
unsigned int data[] = {
0x46dfee97, 0x1f7770a5, 0x828f9191, 0x2a02c6ce, 0xeb64c2b5, 0xcb742396, 0xd4a6417c, 0x5367c00e
};
for(int i = 0; i < 8; i += 2){
TEA_decode(data+i);
}
for(int k = 0;k < 8; k++){
cout<<"0x"<<hex<<data[k]<<", ";
}
return 0;
}
合并起来的程序就是
#include <iostream>
#include <stdint.h>
using namespace std;
void TEA_decode(uint32_t* v){
unsigned int sum = 8*0x114514 + 0x9E3779B9 * 24;
uint32_t key[4] = {
0x6fc6, 0x69d3, 0x68d5, 0x73cc};
uint32_t key1,key2,key3,key4;
key1 = key[2];
key2 = key[3];
key3 = key[0];
key4 = key[1];
uint32_t delta = 0x114514;
for(int i = 31; i >= 0; i--){
if(i == 23){
delta = 0x9e3779b9;
}
if(i == 15){
key1 = key[0];
key2 = key[1];
key3 = key[2];
key4 = key[3];
}
v[1] -= ((v[0] << 4) + key3) ^ (v[0] + sum) ^ ((v[0] >> 5) + key4);
v[0] -= ((v[1] << 4) + key1) ^ (v[1] + sum) ^ ((v[1] >> 5) + key2);
sum -= delta;
}
}
int main(void){
unsigned int data[] = {
0x46dfee97, 0x1f7770a5, 0x828f9191, 0x2a02c6ce, 0xeb64c2b5, 0xcb742396, 0xd4a6417c, 0x5367c00e
};
for(int i = 0; i < 8; i += 2){
TEA_decode(data+i);
}
for(int k = 0;k < 8; k++){
cout<<"0x"<<hex<<data[k]<<", ";
}
return 0;
}
运行,得到输出结果
0x54435349, 0x30797b46, 0x4e6b5f55, 0x525f7730, 0x30446e41, 0x4e415f6d, 0x33375f64, 0x7d212161
同理,将值扔到python脚本中
from libnum import n2s
msg = [0x54435349, 0x30797b46, 0x4e6b5f55, 0x525f7730, 0x30446e41, 0x4e415f6d, 0x33375f64, 0x7d212161]
flag = "".join([n2s(i)[::-1].decode('utf-8') for i in msg])
print(flag)
得到flag:
ISCTF{y0U_kN0w_RAnD0m_ANd_73a!!}
第二章-当记忆被割裂
这题有点像ollvm,但不完全像
我是没恢复出来他的反汇编
但是跟着函数一个一个走就能看到具体的东西了
主要的加密逻辑在enc这个函数里边
从上到下分别是调用了这5个函数
其中分析一下就能看到是有个for循环一样的i值,然后不断在对data进行计算。
写出来python代码
data ^= (i + 102) ^ 82
data += 6
data ^= i + key[i]
return data
然后将key和数据进行解密就得到flag了
key = [ord(i) for i in "i_can_reverse_but_i_can_not_have_you"]
ans = [0xEA, 0x0C, 0x1A, 0x11, 0xF6, 0x2C, 0x1D, 0x3E, 0x17, 0x35, 0x31, 0x29, 0xF4, 0x39, 0x39, 0xD3, 0xC3, 0x2D, 0x00, 0x10, 0x30, 0x3D, 0xCC, 0x00, 0xD3, 0xC0, 0x4B, 0xC6, 0x11, 0xC7, 0x29, 0x3E, 0xBA, 0x60, 0x90, 0x34]
for pos, elem in enumerate(ans):
temp = elem ^ (key[pos % len(key)] + pos)
temp -= 6
temp ^= (102 + pos) ^ 82
print(chr(temp & 0xff),end="")
ISCTF{as_her_never_will_come_back!!}
第三章-逃不出的黑墙
这道题有一说一感觉是这三道题里边最简单的了)
就是一个非常简单的迷宫,只不过迷宫对应的字符变了
我觉得甚至不逆向程序,直接找对应的字符解迷宫就行。乐
就只贴一张我修复过的ida的图了
我取消了很多中间变量(快捷键是 = 号)
目的就是跑到e那边
把maze提取出来
然后用cyberchef处理一下,变成程序可以识别的墙壁(墙 = # = 1)(路 = . = 0)
我们会发现,右边和下边少了两面墙
隐藏通过程序给他补上
maze =
maze = ["[" + i[:-1] + ',1],\\' for i in maze] # 格式化并补上右侧的墙
maze.append(maze[0]) # 补上下边的墙
print("\n".join(maze))
然后写个解迷宫的程序就行
dirs=[(0,1),(1,0),(0,-1),(-1,0)] #当前位置四个方向的偏移量
path=[] #存找到的路径
def mark(maze,pos): #给迷宫maze的位置pos标"2"表示“倒过了”
# print()
maze[pos[0]][pos[1]]=2
def passable(maze,pos): #检查迷宫maze的位置pos是否可通行
return maze[pos[0]][pos[1]]==0
def find_path(maze,pos,end):
mark(maze,pos)
if pos==end:
print(pos,end=" ") #已到达出口,输出这个位置。成功结束
path.append(pos)
return True
for i in range(4): #否则按四个方向顺序检查
nextp=pos[0]+dirs[i][0],pos[1]+dirs[i][1]
#考虑下一个可能方向
if passable(maze,nextp): #不可行的相邻位置不管
if find_path(maze,nextp,end):#如果从nextp可达出口,输出这个位置,成功结束
print(pos,end=" ")
path.append(pos)
return True
return False
def see_path(maze,path): #使寻找到的路径可视化
for i,p in enumerate(path):
if i==0:
maze[p[0]][p[1]] ="E"
elif i==len(path)-1:
maze[p[0]][p[1]]="S"
else:
maze[p[0]][p[1]] =3
print("\n")
for r in maze:
for c in r:
if c==3:
print('\033[0;31m'+"*"+" "+'\033[0m',end="")
elif c=="S" or c=="E":
print('\033[0;34m'+c+" " + '\033[0m', end="")
elif c==2:
print('\033[0;32m'+"#"+" "+'\033[0m',end="")
elif c==1:
print('\033[0;;40m'+" "*2+