查看原文
其他

初学者的静态分析挑战writeup

jishuzhain 看雪学院 2019-09-17

题目来源:

https://www.malwaretech.com/beginner-malware-reversing-challenges


所有挑战都是在不使用调试器的情况下完成的,你的目标应该是能够在不运行exe的情况下完成每个挑战。



隐藏和搜索


exe包含一个或多个未加密的flag,需要找到正确flag。


>>>>

string1难度等级为1星


strings1.exe包含存储在可执行文件中的未加密flag。运行时,程序将输出flag的MD5哈希值,但不输出原始值。你能得到flag吗?


本地解压后如下:



拖进IDA里,查看下:


 

很明显,IDA很智能地识别了一些关键的方法,如md5_hash,猜测这就是生成flag的主方法。


call上面为push eax,入栈操作,很智能地显示了eax的类型,这里可以断定eax为md5_hash函数的参数。


mov eax, off_432294


eax的值来自一个地址,跟进此地址中,在IDA中双击此地址,来到了数据区域,发现此地址为FLAG字符串的起始地址。



那 FLAG{CAN-I-MAKE-IT-ANYMORE-OBVIOUS} 到底是不是flag呢?


在网页中进行检查:

是真的flag。



>>>>

string2难度等级为1星


strings2.exe包含存储在可执行文件中的未加密flag。运行时,程序将输出flag的MD5哈希值,但不输出原始值。你能得到flag吗?


本地解压后如下:



拖进IDA里,查看下,此时与string1完全不一样了。

 


前面是一些IDA为局部变量与参数设置的标识,先不看,来到计算hash的地方。

 


这里,eax依然作为md5_hash函数的参数,eax的值取自var_28的地址。


从最上面翻到eax赋值这里,发现mov操作为规律赋值,猜测可能是一个字符串或者字符数组。



右键转换为字符,查看下var_28赋值了什么,发现是大写字母F。

 


