0%

1.加载一个rootkit

2.与rootkit通信

3.采用合法的网络通信

4.作者写的一个示例rootkit以及其背后的设计选择

5.执行来自内核的命令

6.掩盖你的rootkit的文件系统踪迹的技巧

Rootkit是一种特殊的恶意软件,它的功能是在安装目标上隐藏自身及指定的文件、进程和网络连接等信息,比较多见到的是Rootkit一般都和木马、后门等其他恶意程序结合使用。Rootkit通过加载特殊的驱动,修改系统内核,进而达到隐藏信息的目的。

Loading a Rootkit

1.利用合法的驱动。有很多脆弱的驱动,利用逆向知识,找到一个驱动的0day漏洞是很容易的。并且只需要几句原语即可提升权限,找到一个脆弱的驱动相对来说是简单的,因为兼容性的问题很难被检测。

缺点:跨操作系统版本的兼容性的主要问题取决于所拥有的原语;最有可能的是稳定性问题;最后一件事是你的恶意软件使得受害者蓝屏。

2.买证书。买一个合法的证书就没有稳定性的问题了,但是可能会揭露自己的身份以及会被拉进黑名单。

3.利用已经泄露的证书。这种方式相比购买证书来说更安全,但是在未来可能被检测,并且根据证书发布时间是有操作系统版本限制的。

Communicating with a Rootkit

1.发给C2服务器。防火墙可以阻止或标记对未知/可疑IP地址或端口的传出请求,先进的网络检查可以捕捉到一些试图“融入噪音”的外渗技术。

2.打开一个端口。一些恶意软件通过C2直接连接到受害者主机来控制它。这样设置相对简单。但是可能会被防火墙发现,并且很难融入噪声。

3.应用程序Hooking技术。更高级的恶意软件可能会选择hook一个特定的应用程序的通信作为一个通信通道。这样就很难被检测,特别是如果用的是合法的协议,但是这样不是特别灵活,机器可能没有暴露出该服务。

我们想要的是有限的检测向量,各种环境下的灵活性,假设受害主机会有一些已经揭露出来的服务,入站和出站访问可能被监控。那么第三种方法非常符合我们的需求,除了灵活性。现在要考虑的是如何不依赖一个应用程序。

如果不直接hook一个应用程序,那么我们hook像wireshark这样的工具的网络通信;在恶意数据包中放置一个特殊的指示器(一个”magic”常数);将这些恶意数据包发送到受害者机器上的合法端口;搜索这个常数的数据包,以将数据传递给我们的恶意软件.

Hooking the User-Mode Network Stack

hook Windows Winsock driver

在用户模式下windows上的许多服务都可以被发现,但是如何全面地获取这些流量是一个问题。与winsock相关的网络是由Afd.sys(该驱动负责将PC连接到网络服务,特别是TCP/IP协议)处理的,mswsock.dll用NtDeviceIoControlFiles实现与Afd.sys驱动的通信。逆向分析在mswsock.dll中的一些函数显示大量的通信是由IOCTLs完成的,如果我们可以拦截到这些请求,我们可以窥探所收到的数据。

  1. 在拦截之前要知道IRPs是怎么走的,当对一个设备调用文件句柄上的NtDeviceIoControlFile函数时,内核如何确定要调用什么函数?

首先通过IoGetRelatedDeviceObject检索与Afd驱动程序相关联的设备(检索FILE_OBJECT结构中的DeviceObject成员),然后判断驱动是否支持FastIo(在进行基于IRP 为基础的接口调用前, IO MANAGER 会尝试使用FAST IO 接口来加速各种IO 操作),如果支持那么就调用DriverObject->FastIoDispatch,如果不支持,那么就分配并填写IRP,然后调用IoCallDriver来发送IRP,对于IoCallDriver,内核将通过在DRIVER_OBJECT结构的MajorFunction数组中查找IRP内部指定的“Major function Code”来确定调用哪个函数

  1. 拦截IRPs的两种常见方法。替换掉驱动对象中你想hook的主函数;直接对调度处理程序执行代码hook。

  2. 要选择hooking最好的方法,要考虑的问题(可能有的检测向量有多少种,方法如何可用,检测这种方法昂贵吗)

Hook驱动对象:memory artifacts;对于稳定性,通过用互锁交换替换单个函数,这种方法应该是稳定的。对于兼容性,驱动程序对象有很好的文档记录,并且易于找到;但是检测起很便宜,所有的防病毒软件都需要枚举已加载的驱动程序,并检查主要功能是否在驱动程序的范围内。

Hook驱动程序的调度函数:memory artifacts;除非函数被导出,否则需要自己找到函数,由于补丁保护程序,并不是所有的驱动程序都与此方法兼容,HVCI不兼容的;检测也比较便宜。

Hook文件对象:没有被文档化;稳定;检测成本相对昂贵,反病毒软件必须复制我们的连接过程,并枚举文件对象,以确定设备/驱动程序对象是否被交换。。

Hook文件对象

1.创建我们自己的设备对象和驱动程序对象。
2.修补驱动程序对象的副本。
3.用我们自己的设备替换我们文件对象的DeviceObject指针

首先我们需要获得Afd设备的文件对象,我们可以通过ZwQuerySystemInformation函数,特别是使用SystemHandleInformation信息类来查询系统上的每个打开句柄。此信息类将为每个句柄返回以下结构:

image-20220414112600750

img

有了这些信息我们需要判断句柄是否是Afd设备的。首先通过比较ObjectTypeNumber成员与已知的文件对象类型索引来判断该句柄是否是一个文件对象的,然后我们再将FILE_OBJECT结构的DeviceObject成员与一致的Afd设备比较。

image-20220414112713704

image-20220414112803340

然后我们需要创建我们自己的假的驱动或者设备对象。创建之前先判断是否已经hook过了,Windows内核使用ObCreateObject函数来创建对象,内核大概是为了允许其他Windows驱动创建他们自己的对象,导出了该函数。

image-20220414113235946

image-20220414114728584

image-20220414114639854

