昨天打完的MAR DASCTF,来复个盘~
不过就re做了3/4然后有事提前开溜了hhh,拿了drinkSomeTea和replace的三血心满意足(蜜汁三血执念。
感觉这回的出题人好喜欢TEA啊(正好最近在整理加解密算法),就是TEA和XTEA都出了却不带XXTEA玩有点可惜/doge。
扫雷也复盘完了!好耶!
Reverse drinkSomeTea 是一个逻辑超级明显但是超——坑的题。
鉴于这是复盘,那就直击要害吧,懒得把当时兜兜转转的心路历程复述一遍了>^<。
逻辑很简单,就是将./tea.png
(FileName=“./tea.png”)的内容以二进制形式读入到unk_409988
中,然后以8字节为单位(v7+=8
)对其进行处理(loc_4010A0
这里),再写入./tea.png.out
中。而题目附件给了这个最后输出的./tea.png.out
,需要我们还原./tea.png
。
loc_4010A0
这里没有转函数的原因是出现了花指令干扰静态分析,nop掉(74 03 75 01 E8改成90 90 90 90 90)再重新转代码转函数即可。
然后我们就得到了sub_4010A0()
。
很明显地可以看到是TEA加密,就是说tea.png
经过TEA加密以后得到了tea.png.out
。
a2是加密时的key,退回到上一层可以发现是dword_407030
:
但是如果拿平时的TEA解密脚本跑根本就行不通,当时多用了两三倍的时间去调(patch掉exit,走一遍加密流程进行对比),最后才发现是int的问题,一般TEA加解密都是uint_32的(。就是这个细节浪费了超多时间TvT
最后上解密脚本,基本上把unsigned int改成int就好(改自TEA、XTEA、XXTEA加密解密算法_gsls200808的专栏-CSDN博客 ):
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 #include <stdio.h> #include <stdint.h> void encrypt (int * v, int * k) { int v0=v[0 ], v1=v[1 ], sum=0 , i; int delta=0x9e3779b9 ; int k0=k[0 ], k1=k[1 ], k2=k[2 ], k3=k[3 ]; for (i=0 ; i < 32 ; i++) { sum += delta; v0 += ((v1<<4 ) + k0) ^ (v1 + sum) ^ ((v1>>5 ) + k1); v1 += ((v0<<4 ) + k2) ^ (v0 + sum) ^ ((v0>>5 ) + k3); } v[0 ]=v0; v[1 ]=v1; } void decrypt (int * v, int * k) { int v0=v[0 ], v1=v[1 ], sum=0xC6EF3720 , i; int delta=0x9e3779b9 ; int k0=k[0 ], k1=k[1 ], k2=k[2 ], k3=k[3 ]; for (i=0 ; i<32 ; i++) { v1 -= ((v0<<4 ) + k2) ^ (v0 + sum) ^ ((v0>>5 ) + k3); v0 -= ((v1<<4 ) + k0) ^ (v1 + sum) ^ ((v1>>5 ) + k1); sum -= delta; } v[0 ]=v0; v[1 ]=v1; } int main () { int v[14656 ]={0 },k[4 ]={0x67616C66 , 0x6B61667B , 0x6C665F65 , 0x7D216761 }; FILE *p1 = fopen("./tea.png.out" , "rb" ); fread(&v, 4 , 14656 , p1); fclose(p1); for (int i=0 ;i<14656 ;i+=2 ){ decrypt(&v[i], k); } FILE *p2 = fopen("./tea.png" , "wb" ); fwrite(&v, 4 , 14656 , p2); fclose(p2); return 0 ; }
得到./tea.png
:
得到flag:DASCTF{09066cbb91df55502e6fdc83bf84cf45}
Enjoyit-1 又一道TEA。
附件用ExEinfoPE可以看到
说明是.NET逆向,于是用ILSpy打开。
看到不寻常字符串DotfuscatorAttribute
,用搜索引擎一查可以发现是使用Dotfuscator加密混淆程序的产物(使用Dotfuscator加密混淆程序以及如何脱壳反编译_qwsf01115的专栏-CSDN博客 )。
所以根据文章指引用de4dot(可用release:Release de4dot mod · CodingGuru1989/de4dot )进行反混淆,得到Enjoyit-1-cleaned.exe
,再用ILSpy打开,就可以看到混淆前在Class0里的main函数。
这个逻辑也很简单,无非就是输入text正确以后运行100000秒就会输出flag。
(当然肯定不可能这么走啊,100000s=1666.67min=27.78h,必然是等不起的。
flag产生的逻辑是先用uint_
和text
进行method_3()
的处理,然后再与array3按字节异或。
而text相当于是已知的,关键在 method_1()
这里。
可以看出是一个base64换表,Table在string_0这里:
于是可以先写脚本得到text:
1 2 3 4 5 6 7 8 9 10 import base64from binascii import *src='yQXHyBvN3g/81gv51QXG1QTBxRr/yvXK1hC=' table='abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ' b64table='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' text=base64.b64decode(src.translate(str .maketrans(table,b64table)).encode()) print(text)
然后在主函数往下看,来到了第二个关键函数method_3()
:
显而易见是个改了delta的XTEA加密,传进来的uint_0是主函数的uint_,byte_0是text的前四字节。
依旧是用上面博客的XTEA脚本改了一下,得到XTEA加密后的uint_:
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 #include <stdio.h> #include <stdint.h> void encipher (unsigned int num_rounds, uint32_t v[2 ], uint32_t const key[4 ]) { unsigned int i; uint32_t v0=v[0 ], v1=v[1 ], sum=0 , delta=2654435464 ; for (i=0 ; i < num_rounds; i++) { v0 += (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); sum += delta; v1 += (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum>>11 ) & 3 ]); } v[0 ]=v0; v[1 ]=v1; } void decipher (unsigned int num_rounds, uint32_t v[2 ], uint32_t const key[4 ]) { unsigned int i; uint32_t v0=v[0 ], v1=v[1 ], delta=2654435464 , sum=delta*num_rounds; for (i=0 ; i < num_rounds; i++) { v1 -= (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum>>11 ) & 3 ]); sum -= delta; v0 -= (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); } v[0 ]=v0; v[1 ]=v1; } int main () { uint32_t v[2 ]={288 ,369 }; uint32_t const k[4 ]={0x63 ,0x6f ,0x6d ,0x62 }; unsigned int r=32 ; encipher(r, v, k); printf ("%8x%8x\n" ,v[0 ],v[1 ]); return 0 ; }
因为主函数里后续处理是把8 bit十六进制的两个结果往str里填,所以输出采用十六进制形式。
得到str="6308fe34b7fe6fdb"
最后进行xor处理即可,上exp:
1 2 3 4 5 6 xtea="6308fe34b7fe6fdb" arr3=[2 ,5 ,4 ,13 ,3 ,84 ,11 ,4 ,87 ,3 ,86 ,3 ,80 ,7 ,83 ,3 ,0 ,4 ,83 ,94 ,7 ,84 ,4 ,0 ,1 ,83 ,3 ,84 ,6 ,83 ,5 ,80 ] flag="" for i in range (len (arr3)): flag+=chr (arr3[i]^ord (xtea[i%len (xtea)])) print("flag{" +flag+"}" )
得到flag:flag{4645e180540ffa7a67cfa174cde105a2}
replace u1s1不知道这个标级怎么标的,居然是比StrangeMine还高的困难,也有可能是我tcl吧(
主逻辑还是很简单,输入长度为24的flag并且以”flag{}”包裹(check在sub_401550()
),然后经过某些处理(sub_401AE7()
、sub_401925()
还有可能的IsDebuggerPresent()
)等于某已知字符串(check在sub_401883()
)即可。
最后check这里byte_4080E0
是输入后经过处理存放的关键位置。
从头开始捋处理函数,这里sub_401AE7()
的作用是把输入和已知数组进行xor以后放进byte_4080E0
中。
真的有这么简单吗?
当然不会,困难题目诶。(从这里逆向解的话会得到假flag。
真正的奥秘在下一个函数sub_401925()
这里,看到VirtualProtectEx()
这个改内存读写权限的关键函数,DNA瞬间动了 (别什么奇怪的东西都往DNA里刻啊喂 。
这里很像之前做过的一个题(RE套路/从EASYHOOK学inline hook | c10udlnk_Log ),这种函数一般是搭配WriteProcessMemory()
在运行时对内存进行修改(从而达到跳转到某些函数的目的)。
静态分析的话那篇blog也有讲,但是为了做题方便直接动态调试走起。
因为这里有超——多的反调试,懒得一个个patch了,直接把exit(0)里的功能给patch掉(偷懒大法好。
即把这里六个字节直接patch成90 90 90 90 90 90
,全部nop掉。
变成这个样子:
别忘了动态调试前要把patch的字节保存进exe里(Edit->Patch program->Apply patches to input file
)。
然后开调,断点下到sub_401925()
和IsDebuggerPresent()
这里,记得要随便输入一个符合前两个check的字符串。
从sub_401925()
可以大致看到修改的是IsDebuggerPresent()
的内容。
所以我们按F9直接走到IsDebuggerPresent()
这里,F7步入。
可以看到跳转到了一个函数,而再往里走可以看到这是个被花指令处理的函数所以静态分析down掉了。
很容易就能找到三个跟第一题相同原理的花指令(注意+2那里要nop多一个字节),所以直接patch,然后把数据按c转成code。
最后得到反编译结果:
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 void sub_4015C3 () { int v0[128 ]; __int64 v1[5 ]; char *v2; int v3; int j; int i; VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10 ui64, 0x40 u, &flNewProtect); WriteProcessMemory(hProcess, IsDebuggerPresent_0, Destination, 0x10 ui64, 0 i64); VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10 ui64, flNewProtect, 0 i64); if ( !IsDebuggerPresent() ) { j = 0 ; v3 = 0 ; v1[0 ] = 0 i64; v1[1 ] = 0 i64; v1[2 ] = 0 i64; v1[3 ] = 0 i64; memcpy (v0, &unk_404020, sizeof (v0)); v2 = Str; for ( i = 1 ; i <= 5 ; ++i ) { for ( j = 0 ; j <= 23 ; ++j ) v2[j] = v0[(unsigned __int8)v2[j]]; } for ( i = 0 ; i <= 5 ; ++i ) *((_DWORD *)v1 + i) = ((unsigned __int8)v2[i + 12 ] << 8 ) | ((unsigned __int8)v2[i + 6 ] << 16 ) | ((unsigned __int8)v2[i] << 24 ) | (unsigned __int8)v2[i + 18 ]; for ( i = 0 ; i <= 5 ; ++i ) sprintf (&byte_4080E0[8 * i], "%x" , *((unsigned int *)v1 + i)); } }
就是先把IsDebuggerPresent()
里的内容还原,然后在非调试情况下将输入经过一些处理放到byte_4080E0
中。
这!才是真正的加密函数!
然后这个逻辑也很好逆啦,顺着就是先在unk_404020盒里换五次,然后栅栏密码得到最后的字符串。
写出exp有:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from binascii import *from hashlib import md5ans=unhexlify("416f6b116549435c2c0f1143174339023d4d4c0f183e7828" ) tmps=[0 ,6 ,12 ,18 ] seq=[] for i in range (6 ): tmpl=[x+i for x in tmps] seq=seq+tmpl arr1=[0 for i in range (24 )] for i in range (24 ): arr1[seq[i]]=ans[i] box=[0x00000080 , 0x00000065 , 0x0000002F , 0x00000034 , 0x00000012 , 0x00000037 , 0x0000007D , 0x00000040 , 0x00000026 , 0x00000016 , 0x0000004B , 0x0000004D , 0x00000055 , 0x00000043 , 0x0000005C , 0x00000017 , 0x0000003F , 0x00000069 , 0x00000079 , 0x00000053 , 0x00000018 , 0x00000002 , 0x00000006 , 0x00000061 , 0x00000027 , 0x00000008 , 0x00000049 , 0x0000004A , 0x00000064 , 0x00000023 , 0x00000056 , 0x0000005B , 0x0000006F , 0x00000011 , 0x0000004F , 0x00000014 , 0x00000004 , 0x0000001E , 0x0000005E , 0x0000002D , 0x0000002A , 0x00000032 , 0x0000002B , 0x0000006C , 0x00000074 , 0x00000009 , 0x0000006E , 0x00000042 , 0x00000070 , 0x0000005A , 0x00000071 , 0x0000001C , 0x0000007B , 0x0000002C , 0x00000075 , 0x00000054 , 0x00000030 , 0x0000007E , 0x0000005F , 0x0000000E , 0x00000001 , 0x00000046 , 0x0000001D , 0x00000020 , 0x0000003C , 0x00000066 , 0x0000006B , 0x00000076 , 0x00000063 , 0x00000047 , 0x0000006A , 0x00000029 , 0x00000025 , 0x0000004E , 0x00000031 , 0x00000013 , 0x00000050 , 0x00000051 , 0x00000033 , 0x00000059 , 0x0000001A , 0x0000005D , 0x00000044 , 0x0000003E , 0x00000028 , 0x0000000F , 0x00000019 , 0x0000002E , 0x00000005 , 0x00000062 , 0x0000004C , 0x0000003A , 0x00000021 , 0x00000045 , 0x0000001F , 0x00000038 , 0x0000007F , 0x00000057 , 0x0000003D , 0x0000001B , 0x0000003B , 0x00000024 , 0x00000041 , 0x00000077 , 0x0000006D , 0x0000007A , 0x00000052 , 0x00000073 , 0x00000007 , 0x00000010 , 0x00000035 , 0x0000000A , 0x0000000D , 0x00000003 , 0x0000000B , 0x00000048 , 0x00000067 , 0x00000015 , 0x00000078 , 0x0000000C , 0x00000060 , 0x00000039 , 0x00000036 , 0x00000022 , 0x0000007C , 0x00000058 , 0x00000072 , 0x00000068 ] arr2=[0 for i in range (24 )] for i in range (5 ): for j in range (24 ): arr2[j]=box.index(arr1[j]) arr1=arr2 myInput='' .join(map (chr ,arr1)) print(myInput) flag=myInput.encode() print(md5(flag).hexdigest())
得到flag:flag{Sh1t_you_dec0d3_it}
奇怪的扫雷 这道题赛中没怎么做,赛后复盘静态硬刚+动态调没搞出来,终于等到了带扫雷玩的wp(MAR DASCTF明御攻防赛 PWN、RE Writeup - 安全客,安全资讯平台 ),感觉思路大概没错、关键函数也找对了,但是忽略了一个小地方:
我当时还在纳闷为什么是用代码段的数据来计算md5值,原来是为了检测有没有patch啊(
以及请教队里大佬以后才发现还忽略了一个点,就是md5的update是拼接的,没认真学md5的我一直以为是覆盖的(惭愧,真的要好好学哈希算法。
编写用来hook的dll文件对我来说还是有点难(tcl),就试着用自己的方法做一下好了。
(啊其实就是直接静态硬解,从头捋捋怎么发现函数的。
首先直接findcrypt,找到md5的常数。
通过交叉引用找到上一层。
发现这里有一个前面题目提到的花指令,照例nop掉,转函数,看伪代码。
感觉这是个自己写的处理异常的函数(TopLevelExceptionFilter
这个名字引起警觉)。
然后往下看,先根据交叉引用+识别算法给MD5的一系列函数命个名,方便静态分析。
(也可以盲找AfxMessageBox
的交叉引用,毕竟能出flag的地方一般都在MessageBox。)
更何况这题是赛后复盘,根据大家群里零零散散发出来的截图可以判断flag用AfxMessageBox
给出,而查AfxMessageBox的交叉引用可以发现就这里是放了变量的。
主要逻辑在:
很明显地看到,v12装着最后的md5(也就是flag。
整体逻辑是:
先把IUnknown::operator=()
函数首地址开始的12288字节(0x402000-0x405000)放到Src
里,然后根据触发异常时的eax、ebx、ecx、edx的值 修改部分位置(Src[4101]
、Src[128]
、Src[256]
、Src[0x2000]
),最后把Src
丢进MD5Update里。
把&loc_404FFE + 2
开始的12288字节(0x405000-0x408000)放到Src
里,再Src
丢进MD5Update里。
最后算出MD5,此MD5值即为flag。
庆幸这个MD5算法没有被魔改,不然还得找魔改的地方(瘫。
现在缺的数据就是触发异常时的eax、ebx、ecx、edx的值 了,只要找到check一切好说,可以从时间的增加入手找到操纵游戏的逻辑部分。
游戏类果断上CE(Cheat Engine)找关键内存,先绑上运行的exe文件,调整扫描设置后点击“首次扫描”。
在界面随便点一下开始游戏(让游戏开始计时),然后调整CE右侧设置,一直点“再次扫描”(注意点击间隔在1s以上,不然时间根本就没动过就扫描不出来了),直到左侧只剩少数地址(并且有一个地址的当前值和计时器的相同)为止。
双击这条地址,选中记录,按F6
,查看改写这个地址的位置。
可以看到
在IDA的反汇编窗口中按g
跳到这个地址(0x408803
)
再反编译可以看到
这就是时间增加的函数。
依次查交叉引用,可以发现check应该存在于这个右键松开的函数里,并且可以猜是sub_403D30()
。
为什么我笃定是这个呢,因为这个函数不仅是以if框着的形式调用,而且里面还会触发一个异常:
也就是我们常说的CC断点
这边的逻辑可以猜测是循环check,一旦有不对的地方就直接return 0;
,全部通过以后可触发断点,进而走到TopLevelExceptionFilter
并给flag。
而我们需要的正是这里的eax、ebx、ecx、edx的值 。
而观察整个check函数的汇编可以发现,最后是直接用变量给四个寄存器赋值的,并且在之前的代码中这些变量几乎没有变动(具体可以自己琢磨琢磨,看目的寄存器的位置;只可意会不可言传.jpg),所以可以通过把函数开始的jmp short loc_403D55
patch成jmp short loc_403D9E
。
这样直接跳过循环来到关键位置loc_403D9E
,动态调试时即可在触发int3断点时看到四个寄存器的值。
然后开调,老规矩,记得调试之前要把patch的字节保存进exe里(Edit->Patch program->Apply patches to input file
)。
选高级以后随便点个右键插旗子,然后放任程序走,走到断点时有提示,关掉提示以后可以看到右上角:
四个寄存器的值也拿到了!
接下来就差内存了。因为前面静态分析的时候我们patch了不少地方(花指令+这波跳转),所以要拿最最开始的附件来dump。
打开最最开始的附件,选择File->Script command
,分别在IDC下输入以下脚本dump出两块内存:
1 2 3 4 5 6 7 8 9 static main () { auto i,fp; fp=fopen("./dump_0x402000" ,"wb" ); auto start=0x402000 ; auto size=12288 ; for (i=start;i<start+size;i++){ fputc(Byte(i),fp); } }
1 2 3 4 5 6 7 8 9 static main () { auto i,fp; fp=fopen("./dump_0x405000" ,"wb" ); auto start=0x405000 ; auto size=12288 ; for (i=start;i<start+size;i++){ fputc(Byte(i),fp); } }
然后编写python3脚本,按照我们最开始分析的逻辑得到flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from hashlib import md5with open ("./dump_0x402000" ,'rb' ) as fp: data_0x402000_list=list (fp.read()) with open ("./dump_0x405000" ,'rb' ) as fp: data_0x405000=fp.read() eax=0 v9=eax//2 ebx=0x10 ecx=0x1E edx=0x64 data_0x402000_list[4101 ]=ebx data_0x402000_list[128 ]=ecx data_0x402000_list[256 ]=edx data_0x402000_list[0x2000 ]=v9 data_0x402000=b'' for b in data_0x402000_list: data_0x402000+=b.to_bytes(1 ,'little' ) md5=md5() md5.update(data_0x402000) md5.update(data_0x405000) print(md5.hexdigest())
flag:4a468b1a17760d263b0969963e0a6c9b