发现了是FLAG{STA......,猜测这就是flag。


转换完成后,将这段字符串进行检测即可 ,这里不再记录过程。



>>>>

string3难度等级为2星


strings3.exe包含存储在可执行文件中的未加密flag。运行时,程序将输出flag的MD5哈希值,但不输出原始值。你能得到flag吗?


本地解压后如下:



拖进IDA里,查看下,发现与string1与string2完全不一样。

 


还是原先的步骤,先找到hash生成函数。


这里发现生成hash的函数使用了类的结构,因为call ??0MD5@@QAE@XZ  ; MD5::MD5(void) 执行了类对象的构造方法。


重点来到 call  ?digestString@MD5@@QAEPADPAD@Z ; MD5::digestString(char *)。



这里是生成hash的主方法,IDA标识了eax为char* 类型,与这个方法的参数类型一致,所以判断push eax为参数入栈。


lea eax, [ebp+var_4A0]


eax的值为var_4A0变量的地址,var_4A0变量在上面的call    ds:LoadStringA这个API函数中有涉及。



>>>>

LoadStringA函数

从与指定模块关联的可执行文件中加载字符串资源,并将字符串复制到具有终止空字符的缓冲区中,或者返回指向字符串资源本身的只读指针。


int LoadStringA( HINSTANCE hInstance, UINT uID, LPSTR lpBuffer, int cchBufferMax );


参数

  • hInstance

类型:HINSTANCE

可执行文件包含字符串资源的模块实例的句柄。要获取应用程序本身的句柄,请使用NULL调用GetModuleHandle函数。


  • uID

输入:UINT

要加载的字符串的标识符。


  • lpBuffer

类型:LPTSTR

接收字符串的缓冲区(如果cchBufferMax非零)或者是字符串资源本身的只读指针(如果cchBufferMax为零)。长度必须足以容纳指针(8个字节)。


  • cchBufferMax

输入:int

缓冲区的大小,以字符为单位。如果字符串长于指定的字符数,则该字符串将被截断并以null结尾。如果此参数为0,则lpBuffer将接收指向字符串资源本身的只读指针。


来源: 

https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-loadstringa


win32 API函数的调用约定为stdcall,参数从右至左入栈。


所以上面的4个push操作就是参数入栈。 


mov [ebp+var_8],
eax
mov eax,
1
shl eax,
8
xor edx,
edx
inc edx
shl edx,
4
or eax,
edx
mov [ebp+var_4],
eax
push 3FFh
lea ecx,
[ebp+var_4A0]
push ecx
mov edx,
[ebp+var_4]
push edx
push 0
call ds:LoadStringA

cchBufferMax为3FFh,十进制为1023


lpBuffer为ecx


uID为edx


hInstance为0,也就是为NULL。


mov
[ebp+var_4A0], 0
lea ecx,
[ebp+var_4A0]


lpBuffer为ecx,ecx为var_4A0变量所在的地址,而往上查,发现var_4A0已经被赋值为0,所以ecx为0。


uID为edx,edx=[ebp+var_4],而[ebp+var_4]=eax。


eax的计算是下面这几条汇编指令:


mov eax,
1
shl eax,
8
xor edx, edx
inc edx
shl edx,
4
or eax, edx


最后算得eax=272。


LoadStringA(0, 272, &var_4A0, 1023);


这里还有个快速的方法,F5反编译。直接使用IDAF5插件反编译功能翻译成伪c代码。



LoadStringA函数是加载字符串资源,所以目前需要使用另一个工具,ResourceHacker。


uID为要加载的字符串的标识符,使用ResourceHacker打开string3,来到字符串资源处。


查找下272标识符里的内容:


正确flag。



shellcode


这些挑战利用与位置无关的代码来解密flag,弄清楚shellcode的作用并自己解密。


>>>>

shellcode1.exe


包含存储在可执行文件中的flag。运行时,程序将输出flag的MD5哈希值,但不输出原始值,难度等级1星。


本地解压后:



IDA打开,如下:



还是先找一下生成hash的方法,可以确定 call  ?digestString@MD5@@QAEPADPAD@Z ; MD5::digestString(char *) 为生成hash的方法。


找这个函数的参数,发现前面存在一个push操作,为 push  offset Str ; "2b\n:蹥B*bb"。


发现Str为一个字符串的地址:



可以先将字符串当做flag输入进行检测,经过实践后,发现这里的字符串并不是真正的flag。


说明这个地址保存的字符串在前面的流程中进行了改写,我们继续往上查看汇编代码。


call [ebp+Dst]


直接调用了一个地址,首先这个地址应该是一个函数的开始地址。那么如何得到这个地址的?


继续往上看:


push 10h
push 0
call ds:GetProcessHeap
push eax
call ds:HeapAlloc
mov [ebp+var_4],
eax
mov eax,
[ebp+var_4]
mov dword ptr [eax],
offset Str ; "2b\n:蹥B*bb"
push offset Str ; "2b\n:蹥B*bb"
call strlen
add esp, 4
mov ecx,
[ebp+var_4]
mov [ecx+4],
eax
push 40h
push 1000h
push 0Dh
push 0
call ds:VirtualAlloc
mov [ebp+Dst],
eax
push 0Dh ;
MaxCount
push offset unk_404068 ;
Src
mov edx,
[ebp+Dst]
push edx ;
Dst
call memcpy
add esp, 0Ch
mov esi,
[ebp+var_4]
call [ebp+Dst]


首先往上发现了memcpy方法,属于C库函数。


void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字符到存储区 str1。


edx为str1,unk_404068为str2,0Dh为n。


这里发现将unk_404068的内容复制到了Dst地址处,恰巧Dst为下面call调用的地址。


push
40h
push
1000h
push
0Dh
push
0
call ds:VirtualAlloc
mov [ebp+Dst], eax


这里使用VirtualAlloc生成了一块虚拟内存,并且设置为可执行,说明[ebp+Dst]处的地址是可执行的,具体可以参考MSDN此函数的用法。


我们跟进unk_404068看看,双击unk_404068进入如下:



目前是以数据显示,猜测这里应该是代码(不然call调用了无法执行)。所以选中unk_404068,Edit 选中Code,生成代码。



可以发现,IDA分析后确实生成了代码。


 

call    [ebp+Dst] 这里已经解决了,接着查找这个函数的输入参数。


往上发现了mov  esi, [ebp+var_4],而函数内部也对这个esi寄存器进行了操作,判断这里是寄存器传递参数。


现在的问题变成了(ebp+var_4)地址处的值是什么?


push
10h
push
0
call ds:GetProcessHeap
push eax
call ds:HeapAlloc
mov [ebp+var_4], eax


首先来到第一次对[ebp+var_4]进行操作的地方,这段汇编的意思是调用HeapAlloc生成了一段堆空间,并将起始地址赋值给了[ebp+var_4]。


mov eax,
[ebp+var_4]
mov dword ptr [eax],
offset Str ; "2b\n:蹥B*bb"
push offset Str ; "2b\n:蹥B*bb"
call strlen
add esp, 4
mov ecx,
[ebp+var_4]
mov [ecx+4],
eax


接着[ebp+var_4]赋值给了eax,而eax地址处的存储值变成了Str字符串的起始地址。


Str字符串地址入栈,调用了strlen C库函数。将字符串长度eax赋值给了[ecx+4]。


[ecx+4]为[ebp+var_4][1]


现在我们发现:

[ebp+var_4][0]=Str[0]

[ebp+var_4][1]=eax=strlen(Str)


进一步,[ebp+var_4]目前的值为Str存储的字符串的地址。


而上面我们发现,esi的值为[ebp+var_4]的值。


所以现在进入 call  [ebp+Dst] 函数里进行分析。


sub_404068 proc near ;
DATA XREF:
start+5Eo
.data:00404068 mov edi,
[esi]
.data:0040406A mov ecx,
[esi+4]
.data:0040406D
.data:0040406D loc_40406D: ;
CODE XREF:
sub_404068+Aj
.data:0040406D rol byte ptr [edi+ecx-1], 5
.data:00404072 loop loc_40406D
.data:00404074 retn
.data:00404074 sub_404068 endp


mov esi, [ebp+var_4]
call [ebp+Dst]

call strlen
add esp, 4
mov ecx, [ebp+var_4]
mov [ecx+4], eax


esi为Str字符串的起始地址,[esi+4]为eax的值,eax的值是strlen的结果,所以是字符串的长度。


rol byte ptr [edi+ecx-1], 5


这句汇编的含义是将Str的每个字符串循环左移5位之后再放回原位,所以最后的值是Str字符串。


之后可计算出flag:


FLAG{SHELLCODE-ISNT-JUST-FOR-EXPLOITS}



>>>>

shellcode2.exe


包含存储在可执行文件中的flag。运行时,程序将输出flag的MD5哈希值,但不输出原始值。


按照惯例,使用IDA打开。



发现了涉及hash的函数,接着找输入参数。


push  edx   ; char *   这里是通过edx传入参数。


lea   edx, [ebp+str[0]] ;edx保存了一个字符数组的首地址。


因为之前已做过了,这里已经手工对连续排列的参数进行创建数组的处理。


猜测数组保存的应该就是输入的flag,查看上述流程里对该数组做了哪些操作。


call [ebp+Dst]


依据前面的经验,这里Dst为堆空间地址,之后通过mencpy复制了内容,所以所在的地址为代码,所以现在找到这个地址查看下,


也就是push  offset sub_404040 ; Src  中的sub_404040。


双击进入,如果发现是db ... 等显示,可以按字母c转换为代码格式。



这里已经转换为代码格式,按F5让IDA自动生成伪代码,看看这个函数的作用。



翻到下面,看看具体逻辑。 



这里已经重命名了很多代码,当然主要的知识点就是 LoadLibraryA与GetProcAddress。


通过这个两个API函数,可以调用系统dll里具体的方法。


v78 = LoadLibraryA(&msvcrt.dll);

v27 = LoadLibraryA(&kernel32.dll);



_GetModuleFileNameA = (void (__stdcall *)(_DWORD,
char *,
signed int))GetProcAddress(v27, &GetModuleFileNameA);

_fopen = (int (__cdecl *)(char *,
char *))GetProcAddress(v78, &fopen);

_fseek = (void (__cdecl *)(int,
signed int, _DWORD))GetProcAddress(v78, &fseek);

_fread = (void (__cdecl *)(char *,
signed int,
signed int,
int))GetProcAddress(v78, &fread);

_fclose = (int (__cdecl *)(int))GetProcAddress(v78, &fclose);


这里加载了两个dll,之后获取了5个函数的内存实际地址。


_GetModuleFileNameA(0, &v11,
260);
// v11=当前exe执行的绝对路径

ptr_file = _fopen(&v11, &rb);

_fseek(ptr_file,
78,
0);
// 移动文件指针至离文件开始位置78个字节处

_fread(v79,
38,
1, ptr_file);
// 读取38个字节放入v79数组中

// This program cannot be run in DOS mode

result = _fclose(ptr_file);

v2_36 = *(_DWORD *)(heap_str +
12);
// 36(十进制)

v3_str = *(_DWORD *)(heap_str +
8);
// str字符串的地址


获取到当前exe执行程序的绝对路径,之后设置文件指针,移动到离文件开始位置78个字节处,接着读取38个字节的内容放入v79数组中。实际读取的内容为“This program cannot be run in DOS mode”。


之后关闭文件,取出之前申请的堆空间里的两个数值进行赋值。


v4_0 = 0;

do

{

LOBYTE(result) = v79[v4_0];

*(_BYTE *)(v3_str + v4_0++) ^= result;

}

while ( v4_0 != v2_36 );

return result;

 

这里就是最后的计算逻辑,result等于v79数组的首地址,也就是之前“This program cannot be run in DOS mode”的内容。


v3_str为[ebp+str[0]至[ebp+str[0]+23h]的数组内容,两个数组之间每个索引值进行异或操作赋值给v3_str(具体要看实际汇编指令,翻译成的伪代码不一定正确),循环36次后结束。


text:0040227F call ??0MD5@@QAE@XZ ; MD5::MD5(void)

.text:00402284 mov [ebp+str[0]], 12h

.text:00402288 mov [ebp+str[0]+1], 24h

.text:0040228C mov [ebp+str[0]+2], 28h

.text:00402290 mov [ebp+str[0]+3], 34h

.text:00402294 mov [ebp+str[0]+4], 5Bh

.text:00402298 mov [ebp+str[0]+5], 23h

.text:0040229C mov [ebp+str[0]+6], 26h

.text:004022A0 mov [ebp+str[0]+7], 20h

.text:004022A4 mov [ebp+str[0]+8], 35h

.text:004022A8 mov [ebp+str[0]+9], 37h

.text:004022AC mov [ebp+str[0]+0Ah], 4Ch

.text:004022B0 mov [ebp+str[0]+0Bh], 28h

.text:004022B4 mov [ebp+str[0]+0Ch], 76h

.text:004022B8 mov [ebp+str[0]+0Dh], 26h

.text:004022BC mov [ebp+str[0]+0Eh], 33h

.text:004022C0 mov [ebp+str[0]+0Fh], 37h

.text:004022C4 mov [ebp+str[0]+10h], 3Ah

.text:004022C8 mov [ebp+str[0]+11h], 27h

.text:004022CC mov [ebp+str[0]+12h], 3Dh

.text:004022D0 mov [ebp+str[0]+13h], 6Eh

.text:004022D4 mov [ebp+str[0]+14h], 25h

.text:004022D8 mov [ebp+str[0]+15h], 48h

.text:004022DC mov [ebp+str[0]+16h], 6Fh

.text:004022E0 mov [ebp+str[0]+17h], 3Ch

.text:004022E4 mov [ebp+str[0]+18h], 58h

.text:004022E8 mov [ebp+str[0]+19h], 3Ah

.text:004022EC mov [ebp+str[0]+1Ah], 68h

.text:004022F0 mov [ebp+str[0]+1Bh], 2Ch

.text:004022F4 mov [ebp+str[0]+1Ch], 43h

.text:004022F8 mov [ebp+str[0]+1Dh], 73h

.text:004022FC mov [ebp+str[0]+1Eh], 10h

.text:00402300 mov [ebp+str[0]+1Fh], 0Eh

.text:00402304 mov [ebp+str[0]+20h], 10h

.text:00402308 mov [ebp+str[0]+21h], 6Bh

.text:0040230C mov [ebp+str[0]+22h], 10h

.text:00402310 mov [ebp+str[0]+23h], 6Fh


算出flag为FLAG{STORE-EVERYTHING-ON-THE-STACK}



VM


第一次逆向虚拟机指令,这次的样本比较简单。


vm1.exe实现了一个简单的8位虚拟机(VM),试图阻止逆向工程师检索flag。VM的RAM包含加密标flag和一些字节码来解密它。你能弄清楚VM是如何工作的吗并解密flag?ram.bin中提供了VM的RAM副本(此数据与执行前恶意软件VM的ram内容相同,并包含自定义程序集代码和加密flag)


解压后如图:



IDA打开vm1.exe,找到关键函数。 



ecx为输入参数,来源于Dst字符串。



往上溯源查看Dst是什么,通过memcpy函数可知Dst来自于Src字符串内容。


call  sub_4022E0     

; Src的地址赋值给了Dst

; 而sub_4022E0内部对Dst进行了操作,Dst的内容为虚拟机代码与数据,处理完毕后会得到flag。

; 所以也就是对unk_404040地址的内容进行操作。


进入sub_4022E0查看,流程有点多,直接F5查看下大致流程。



sub_402270函数对三个字节进行了处理,跟进看看。


 

现在很清楚了,对Dst字符串数组进行了处理与解码。


选中Dst数据内容,导出后编写Python脚本,得到最终的flag。


# coding:utf-8


vm_chars = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xDE, 0x7E, 0x7D, 0x55, 0x1E, 0x05, 0xE6, 0x9F,
0xE4, 0xA6, 0x47, 0x50, 0x02, 0x01, 0xC7, 0xFC, 0xCB, 0x60,
0x09, 0xC6, 0x0E, 0x2E, 0x41, 0x65, 0xA4, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x1D, 0xBD, 0x01, 0x05,
0x53, 0x01, 0x12, 0x48, 0x01, 0x10, 0xE6, 0x01, 0x13, 0x8A,
0x01, 0x0D, 0x47, 0x01, 0x16, 0x13, 0x01, 0x0A, 0x15, 0x01,
0x00, 0x98, 0x01, 0x02, 0x3C, 0x01, 0x18, 0xD9, 0x01, 0x1A,
0x57, 0x01, 0x06, 0xAB, 0x01, 0x1B, 0xC6, 0x01, 0x01, 0x32,
0x01, 0x17, 0x20, 0x01, 0x15, 0x6F, 0x01, 0x11, 0x2D, 0x01,
0x08, 0xC9, 0x01, 0x09, 0xE7, 0x01, 0x03, 0x12, 0x01, 0x0C,
0x2F, 0x01, 0x0E, 0x88, 0x01, 0x19, 0x6C, 0x01, 0x04, 0x65,
0x01, 0x1E, 0xAE, 0x01, 0x14, 0x59, 0x01, 0x1F, 0x91, 0x01,
0x1C, 0x5D, 0x01, 0x0F, 0xAE, 0x01, 0x0B, 0x15, 0x01, 0x07,
0xCC, 0x02, 0x20, 0x00, 0x03, 0x00, 0x00, 0x02, 0x21, 0x00,
0x03, 0x01, 0x00, 0x02, 0x22, 0x00, 0x03, 0x02, 0x00, 0x02,
0x23, 0x00, 0x03, 0x03, 0x00, 0x02, 0x24, 0x00, 0x03, 0x04,
0x00, 0x02, 0x25, 0x00, 0x03, 0x05, 0x00, 0x02, 0x26, 0x00,
0x03, 0x06, 0x00, 0x02, 0x27, 0x00, 0x03, 0x07, 0x00, 0x02,
0x28, 0x00, 0x03, 0x08, 0x00, 0x02, 0x29, 0x00, 0x03, 0x09,
0x00, 0x02, 0x2A, 0x00, 0x03, 0x0A, 0x00, 0x02, 0x2B, 0x00,
0x03, 0x0B, 0x00, 0x02, 0x2C, 0x00, 0x03, 0x0C, 0x00, 0x02,
0x2D, 0x00, 0x03, 0x0D, 0x00, 0x02, 0x2E, 0x00, 0x03, 0x0E,
0x00, 0x02, 0x2F, 0x00, 0x03, 0x0F, 0x00, 0x02, 0x30, 0x00,
0x03, 0x10, 0x00, 0x02, 0x31, 0x00, 0x03, 0x11, 0x00, 0x02,
0x32, 0x00, 0x03, 0x12, 0x00, 0x02, 0x33, 0x00, 0x03, 0x13,
0x00, 0x02, 0x34, 0x00, 0x03, 0x14, 0x00, 0x02, 0x35, 0x00,
0x03, 0x15, 0x00, 0x02, 0x36, 0x00, 0x03, 0x16, 0x00, 0x02,
0x37, 0x00, 0x03, 0x17, 0x00, 0x02, 0x38, 0x00, 0x03, 0x18,
0x00, 0x01, 0x19, 0x00, 0x04, 0x00, 0x00, 0x00
]


vm_code = vm_chars[255:]

result = 1
n = 0

def decode(one, two, three):
global tmp
if one == 1:
vm_chars[two] = three
elif one == 2:
tmp = vm_chars[two]
else:
if one != 3:
return 0
vm_chars[two] = vm_chars[two] ^ tmp
return 1


while result:
one = vm_code[n]
two = vm_code[n + 1]
three = vm_code[n + 2]
n += 3
result = decode(one, two, three)

print(bytearray(vm_chars))


FLAG{VMS-ARE-FOR-MALWARE} 



ransomware


ransomware1勒索解密,难度等级1星。


题目说是一个脚本小子写的勒索软件,问能否解密flag.txt文件?


仅为静态分析,不需要使用调试器。


下载解压完如图:



文件夹为加密过的文件,exe为加密程序demo。



先分析exe文件,使用IDA打开。


自动识别了start程序,进入看看。



进入sub_401000。



之前已分析过,所以已经做了些注释与重命名。


这个函数有两个参数,通过API可知,有打开文件与写入文件的操作,这里应该就是加密的核心了。


查snprintf函数的四个参数,结合已经加密的文件特征可以知道a1为需要加密的原始文件的文件名,这里的作用仅仅是拼接字符串作为加密后的文件名。


接下来的CreateFileA函数是打开已经存在的文件,第二个CreateFileA函数是新建一个文件。


do while循环里第一步先检查读取文件时是否成功,如果成功读取4096个字节则执行for循环。


for循环循环的次数为ReadFile实际读取的字节数v6,如果文件超过4096个字节,则v6第一次读取就为4096。


如果文件小于4096个字节,则v6为实际文件的大小。


for循环里有个参数a2未知,将a2的值加上i与0x20的取模运算后的结果再与当前文件从文件开头的第i个字节的值进行异或,最后的结果再赋值到源位置里。


循环4096次后,就结束循环。最后关闭文件句柄,退出。


加密的原理是对文件的前4096个字节进行计算后重新赋值,如果文件小于4096就对整个文件内容进行处理再在对应位置赋值。


问题的关键是a2的值是什么?


第一次不使用a2,直接对flag.txt进行处理,发现打开后是乱码,说明解密不成功。


这里的话确实要发挥脑洞了,压缩包里给了两类文件,一类是加密后的图片,一类是存在flag的txt文件。


先看图片文件,发现是英文命名的,后缀都为jpg格式。既然a2的值是固定的,加上jpg格式都是有一定特征的,那么如果这些jpg文件里有部分内容是一样的话,说明就可以通过再进行异或运算得到a2这个key的值。


使用Beyond Compare依次打开这些图片,然后一一比较,发现前22个字节是一致的(这里以为找到了突破口,发现却不是)。



说明图片是突破口,然后弄了半天没有发现如果找到图片开头的特征码,因为每个jpg文件好像除了开头两个字节是特征码之外,其余的字节不一定是一致的,我陷入了无解中。


然后突然觉得这个很难解密了,因为无法知道a2的值到底是什么。


带着怀疑就复制加密后的文件名Chrysanthemum去谷歌搜索下,发现了原来是Win7 自带的壁纸图片,啊啊啊,确实不能放过任何信息。



突然醒悟,或许这就是突破口,马上打开Win7寻找原始的壁纸图片,编辑器打开,与加密后的文件进行对比。 



源壁纸文件如下图:


 

加密后的壁纸文件如下图: 



最后,通过上述两个图片里前4096个字节的每个字节进行异或运算后得到规律发现a2的取值是一个字节数组。


key = [

0x6E, 0xF7, 0x9B, 0x0E, 0x45, 0x55, 0x5E, 0x95,

0x96, 0xFE, 0xAB, 0x75, 0x80, 0xB4, 0x0E, 0x40,

0x3D, 0xF5, 0xA7, 0x1B, 0xED, 0xD5, 0x5B, 0x80,

0xA9, 0xD3, 0x8D, 0x2C, 0xB8, 0x0A, 0x40, 0x0F

]


并且经过32个字节运算后,会重复这个过程,直到处理的字节数达到4096个。


得到key值后,可以通过之前逆出的加密流程对flag.txt_encrypted进行解密了。


解密脚本如下:


# coding:utf-8
key = [
0x6E,
0xF7,
0x9B,
0x0E,
0x45,
0x55,
0x5E,
0x95,
0x96,
0xFE,
0xAB,
0x75,
0x80,
0xB4,
0x0E,
0x40,
0x3D,
0xF5,
0xA7,
0x1B,
0xED,
0xD5,
0x5B,
0x80,
0xA9,
0xD3,
0x8D,
0x2C,
0xB8,
0x0A,
0x40,
0x0F
]


decrypted =
''
file_name =
"flag.txt_encrypted"


with
open(file_name,
'rb')
as f:
encrypted =
f.read()


for i in
range(len(encrypted)):
decrypted += chr(encrypted[i] ^ key[i %
0x20])


with
open(file_name.replace('_encrypted',
''),
'w')
as f:
f.write(decrypted)
print(decrypted)


感谢阅读!




- End -



看雪ID: jishuzhain  

https://bbs.pediy.com/user-678001.htm  



本文由看雪论坛 jishuzhain 原创

转载请注明来自看雪社区



往期热门回顾

1、几款Android反编译器对循环结构的还原能力测试记录

2、一种基于编译器的的JS混淆及反混淆方案

3、记一次纯脚本载荷攻击的样本分析

4、MuddyWater(污水)APT组织针对塔吉克斯坦的攻击活动的分析








京华结交尽奇士,意气相期矜豪纵。今夏与君相约看雪,把酒言欢,共话安全技术江湖梦。


10大议题正式公布!第三届看雪安全开发者峰会重磅来袭!




      ↙点击下方“阅读原文”,查看更多干货

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存