最后我们需要hook我们的假的驱动对象,可以用前面提到过的方法,将DRIVER_OBJECT结构中的MajorFunctions数组部分替换为我们的hook函数,(要注意我们要hook的是我们自己创建的驱动对象,而不是真正的驱动对象

image-20220414141558734

最后一步就是将FILE_OBJECT的DeviceObject成员替换成我们自己的设备。

现在文件对象被Hook了,对IoGetRelatedDeviceObject的任何调用都将返回我们的假设备,IoCallDriver将使用该设备调用我们的修补过的MajorFunctions数组。

剩下的工作:检查我们是否正在hook被调用的MajorFunction,如果是,则调用为该主函数传递原始设备对象和原始调度函数的hook函数;确保当MajorFunction是IRP_MJ_CLEANUP时保存了原始的DeviceObject。

image-20220414143143109

How the Spectre Rootkit Abuses the User-Mode Network Stack

Abusing the Network

有了前一步的hook,现在我们已经可以拦截Afd驱动的IRPs了。并且我们可以拦截所有用户模式的网络流量,通过任何套接字发送和接收我们自己的数据。

Packet Structure

img

Processing

收到数据包后先对数据包的magic constant部分进行判断。

img

在发出包之前,我们需要创建一个完整的数据包

img

Packet Handlers

img

其他数据包处理程序继承这个基类。在上面的类中需要注意的两个关键问题是,调度器不仅会在处理程序的构造函数中传递指向自身的指针,而且实际的数据包将被传递给数据包处理程序的ProcessPacket函数。

一旦数据包被填充完,process handler会采取以下步骤

img

通过将一个指针传递给相关的包处理程序,该包处理程序可以递归地处理一个新的包。示例XorPacketHandler:

img

此XOR_PACKET实际上并不执行恶意操作。相反,它充当了一个封装的数据包。当Xor软件包处理程序收到一个数据包时,它将使用XorKey消除XorContent;递归地将XorContent分派为一个新的数据包。

该模型允许您创建无限的封装层。

Executing the Commands

首先我们需要了解从用户模式上下文执行命令的过程

img

关于内核模式,让我们从创建获取输出所需的管道开始,下面是创建管道在后台所做的事情。

img

image-20220414150937355

image-20220414151108045

image-20220414151134723

现在我们有了管道,我们需要创建实际的流程。我们将使用ZwCreateUserProcess,因为这是kernelbase.dll使用自己来创建进程的方法。(这一部分在后面详细说)

image-20220414152129288

我们需要从从进程的属性列表开始,我们必须设置的最重要的属性是PsAttributeImageName。这将指定新进程的映像文件名。

接下来我们需要为进程填充RTL_USER_PROCESS_PARAMETERS结构,主要填充参数有窗口标志和输出到我们的管道的句柄,当前目录、命令行参数、进程映像路径和默认的桌面名称。这些都封装在函数StartProcess中。

image-20220414151508016

image-20220414152501417

image-20220414151937052

然后我们就可以用ZwCreateUserProcess开启进程了,一旦进程退出,类似于我们在用户模式下所做的事情,我们可以调用ZwReadFile来从未命名的管道中读取输出。

image-20220414152615260

Finding Unexported ZwXx Functions Reliably

ZwCreateUserProcess这样的函数,不能被 Windows 内核可靠导出。需要有一种稳定性的方法去寻找到这养的函数。 ZwCreateUserProcess函数设置的系统调用号与Ntxx(用户模式下)中的相同,因此可以用后者的系统调用号寻找Zwxx。几乎所有的NtXx实现都惊人的一致,在参数传入后,把系统调用号(在内核寻找真正的处理函数使用)保存至eax内,之后判断cpu是否支持快速调用,如果支持使用syscall进入内核,反之使用中断门进入内核,这两种方式除了使用不同的堆栈切换方式和效率外并未有其他本质区别。(还可以看一下这篇动态获取系统调用

系统调用号在不同的 Windows 版本之间会发生变化; 硬编码系统调用不是一个好的方法。相反,可以选择自己解析 ntll.dll 来提取系统调用号。

1.rootkit先加载ntdll.dll

image-20220414153415044

2.我们通过枚举 ntdll.dll 模块的导出地址表(EAT)来找到 NtCreateUserProcess 函数。

image-20220414153531952

3.因为Ntxx函数都是按照以mov r10, rcx``mov eax,[system call number]这样的汇编代码开始的,所以我们很容易提取其系统调用号。

image-20220414154142166

Zwxx函数采用的是下列的格式:

1
2
3
push rax
mov eax, [system call number]
jmp KiServiceInternal

要找到ZwXx函数,我们需要在ntoskrnl.exe的可执行节中搜索上述格式

image-20220414154856315

Hiding a Rootkit

MiniFilter

微过滤驱动通过过滤管理驱动进行注册来实现拦截特定文件I/O.

Minifilter可以用于掩盖我们的根工具包在文件系统上的存在。它可以将某一文件的所有文件访问权限定向到另一个文件。我们可以使用此功能将对驱动程序文件的访问重定向到另一个合法的驱动程序。

img

利用MiniFilter最简单的方法就是自己成为一个MiniFilter,这个方法的检测向量主要是注册表和内存痕迹,不用担心稳定性和实用性,但是检测比较便宜,除了注册表工件之外,注册为MiniFillter的驱动程序也可以通过FltEnumerateFilters等API轻松枚举。

第二个方法是Hook一个MiniFilter,有三种方式,第一种是代码Hook现有的过滤器的回调;第二种是在受害驱动程序使用FLT_REFISTRATION结构体获得你自己的回调之前重写该结构;第三种是DKOM(直接内核对象操作技术)一个现有的过滤器实例,并用您的替换原始回调。第一种方式虽然简单,但是有很多缺点。对于第三种方式,这是一种半文档化的方法,可以通过FltEnumerateFiltersFltEnumerateInstances这两个API枚举过滤器和实例,为某个操作调用的函数在FLT_INSTANCE结构中的CallBackNodes数组中指定。这种方式会有内存痕迹,对于稳定性来说,虽然获得一个FLT_INSTANCE结构是文档化的,但是FLT_INSTANCE结构它自己不是文档化的,并且检测并不昂贵,反病毒软件需要偶尔枚举注册的过滤器在CallBackNodes数组中的钩子及其实例。

img

在代码中没有找到利用通过微过滤驱动的部分,只看到inf文件中有以下内容,猜测采用的是第一种方法们也就是自己成为一个MiniFilter.

image-20220414164650059

Maze

1.首先PEid查壳,查到是UPX的壳

image-20211011192051606

2.UPX脱壳,之前在网上找脱壳工具,但是其实UPX是开源的,在github上下载后,在cmd下执行upx -d yourfile.exe就可以脱壳,拖进IDA是下面的样子,发现f5不能查看伪代码,再看左边的函数栏,没有main函数。

image-20211011195316028

网上搜索后发现是加了花程序

image-20211011195716680

查看上图的jnz跳转到了下一行代码。相当于没有跳转,接下来的call调用的一个不是地址的地址,网上的方法是将jnz指令nop掉(nop指令也就是控指令,执行的时候不做任何事,有时候可用于短接某些触点或用nop指令将不要的指令覆盖掉

3.先将jnz指令nop掉(Edit->Patch program->Assemble),因为call后面很有可能事有用的数据,所以用d先转换为数据,jnz后面的也是花指令,所以应该把该数据包的高字节部分也nop掉,但是IDA总是不成功,所以用OD,修改后右键复制到可执行文件,然后右键保存

image-20211011201009496

image-20211011203812041

image-20211011203847749

4.拖进IDA反汇编,这个代码逻辑比较简单,wsad其实就是玩游戏的上下左右,

初始dword_408078=7,dword_40807c = 0,需要经过十四次移动修改两个变量为5和-4,按照迷宫题目的经验应该有地图或者字符。提取出来自己排列一下

flag{ssaaasaassdddw}

image-20211012094241147

image-20211012154856963

image-20211012154814391

在这篇博文中,详细分析了与名为 APT21 的 APT(高级持续威胁)攻击者相关联的恶意文件(称为“Travelnet”的后门)。

Travelnet

第一个文件是用于将恶意 DLL(NetTraveler 木马)注册为服务的投放程序。 该木马的主要目的是收集有关环境的信息,例如用户名、主机名、主机的 IP 地址、Windows 操作系统版本、CPU 的不同配置、有关内存消耗的信息、进程列表。

恶意进程对磁盘以及 USB 驱动器和网络共享上的.doc/.docx/.xls/.xlsx/.txt/.rtf/.pdf 文件感兴趣,以便渗漏它们。在整个感染过程中,会创建多个.ini 配置文件,并且恶意软件还能够在受感染的机器上下载和执行其他文件。数据使用基于Lempel-Ziv的自定义算法进行压缩,并使用修改后的 Base64 算法进行编码,然后再将其泄露到命令和控制服务器。

1.Dropper

恶意软件执行的第一步包括创建名为“立即安装服务”的互斥锁(注意空格)。互斥锁用于避免已感染的计算机再次感染

API:CreateMutexA

创建一个配置文件config_t.dat,配置文件由WritePrivateProfileStringA填充

API:CreateFileA

    WritePrivateProfileStringA

在恶意代码的精确位置发现加密字节,加密包含了XOR操作,解密结束后发现是URL(C2服务器)

注意在文件0x334偏移处有一字节表示恶意软件是否用代理,如果是0,配置文件中UP=0,如果是1,UP=1,并且还有PS (proxy address), PP (proxy port), PU (proxy user), PW (proxy password), PF (unknown)

RegQueryValueExA该函数用来搜索与“HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Svchost”相关联的netsvcs(svchost.exe)的类型和数据

API:RegQueryValueExA

恶意文件枚举主机上所有可用的服务,将其与硬编码列表中的进行比较,第一个系统上没有的服务便被用来当作恶意目的。策略:枚举所有服务相对应的键值,查看服务是否被安装。第一个没被安装的服务是FastUserSwitchingCompatibility, RegOpenKeyExA 用于检查服务是否存在。

与该服务相关联的dll将被删除,然后一个同名服务将被创建冒充为合法服务,服务的二进制路径为%SystemRoot%\System32\svchost.exe-k netsvcs,可以在网址(https://lolbas-project.github.io/查看到更多的合法进程和dll。

API:RegOpenKeyExA

    DeleteFileA

    CreateServiceA

在该服务下方一个新的键值parameters被创建,这个键值被用来将恶意dll注册为一个服务.

img

进程创建一个temp.bat的空文件作为初始可执行文件,该文件的目的是通过添加ServiceDll条目,将前面恶意程序删除后重新创建的dll注册为服务。因为文件时重新创建的,所以为了不引起怀疑,修改时间戳是一种规避技术。

现在这个dll中都是恶意代码,甚至路径看起来都是合法的。将DLL文件注册为服务是一种持久性机制。使用StartServiceA启动新创建的服务,并将执行流传递给DLL导出函数ServiceMain:

API:RegCreateKeyA

    WriteFileA

    StartServiceA

2.DLL File

恶意软件执行的第一个步骤之一是调用GetProcessWindowsStation,该API返回当前窗口的句柄,然后使用OpenWindowsStationA打开交互式窗口Winsta0。进程使用setProcessWindowsStation函数将指定的窗口Winsta0分配给调用进程,该窗口是唯一的交互式窗口(服务应该是交互式的)。与之前一样,这个进程也会创建一个互斥锁,如果存在,将会退出而不重新感染主机。

API:GetProcessWindowsStation

    OpenWindowsStationA

    setProcessWindowsStation

    CreateMutexA

它从由第一个进程创建的配置文件config_t.dat中检索几个元素:WebPage、DownCmdTime、UploadRate、AutoCheck、UP和CheckedSuccess(此时不存在,因此函数返回0)。使用GetPrivateProfileString和GetPrivateProfileInt提取所有值。

API:GetPrivateProfileString

    GetPrivateProfileInt

进程会创建一个与执行文件同名的log文件,该文件会枚举C:\ProgramFile(x86) 下的路径,并将输出复制到新创建的文件中

API:CreateFileA

恶意软件正在查找名为“C:\Users<Username>\AppData\Local\Microsoft\Windows\History\History.IE5\index.dat”的文件,该文件包含Internet浏览历史活动,包括基于Internet的搜索和打开的文件.故恶意软件打开注册键Shell Folders并提取History值,以及从Internet Explorer键中提取Version值,获得的信息附加到log文件中(包括操作系统版本)

API:RegOpenKeyExA

    RegQueryValueExA

    GetVersionExA

网络通信中使用的用户代理始终设置为“Mozilla/4.0(compatible; MSIE 6.0)”。还有一个Accept 请求(httP标头。该进程尝试连接到(http://www.microsoft.com/info/privacy_security.htm (此URL过去可用)以验证是否存在网络连接,如果成功,也会把相应的信息输出到log文件。

img

API:InternetOpenA

    (httpOpenRequestA

如果连接不成功,进程会找到explorer.exe并试图打开。基本上,攻击者的目的是通过调用OpenProcessToken来窃取explorer.exe进程令牌,以打开与explorer.exe关联的访问令牌,然后使用ImpersonateLoggedOnUser函数模拟用户的安全上下文

API:Process32First 

    Process32Next

    OpenProcess

    OpenProcessToken

    ImpersonateLoggedOnUser

该进程使用RegOpenKeyExA打开“HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Internet Settings”注册表项,然后提取ProxyEnable值以查看计算机是否使用代理服务器,并且提取ProxyServer(网络上代理服务器的主机名/IP)和ProxyOverride(绕过代理服务器的主机名/IP)。收集到这些信息后,再次连接同样地URL,成功后会将信息输出到log文件。

API:RegOpenKeyExA

如果该方法奏效,恶意程序会修改config_t.dat文件中地值,如果失败就会将失败信息记录在log文件,一共会尝试四次方法。如果所有方法都失败了,感染就会停止,并且会执行一些自我删除操作。

如果有一个方法奏效,那么恶意软件会休眠一分钟,然后创建一个线程1,再休眠十秒创建一个线程2

2.1 Thread1

首先,线程检索与“C:\”目录关联的卷序列号(“A2C9-AD2F”)。该序列号将用作与C2服务器通信时的主机id。此外,它还查找计算机的NETBIOS名称,与当前线程关联的用户名,检索计算机的主机名,使用gethostbyname/inet\n函数打印计算机的IP地址。枚举可用的磁盘驱动器,它感兴趣的是类型3。获取系统物理和虚拟内存,除此以外还获取了其他的键值信息,并将目前为止提取地信息存储在system_t.dll中,以便对其进行过滤。

下一步是创建管道,用作进程间通信机制,创建一个新的进程“ipconfig/all”,输出将通过管道传输回原始流程。

API:getVolumeInformation

    GetComputerNameA

    GetUserNameA

    gethostname

    GetLogicalDrivers

    GetDriverTypeA

    GlobalMemoryStatus

    CreatePipe

    CreateProcessA

    ......

恶意软件使用GetPrivateProfileInt函数检查config_t.dat中的UP值。根据卡巴斯基报告,system_t.dll文件的内容将使用基于Lempel-Ziv的自定义算法进行压缩,并使用修改后的Base64算法进行编码。

编码后地数据通过一个向vipmailru[.]com(C2服务器)的GET请求进行过滤。如果服务器返回成功,那么就过滤成功,恶意程序会删除system_t.dll文件,进程会执行另一个GET请求(参数包含action = getcmd),请求的结果有相应的开头和结束的格式,在开头结束中间的数据会被保存在stat_t.ini。进程会再执行一个GET请求(参数包含action = gotcmd)

如前所述,如果一切正常,则文件需要包含“Success”字符串的(httP响应。进程正在删除一个名为“C:\Windows\SysWOW64\dnlist.ini”的文件,该文件目前不存在。该文件将创建,并使用以下数据填充该文件:

img

删除文件stat_t.ini,然后调用GetACP,该API返回操作系统的当前Windows ANSI代码页标识符。因为在dnlist.ini中ScanAll的值为True,所以恶意软件扫描所有可用的磁盘驱动器,然后将它们的类型与3(固定驱动器)或4(远程驱动器)进行比较。枚举所有文件以及路径,路径记录命名为dn,文件命名为fn,如下图,保存在enumfs.ini。这样针对c:\的操作是递归的,并应用于每个目录。枚举完成后发送给服务器。

img

API:GetLogicalDrivers

    GetDriverTypeA

恶意进程试图打开目前不存在的uenumfs.ini,然后枚举在“C:\User\AppData\Local\Temp\ntvba00.tmp\”中找到的文件。此特定目录将由Thread2创建,并包含已选择要导出到C2服务器的所有文件。现在该进程再次使用参数“action=getdata”联系 C2 服务器。 它需要以下响应之一:“A2C9AD2F:UNINSTALL”、“A2C9AD2F:UPDATE”、“A2C9AD2F:RESET”或“A2C9AD2F:UPLOAD”(注意“A2C9AD2F”是之前提取的卷序列号)

Case1:UNINSTALL

删除相应键值,并且删掉enumfs.ini、dnlist.ini、udidx.ini、uenumfs.ini、stat_t.ini文件,C2服务器通过执行带有“action=updated”参数的GET请求得知操作已完成。

Case2:UPDATE

删除上述的同样的键值以及文件,使用“action=datasize”参数向C2服务器发出GET请求,如果一切正常,(httP响应应包括“Success:”.恶意软件会下载一个update.exe的文件。

下载文件的字节与“MZ”(Windows中可执行文件、DLL文件的格式)进行比较,并且它还查找特定偏移量处的“PE”字符串。下载的文件保存为“C:\Windows\install.exe”,并由恶意进程运行。然后执行同样的GET请求表示操作已完成。

Case3:RESET

同样删除ini文件,然后执行同样的GET请求表示操作已完成。

Case4:UPLOAD

此情况与UPDATE相同,不同之处在于未删除任何文件/注册表项。

在执行流通过所有案例后,流程休眠60秒,然后返回到循环中。

2.2 Thread2

主要是一些窗口的操作,恶意软件对参数为DBT_DEVICEARRIVAL(0x8000)的WM_DEVICECHANGE(0x219)消息感兴趣,这意味着有插入了新的USB驱动器或系统上安装了网络共享文件夹这样的事件

从dnlist.ini解析的USearch和UTypeLimit值应设置为True,dnlist.ini中的UAuto值也应设置为False(这可能表明是否应自动过滤目标文件)。攻击者还对Types参数(目标扩展)感兴趣,稍后我们将了解原因。其想法是扫描插入的每个设备以及主机上安装的网络共享,并在uenumfs.ini文件中创建一个“文件系统”结构(就像在Thread1中所做的那样)

与第一种情况一样,此搜索将递归地应用于驱动器上找到的每个目录。该进程创建一个“C:\Users<Username>\AppData\Local\Temp\ntvba00.tmp\”目录,其属性设置为hidden。还将创建以下文件:“C:\Windows\SysWOW64\uenumfs.ini”(其内容将类似于enumfs.ini)。进程还会比较文件的后缀,并且为符合文件格式的文件计算一个HASH值(文件名和最后修改时间)。文件会被复制到tmp的文件夹下,并以新的文件名命名(hidden file–year-month-day-hour-minute-5f7a78e7927532ba2a930ec8d47e252a)

该进程将创建“C:\Windows\SysWOW64\udidx.ini”文件,并将添加所有按照前面说明计算的哈希,新文件的最后修改时间戳设置为从初始文件提取的值。

最后,该文件使用DefWindowProcA API来确保应用程序不处理的窗口消息具有默认处理功能。

Reference

(https://cybergeeks.tech/dissecting-apt21-samples-using-a-step-by-step-approach/

中断请求级别(IRQL)

前面提到了线程和线程的优先级,当想要执行的线程比可用的处理器更多时,就会考虑到这些优先级。同时,硬件设备需要通知系统有什么事情需要注意。一个简单的例子是由磁盘驱动器执行的I/O操作。一旦操作完成,磁盘驱动器将通过请求中断来通知完成。此中断连接到中断控制器硬件,该硬件随后将请求发送到处理器进行处理。下一个问题是,哪个线程应该执行关联的中断服务例程(ISR)?

每个硬件中断都与一个优先级相关,称为中断请求级(IRQL)。每个处理器的上下文都有自己的IRQL,就像任何寄存器一样。IRQL应该像任何其他CPU寄存器一样被处理。

其基本规则是,处理器要执行具有最高IRQL的代码。例如,如果一个CPU的IRQL在某个点为零,并且会出现一个相关IRQL为5的中断;将其状态(上下文)保存在当前线程的内核堆栈中,将其IRQL提升到5,然后执行与中断关联的ISR;一旦ISR完成,IRQL将降到它以前的级别,恢复之前执行的代码,就好像中断不存在一样。当ISR正在执行时,其他IRQL为5或更少的中断不能中断该处理器。但是,如果新中断的IRQL高于5,CPU将再次保存其状态,将IRQL提升到新级别,执行与第二个中断相关的第二个ISR,完成后,将回落到IRQL5,恢复其状态并继续执行原始ISR。本质上,只是暂时提高与IRQL相等或较低的IRQL块代码。中断发生时事件的基本序列。

一句话概括就是,当优先级别高的中断来临时,处在优先级低的中断处理程序会被打断,进入到更高级别的中断处理函数。

img

img

上面两个图描述的场景中,所有ISR的执行都是由首先被中断的同一线程完成的。Windows没有一个特殊的线程来处理中断;它们都由当时在被中断的处理器上运行的任何线程来处理。当处理器的IRQL为2或更高时,上下文切换是不可能的,所以在这些ISR执行时,其他线程无法偷偷溜进来。

下面有一些重要的IRQL(0-2为软件中断,3-31为硬件中断):

img

其中Device IRQL用于硬件中断的一系列级别(x64/ARM/ARM64为3至11,x86为3至26)。最高级别HIGH_LEVEL被处理链表操作的一些API所使用。实际值分别为15(x64/ARM/ARM64)和31(x86)。

用户模式的代码是运行在最低优先级的PASSIVE_LEVEL级别。驱动程序的DiverEntry、派遣函数等也是运行在该级别,他们可以在必要时申请进入DISPATCH_LEVEL。驱动程序的StartIO函数和DPC函数以及windows负责线程调度的组件都是运行在DISPATCH_LEVEL。

当处理器的IRQL被提高到2或更高时,都会对执行代码施加某些限制:

  • 分页内存随之可能会从物理内存交换到磁盘文件。访问非物理内存中的内存,会引发一个页故障,从而执行这个异常的处理函数。说明从非分页池访问数据总是安全的,而从分页池或用户提供的缓冲区访问数据则不安全,应该避免。

  • 等待任何调度程序内核对象(例如互斥锁或事件)会导致系统崩溃,除非等待超时为零,这仍然是允许的。

如果在高于缺页中断的中断优先级上再发生缺页中断,内核就会崩溃。所以在DISPATCH_LEVEL级别以上,绝对不能使用分页内存,一旦使用分页内存,就有发生缺页中断的可能,这样会导致内核崩溃。

提一下分页和非分页内存:分页内存是指,暂时不会被用到的虚拟内存页面的内容,可以存储在磁盘倒换文件中,在需要时,可以将磁盘文件中的内容导入到物理内存中,暂时不用时,可以倒换到磁盘文件中。非分页内存是指虚拟内存映射到的物理内存是常驻在物理内存中,不可以被交换到磁盘文件中。

提升或降低IRQL

在内核模式下,IRQL可以用KeRaiseIrql函数来提高,然后用KeLowerIrql来降低。下面是一个代码片段,它将IRQL提升到DISPATCH_LEVEL(2),然后在这个IRQL上执行一些指令后将其降低回来。(**note:**如果提高了IRQL,请确保在相同的函数中降低它。此外,确保KeRaiseIrql与KeLowerIrql是否真的实现了IRQL的提升与降低否则,系统就会崩溃。)

img

线程优先级vsIRQL

IRQL是一个处理器的一个属性。优先级是一个线程的一个属性。线程优先级只针对应用程序而言,是指某线程是否有更多的机会运行在CPU上,所以仅在IRQL<2上有意义。一旦一个正在执行的线程将IRQL提高到2或更高,它的优先级就不再意味着任何东西。理论上它将继续执行,直到它将IRQL降低到2以下。

延迟过程调用

延迟过程调用(DPC)是Windows的机制,允许高优先级任务如中断处理程序延迟所需的低优先级任务稍后执行。这使得设备驱动程序与其他低层事件消费者更快地执行其处理的高优先级部分,调度非关键的附件处理稍后以较低优先级执行。

用一个实例说明一下

img

用户模式线程将打开一个文件的句柄,并使用ReadFile函数执行读取操作。由于该线程可以进行异步调用,因此它几乎会立即恢复控制,并可以执行其他工作。接收此请求的驱动程序调用文件系统驱动程序(例如NTFS),它可以调用它下面的其他驱动程序,直到请求到达磁盘驱动程序,该驱动程序在实际的磁盘硬件上启动操作。

当硬件完成读取操作时,它会发出一个中断。这将导致与该中断相关联的ISR在Device IRQL上执行(请注意,处理该请求的线程是任意的,因为该中断是异步到达的)。一个典型的ISR会访问设备的硬件,以获得操作的结果。它的最后操作应该是完成最初的请求。

完成请求是通过调用IoCompleteRequest。但是有一个问题是该函数只能在IRQL<= DISPATCH_LEVEL(2)的情况下被调用,这一位置ISR不能调用该函数,那么ISR做什么呢?

允许ISR尽快调用IoCompleteRequest(以及其他具有类似限制的函数)的机制是延迟过程调用(DPC)。DPC是一个用于封装函数的对象,封装的函数函数在DISPATCH_LEVEL调用。在这个IRQL中,允许调用IoCompleteRequest。

DPC是通过DPC对象实现的。当设备驱动程序或其他内核态程序发出DPC请求时,操作系统内核创建DPC对象,投寄到DPC队列尾部。当Windows操作系统的IRQL降低到Dispatch/DPC级,操作系统检查DPC队列,逐个执行挂起的DPC,直至队列为空或者发生IRQL更高的中断。

注册了ISR的驱动程序提前准备了一个DPC(从非分页池分配KDPC结构,并使用回调函数KeInitializeDpc初始化它);然后,当调用ISR时,就在退出该函数之前,ISR请求DPC通过KeInsertQueueDpc排队来尽快执行它。当DPC函数执行时,它会调用IoCompleteRequest。所以DPC作为一种妥协——它运行在IRQL DISPATCH_LEVEL(2)上,这意味着不会发生调度,没有分页内存访问,等等。

Note:默认情况下,KeInsertQueueDpc将DPC排队到当前处理器的DPC队列。当ISR返回时,在IRQL可以降回到零之前,将检查在处理器的队列中是否存在DPC。如果有,处理器下降到IRQLDISPATCH_LEVEL(2),然后以FirstInFirstOut(FIFO)的方式处理队列中的DPC,调用各自的函数,直到队列为空。只有这样,处理器的IRQL才能降至零,并恢复执行在中断到达时被干扰的原始代码。

img

img

异步过程调用

APCs也是封装要调用的函数的数据结构,但是与DPC相反,APC的目标是特定的线程,因此只有该线程才能执行该函数。这意味着每个线程都有一个与其关联的APC队列。

有几种类型的APC:

用户模式APCs:只有当线程进入可警报状态时,这些操作才会在PASSIVE_LEVEL的用户模式下执行。这通常是通过调用一个API来实现的,如SleepEx,WaitForSingleObjectEx, WaitForMultipleObjectsEx和类似的API。可以将这些函数的最后一个参数设置为TRUE,以使线程处于可警报状态(等待状态)。在这种状态下,它会查看它的APC队列,如果不是空的,则APC执行,直到队列为空。

正常的内核模式APCs:这些操作在PASSIVE_LEVEL(0)的内核模式下执行,抢占用户模式代码和用户模式APCs。

特殊的内核APCs:这些操作在APC_LEVEL(1)的内核模式下执行,并抢占用户模式代码、普通内核APCs和用户模式APCs。I/O系统使用这些APCs来完成I/O操作。

Critical Regions and Guarded Regions

Critical Region阻止用户模式和正常的内核APC执行(特殊的内核APC仍然可以执行)。线程通过KeEnterCriticalRegion\KeLeaveCriticalRegion进入\离开一个关键区域。内核中的一些函数需要在一个关键区域内,特别是在使用执行资源时(请参阅本章后面的“执行资源”一节)。

Guarded Region可阻止所有APCs的执行。线程通过KeEnterGuardedRegion\KeLeaveGuardedRegion进入\离开一个保护区域。两个函数调用数量必须相同。

**note:**将IRQL提高到APC_LEVEL将禁用所有apc的交付。

结构化异常处理(SEH)

异常与中断有些相似,主要的区别是异常是同步的,并且在技术上具有可重复性。如果发生异常,内核会捕获此异常,并允许如有可能的代码处理异常。这种机制称为**结构化异常处理(SEH)**,可用于用户模式代码和内核模式代码。

内核异常处理程序是基于**中断调度表(IDT)**调用的,该表还支持中断向量和ISR之间的映射。

常见的异常:

Division by zero (0) ;

Breakpoint (3):内核透明地处理它,将控制件传递给附加的调试器;

Invalid opcode (6):如果CPU遇到未知指令,则会引发此故障;

Pagefault(14):如果用于将虚拟地址转换为物理地址的页面表条目将有效位设置为零,这表示(就CPU而言)该页面没有驻留在物理内存中,则CPU会引发此故障。

由于以前的CPU故障,内核还引发了其他一些异常。例如,如果引发了页面故障,则内存管理器的页面故障处理程序将尝试找到未驻留在RAM中的页面—如果该页面根本不存在,则内存管理器将引发访问冲突异常—一旦引发异常,内核将为处理程序搜索发生异常的函数(除了它透明处理的一些异常,如断点(3))—如果没有找到,它将向上搜索调用堆栈,直到找到这样的处理程序—如果调用堆栈已耗尽,则系统将会崩溃。

img

__try/__except

下图是关于第四章的读取用户模式缓冲区的部分,如果因为缓冲区没有数据这样的错误,造成系统崩溃就不好了。Except中的EXCEPTION_EXECUTE_HANDLER表明任何一场都会被处理,也可以调用GetExceptionCode函数查看实际的异常,如果不能,可以告诉内核继续寻找调用堆栈上的处理程序:

img

img

非法访问是只有当被禁止访问的地址在用户空间中时才能被捕获。如果它位于内核空间中,那么它将不会被捕获,并且仍然会导致系统崩溃。这应该是有意义的,因为已经发生了一些不好的事情,而且内核不会让驱动程序侥幸逃脱惩罚。另一方面,用户模式地址不受驱动程序的控制,因此可以捕获和处理这种异常。

驱动程序(和用户模式代码)也可以使用SEH机制来抛出自定义异常。内核提供了泛型函数ExRaiseStatus,以引发任何异常和一些特定的函数如ExRaiseAccessViolation。

__try/__finally

这是关于确保某些代码无论如何都能执行——代码是干净地退出还是由于异常而中途退出。SEH的异常结束处理模型主要由try-finally语句来完成。终止处理就是保证应用程序在一段被保护的代码发生中断后(无论是异常还是其他)还能够执行清理工作,清理工作包括关闭文件、清理内存等。

img

如上面的例子,如果在分配和释放之间由return语句或者有异常,那么资源将不会被释放,这个时候就需要用到__try/__finally,确保能执行释放操作。

img

这样的话即使__try结构中有return语句,也会在__finally区块调用完再返回。如果发生异常,__finally块首先运行,然后内核在调用堆栈中搜索处理程序。

用c++ RAII替代__try/__finally

RAII(资源获取就是初始化),是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。

系统崩溃

我们已经知道,如果在内核模式下发生未处理的异常,系统就会崩溃,通常是“死亡蓝屏”。在本节中,我们将讨论当系统崩溃时会发生什么,以及如何处理它。

BSOD是一种保护机制,如果泵应该被信任的内核代码做了一些无法预料的坏事,那么停止一切,避免一些重要的文件或者注册表项损坏,是最安全的方法。

如果系统崩溃,则可以将事件条目写入事件日志。最重要的设置是生成转储文件。转储文件会捕获崩溃时的系统状态,因此以后可以通过将转储文件加载到调试器中来进行分析。转储不是在崩溃时写入目标文件的,而是写入第一页文件的。只有当系统重新启动内核时,当系统注意到页面文件中存在转储信息时,它才会将数据复制到目标文件中。其原因与在系统崩溃时,将某些东西写入一个新文件可能太危险有关;系统可能不够稳定。最好的办法是将数据写入已经以任何方式打开的页面文件。缺点是,页面文件必须足够大,才能包含转储,否则将不会写入转储文件。内存转储主要有:小内存转储、内核内存转储、完整内存转储

、自动内存转储、灵活内存转储(win10+:这类似于一个完整的内存转储,除了如果崩溃的系统托管客户虚拟机,它们当时使用的内存不会被捕获。这有助于减少可能托管许多虚拟机的服务器系统上的转储文件大小。)

崩溃dump信息

一旦您有了崩溃转储,您可以通过选择文件/打开转储文件并导航到该文件,在WinDbg中打开它。

分析一个dump文件

~ns命令用来切换处理器,!running列出崩溃时在所有处理器上运行的线程,添加-t作为一个选项,将显示每个线程的调用堆栈。

!stacks命令列出了所有线程的所有线程堆栈。一个更有用的变体是一个搜索字符串,它只列出了包含该字符串的模块或函数出现的线程。这允许在整个系统中定位驱动程序的代码(因为它在崩溃时可能还没有运行,但它在某些线程的调用堆栈上)。

系统挂起

系统崩溃是通常被调查的最常见的转储类型。但是,您可能需要使用另一种类型的转储文件:一个挂起的系统。一个挂起的系统是一个无响应的或接近无响应的系统。事情似乎在某种程度上被停止或被锁定——系统不会崩溃,所以我们要处理的第一个问题是如何转储系统?

Note:转储文件包含某些系统状态,它不必与崩溃或任何其他坏状态相关。有一些工具(包括内核调试器)可以随时生成转储文件。

线程同步

一个比较好的例子是一个驱动用链接列表去收集数据列表,因为该驱动程序可以被来自一个或多个进程中的多个线程的多个客户端调用,所以对于数据的访问操作涉及到了线程之间的同步互锁操作。

互锁操作

互锁函数集提供了利用硬件原子执行的方便操作,这意味着不涉及软件对象。如果使用这些函数可以完成工作,那么应该使用它们,因为它们可以尽可能有效。

img

调度对象

img

Objects:指定要等待的对象。请注意,这些函数适用于对象,而不是句柄。如果您有一个句柄(可能由用户模式提供),请调用对象对象句柄以获取指向该对象的指针。

WaitReason:等待原因的列表很长,但是驱动程序通常应该将其设置为Executive,除非它因为用户请求而等待,如果是这样,请指定UserRequest。

WaitMode:大多数驱动程序应该指定KernelMode。

Alertable:指示线程是否处于可警报状态。可警报状态允许传递用户模式异步过程调用(APC)。如果等待模式为用户模式,则可以交付用户模式apc。大多数驱动程序都应该指定false。

Timeout:指定要等待的时间。如果指定了NULL,等待是无限的,只要对象成为信号。

Count:要等待运行的对象数。

Object[]:要等待的对象指针数组。

WaitType:指定是等待所有对象同时发出信号(WaitAll)还是仅等待一个对象(WaitAny)。

WaitBlockArray:内部用于管理等待操作的结构数组。如果对象的数量是<=THREAD_WAIT_OBJECTS(当前为3),那么这是可选的——内核将使用每个线程中存在的内置数组。如果对象数量较高,驱动程序必须从非分页池中分配正确的结构大小,并在等待结束后解除它们。

KeWaitForSingleObject返回值:

img

img

Mutex

互斥锁是许多可以随时访问共享资源的一个线程中的一个典型问题的经典对象。互斥锁在空闲时就会发出信号。一旦线程调用等待函数并满足等待,互斥锁就会变成无信号,线程就会成为互斥锁的所有者。

Note: 如果线程是互斥的所有者,它是唯一可以释放互斥的线程。

互斥锁可以通过同一线程多次获取。第二次尝试会自动成功,因为该线程是互斥锁的当前所有者。这也意味着线程需要以它所获得的相同的次数来释放互斥锁;只有这样,互斥锁才会再次获得自由(发出信号)。

使用互斥锁需要从非页面池中分配一个KMUTEX结构。

KeInitializeMutex必须调用一次初始化mutex;

等待函数之一传递已分配的KMUTEX结构的地址;

KeReleaseMutex:当作为互斥锁的所有者的一个线程想要释放它时,就会调用它。

img

因为无论什么时候释放互斥锁都很重要,所以最好使用__tay/__finally,更方便的方式是c++的RAII。

Fast Mutex

快速互斥体是典型的互斥锁的替代品,提供更好的性能。它不是一个调度对象,所以它有自己的获取和释放互斥锁的API。只能用于内核模式。与常规的互斥锁相比,它具有以下特征:

不能递归地获取一个Fast Mutex,这样做会导致死锁;

当获得Fast Mutex时,CPU IRQL被提升到APC_LEVEL(1),这将阻止任何APC传递到该线程;

Fast Mutex只能无限期地等待-没有办法指定超时。

Fast Mutex比常规Mutex速度稍快。事实上,大多数需要互斥锁的驱动程序使用Fast Mutex,除非有令人信服的理由使用常规Mutex.

结构体:FAST_MUTEX,函数:ExInitializeFastMutex、ExAcquireFastMutex、ExAcquireFastMutexUnsafe、ExReleaseFastMutex、ExReleaseFastMutexUnsafe。

信号量

通常KeInitializeSemaphore初始化为最大值,当值大于零时,信号量为有信号。

事件

事件主要分为两种:需要手动重置的以及自动重置的。

通知事件(手动重置)—设置此事件时,将释放任意数量的等待线程,并且事件状态保持设置(信号),直到明确重置。

同步事件(自动重置)—设置此事件时,最多释放一个线程(无论有多少线程等待事件),一旦释放,事件将自动返回重置(无信号)状态。

操作和用户模式雷同,不过函数需要加Ke前缀。

执行资源

互斥锁虽然保证的资源不被破坏,但是是以牺牲并发性为代价,遇上只需要进行读取的线程操作,这样效率就降低了很多。

内核提供了另一个面向此场景的同步原语,称为单个写入器、多个阅读器。此对象是执行资源,它是另一个特殊对象,它不是调度程序对象。

结构:ERESOURCE

函数:ExInitializeResourceLite、ExAcquireResourceExclusiveLite、ExAcquireResourceSharedLite、ExReleaseResourceLite

要用acquire以及release函数需要禁用正常的内核APCs。Acquire之前可以调用KeEnterCtriticalRegion,释放之后可以用KeLeaveCtriticalRegion.

High IRQL 同步

线程在有些情况下是不能等待的,特别是,当处理器的IRQL是DISPATCH_LEVEL(2或者更高)。

当只有一个CPU时,可以如下图操作,但是现实情况往往不止一个CPU

img

如果一个CPU的IRQL提高到2,如果一个DPC需要执行,它可能会干扰另一个IRQL可能为零的CPU。在这种情况下,这两个函数可能同时执行,访问共享资源。

为了解决上述问题,需要像互斥锁这样的东西,但它可以在处理器之间同步,而不是线程。这是因为当CPU的IRQL为2或更高时,线程本身就会失去了意义,因为调度程序不能在该CPU上工作。这种对象实际上确实存在,即 Spin Lock。

Spin Lock(自旋锁)

自旋锁是内存中的一个简单位,它通过API提供原子测试和修改操作。当一个CPU试图获得一个自旋锁,但它目前并未被释放,CPU继续在自旋锁上“自旋”(就是不停的询问是否可以获取自旋锁),忙着等待它被另一个CPU释放(记住,使线程进入等待状态不能在>=DISPATCH_LEVEL地方完成)。

驱动程序必须在=<DISPATCH_LEVEL的级别中使用自旋锁。

在前面说的场景中,需要分配和初始化一个自旋锁。每个需要访问共享数据的函数都需要将IRQL提高到2(如果还没有),获取自旋锁,对共享数据执行工作,最后释放自旋锁并降低IRQL(如果适用;不适用于DPC)。

结构:KSPIN_LOCK

函数:KeInitializeSpinLock

img

获取自旋锁总是一个分两步进行的过程:首先,将IRQL提高到适当的级别,这是任何试图同步访问共享资源的函数的最高级别。在前面的示例中,此关联的IRQL为2。第二,获取自旋锁。

img

img

Work Items

work items是用来描述系统线程池的函数的术语。驱动程序可以分配和初始化工作项,指向驱动程序希望执行的函数,然后工作项可以排队到池中。这与DPC非常相似,主要的区别是工作项总是在IRQLPASSIVE_LEVEL上执行,这意味着该机制可以用于从在IRQL2上运行的函数在IRQL0上执行操作。例如,如果DPC例程需要执行IRQL2中不允许的操作(例如打开文件),那么它可以使用工作项来执行这些操作。

内核提供了两个函数允许驱动创建线程:PsCreateSystemThread、

IoCreateSystemThread。

函数IoAllocateWorkItem初始化工作项,函数会返回一个指向不透明的IO_WORKITEM指针。完成后必须用IoFreeWorkItem释放。

如果需要动态地分配相应大小的IO_WORITEM,使用IoSizeofWorkItem,然后调用IoInitializeWorkItem,完成后用IoUninitializeWorkItem.

img

img

joker1

没有加壳

image-20210909150331784

F5发现不能反汇编(确定不是IDA的问题,网上说是堆栈市镇偏移出错,选项-常规-点击堆栈指针,快捷键alt+k,值改为0【PS:堆栈不平衡笔记在最后】

在最后一个AC处修改位0

image-20210909162139584

image-20210909162010697

反汇编后伪代码如下,首先是输入长度为24,然后主要函数为wrong omg encrypt三个函数

image-20210909162739475

wrong是加密

image-20210909163728424

omg是比较wrong加密结果和unk_4030C0地址的值是否相同

image-20210909164135177

写个脚本,得出来的flag{fak3_alw35_sp_me!!}是个假的

1
2
3
4
5
6
7
8
9
10
11
12
import string
s=[102, 107, 99, 100, 127, 97, 103, 100, 59, 86, 107, 97, 123, 38, 59, 80, 99, 95, 77, 90, 113, 12, 55, 102]
out=[]
for i in range(0,24):
if i&1 :
s[i]+=i
out.append(s[i])
else:
s[i]^=i
out.append(s[i])
for i in out:
print(chr(i),end='')

注意一下还有一个Encrypt函数,这个函数也是不能反汇编的

image-20210909172356488

image-20210909172621478

动态OD调试试试,进加密函数看看

image-20210909182844710

(或者OllyDump脱壳)IDA动态调试,看到原来的data数据变成了汇编代码,从encrypt函数的起始地址0x401500开始选取,选取所有没有编译的text段数据,按c,点击force

https://www.leadroyal.cn/p/370/

在这里插入图片描述

然后在0x401500处右击创建函数,现在就能看到encrpty函数里的内容了,大概就是将带入的数据与Buffer进行异或操作,然后得到unk_403040地址上的值

image-20210909192327017

1
2
3
4
5
6
7
8
import string

result=[0xe,0xd,0x9,0x6,0x13,0x5,0x58,0x56,0x3e,0x6,0xc,0x3c,0x1f,0x57,0x14,0x6b,0x57,0x59,0xd]
flag=""
haha="hahahaha_do_you_find_me?"
for i in range(19):
flag+=chr(ord(haha[i])^(result[i]))
print(flag)

flag不全

image-20210909193101144

image-20210909193617976

https://blog.csdn.net/CSNN2019/article/details/115328038

堆栈不平衡

IDA f5无法反汇编,出现如图错误一般是因为程序代码有一些干扰代码,比如用push + n条指令 + retn来实际跳转,而IDA会以为是retn是函数要结束,结果分析后发现调用栈不平衡

简单来说就是调用函数后。使用完堆栈esp要回到ebp的位置(大概这么理解

image-20210909153157763

image-20210909155612149

https://www.cnblogs.com/saintlas/p/7093561.html

2020ACTF新生赛 rome

1.用IDA32直接打开exe,函数伪代码如下,是一些字符串操作,我们写一个脚本

image-20210908152000301

image-20210908160822925

最终flag等于flag{Cae3ar_th4_Gre@t}

2019红帽杯EasyRE

首席按IDA64打开,shift+f12查看字符串,看到关键字符串you found me

image-20210831171908454

双击进去,按X键,切换到关键代码

image-20210831172039819

F5反汇编,第一次输入的每个字符与索引异或,判断是否与v17到v52的字符串相等,可以写一个脚本,运行结果:Info:The first four chars are flag

image-20210906144222336

下面的代码是讲第二次输入的字符串base64加密,然后判断字符串与off_6cc090是否相等

image-20210906150112228

运行结果是看雪论坛的一个网址(又被耍了

没想到用到的是这两个字符串

image-20210906151316247

点进函数,v2的生成类似于是随机产生一个四位的key,然后发现fg,这里是将字符串一到四位与key[i]进行异或,所以可以反过来求key ,猜测其后的就是flag,(HIBYTE作用是获取高字节,也就是数组最后一位)

猜测for循环里面就是flag,再次将字符串与key[i%4]进行异或得到flag

image-20210906151418161

image-20210906153301871

1
flag{Act1ve_Defen5e_Test}

Windows调试工具**

主要简单介绍了Cdb、Ntsd、Kd、Windbg四个调试器,但是Windbg是唯一具有图形用户界面的调试器

Windbg介绍**

Windbg主要有三种命令:

标准命令,这些命令被内置到调试器中,它们对正在调试的目标进行操作。

元命令,这些命令以“.”开始。它们操作调试过程本身,而不是直接操作被调试的目标。

Bang(扩展)命令,这些命令以“!”开头,提供了调试器的强大功能。所有的扩展命令都是在扩展dll中实现的。默认情况下,调试器加载一组预定义的扩展dll,但可以从调试器目录或其他源加载其他dll.

Note:不用双击内核调试直接本地打开即可

因为目前windbg正在调试内核,调试器不在notepad.exe进程上下文中,为了达到目的,需要枚举系统中的进程并切换到notepad.exe

!process 0 0 //枚举所有进程

.process /i /r /p //切换到进程

kernel调试时windbg并不会加载用户态dll,但是切换到指定进程后可以加载用户态调试符号,如下:

.reload /user /f

线程介绍

输入命令 k显示当前线程堆栈,带有符号的地址的一般格式是“模块名!函数名+偏移量”。该偏移量是可选的,如果它恰好是这个函数的开始,则可以为零。

命令 ~ns表示切换线程

img

img

进程ID显示为十六进制,输入下面的命令可以将其转化为十进制,与任务管理器对比发现是正确的

img

也可以在十进制前加个前缀“0n”这样可以得到十六进制的结果

img

显示当前活动的线程

img

显示当前线程的teb(线程环境块),该命令显示的是幕后真实的结构,可以用dt命令查看实际结构

img

以下只显示了部分信息

img

我们可以点击查看NtTib

img

img

断点

下个断点

img

可以用bl命令查看断点,e表示启用,d表示禁用

img

因为下了断点,所以我们打开文件的时候windbg会断在这里

img

我们现在可以看看调用堆栈是什么样的

img

如果想知道正在打开的是什么文件。我们可以根据CreateFileW函数的调用约定来获得这些信息。由于这是一个64位进程(处理器是Intel/AMD),调用约定声明第一个整数/指针参数在RCX、RDX、R8和R9寄存器中传递。由于CreateFileW中的文件名是第一个参数,因此相关的寄存器是RCX。我们用命令r rcx查看寄存器,紧接着用 db 地址查看指向的内存,但是因为字符是ASCII所以看起来不是很方便,我们用 du 地址查看Unicode地址

img

img

如果不想用地址,也可以在寄存器前面加“@”

img

再下一个断点

img

u(unassemble)命令列出即将执行的8条指令,注意该值0x55已被复制到EAX寄存器中。这是NtCreateFile的系统服务编号,如第1章所述。所示的系统所有指令是导致转换到内核,然后执行NtCreateFile系统服务本身的指令。

img

可以用指令p(f10)单步运行,也可以用t(f11)步入函数中,单步执行到ret后,我们要知道在x64调用约定中,函数的返回值存储在EAX或RAX中。对于系统调用,它是一个无状态,所以EAX包含了返回的状态:

img

下一个NtWriteFile的断点,然后修改文件在保存的时候会断下来,紧接着用k查看调用堆栈

img

用u指令查看到,8 是NtWriteFile系统服务编号

img

本地内核调试

本地内核调试(LKD)与内核调试的区别就是无法下断点,只能看见系统当前运行状态。

按照书上的步骤,先在管理员权限下的cmd输入命令

bcdedit /debug on

然后重启系统,以管理员身份打开windbg,选择attach to kernel,然后选择Local,点击ok

img

本地内核调试详细教程

​ 输入!process 0 0显示如下img 输入.reload同样没用,按照他的提示输入命令!sym noisy,然后输入.reload,同时查看一下符号路径是否正确。最后得到现在的进程情况

img

PROCESS后的是进程的EPROCESS地址(内核空间);

SessionId表示进程所处的会话;

Cid是独一无二的进程ID(Client ID);

Peb是进程环境块(PEB)的地址,这个地址在用户空间中;

ParentCid是Parent Client ID)父进程的进程ID。注意,父进程可能不再存在,并且可以重用此ID;

DirBase是该进程的Master Page Directory的物理地址,用作虚拟地址转换的基础。在x64上,这被称为Pagr Map Level 4,在x86上,它是Page Directory Pointer Table(PDPT);

ObjectTable是指向进程的私有句柄表的指针;

HandleCount是进程的句柄数;

Image是可执行文件名或者是对于那些与可执行文件没有关联的程序的特殊进程名称(exp: Secure System,system)。

!process命令至少接受两个参数,第一个参数用EPROCESS地址表示感兴趣的进程,0表示所有进程。第二个参数是所需的详细信息级别,其中零表示最少的详细信息量(一个位掩码)。可以添加第三个参数来搜索特定的可执行文件。 列出所有正在运行的csrss.exe。

img

可以通过指定地址以及更高级别的详细信息来列出相关进程的详细信息

img

其余的命令等到具体用到的时候再一一查阅

内核驱动程序调试教程

我们调试在第四章写好的驱动程序,首先安装驱动,但是不能加载,因为加载就会进入驱动入口。(我们需要在入口下一个断点。

img

img

具体的调试过程其实和平时双机调试程序的时候差不多,因为这部分已经熟悉就不详细写了。

Reverse1

1.查到无壳,用IDA直接打开exe分析,一步一步到下图,好像看不出来个什么

image-20210811105908854

查看字符串,一般来说会有flag提示的字符串,查找调用的位置

image-20210811110017965

可以看到关键的str2,注意函数种有一个替换操作,111是字母o,48是数字0

image-20210811110217990

image-20210811110303832

所以flag是flag{hell0_w0rld}

​ 在本章中,将使用我们在前几章中学到的许多概念,并构建一个简单但完整的驱动程序和客户机应用程序,同时填补一些缺失的细节。我们将部署驱动程序并使用它的功能——在内核模式下执行用户模式不可用的一些操作。

1.介绍

​ 我们将要用一个简单的内核驱动来解决的问题是用Windows API设置线程优先级的不灵活性。在用户模式下,一个线程的优先级是由其进程PriorityClass与具有有限数量级别的每个线程基础上的偏移量的组合决定的。

​ 改变进程/优先级:SetPriorityClass,该函数设置的优先级是线程的默认优先级,也可以使用SetThreadPriority设置线程优先级(都是用偏移量设置),下面的表格展示了基于进程优先级类和线程的优先级偏移量的可用线程优先级。

img

​ 实时优先级类并不意味着Windows是一个实时操作系统;此外,由于实时优先级非常高,并且与许多做重要工作的内核线程竞争,这样的进程必须使用管理员特权运行;否则,试图将优先级类设置为实时会将值设置为“高”。

​ 只有一小部分优先级可以直接设置。我们希望创建一个驱动程序,以规避这些限制,并允许将线程的优先级设置为任何数字,而不管其进程优先级类如何。

2.驱动初始化

​ 创建一个新的项目命名为PriorityBooster并且删掉INF文件,新建一个cpp文件,大多数软件驱动程序需要在驱动器条目中执行以下操作:

  • 设置一个卸载例程;

  • 设置驱动程序所支持的调度例程;

  • 创建一个设备对象;

  • 创建一个到设备对象的符号链接。

    一旦执行了所有这些操作,驱动程序就已经准备好接受请求了。第一步是添加一个卸载例程,并从驱动程序对象指向它。以下是新的具有卸载程序的DriverEntry,当然我们需要根据我们实际工作中的需要添加代码。

img

​ 接下来我们需要设置我们要支持的调度例程,实际上,所有的驱动程序都必须支持IRP_MJ_CREATE和IRP_MJ_CLOSE,否则就无法为该驱动程序的任何设备打开手柄。所以我们将以下内容添加到DriverEntry中。

1
2
3
DriverObject->MajorFunction[IRP_MJ_CREATE] = PriorityBoosterCreateClose;

DriverObject->MajorFunction[IRP_MJ_CLOSE] = PriorityBoosterCreateClose;

​ 所有的major function都有同样的类型(它们是函数指针数组的一部分),因此我们需要给函数PriorityBoosterCreateClose加一个类型NTSTATUS

img

传递信息给驱动

​ 我们的目的是为了设置现成的优先级,所以我们需要一种方式告诉驱动程序是哪个线程以及设置什么值。从我们的目的处罚,我们需要用到WriteFile或者 DeviceIoControl,一般的思维是使用前者,但是在这里,后者是一种向驱动程序传递数据的通用机制。由于更改线程的优先级不是纯粹的写操作,所以我们将使用DeviceIoControl。此函数具有以下原型(我们主要关注三个参数):

img

​ 操作的控制代码标识要执行的特定操作以及执行该操作的设备的类型。每个控制代码的文档都提供了lpInBuffer,nInBufferSize,lpOutBuffer和nOutBufferSize参数的使用细节。

​ 在驱动程序方面,DeviceIoControl对应于IRP_MJ_DEVICE_CONTROL的major function代码。我们将其添加到调度例程的初始化中:

img

客户端/驱动程序通信协议

​ 前面提到我们需要ControlCode以及Inputbuffer(包括线程id以及设置的优先级)这些信息片段必须可由驱动程序和客户端同时使用。客户端将提供数据,而驱动程序将对其采取行动。这意味着这些定义必须包含在驱动程序和客户端代码必须包含的单独文件中。

​ 基于此我需要创建一个头文件,这个头文件需要定义驱动程序期望从客户端获得的数据结构和用于更改线程优先级的控制代码。(请注意为什么不用DWORD,因为该类型只在用户模式下定义,ULONG在用户以及内核模式下都有定义)

img

​ ControlCode必须使用CTL_CODE宏构建,该宏接受组成最终ControlCode的四个参数。CTL_CODE的定义如下:

1
2
3
#define CTL_CODE( DeviceType, Function, Method, Access ) (

((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
  • DeviceType定义设备类型,可以是WDK头文件中的某个FILE_DEVICE_XXX常量,但是这个变量是针对于基于硬件的驱动程序。(我们的是基于软件的,尽管如此,微软的文档还是规定,第三方驱动程序应该从0x8000开始。

  • Function 一个表示特定操作的升序数字。如果没有其他内容,那么这个数字在同一驱动程序的不同控制代码之间必须有所不同。同样,任何数字都可以,但官方文件显示,第三方驱动程序应该从0x800开始。

  • Method ControlCode中最重要的部分。它指示Client提供的输入和输出缓冲区如何传递给驱动程序。对于我们的驱动程序,我们将使用最简单的值METHOD_NEITHER。

  • Access 指示此操作是来自FILE_WRITE_ACCESS、FILE_READ_ACCESS还是FILE_ANY_ACCESS。典型的驱动程序只使用FILE_ANY_ACCESS并处理IRP_MJ_DEVICE_CONTROL处理程序中的实际请求。

img

创建设备对象

​ 一个典型的软件驱动程序只需要一个设备对象,其中有一个符号链接指向它,这样用户模式客户端就可以获得句柄。用IoCreateDevice实现该功能。

img

  • DeviceExtensionSize 除了sizeof(DEVICE_OBJECT)之外,还将分配的额外字节。可用于将某些数据结构与设备相关联。它对于仅创建一个设备对象的软件驱动程序不那么有用,因为设备所需的状态可以简单地由全局变量来管理。

  • Devicetype 与某些类型的基于硬件的驱动程序相关。对于软件驱动程序,应该使用值FILE_DEVICE_UNKNOWN。

  • Exclusive 是否应该允许多个文件对象打开同一设备?大多数驱动程序应该指定false,但在某些情况下,TRUE更合适;它强制单个客户端到设备。

  • DeviceObject 返回的指针,作为指针传递给一个指针。如果成功,Io创建device将从非页面池中分配结构,并将生成的指针存储在取消引用的参数中

​ 在调用IoCreateDevice之前,我们必须创建一个UNICODE_STRING来保存内部设备的名称.

img

​ 设备名称可以是任何东西,但是应该在设备对象管理器目录中。有两种方法可以用一个常量字符串来初始化一个UNICODE_STRING。第一个是使用RtlInitUnicodeString。但是RtlInitUnicode字符串必须计算字符串中的字符数,才能适当地初始化长度和最大长度。有一种更快的方法,使用RTL_CONSTANT_STRING宏,在编译时静态计算字符串的长度,这意味着它只能对常量字符串正确操作。

​ 如果一切顺利,我们现在有了一个指向设备对象的指针。下一步是通过提供一个符号链接,使用户模式调用者可以访问此设备对象。以下行创建符号链接并将其连接到我们的设备对象:

img

​ IoCreateSymbolicLink通过接受符号链接和链接的目标来完成工作。注意,如果创建失败,我们必须通过调用IoDeleteDevice来撤销到这里我们所做的所有操作(目前我们只是创建了对象,所以删除就好了)。如果驱动器条目返回任何故障状态,则不调用卸载例程。如果我们有更多的初始化步骤要做,我们将必须记住在那之前撤销所有内容,直到出现失败。(第五章有更好的处理方式)

​ 符号链接这一步成功后,DriverEntry就返回成功,驱动程序就可以接受请求了。

​ 卸载程序:在我们的例子中,有两件事(设备对象创建和符号链接创建)。我们将按相反的顺序撤销它们:

img

客户端代码

​ 客户端项目(控制台桌面项目)首先需要include由驱动端创建的头文件并且需要在主函数中接受命令行参数(线程ID和优先级),请求驱动程序将线程的优先级更改为给定的值。

然后需要打开设备句柄(note:如果驱动程序此时没有加载,这意味着没有设备对象,也没有符号链接,我们将得到一个错误号2

img

​ 打开设备对象后将参数传给ThreadData结构体,然后开始DeviceIoControl

​ 该函数主要调用IRP_MJ_DEVICE_CONTROL主函数例程到达驱动程序。

创建和关闭派遣函数

所需要的只需以一个成功的状态完成请求。以下是完整的创建/关闭调度例行程序实现,IoCompleteRequest函数的第一个参数是将IRP返回给它的创建者(通常是I/O manager),第二个参数是驱动程序可以向其客户端提供的临时优先级提升值。在大多数情况下,零的值是最好的(IO_NO_INCREMENT被定义为零),因为请求同步完成,因此调用者不必获得优先级提升。详细可见第六章

img

DeviceToControl派遣函数

这一部分是项目的核心工作,首先需要IRP信息,关键是查看与当前设备层相关联的IO_STACK_LOCATION内部。调用IoGetCurrentIrpStackLocation将返回一个指向正确的IO_STACK_LOCATION的指针。IO_STACK_LOCATION的主要组成部分是一个名为Parameters的巨大联合成员,它包含一组结构,每种类型的IRP都一个。在IRP_MJ_DEVICE_CONTROL的情况下,要看的结构是DeviceIoControl。在该结构中,我们可以找到客户端所传递的信息,如控制代码、缓冲区及其长度。在我们的例子中,实际上只有一个IO_STACK_LOCATION.

其次需要检查控制代码是否是驱动程序支持的,如果没有,我们只将状态设置为成功以外的东西,我们需要的最后一段通用代码是在switch块之后完善IRP,无论它是否成功。

img

然后第一步是检查我们收到的缓冲区是否大到足以包含一个线程数据对象。

img

接下来让我们看看优先级是否在1到31的范围内,如果没有,则中止:

img

设置优先级之前,我们需要获得内核空间中真是的线程对象的指针,用到PsLookupThreadByThreadId函数,通过线程Id获得(note:include ntfis.h)。

img

函数的第一个参数是一个句柄,这个ID它是一个作为句柄输入的ID。其原因与进程和线程id的生成方式有关。这些都是由全局私有内核句柄表生成的,所以句柄“值”是实际的id。ULongToHandle宏提供了必要的转换。(请记住,64位系统上的句柄是64位,但客户端提供的线程ID总是32位。)

KeSetPriorityThread需要的参数是一个PKTHREAD,但是PETHREAD的第一个成员就是前者类型,所以两者可以相互转换

img

安装和测试

1.管理员打开cmd

sc create booster type= kernel binPath= c:\PriorityBooster.sys

booster是注册表键值,所以必须是独一无二的

img

2.加载驱动

sc start booster(note:生成文件后缀没有recipe只有sys)

img

然后采用ProcessExplorer查看cmd.exe的一个线程,将其作为实践对象

3.使用线程ID和所需的优先级运行客户端:

booster 3384 25

img

img

img