moectf补题
XDSEC/MoeCTF_2022:
MoeCTF 2022 Challenges and Writeups (github.com)
reverse
EquationPy
python逆向
pyc与py
在写数据库上机作业时,已经遇见过pyc文件了
1 2 3 4 5 6 7 ├─admin │ │ admin.py │ │ __init__.py │ │ │ └─__pycache__ │ admin.cpython-38.pyc │ __init__.cpython-38.pyc
所有pyc文件都放到__pycache__
目录下面,且pyc文件都是和其父目录中的py文件一一对应的
比如这里admin.py对应到admin.cpython-38.pyc
既然cache是缓存的意思,那么可以推测,pyc是py文件编译形成的,用来加速运行的.因为直接运行编译好的二进制文件必然比解释运行py源文件快
查阅资料:
pyc是一种二进制文件,是由py文件经过编译后,生成的文件,是一种byte
code,py文件变成pyc文件后,加载的速度有所提高,而且pyc是一种跨平台的字节码,是由python的虚拟机来执行的,这个是类似于JAVA或者.NET的虚拟机的概念。pyc的内容,是跟python的版本相关的,不同版本编译后的pyc文件是不同的,3.7编译的pyc文件,3.6版本的
python是无法执行的。
python命令行编译
1 python -m py_compile main.py
这条命令执行之后就会在当前目录下生成一个子目录__pycache__
,其中就有main.cpython-38.pyc文件
这个玩意儿长啥样?
image-20221110123037633
反编译
可以使用在线网站 反编译
也可以使用uncompyle6反编译,但是在我电脑上直接卡死
用在线网站反编译之后得到的是一个
1 2 3 4 5 6 7 8 9 10 11 print ('Maybe z3 can help you solve this challenge.' )print ('Now give me your flag, and I will check for you.' )flag = input ('Input your flag:' ) if len (flag) == 22 and ord (flag[0 ]) * 7072 + ord (flag[1 ]) * 2523 + ord (flag[2 ]) * 6714 + ord (flag[3 ]) * 8810 + ord (flag[4 ]) * 6796 + ord (flag[5 ]) * 2647 + ord (flag[6 ]) * 1347 + ord (flag[7 ]) * 1289 + ord (flag[8 ]) * 8917 + ord (flag[9 ]) * 2304 + ord (flag[10 ]) * 5001 + ord (flag[11 ]) * 2882 + ord (flag[12 ]) * 7232 + ord (flag[13 ]) * 3192 + ord (flag[14 ]) * 9676 + ord (flag[15 ]) * 5436 + ord (flag[16 ]) * 4407 + ord (flag[17 ]) * 6269 + ord (flag[18 ]) * 9623 + ord (flag[19 ]) * 6230 + ord (flag[20 ]) * 6292 + ord (flag[21 ]) * 57 == 10743134 and ...... and ord (flag[0 ]) * 5926 + ord (flag[1 ]) * 9095 + ord (flag[2 ]) * 2048 + ord (flag[3 ]) * 4639 + ord (flag[4 ]) * 3035 + ord (flag[5 ]) * 9560 + ord (flag[6 ]) * 1591 + ord (flag[7 ]) * 2392 + ord (flag[8 ]) * 1812 + ord (flag[9 ]) * 6732 + ord (flag[10 ]) * 9454 + ord (flag[11 ]) * 8175 + ord (flag[12 ]) * 7346 + ord (flag[13 ]) * 6333 + ord (flag[14 ]) * 9812 + ord (flag[15 ]) * 2034 + ord (flag[16 ]) * 6634 + ord (flag[17 ]) * 1762 + ord (flag[18 ]) * 7058 + ord (flag[19 ]) * 3524 + ord (flag[20 ]) * 7462 + ord (flag[21 ]) * 11 == 11118093 : print ('Congratulate!!!You are right!' ) else : print ('What a pity...Please try again >__<' )
中间省去了一大段约束条件,因为没有缩进,格式很乱,可以在一个and上ctrl+f2选择所有and,然后移动光标到and最后按回车,相对于刚才稍微明朗点了
image-20221110161830590
实际上整个是一个22元一次方程组,怎么解这个方程组呢?z3
Z3模块
z3用于在约束条件下求一组解
python安装z3模块
官网z3-solver ·
PyPI
之后就可以在python中使用了
使用
从CTF入门z3 solver -
翻车鱼 (shi1011.cn)
[原创]Z3求解约束器及例题-CTF对抗-看雪论坛-安全社区|安全招聘|bbs.pediy.com
Z3Py
Guide (ericpony.github.io)
Z3简介及在逆向领域的应用
- 腾讯云开发者社区-腾讯云 (tencent.com)
基本类型
Z3类型
意义
Int
整数
Bool
布尔
Array
数组
BitVec
位域
Real
实数
基本语句
基本语句
意义
例子
Solver
创建一个求解器,可以添加约束条件
s=Solver()
add
添加约束条件
check
检查在给定约束条件下是否有解
model
求出满足所有约束条件的一个解
比如想要求解椭圆\(\frac{x^2}{25}+\frac{y^2}{16}=1\) 与直线\(y=x-1\) 在y轴右侧的一个焦点
1 2 3 4 5 6 7 8 9 from z3 import *x=Real('x' ) y=Real('y' ) s=Solver() s.add(x*x/25 +y*y/16 ==1 ) s.add(y==x-1 ) s.add(x>0 ) if (s.check()==sat): print (s.model())
1 [x = 3.6949050343?, y = 2.6949050343?]
ISCC-2018 reverse-My math is
bad
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall main (int a1, char **a2, char **a3) { puts ("=======================================" ); puts ("= Welcome to the flag access machine! =" ); puts ("= Input the password to login ... =" ); puts ("=======================================" ); __isoc99_scanf("%s" , input); if ( (unsigned int )check() ) { puts ("Congratulations! You should get the flag..." ); flag(); } else { puts ("Wrong password!" ); } return 0LL ; }
全局位置有一个字符串input,首先check会检查input,如果通过,则调用flag函数给出flag,其中check和flag中都有加密
这个输入的字符串input在bss节[0x6020A0,0x6020C0)
image-20221002154256280
check
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 __int64 __fastcall check () { __int64 result; __int64 v1; __int64 v2; __int64 v3; __int64 v4; __int64 v5; __int64 v6; __int64 v7; __int64 v8; __int64 v9; __int64 v10; __int64 v11; __int64 v12; if ( strlen (input) != 32 ) return 0LL ; v1 = unk_6020B0; v2 = unk_6020B4; v3 = unk_6020B8; v4 = unk_6020BC; if ( dword_6020A4 * (__int64)*(int *)input - dword_6020AC * (__int64)dword_6020A8 != 0x24CDF2E7C953DA56 LL ) goto LABEL_12; if ( 3LL * dword_6020A8 + 4LL * dword_6020AC - dword_6020A4 - 2LL * *(int *)input != 0x17B85F06 ) goto LABEL_12; if ( 3 * *(int *)input * (__int64)dword_6020AC - dword_6020A8 * (__int64)dword_6020A4 != 0x2E6E497E6415CF3E LL ) goto LABEL_12; if ( 27LL * dword_6020A4 + *(int *)input - 11LL * dword_6020AC - dword_6020A8 != 0x95AE13337 LL ) goto LABEL_12; srand(dword_6020A8 ^ dword_6020A4 ^ *(_DWORD *)input ^ dword_6020AC); v5 = rand() % 50 ; v6 = rand() % 50 ; v7 = rand() % 50 ; v8 = rand() % 50 ; v9 = rand() % 50 ; v10 = rand() % 50 ; v11 = rand() % 50 ; v12 = rand() % 50 ; if ( v4 * v6 + v1 * v5 - v2 - v3 != 0xE638C96D3 LL ) goto LABEL_12; if ( v4 + v1 + v3 * v8 - v2 * v7 == 0xB59F2D0CB LL && v1 * v9 + v2 * v10 - v3 - v4 == 0xDCFE88C6D LL && v3 * v12 + v1 - v2 - v4 * v11 == 0xC076D98BB LL ) { result = 1LL ; } else { LABEL_12: result = 0LL ; } return result; }
前四个约束条件组成方程组可以确定输入的前四个双字,
后面的srand种子就是前四个双字的异或,也可以确定了,然后产生的v5~v12随机数也可以确定了,
下面的运算又可以根据四个约束条件确定后四个双字
1 2 if ( dword_6020A4 * (__int64)*(int *)input - dword_6020AC * (__int64)dword_6020A8 != 0x24CDF2E7C953DA56 LL ) goto LABEL_12;
第一个双字和第二个双字的积减去第三个双字和第四个双字的积得是0x24CDF2E7C953DA56
1 2 3 if ( 3LL * dword_6020A8 + 4LL * dword_6020AC - dword_6020A4 - 2LL * *(int *)input != 0x17B85F06 ) goto LABEL_12;
\[
(3*input[8:11]+4*input[12:15])-(1*input[4:7]+2*input[0:3])=17B85F06h
\]
前四个约束条件就可以写成:
1 2 3 4 dword0*dword1-dword2*dword3==0x24CDF2E7C953DA56 (3*dword2+4*dword3)-(1*dword1+2*dword0)==0x17B85F06 3*dword0*dword3-dword2*dword1==0x2E6E497E6415CF3E (27*dword1+dword0)-(11*dword3+dword2)==0x95AE13337
写z3脚本解方程
1 2 3 4 5 6 7 8 9 10 11 12 13 from z3 import *dword0=Int('dword0' ) dword1=Int('dword1' ) dword2=Int('dword2' ) dword3=Int('dword3' ) solve( dword0*dword1-dword2*dword3==0x24CDF2E7C953DA56 , (3 *dword2+4 *dword3)-(1 *dword1+2 *dword0)==0x17B85F06 , 3 *dword0*dword3-dword2*dword1==0x2E6E497E6415CF3E , (27 *dword1+dword0)-(11 *dword3+dword2)==0x95AE13337 , )
运行结果
1 2 3 4 5 PS C:\Users\86135 \Desktop\reverse> python exp.py [dword3 = 862734414 , dword1 = 1801073242 , dword2 = 829124174 , dword0 = 1869639009 ]
下面解出v5~v12的随机数值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream> using namespace std;int dword3 = 862734414 ;int dword1 = 1801073242 ;int dword2 = 829124174 ;int dword0 = 1869639009 ;int main () { srand (dword1 ^ dword2 ^ dword3 ^ dword0); int v5 = rand () % 50 ; int v6 = rand () % 50 ; int v7 = rand () % 50 ; int v8 = rand () % 50 ; int v9 = rand () % 50 ; int v10 = rand () % 50 ; int v11 = rand () % 50 ; int v12 = rand () % 50 ; cout<<v5<<endl<<v6<<endl<<v7<<endl<<v8<<endl<<v9<<endl<<v10<<endl<<v11<<endl<<v12<<endl; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/reverse] └─# g++ main.cpp -O0 -o main -m32 ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/reverse] └─# ./main 22 39 45 45 35 41 13 36
下面又有四个约束条件
1 2 3 4 v4 * v6 + v1 * v5 - v2 - v3 == 0xE638C96D3LL v4 + v1 + v3 * v8 - v2 * v7 == 0xB59F2D0CBLL v1 * v9 + v2 * v10 - v3 - v4 == 0xDCFE88C6DLL v3 * v12 + v1 - v2 - v4 * v11 == 0xC076D98BBLL
目标是解出v1~v4,对应输入的后四个双字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from z3 import *v5=22 v6=39 v7=45 v8=45 v9=35 v10=41 v11=13 v12=36 v1=Int('v1' ) v2=Int('v2' ) v3=Int('v3' ) v4=Int('v4' ) solve( v4 * v6 + v1 * v5 - v2 - v3 == 0xE638C96D3 , v4 + v1 + v3 * v8 - v2 * v7 == 0xB59F2D0CB , v1 * v9 + v2 * v10 - v3 - v4 == 0xDCFE88C6D , v3 * v12 + v1 - v2 - v4 * v11 == 0xC076D98BB , )
1 2 3 4 5 PS C:\Users\86135 \Desktop\reverse> python exp2.py [v1 = 811816014 , v4 = 1195788129 , v2 = 828593230 , v3 = 1867395930 ]
到此输入全部被解出了,将其转换为字符串
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 #include <iostream> using namespace std ; int dwords[8 ] = { 1869639009 , 1801073242 , 829124174 , 862734414 , 811816014 , 828593230 , 1867395930 , 1195788129 }; struct Bitfield { char bytes[4 ]; int aInt; Bitfield(const int &x){ bytes[0 ]=(unsigned char )x; bytes[1 ]=(unsigned char )(x>>8 ); bytes[2 ]=(unsigned char )(x>>16 ); bytes[3 ]=(unsigned char )(x>>24 ); } friend ostream &operator<<(ostream &os,const Bitfield &bf){ for (int i=0 ;i<4 ;++i){ os<<bf.bytes[i]; } return os; } }; int main () { for (int i=0 ;i<8 ;++i){ Bitfield bf (dwords[i]) ; cout <<bf; } return 0 ; }
1 2 3 PS C:\Users\86135\Desktop\reverse> g++ main2.cpp -O0 -o main2 PS C:\Users\86135\Desktop\reverse> ./main2 ampoZ2ZkNnk1NHl3NTc0NTc1Z3NoaGFG
然后输入到程序就可以得到flag
1 2 3 4 5 6 7 8 9 ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/reverse] └─# ./mathbad ======================================= = Welcome to the flag access machine! = = Input the password to login ... = ======================================= ampoZ2ZkNnk1NHl3NTc0NTc1Z3NoaGFG Congratulations! You should get the flag... flag{th3_Line@r_4lgebra_1s_d1fficult!}
回到本题
要使用z3模块解体,首先需要注册变量,比如x=Int("x")
这种
而现在22个变量如果手动更换成x,y,z这种字母,太浪费时间,太死脑筋
师傅直接用循环注册了一个flag变量数组
1 2 flag = [Int("input[%d]" %i) for i in range (22 )] s = Solver()
然后怎么添加约束条件呢?利用这个and,再每个等式前面和后面都加上点东西
就要这种光标排山倒海的效果
image-20221110163329271
把原来有的ord函数也去掉
1 2 3 4 5 6 7 8 9 10 from z3 import *flag=[Int("flag[%d]" %i)for i in range (22 )] s=Solver() s.add((flag[0 ]) * 7072 + (flag[1 ]) * 2523 + (flag[2 ]) * 6714 + (flag[3 ]) * 8810 + (flag[4 ]) * 6796 + (flag[5 ]) * 2647 + (flag[6 ]) * 1347 + (flag[7 ]) * 1289 + (flag[8 ]) * 8917 + (flag[9 ]) * 2304 + (flag[10 ]) * 5001 + (flag[11 ]) * 2882 + (flag[12 ]) * 7232 + (flag[13 ]) * 3192 + (flag[14 ]) * 9676 + (flag[15 ]) * 5436 + (flag[16 ]) * 4407 + (flag[17 ]) * 6269 + (flag[18 ]) * 9623 + (flag[19 ]) * 6230 + (flag[20 ]) * 6292 + (flag[21 ]) * 57 == 10743134 ) ... s.add((flag[0 ]) * 5926 + (flag[1 ]) * 9095 + (flag[2 ]) * 2048 + (flag[3 ]) * 4639 + (flag[4 ]) * 3035 + (flag[5 ]) * 9560 + (flag[6 ]) * 1591 + (flag[7 ]) * 2392 + (flag[8 ]) * 1812 + (flag[9 ]) * 6732 + (flag[10 ]) * 9454 + (flag[11 ]) * 8175 + (flag[12 ]) * 7346 + (flag[13 ]) * 6333 + (flag[14 ]) * 9812 + (flag[15 ]) * 2034 + (flag[16 ]) * 6634 + (flag[17 ]) * 1762 + (flag[18 ]) * 7058 + (flag[19 ]) * 3524 + (flag[20 ]) * 7462 + (flag[21 ]) * 11 == 11118093 ) if (s.check()==sat): result = s.model() for i in range (22 ): print (result[flag[i]])
这样写完之后,最终打印的是result[flag[i]]
,不是result[i]
,这是因为一开始是将flag[0]注册为第一个变量,flag[1]注册为第二个变量
这样打印出来的是字符的ascii值,怎么才能打印出字符呢?
1 print (chr (int (str (m[flag[i]]))),end = '' )
为了搞明白为啥需要这么多类型转换,总结一下python中int,byte,char,str之间的转换函数
python中的类型转换
函数
说明
int(x [,base ])
将字符串x转换为⼀个整数
float(x )
将字符串x转换为⼀个浮点数
complex(real [,imag ])
创建⼀个复数,real为实部,imag为虚部
str(x )
将对象 x 转换为字符串
repr(x )
将对象 x 转换为表达式字符串
eval(str )
⽤来计算在字符串中的有效Python表达式,并返回⼀个对象
tuple(s )
将序列 s 转换为⼀个元组
list(s )
将序列 s 转换为⼀个列表
chr(x )
将⼀个整数转换为⼀个Unicode字符
ord(x )
将⼀个字符转换为它的ASCII整数值
hex(x )
将⼀个整数转换为⼀个⼗六进制字符串
oct(x )
将⼀个整数转换为⼀个⼋进制字符串
bin(x )
将⼀个整数转换为⼀个⼆进制字符串
对于int(str,[,base])
,base通过一个整数值指定将str理解为10进制或者16进制的字面量进行转换
1 2 3 4 5 6 7 8 >>> int ("10" ,10 )10 >>> int ("0x10" ,10 )Traceback (most recent call last): File "<stdin>" , line 1 , in <module> ValueError: invalid literal for int () with base 10 : '0x10' >>> int ("0x10" ,16 )16
如果str="0x10"带了0x前缀,此时base=16可以将str转换为16进制整数,但是尝试转换为10进制就会出错
但是如果str="0x10"则既可以识别为10进制,也可以识别为16进制
回过头来理解print(chr(int(str(m[flag[i]]))),end = '')
这样写首先将一个整数转为str然后又转为int,看似是转了个寂寞,其实一个函数也不能少
chr接收一个int,但是m[flag[i]]
实际上是Solver.model()的返回值,是一个modelRef引用.为啥不是整数?因为Solver还可以求解浮点数方程组.因此需要对modelRef不能直接丢给chr
并且modelRef也没有直接丢给int函数,这是因为int函数只接受str作为参数,modelRef没有像str的自动类型转换,因此首先需要调用一个str函数将modelRef转为字符串类型
为啥str函数可以接收这个不是python自己的,而是来自其他模块的类型modelRef?
因为str的参数可以是任何obj对象,显然modelRef是一个对象,可以被str通吃
str干了啥?
str函数会调用该类的__str__
函数,如果没有显式定义这个函数,则隐式的__str__
函数会将对象的地址信息转换成字符串返回给str函数,然后str再将这个值返回给用户,比如
1 <__main__.A object at 0x0000026784BF8730 >
如果有显式实现__str__
函数,则str会获得该函数的返回值,比如
1 2 3 4 5 6 7 8 9 class A : a='' def __init__ (x ): a=x def __str__ (self ) -> str : return "123" a=A() print (str (a))
这样运行结果就是123
如果还重载了__repr__
函数,则str函数首先调用显式的__repr__
函数获得返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class A : a='' def __init__ (x ): a=x def __repr__ (self ) -> str : return "123" def __str__ (self ) -> str : return "456" a=A() print (str (a))
运行结果:456
因此可以估计,Solver.model()函数返回的东西,要么重载了__str__
函数,要么重载了__repr__
函数
1 2 3 4 5 def model (self ): try : return ModelRef(Z3_solver_get_model(self .ctx.ref(), self .solver), self .ctx) except Z3Exception: raise Z3Exception("model is not available" )
如果程序没有问题则返回了一个ModelRef类型的对象
1 2 3 4 5 class ModelRef (Z3PPObject ): ... def __repr__ (self ): return obj_to_string(self ) ...
这里调用了一个obj_to_string函数,这个函数也是z3模块提供的
1 2 3 4 def obj_to_string (a ): out = io.StringIO() _PP(out, _Formatter(a)) return out.getvalue()
这里z3模块函数Formatter会根据z3对象a的实际类型,决定返回什么样的字符串
D_flat
给了好多个文件
1 2 3 4 5 6 7 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2022/7/19 18:10 410 D_flat.deps.json -a---- 2022/7/19 18:11 5632 D_flat.dll -a---- 2022/7/19 18:11 149504 D_flat.exe -a---- 2022/7/19 18:11 10384 D_flat.pdb -a---- 2022/7/19 18:10 253 D_flat.runtimeconfig.json
我认识的有
dll,动态链接库
exe,主程序,入口点
pdb,符号文件,ida调试时可以载入
我认识json是字符串存储的js对象,但是不知道这里两个json文件可以干啥
用exeinfope查dll文件的壳发现是C#编写的,
用ida64打开D_flat.exe之后就傻眼了,看不到一点flag的痕迹,感觉都是什么.NET平台还有C#那一套东西的环境初始化
师傅说用dnspy反汇编D_flat.dll,刚用dnspy蒙蔽的很,但是转到入口点五个字我还是认得的
image-20221110180846574
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 using System;using System.Text;internal class D_flate { private static void Main () { int f = 0 ; int [] flag = new int [] { 109 , 111 , ... 33 , 125 }; Console.WriteLine("In music theory, there is a note that has the same pitch as D flat." ); Console.WriteLine("Do you know it?\nNow plz input your flag:" ); string input = Console.ReadLine(); byte [] byteArray = Encoding.ASCII.GetBytes(input); for (int i = 0 ; i < input.Length; i++) { if (flag[i] == (int )byteArray[i]) { f++; } } if (f == flag.Length) { Console.WriteLine("TTTTTQQQQQQLLLLLLL!!! This is your flag!" ); return ; } Console.WriteLine("QwQ, plz try again." ); } }
怎么C#程序长得跟java这么像?类里面才能有main函数是吧?
实际上逻辑很简单,就是输入一串和这个flag数组比一下,如果一模一样则成功
解法也很简单
1 2 3 4 5 6 7 8 9 10 11 #include <iostream> #include <vector> using namespace std;vector<char > flag = {109 , 111 , 101 , 99 , 116 , 102 , 123 , 68 , 95 , 102 , 108 , 97 , 116 , 101 , 95 , 105 , 115 , 95 , 67 , 95 , 115 , 104 , 97 , 114 , 112 , 33 , 125 }; int main () { for (auto c : flag) { cout << c; } }
1 2 3 PS C:\Users\86135 \Desktop\moectfwp\MoeCTF_2022-main\Challenges\Reverse\D_flat\D_flat> g++ main.cpp -o main PS C:\Users\86135 \Desktop\moectfwp\MoeCTF_2022-main\Challenges\Reverse\D_flat\D_flat> ./main moectf{D_flate_is_C_sharp!}
解是解出来了,但是还有几个问题,
1.exe文件是干啥的?主要逻辑都放在dll中了,要exe有何用?
2.我怎么上来就知道应该去dll中找茬?
3.exe中干了啥
chicken_soup
花指令,当时只知道这是花指令,不知道怎么处理花指令,幸好逻辑清晰,看汇编也能分析
实际上把花指令都改成nop指令就可以让ida正确反编译
直接用ida打开程序反编译main函数,输入input要求是38个字符,经过两个加密函数,encrypt1,encrypt2之后,与密文target比较,如果一模一样就通过
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 int __cdecl main (int argc, const char **argv, const char **envp) { int result; char input[100 ]; puts ("I poisoned the program... Can you reverse it?!" ); puts ("Come on! Give me your flag:" ); scanf ("%s" , (char )input); if ( strlen (input) == 38 ) { ((void (__cdecl *)(char *))encrypt1)(input); ((void (__cdecl *)(char *))encrypt2)(input); if ( strcmp (input, &target) ) puts ("\nTTTTTTTTTTQQQQQQQQQQQQQLLLLLLLLL!!!!" ); else puts ("\nQwQ, please try again." ); result = 0 ; } else { puts ("\nQwQ, please try again." ); result = 0 ; } return result; }
target一直,那么问题转化为求两个加密函数的逆
然而这个encrypt1并没有被ida识别为函数,他长得特别丑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .text:00401000 encrypt1: ; CODE XREF: _main+85↓p .text:00401000 push ebp .text:00401001 mov ebp, esp .text:00401003 sub esp, 14h .text:00401006 push ebx .text:00401007 push esi .text:00401008 push edi .text:00401009 jz short near ptr loc_40100D+1 .text:0040100B jnz short near ptr loc_40100D+1 .text:0040100D .text:0040100D loc_40100D: ; CODE XREF: .text:00401009↑j .text:0040100D ; .text:0040100B↑j .text:0040100D jmp near ptr 13855D9h .text:0040100D ; --------------------------------------------------------------------------- .text:00401012 align 4 .text:00401014 dd 8B09EB00h, 0C083F845h, 0F8458901h, 89084D8Bh, 558BF44Dh .text:00401014 dd 1C283F4h, 8BF05589h, 88AF445h, 83FF4D88h, 8001F445h .text:00401014 dd 7500FF7Dh, 0F4558BEEh, 89F0552Bh, 458BEC55h, 1E883ECh .text:00401014 dd 73F84539h, 84D8B1Fh, 0FF84D03h, 8B0151B6h, 45030845h .text:00401014 dd 8B60FF8h, 558BCA03h, 0F8550308h, 0A3EB0A88h, 8B5B5E5Fh .text:00401014 dd 0CCC35DE5h, 0CCCCCCCCh .text:00401080 ; ---------------------------------------------------------------------------
00401009
和0040100B这两个地方的指令很奇怪,跳转到同一个地方,loc_40100D+1,也就是说越过了loc_40100D这个地方的"花指令"
怎么让ida看起来正常点?可以在00401009下断点然后动态调试过去,此时.text:0026100D
db 0E9h这条花指令就非常明显了,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .text:00261000 encrypt1: ; CODE XREF: _main+85↓p .text:00261000 push ebp .text:00261001 mov ebp, esp .text:00261003 sub esp, 14h .text:00261006 push ebx .text:00261007 push esi .text:00261008 push edi .text:00261009 jz short loc_26100E .text:0026100B jnz short loc_26100E .text:0026100B ; --------------------------------------------------------------------------- .text:0026100D db 0E9h .text:0026100E ; --------------------------------------------------------------------------- .text:0026100E .text:0026100E loc_26100E: ; CODE XREF: .text:00261009↑j .text:0026100E ; .text:0026100B↑j .text:0026100E mov dword ptr [ebp-8], 0 .text:00261015 jmp short loc_261020 .text:00261017 ; --------------------------------------------------------------------------- .text:00261017 .text:00261017 loc_261017: ; CODE XREF: .text:00261072↓j ...
把它改成nop空操作指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .text:00261000 encrypt1: ; CODE XREF: _main+85↓p .text:00261000 push ebp .text:00261001 mov ebp, esp .text:00261003 sub esp, 14h .text:00261006 push ebx .text:00261007 push esi .text:00261008 push edi .text:00261009 jz short loc_26100E .text:0026100B jnz short loc_26100E .text:0026100D nop .text:0026100E .text:0026100E loc_26100E: ; CODE XREF: .text:00261009↑j .text:0026100E ; .text:0026100B↑j .text:0026100E mov dword ptr [ebp-8], 0 .text:00261015 jmp short loc_261020
然后在encrypt1上按p键转化为函数,就可以用F5反编译了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 unsigned int __cdecl encrypt1 (const char *a1) { unsigned int result; unsigned int i; for ( i = 0 ; ; ++i ) { result = strlen (a1) - 1 ; if ( i >= result ) break ; a1[i] += a1[i + 1 ]; } return result; }
一目了然,赏心悦目
出题人怎么出的题?用VC++内联汇编写的
1 2 3 4 5 6 7 8 9 10 11 void enc1 (unsigned char * input) { __asm { jz label jnz label _emit 0xe9 label: } for (int i = 0 ; i < strlen ((const char *)input) - 1 ; i++) input[i] += input[i + 1 ]; }
emit
指令直接在代码段插入数据,反汇编器会直接把插入的数据也当成代码,尝试反汇编,发现没有对应任何指令,寄了.这就是花指令
前面两个jz,jnz保证跳过emit定义的数据,将控制转移到label,顺势执行下面的for循环了
fake_key
分析fake_key.exe
当时通了,但是有一个疑问是,key是啥时候被修改的
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 int __cdecl main (int argc, const char **argv, const char **envp) { char input[112 ]; int lenInput; int lenStr; int j; int i; sub_401800(argc, argv, envp); lenStr = strlen (key); puts ("I changed the key secretly, you can't find the right key!" ); puts ("And I use random numbers to rot my input, you can never guess them!" ); puts ("Unless you debug to get the key and random numbers..." ); puts ("Now give me your flag:" ); scanf ("%s" , input); lenInput = strlen (input); for ( i = 0 ; i < lenInput; ++i ) input[i] ^= key[i % lenStr]; for ( j = 0 ; j < lenInput; ++j ) input[j] += rand() % 10 ; if ( (unsigned int )strcmp (input, &target) ) puts ("\nRight! TTTTTQQQQQLLLLL!!!" ); else puts ("QwQ, plz try again." ); return 0 ; }
这里key一开始是"yunzh1jun"字样
1 2 .data:0000000000403040 key db 'yunzh1jun',0 ; DATA XREF: sub_401550+5↑o .data:0000000000403040 ; sub_401550+2A↑o ...
题目中提示"I changed the key
secretly",偷着把key改了,当时在key上看的交叉引用发现确实有个函数会修改key
image-20221110191538085
去sub_401550函数看看
1 2 3 4 5 6 7 8 char *sub_401550 () { char *result; result = &Str[strlen (Str)]; strcpy (result, "TCL,trackYYDS" ); return result; }
也就是在原来key后附加了"TCL,trackYYDS".
现在的问题是,这个sub_401550是被谁,再何时调用的?
image-20221110191710527
看交叉引用,该函数的入口被维护在一张RUNTIME_FUNCTION表上,这个表又叫ExceptionDir,看来是
1 2 3 4 5 6 7 8 9 10 11 12 .pdata:0000000000405000 ExceptionDir RUNTIME_FUNCTION <rva nullsub_1, rva algn_401001, rva stru_406000> .pdata:000000000040500C RUNTIME_FUNCTION <rva pre_c_init, rva algn_401121, rva stru_406004> .pdata:0000000000405018 RUNTIME_FUNCTION <rva pre_cpp_init, rva algn_401179, rva stru_40600C> .pdata:0000000000405024 RUNTIME_FUNCTION <rva sub_401180, rva algn_4014A6, rva stru_406014> .pdata:0000000000405030 RUNTIME_FUNCTION <rva WinMainCRTStartup, rva algn_4014D2, \ .pdata:0000000000405030 rva stru_406028> .pdata:000000000040503C RUNTIME_FUNCTION <rva start, rva algn_401502, rva stru_406048> .pdata:0000000000405048 RUNTIME_FUNCTION <rva sub_401510, rva algn_401529, rva stru_406068> .pdata:0000000000405054 RUNTIME_FUNCTION <rva sub_401530, rva algn_40153C, rva stru_406070> .pdata:0000000000405060 RUNTIME_FUNCTION <rva nullsub_2, rva algn_401541, rva stru_406074> .pdata:000000000040506C RUNTIME_FUNCTION <rva sub_401550, rva strcmp, rva stru_406078> .pdata:0000000000405078 RUNTIME_FUNCTION <rva strcmp, rva main, rva stru_406084>
这个表也是PE头的导入目录之一
image-20221110192410257
程序基址在0x400000加上0x5000,正好就是ida中的ExceptionDir
查了一下,这是win64上的异常处理Windows
X64的Ring3层异常处理机制 | DbgTech
想要彻底弄明白,首先得把异常处理这一套弄明白,留作后话
分析fake_key.elf
还给了一个linux上的程序,这个会修改key的函数放在一个_init_array
的表中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .init_array:0000000000003D88 ; ELF Initialization Function Table .init_array:0000000000003D88 ; =========================================================================== .init_array:0000000000003D88 .init_array:0000000000003D88 ; Segment type: Pure data .init_array:0000000000003D88 ; Segment permissions: Read/Write .init_array:0000000000003D88 _init_array segment qword public 'DATA' use64 .init_array:0000000000003D88 assume cs:_init_array .init_array:0000000000003D88 ;org 3D88h .init_array:0000000000003D88 initArray dq offset loc_11E0 ; DATA XREF: LOAD:0000000000000168↑o .init_array:0000000000003D88 ; LOAD:00000000000002F0↑o ... .init_array:0000000000003D90 dq offset changeKey .init_array:0000000000003D90 _init_array ends .init_array:0000000000003D90 .fini_array:0000000000003D98 ; ELF Termination Function Table .fini_array:0000000000003D98 ; =========================================================================== .fini_array:0000000000003D98 .fini_array:0000000000003D98 ; Segment type: Pure data .fini_array:0000000000003D98 ; Segment permissions: Read/Write .fini_array:0000000000003D98 _fini_array segment qword public 'DATA' use64 .fini_array:0000000000003D98 assume cs:_fini_array .fini_array:0000000000003D98 ;org 3D98h .fini_array:0000000000003D98 finiArray dq offset __do_global_dtors_aux .fini_array:0000000000003D98 ; DATA XREF: __libc_csu_init+1D↑o .fini_array:0000000000003D98 _fini_array ends
这个_init_array
表在节头表中
image-20221110193559281
在initArray上按x看交叉引用
image-20221110194420955
发现__libc_csu_init
函数会引用该表
这个函数干了啥?
1 2 3 4 5 6 7 8 9 10 11 12 13 void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3){ signed __int64 end; __int64 begin; init_proc(); end = ((char *)&finiArray - (char *)&initArray) >> 3 ; if ( end ) { for ( begin = 0LL ; begin != end; ++begin ) ((void (__fastcall *)(_QWORD, __int64, __int64))*(&initArray + begin))(a1, a2, a3); } }
首先调用了一个init_proc
这个函数判断gmon_start标志是否已经起来了,如果否则调用_gmon_start__函数
参考了
Linux
程序符号__gmon_start__ - 简书 (jianshu.com)
c
- What is the "gmon_start " symbol? - Stack
Overflow
1 2 3 4 5 6 7 8 9 __int64 (**init_proc())(void ) { __int64 (**result)(void ); result = &_gmon_start__; if ( &_gmon_start__ ) result = (__int64 (**)(void ))_gmon_start__(); return result; }
这里的mon应该是monitor,监视器的意思,他好像会监视程序的性能,记录一些数据,反正和程序逻辑关系不大,
然后类似于迭代器,计算end,即init_array的最后一项之后,
end = ((char *)&finiArray - (char *)&initArray) >> 3;
这里右移三位相当于除以8,原因是_init_array
中维护的都是函数入口地址,每个地址都是8字节的,除以八才得到下标
然后begin从0遍历到end,也就是遍历了这个_init_array
表
每次遍历都会执行一个((void (__fastcall *)(_QWORD, __int64, __int64))*(&initArray + begin))(a1, a2, a3);
把_init_array
中的表项,也就是函数入口点,带着(a1,a2,a3)三个参数去执行
显然initArray中有两个表项
1 2 3 4 5 6 7 .init_array:0000000000003D88 _init_array segment qword public 'DATA' use64 .init_array:0000000000003D88 assume cs:_init_array .init_array:0000000000003D88 ;org 3D88h .init_array:0000000000003D88 initArray dq offset loc_11E0 ; DATA XREF: LOAD:0000000000000168↑o .init_array:0000000000003D88 ; LOAD:00000000000002F0↑o ... .init_array:0000000000003D90 dq offset changeKey .init_array:0000000000003D90 _init_array ends
这个changeKey函数是第二个被执行的,显然它用不到a1,a2,a3三个参数.
下面问题是,_libc_csu_init
这个函数是干啥的,被谁,在啥时候执行
交叉引用表明只有start函数会调用它
image-20221110195924354
start是程序入口点,它调用了_libc_start_main
,这个函数会同时带着main函数和_libc_csu_init
函数作为参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void __fastcall __noreturn start (__int64 a1, __int64 a2, void (*a3)(void )) { __int64 v3; int v4; __int64 v5; char *retaddr; v4 = v5; v5 = v3; _libc_start_main( (int (__fastcall *)(int , char **, char **))main, v4, &retaddr, (void (*)(void ))_libc_csu_init, fini, a3, &v5); __halt(); }
下面问题就是_libc_start_main
的逻辑了,这实际上是linux程序从加载到main之前的逻辑,参考程序员的自我修养.留作后话
可以确定的一点是_libc_csu_init
是先于main函数执行的,因此main执行之前key已经被修改了
怎么出的题?
1 2 3 4 __attribute((constructor)) static void fun () { strcat (key,"TCL,trackYYDS" ); }
这个fun函数被__attribute((constructor))
修饰,这是GNU编译器的拓展语法
__attribute__介绍
参考attribute ((constructor))用法解析
- 简书 (jianshu.com)
__attribute__可以设置函数属性(Function Attribute)、变量属性(Variable
Attribute)和类型属性(Type
Attribute)。__attribute__前后都有两个下划线,并且后面会紧跟一对原括弧,括弧里面是相应的__attribute__参数
__attribute__语法格式为:attribute ( ( attribute-list ) )
如果函数被设定为constructor属性,则该函数会在main()函数执行之前被自动的执行;若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。
是一种比较明显的反调试手段
fake_code
又是异常处理,遗留的问题是,如何过滤异常,只对除零异常感兴趣
这样出的题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int FilterFunc (int dwExceptionCode) { if (dwExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO) return EXCEPTION_EXECUTE_HANDLER; return EXCEPTION_CONTINUE_SEARCH; } ... __try { index = (0x7f * index + 0x66 ) % 0xff ; tmp = index >> 7 ; tmp = 1 / tmp; } __except (FilterFunc(GetExceptionCode())) { key = (97 * key + 101 ) % 233 ; key ^= 0x29 ; }
try块中不管发生了什么异常,只要是异常,都会进入except块
这except的参数是调用了FilterFunc函数,给FilterFunc函数传递的参数是GetExceptionCode(),也就是try中发生的异常的类型.
FilterFunc中,当传递的参数是除零异常时,返回EXCEPTION_EXECUTE_HANDLER这么一个枚举值,否则都会返回EXCEPTION_CONTINUE_SEARCH这么一个枚举值
然后except根据FilterFunc的返回值判断是否进入except块,怎么个评判标准呢?
参考Exception-Handler
Syntax - Win32 apps | Microsoft Learn
Value
Meaning
EXCEPTION_EXECUTE_HANDLER
The system transfers control to the
exception handler, and execution continues in the stack frame in which
the handler is
found. 系统将控制移交给异常处理函数,也就是当前函数栈帧中的except块
EXCEPTION_CONTINUE_SEARCH
The system continues to search for a
handler. 系统将不是用当前函数栈帧中的except块中的异常处理,而是沿着seh链上溯早期注册的异常处理函数,而早期注册的异常处理函数都是系统注册的或者运行环境注册的,没有针对性,相当于是走个过场,也不会对程序逻辑造成很大的影响. 而除零异常会被返回EXCEPTION_EXECUTE_HANDLER,进入云之君自定义的except块,这个块就有实际作用了.
EXCEPTION_CONTINUE_EXECUTION
The system stops its search for a handler
and returns control to the point at which the exception occurred. If the
exception is noncontinuable, this results in an
EXCEPTION_NONCONTINUABLE_EXCEPTION
exception. 系统忽略异常,控制还给程序异常点继续执行. 如果异常地太厉害执行不动了,将会又触发EXCEPTION_NONCONTINUABLE_EXCEPTION异常
broken_hash
当时的做法(非预期)
当时想到一个非预期解,但是认为BYTE,WORD,还有char,int,unsigned等等这些玩意儿转换太费脑子了就没整,实际上不难
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 puts ("This is a surprise!" );printf ("Give me your flag: " );scanf ("%s" , input);index = -1 i64; do ++index; while ( input[index] );if ( index == 88 ){ encrypt(input); for ( i = 0 ; i < 88 ; ++i ) { temp_check = check && encrypted[i] == target[i]; check = temp_check; if ( !temp_check ) break ; } v7(); if ( check ) printf ("%s" , aTtttqqqqqqqlll); else printf ("%s" , aWhatAPityPlzTr); result = 0 ; }
逻辑就是输入要有88个字符,然后encrypt函数对输入进行加密,加密之后密文放在encrypted数组中,再和实现放置的密文target进行比较,全对则通过
关键就在于encrypt这个函数,出题人参考的crypto-algorithms/sha1.c
at master · B-Con/crypto-algorithms
(github.com) .fake_code这个题里面的SHA1加密代码是直接抄的,然后改了几个参数
sha1加密的主逻辑长这样,函数名还有变量名有修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __int64 __fastcall sha1 (__int64 text) { int i; char single[12 ]; char ctx[128 ]; unsigned __int8 hash[24 ]; for ( i = 0 ; i < 88 ; ++i ) { single[0 ] = *(_BYTE *)(text + i); sha1_init((__int64)ctx); sha1_update((__int64)ctx, (__int64)single, 1u i64); sha1_final((__int64)ctx, (__int64)hash); encrypted[i] = hash[0 ] | (hash[1 ] << 8 ) | (hash[2 ] << 16 ) | (hash[3 ] << 24 ); } return (unsigned int )check; }
笑死,sha1加密每次只对一个字符进行,他将输入的88个字符分别进行sha1加密,然后将hash值一通按位或放到encrypted[1]上
对单个字符的加密就失去了sha1加密的意义了,可以暴力枚举单个字符的ascii值然后加密之后encrypted[i]和target[i]直接进行比较,这就解出了input[i]
云之君对sha1加密中的一些参数进行了魔改
首先是sha1_init初始化中的参数
左原始,右魔改
然后sha1_transform函数中也有魔改
sha1_transform_2
原来是m[i] = (m[i] << 1) | (m[i] >> 31);
魔改为m[i] = (m[i] << 2) | (m[i] >> 30);
就这两个改动
下面就可以暴力破解了
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 41 42 43 44 45 46 47 #include <iostream> #include <vector> #include <algorithm> #include "sha1.h" using namespace std ; unsigned int target[88 ] = { 0x64744C9A , 0x047C2FF1 , 0xA2D74292 , 0x85BEF77E , 0x711FCBF7 , 0x669E1609 , 0x6BBD9DB6 , 0x6941C8A4 , 0xB16E48B3 , 0xDE321186 , 0x5251E8C2 , 0xFB8F95A7 , 0x711FCBF7 , 0xCB5C3FAD , 0x36568AF5 , 0xFB8F95A7 , 0x82ACF96A , 0x75DCD570 , 0x7EF00E40 , 0xFB8F95A7 , 0x4BE9314A , 0xCB5C3FAD , 0xA2D74292 , 0xDE321186 , 0xFB8F95A7 , 0x46927FA8 , 0xB16E48B3 , 0xD7C1A410 , 0x567375C3 , 0x711FCBF7 , 0xFB8F95A7 , 0x9C19F0F3 , 0xD035E914 , 0xFB8F95A7 , 0x6941C8A4 , 0x0B7D1395 , 0xD7C1A410 , 0xC87A7C7E , 0xFB8F95A7 , 0xD7C1A410 , 0xDE321186 , 0x5251E8C2 , 0xFB8F95A7 , 0xD5380C52 , 0xBEA99D3B , 0xCEDB7952 , 0xFB8F95A7 , 0x73456320 , 0xD7C1A410 , 0xDE321186 , 0xFB8F95A7 , 0x581D99E5 , 0xA2D74292 , 0x711FCBF7 , 0xFB8F95A7 , 0x06372812 , 0xFB8F95A7 , 0x73456320 , 0xCEDB7952 , 0xEF53E254 , 0xFB8F95A7 , 0x9F12424D , 0x669E1609 , 0xFB8F95A7 , 0x9C19F0F3 , 0xFECF7685 , 0x0B7D1395 , 0x1833E8B1 , 0xFB8F95A7 , 0x9F66DD04 , 0xA2D74292 , 0xD7C1A410 , 0xFB8F95A7 , 0x6941C8A4 , 0x866CAF4F , 0x047C2FF1 , 0x64744C9A , 0xFB8F95A7 , 0xD5380C52 , 0xCEDB7952 , 0xDE321186 , 0x81453D43 , 0xCB5C3FAD , 0xB16E48B3 , 0xC578F843 , 0xCEDB7952 , 0xDE321186 , 0xE38C6F07 }; int sha1 (BYTE c) { SHA1_CTX ctx; BYTE single[12 ]; BYTE hash[24 ]; single[0 ] = c; sha1_init(&ctx); sha1_update(&ctx, single, 1 ); sha1_final(&ctx, hash); return hash[0 ] | (hash[1 ] << 8 ) | (hash[2 ] << 16 ) | (hash[3 ] << 24 ); } int main () { for (int t = 0 ; t < 88 ; ++t) { for (char i = 20 ; i < 127 ; ++i) { if (sha1(i) == target[t]) { cout << i; } } } return 0 ; }
1 2 3 PS C:\Users\86135 \Desktop\moectfwp\MoeCTF_2022-main\Challenges\Reverse\Broken_hash> g++ main.cpp sha1.c -o main PS C:\Users\86135 \Desktop\moectfwp\MoeCTF_2022-main\Challenges\Reverse\Broken_hash> ./main moectf{F1nd_th3_SEH_7hen_B1a5t_My_Fla9_and_Y0u_Can_Get_A_Cup_Of_Milk_Tea_From_YunZh1Jun}
但是看了flag就知道这不是预期解,flag中提到了SEH,但是我没有看到?
预期解
云之君一直说,patch程序然后写交互脚本,但是我听不懂她在说啥.看了题解才想明白为啥要这样patch程序
loc_140001DEF
把左边第7行的lea rax,dword_140005000
改成lea rax,[rsp+E8h+var_C8]
,也就是lea rax,[rsp+20h]
然后第8行的add rax,160h
改成nop
这样第9行将rax交给rdx作为参数,实际上就是将var_C8的地址放到了rdx上,然后就要调用printf函数了.var_C8是自变量i的值
这里调用printf打印i值,其目的是啥呢?
蓝色框框里是判断输入的加密是否和已经存储的密文一致,不一致则会跳转黄色框框也就是loc_140001DEF,一致则跳转红色框框报告正确
image-20221111001926390
如果进入了黄色框框,var_8C也就是循环变量是没有清零的,在黄色框框中调用printf函数打印出var_8C的值,如果var_8C=1就说明第一个输入字符对上号了,此时flag就可以确定第一个字符,然后枚举第二个字符,当var_8C打印出2的时候,flag就确定了第二个字符,以此类推
然而这里能够一个一个字符的确认就是因为sha1每次只对一个字符加密.如果sha1对整个输入同时加密,则这种方法也不能用.
也就是说,只要是"预期解"能用,"非预期解"也能用,两种解法实际上是一个道理
下面研究一下预期解的解密脚本
解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import subprocessflag = 'moectf{' for i in range (7 , 88 ): for j in range (0x21 ,0x7f ): tmp = flag + chr (j) * (88 - i) p = subprocess.Popen(["Broken_hash.exe" ], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.stdin.write(tmp.encode()) p.stdin.close() out = p.stdout.read() p.stdout.close() if out[-1 ] > i: flag += chr (j) print (flag) break flag += '}' print (flag)
执行之后
1 2 3 4 5 6 7 8 9 10 11 PS C:\Users\86135\Desktop\moectfwp\MoeCTF_2022-main\Challenges\Reverse\Broken_hash> python ./solve.py moectf{F moectf{F1 moectf{F1n moectf{F1nd moectf{F1nd_ moectf{F1nd_t ... moectf{F1nd_th3_SEH_7hen_B1a5t_My_Fla9_and_Y0u_Can_Get_A_Cup_Of_Milk_Tea_From_YunZh1Ju moectf{F1nd_th3_SEH_7hen_B1a5t_My_Fla9_and_Y0u_Can_Get_A_Cup_Of_Milk_Tea_From_YunZh1Jun moectf{F1nd_th3_SEH_7hen_B1a5t_My_Fla9_and_Y0u_Can_Get_A_Cup_Of_Milk_Tea_From_YunZh1Jun}
subprocess模块
subprocess模块允许我们启动一个子进程,并且控制其输入输入输出
Popen() 方法
Popen 是 subprocess的核心,子进程的创建和管理都靠它处理。
构造函数:
1 2 3 4 class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0,restore_signals=True, start_new_session=False, pass_fds=(), *, encoding=None, errors=None)
常用参数:
args:shell命令,可以是字符串或者序列类型(如:list,元组)
bufsize:缓冲区大小。当创建标准流的管道对象时使用,默认-1。
0:不使用缓冲区
1:表示行缓冲,仅当universal_newlines=True时可用,也就是文本模式
正数:表示缓冲区大小 负数:表示使用系统默认的缓冲区大小。
stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
preexec_fn:只在 Unix 平台下有效,用于指定一个可执行对象(callable
object),它将在子进程运行之前被调用
shell:如果该参数为 True,将通过操作系统的 shell
执行指定的命令。
cwd:用于设置子进程的当前目录。
env:用于指定子进程的环境变量。如果 env =
None,子进程的环境变量将从父进程中继承。
wp中stdin,stderr,stdout都设置为subprocess.PIPE,这个值等于-1
意思是将标准输入输出错误都重定向到管道,然后才可以调用p.stdin.write()
,还有p.stdout.read()
两个函数
否则子进程的三标还是指向键盘和屏幕的,此时调用p.stdin.write()
函数会报错
gogogo
go语言逆向题,没有扣符号表
很容易就能找到这么一个函数main_flagHandler
推测和flag有关系,然而这个函数的反汇编很抽象,推测是初始化的go语言的环境.
看了官方wp说关键函数是main_check
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 41 42 43 44 45 void __fastcall main_check (string flag, bool _r0) { .... if ( (unsigned int )&v24 <= *(_QWORD *)(v3 + 16LL ) ) runtime_morestack_noctxt(); v27 = v2; qmemcpy(&v23.cap, "---moeCTF2022---" , 16LL ); runtime_newobject((runtime__type_0 *)flag.str, (void *)flag.len); v26 = v4; qmemcpy(v4, "---moeCTF2022---" , 16LL ); runtime_stringtoslicebyte((runtime_tmpBuf *)flag.str, (string )__PAIR128__(v5, flag.len), v19); v6.tab = (runtime_itab_0 *)&v23.cap; v6.data = (void *)16LL ; main_AesEncrypt(v20, v21, v22, v23, v6); if ( &v23 == (__uint8 *)-16LL ) { v23.array = v27; ptr = v7; v23.len = 2LL * (_QWORD)v27; runtime_makeslice(0LL , 16LL , v8, (void *)(2LL * (_QWORD)v27)); v11 = v23.array ; v12 = v23.len; v13 = ptr; v14 = 0LL ; v15 = 0LL ; while ( (int )v11 > v14 ) { v16 = v13[v14]; if ( (unsigned int )v15 >= v12 ) runtime_panicIndex(); (*v15)[v9] = byte_7F65E7[v16 >> 4LL ]; v10 = &(*v15)[1LL ]; v17 = byte_7F65E7[v16 & 0xF LL]; if ( v12 <= (unsigned int )&(*v15)[1LL ] ) runtime_panicIndex(); (*v15)[v9 + 1LL ] = v17; ++v14; v15 = (runtime_tmpBuf *)((char *)v15 + 2LL ); } v18 = v9; runtime_slicebytetostring(v15, v13, v12, (string )__PAIR128__((unsigned int )v10, v12)); if ( v18 == 96LL ) runtime_memequal(); } }
看来是main_check干了这么几件事:
首先将输入使用AES加密,然后和事先存放好的密文对照
AES加密中主要需要确定(原文,密钥,偏移量,模式)等等变量
前面有两个"---moeCTF2022---"字样正好16字节,而密钥和偏移量也要求是16字节的,因此可以推测这就是密钥和偏移量
1 2 3 qmemcpy(&v23.cap, "---moeCTF2022---", 16LL); ... qmemcpy(v4, "---moeCTF2022---", 16LL);
如何确定模式呢?进main_AesEncrypt函数看看
1 2 3 4 5 6 7 // local variable allocation has failed, the output may be wrong! void __fastcall main_AesEncrypt(error_0 _r1, int a2, int a3, int a4, void *a5) ... crypto_cipher_NewCBCEncrypter(iv_8, v20, v15); ... } }
这有个crypto_cipher_NewCBCEncrypter
函数,因此推测使用CBC模式
综上找个在线网站AES在线加密解密工具 -
MKLab在线工具
解一下密
image-20221111090020054
还真就解出来了
但是总感觉这样是瞎猫碰见死耗子,推测的地方太多了,怎么合情合理地推断呢?
首先要知道程序到底干了啥?控制什么时候执行到main_flagHandler?参数都是什么?
go源代码
一看源代码,总共加起来150行左右,咱也不知道ida怎么就反编译了这么多,跟老太太裹脚一样
lr
Gin
是一个用Golang
编写的Web
框架,
是一个基于Radix
树的路由,小内存占用,没有反射,可预测的API
框架,速度挺高了近40倍,支持中间件、路由组处理、JSON等多方式验证、内置了JSON
、XML
、HTML
等渲染。
也就是说server这个程序是一个go语言Gin框架搭建的应用层web服务器
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "log" ) func main () { if err := RouterInit(); err != nil { log.Panic(err) } err := Run() if err != nil { log.Panic(err) } }
首先调用RouterInit函数,然后调用Run函数
RouterInit
1 2 3 4 5 6 7 8 9 10 11 func RouterInit () error { gin.SetMode("release" ) engine = gin.Default() engine.GET("/welcome" , welcomeHandler) flagRouter := engine.Group("/find" ) flagRouter.Use(authRequired) flagRouter.GET("/" , findHandler) flagRouter.GET("/flag" , flagHandler) return nil }
这里面注册了如果收到GET方法的报文并且负载是"/welcome",则调用welcomeHandler函数
image-20221111091736911
然后后面干了啥呢?
上网搜了一下,这是一个名叫"路由"的机制,推测是这么一个意思:
可以认为,engine就是访问localhost:8080
之后立刻得到的页面的
然后/welcome
被路由到welcomeHandler函数,执行完后再返回engine对一个你得页面
flagRouter是一个新的路由,也是engine的子路由,只要是/find
路由分支,都会由engine进入flagRouter路由
然后给他注册了两个路由,如果是/find/
就会转到findHandler
如果是/find/flag/
就会转到flagHandler
当然这里只是注册,不会执行,实际上执行是在Run之后
Run
1 2 3 4 5 6 7 8 9 10 func Run () error { log.Println("Service running at [127.0.0.1:8080]" ) fmt.Println("Type localhost:8080/welcome in your browser and open it" ) err := engine.Run("127.0.0.1:8080" ) if err != nil { return err } return nil }
engine.Run就在本机的8080端口上启动HTTP服务了,此后程序一直监听该端口上的客户请求.
根据RouterInit中注册的路由,回应客户的请求,然后恢复到监听状态准备下一个客户的请求(或者一个接待线程,一个服务线程,到底怎么实现,现在不关心)
flagHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func flagHandler (ctx *gin.Context) { flag = ctx.Query("flag" ) if flag == "" { ctx.JSON(400 , gin.H{ "message" : "please input your flag and I will check it" , }) ctx.Abort() return } if !check(flag) { ctx.JSON(200 , gin.H{ "message" : "flag incorrect, please try again" , }) ctx.Abort() return } else { ctx.JSON(200 , gin.H{ "message" : "congratulations" , }) } ctx.SetAccepted() }
flag以GET负载的形式传递给后端
然后后端首先对flag判空,然后调用了check检查flag的正确性
check
1 2 3 4 5 6 7 8 9 10 11 12 13 func check (flag string ) bool { encFlag := "200c2c3ef00f31999df93d6919aa33e42dde307be02017ebf47067099ed0bddc525d5dba0f83c122159b89ae715907cc" key := []byte ("---moeCTF2022---" ) iv := []byte ("---moeCTF2022---" ) encrypt, err := AesEncrypt([]byte (flag), key, iv) if err != nil { return false } if hex.EncodeToString(encrypt) == encFlag { return true } return false }
check中就是Aes加密了,加密之后的密文和encFlag这个值进行比较,如果相同则返回true,再回到flagHandler中进入else块
1 2 3 4 5 else { ctx.JSON(200 , gin.H{ "message" : "congratulations" , }) }
然后就会打印恭喜发财了
看来自己写的函数,对应到符号表中的函数名都会加上main_前缀
image-20221111094105703