【wp】2021MAR-DASCTF_逆向

昨天打完的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; /* set up */
int delta=0x9e3779b9; /* a key schedule constant */
int k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i < 32; i++) { /* basic cycle start */
sum += delta;
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
} /* end cycle */
v[0]=v0; v[1]=v1;
}
//解密函数
void decrypt (int* v, int* k) {
int v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
int delta=0x9e3779b9; /* a key schedule constant */
int k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i<32; i++) { /* basic cycle start */
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
} /* end cycle */
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 base64
from binascii import *

src='yQXHyBvN3g/81gv51QXG1QTBxRr/yvXK1hC='
table='abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b64table='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
text=base64.b64decode(src.translate(str.maketrans(table,b64table)).encode())
print(text)

# text=b'combustible_oolong_tea_plz'

然后在主函数往下看,来到了第二个关键函数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>

/* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */

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]; // [rsp+30h] [rbp-50h] BYREF
__int64 v1[5]; // [rsp+230h] [rbp+1B0h]
char *v2; // [rsp+258h] [rbp+1D8h]
int v3; // [rsp+264h] [rbp+1E4h]
int j; // [rsp+268h] [rbp+1E8h]
int i; // [rsp+26Ch] [rbp+1ECh]

VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10ui64, 0x40u, &flNewProtect);
WriteProcessMemory(hProcess, IsDebuggerPresent_0, Destination, 0x10ui64, 0i64);
VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10ui64, flNewProtect, 0i64);
if ( !IsDebuggerPresent() )
{
j = 0;
v3 = 0;
v1[0] = 0i64;
v1[1] = 0i64;
v1[2] = 0i64;
v1[3] = 0i64;
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 md5

ans=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()) # 别忘了最后提交md5

得到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。

整体逻辑是:

  1. 先把IUnknown::operator=()函数首地址开始的12288字节(0x402000-0x405000)放到Src里,然后根据触发异常时的eax、ebx、ecx、edx的值修改部分位置(Src[4101]Src[128]Src[256]Src[0x2000]),最后把Src丢进MD5Update里。
  2. &loc_404FFE + 2开始的12288字节(0x405000-0x408000)放到Src里,再Src丢进MD5Update里。
  3. 最后算出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_403D55patch成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 md5

with 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

本文作者: c10udlnk
本文链接: https://c10udlnk.top/p/wpFor-2021MARDASCTF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 c10udlnk_Log (https://c10udlnk.top)!