逆向过程
给了个docker,实际运行的只有一个dll,按道理应该能直接在windows跑起来,但不知道为啥报错,所以这次几乎都是做的静态分析
首先比较坑的一点,因为dll用了异步编程,所以编译的时候会编译成状态机的形式,用dnspy看的话只能反编译到状态机的代码,用ilspy才能完整反编译
dll提示需要输入16字节的16进制,然后起了一个python进程,但附件下没有别的文件,说明dll内还有没提取出来的文件

用die提取出文件

saw.py首先和dll进行了socket连接,然后以调试模式启动了所有java文件,并向java传入了调试端口,传入后调用了set_trace

一开始看python和java的时候比较迷惑,因为python中的a和iv没有声明,但是java这里又有看起来很像a和iv的代码,dll中也有一个a,估计这几个应该是相同的变量。看到global和continue后基本确定是java通过调试的方法对python代码进行修改,其中java通过第三方python库remote_pdb对python进行调试
首先Saw.main通过python传入的端口与python进行通信,java调用bufferedReader.readLine();后,会阻塞等待python的回复,回复通过调用set_trace实现。收到回复后java便可以通过printWriter.println对python进行调试,这点从java调用bufferedReader.readLine()的次数和python中调用set_trace的次数一致可以得到验证
然后Saw.main通过反射调用获取了jdwp调试的端口,并将其赋值到python的a变量中,然后对iv也进行了赋初值,随后就进行了休眠

python恢复运行后,向dll发送了一个int 0和刚刚从java得到的端口a

dll起完python进程后调用了Hack()方法,等待python的回信。首先收到python发送的\x00\x00\x00\x00,即len为0,然后将读取到的内容复制给a,也就是java的jwdp调试端口,接着调用了Slash()方法

Slash()与刚获得的jdwp调试端口建立连接,然后发送了JDWP-Handshake与java握手

搜索jdwp协议,可以找到官方的说明手册,对照官方文档可以知道具体操作只需关注header的第10和11字节,根据command set和command就可以知道dll发包具体都在做什么,下面只对影响分析的包进行分析

首先是1-7,对应的操作是获取各种数据类型的大小,如fieldIDSize、methodIDSize,存入vals中,用作初始化

然后是1-3,返回已加载的类的所有信息,存入swcl、ccl、hcl中(看名字也能懂,swcl=SawClass)这步的作用主要是获得类的referenceTypeID留作后续和java通信用

2-4查询所有字段的信息,这里记录下了Saw.calc和Curse.hack的id,存入swcm和ccm

2-5获取所有方法的信息,存入hcm

然后dll调用了Track方法,3-2是设置字段值,这里根据刚刚Slash中获得的calc的id设置其值为0,让Saw.main停止休眠开始运行

然后dll主循环开始,Shreck和Breck对我们理解逻辑意义不大。一开始先将我们的输入存入s[32:26]中,然后调用了多次Stop(Crack()),最后循环调用Hack()方法
Crack与python进行通信,将传入的msg长度和msg发送到python,然后读取python的回复

python端的处理是将传入的msg进行aes解密

解密脚本如下:
1 | from Crypto.Cipher import AES |
获得了许多类似b’\x04\x00\x00\x00\x02J\x00\x00\x00\x00\x00\x00\x00\x01J\x00\x00\x00\x00\x00\x00\x00\x00’的结果,作为输入传入了Stop()。3-3对应的是InvokeMethod,传入参数并调用方法,这里是根据传入的data调用了Hack类的方法,以上述data为例,首字节data[0]=’\x04’代表方法号,\x00\x00\x00\x02代表有两个参数,参数以’J’开头,第一个参数为\x00\x00\x00\x00\x00\x00\x00\x01,即0x1,第二个为\x00\x00\x00\x00\x00\x00\x00\x00,即0x0。方法号应该能有方法具体对应,但在比赛的时候是直接猜0-7就是刚好对应Hack.a~h,事实证明也确实是这样。最后Task()返回Hack方法的执行结果

而dll这边发送向python发送完0之后,开始循环调用Hack()和Horror()

Hack()方法与python通信获得一个Curse类并反射调用其Do方法

这里先分析一下python主循环的逻辑
1 | # Saw.py |
首先是上面提到过的aes解密net端传过来的密文,当接收到0的时候结束
接下来大量的调用了lol(crack(data)),其中在crack中调用了set_trace与java通信
java被dll设置calc后恢复了执行,开始执行java的主循环,首先进入了一个小循环中,用于处理python crack中的set_trace,这里首先判断了一下python中的r(注意这个r是当前最小作用域中的r,可能是crack中的r,也可能是python主循环中的r)是否为0,不为0就将python中的r进行aes解密并再次赋值给r

python中的lol函数会将解密完的r写入Curse.dll并发送给net端,我们将所有dll用脚本dump下来,再反编译便可以得到Curse.Do具体做了什么,如下:
1 | from Crypto.Cipher import AES |
发送完dll后,python发送了一个0到net端,net开始循环执行horror函数,而python开始循环调用trace函数,这里r一开始是0

java端检测到r=0后退出小循环,开始调试python

这里hack调用了Curse.magic,但这个方法是空的。在比赛的时候一直以为是net通过jdwp动态注入,但过了一遍协议并没有看到合适的,而且在net和python都没有找到相关的代码(写wp写到这我才发现他确实是空的/(ㄒoㄒ)/~~)。但考虑到net端的Horror和java是同步运行的,这里先看Horror具体做了什么
Horror中首先是2-6获取Curse.hack的objectID,然后10-1读取字符串的值,读取后对其进行aes解密,然后1-11 CreateString,最后3-2设置Curse.hack的值

解密的k和iv在net端main函数刚开始的地方,可以看到这里将s[1017:1033]作为iv,但是我在哪都找不到这段赋值在哪,突然想起虽然不能完整的动调但调到这里还是没问题的,于是用dnspy动调取出iv:0000000000008CAB20E0EB5D0E419101和k:F8963561A24B24F219999D30D026F9421CFA89731CAFE47E47560A3AAF557559
脚本解密java的密文
1 | from Crypto.Cipher import AES |
可以看到,最后java执行完调试命令后设置r为0,进行下一轮循环,python检测到r=0后也进入下一轮循环,至此便分析出整个程序一轮的执行流程(不容易啊)
代码还原
根据上面的分析,我们可以粗糙的将原程序用整合到一个脚本当中
1 | from Crypto.Cipher import AES |
经过不断简化之后,可以大致看出是个魔改的aes
1 | from Crypto.Cipher import AES |
最后不想管了让gpt给我梭(这里只搞了前16字节,由于前后组加密互不影响一组一组解就行)
得到flag:flag{I_ju5t_w0nd3r_1f_y0u_l0v3_C#_j4v4_pyth0n??}
1 | # decrypt_custom_aes_like.py |
心得
感觉这道题出题人要比做题的人更加辛苦()能够差不多纯静态做出来也是相当幸运,在做题的时候关注到C#,python和java之间代码的高度相关性,所以也是很快就理清了整体框架,就是最后被Hack.magic()卡了大半天太难崩了😂
