type
status
date
slug
summary
tags
category
icon
password
Catagory
Materials
Retired
Retired
Due date
Jun 15, 2024 12:43 PM
Status
Belong in

Shellcode簡介

作者把BOF攻擊流程分為三個部分
開發shellcode時自問下列問題
  1. 有多少opcode在裡面? 有些opcode會產生更多opcode
  1. 分別是哪些指令?
    1. 某些shellcode在載入記憶體時會自行改變,因為指令可以自行修改指令
    2. 自行修改指令是必要的,如前述multibyte環境下需要額外加上INC EDX 讓解析出來的指令不會因BAD CHAR導致無法執行
  1. 是否有一些參數或資料在裡面? victim環境中是否有shellcode所需執行的必要條件,如dll, function, parameters in different version dll,沒有是不是要自己寫或透過loader的方式進行
  1. 是否有一些不必要的指令在裡面? 如前述multibyte環境下需要額外加上INC EDX 讓解析出來的指令不會因BAD CHAR導致無法執行
  1. 是否有一些編碼或解碼器在裡面? encoder/decoder可以將原本的shellcode經過編碼,舉例去除bad chars的shell code進行編碼,並將解碼指令放到shellcode之前,當載入到記憶體中的時候先執行解碼指令再執行shellcode,看起來像是動態修改指令
💡
尋找opcode可以用windbg 或是 Immunity Debugger外掛mona.py
 

從 C 語言到 Shellcode

notion image
notion image
尚未執行前,EIP指向PE header開頭
尚未執行前,EIP指向PE header開頭

分析Function

語系中文環境

可以看到從004012C1~004012CD是主要程式碼,通常以CALL+JMP來實現DLL Function call(請參考 )。
Printf function 位址
執行緒到004012C1尚未執行,若執行此行可以看到他會先至00401820位置
執行緒到004012C1尚未執行,若執行此行可以看到他會先至00401820位置
執行完004012C1至00401820位址<JMP DWORD PTR DS:[&msvcrt.printf>] ,可以看到printf實際上是在msvcrt.dll裡面,實際位址在77C1186A(真正執行的記憶體位址)
執行完004012C1至00401820位址<JMP DWORD PTR DS:[&msvcrt.printf>] ,可以看到printf實際上是在msvcrt.dll裡面,實際位址在77C1186A(真正執行的記憶體位址)
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
exit function 位址
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
綜上針對printf、exit function分析可知這2個function 在msvcrt.dll中,且實際執行的記憶體位址分別在77C186A、77C09E7E,這2個記憶體位址在不同的作業系統版本中可能會改變,即使在同一個環境下也極有可能改變。原因如下:
  1. 這2個位址可能會隨著msvcrt.dll被載入的記憶體的基底位址不同而不同。在windows xp基本不太會改變。但vista之後至windows7微軟預設在作業系統中加入ASLR(Address Space Layout Randomization)機制,導致每次重開機後dll載入的基底位址會不一樣。
  1. 函式位址也可能會因為dll檔案版本不同導致內部所有函式的位址也跟著改變
分析參數傳遞
💡
Shellcod.00403000為OllyDbg表示式,即表示在模組shellcod裡面記憶體00403000的位址
notion image
004012BA準備printf參數Hello World!\n 字串,將Shellcod.00403000位址拷貝進[ESP](即表示ESP指向00403000;又00403000的值位Hello World!\n字串),從下圖ESP=0022FF60;[ESP]=00403000
從記憶體傾印區中可以看到00403000~00403008的位址為printf之參數48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A 00字串後面會加上一個NULL作為字元結尾,後面多的NULL就不是屬於該參數;對應的位址也是由小到大(little edian)
從記憶體傾印區中可以看到00403000~00403008的位址為printf之參數48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A 00字串後面會加上一個NULL作為字元結尾,後面多的NULL就不是屬於該參數;對應的位址也是由小到大(little edian)

語系英文環境

Printf function 位址
在Windows XP sp3語系設為英文的環境又使用Dev-C++,msvcrt.printf位址在77C4186A
notion image
exit function 位址
在Windows XP sp3語系設為英文的環境又使用Dev-C++,msvcrt.exit位址在77C39E7E
notion image

延伸shellcode撰寫概念

💡
以下記憶體位址使用語系中文的環境
可以看到Hello, World!\n 字串被配置了一個00403000~00403008記憶體區(in stack),再撰寫shellcode時其實os並不會幫我們的shellcode配置記憶體,沒辦法使用像MOV DWORD PTR SS:[ESP],ShellCod.00403000 直接複製配置好的記憶體位址到[ESP]。
How?將shellcode一段一段push到stack裡面,再從stack中抓取指令。下面示範如何將printf("Hello World!\n");exit(0); 寫成shellcode

準備參數

Hello, World!\n 字串參數
notion image
參數 0 for exit function
Function call
組語的 CALL 指令有個特點,就是會把下一行組語的位址紀錄在堆疊中,以至於呼叫的函式執行結束之後,其 function epilogue 的 RETN 指令會返回到當初呼叫它的地方
通常並不會直接使用 CALL <function address>的方式進行處理,常用的方式是先將位址儲存在暫存器中在call address。
因此前面分析printf function實際位址在(77C186A);exit function 實際位址在(77C09E7E)
執行完004012C1至00401820位址<JMP DWORD PTR DS:[&msvcrt.printf>] ,可以看到printf實際上是在msvcrt.dll裡面,實際位址在77C1186A(真正執行的記憶體位址)
執行完004012C1至00401820位址<JMP DWORD PTR DS:[&msvcrt.printf>] ,可以看到printf實際上是在msvcrt.dll裡面,實際位址在77C1186A(真正執行的記憶體位址)
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
執行完004012C0至00401810位址<JMP DWORD PTR DS:[&msvcrt.exit>] ,可以看到exit實際上是在msvcrt.dll裡面,實際位址在77C09E7E(真正執行的記憶體位址)
綜合上述寫完的opcode如下

取得opcode

使用Windbg取得opcode

使用Immunity Debugger外掛模組mona.py取得opcode

mona.py 放在Immunity debugger預設安裝路徑下的PyCommands
選擇資料夾儲存mona log
選擇資料夾儲存mona log
notion image
將資料push in stack 才需要把位址反過來,MOV儲存位址到暫存器不需要特地將位址反過來,mona會直接反過來
將資料push in stack 才需要把位址反過來,MOV儲存位址到暫存器不需要特地將位址反過來,mona會直接反過來

The shellcode in opcode

直接利用opcode寫出與前面shellcode一樣的效果
直接利用opcode寫出與前面shellcode一樣的效果

分析TestShellcode

main範圍0x401290~0x4012E4
main範圍0x401290~0x4012E4
組語的 CALL 指令有個特點,就是會把下一行組語的位址紀錄在堆疊中,以至於呼叫的函式執行結束之後,其 function epilogue 的 RETN 指令會返回到當初呼叫它的地方
004012C6~004012D0是我們使用函式指標指向char shellcode[] ,004012D0CALL EAXfp()
notion image
下圖可以看到[EAX]儲存00402000位址,其ASCII值h!\n 字元,[ESP]因為是函式指標故return address儲存的是指向的function實際位址(00403000),
執行緒來到004012D0位址尚未執行,執行004012C0 MOV EAX,DWORD PTR SS:[ESP-4]
執行緒來到004012D0位址尚未執行,執行004012C0 MOV EAX,DWORD PTR SS:[ESP-4]
執行完004012D0 CALL EAX ,
執行完004012D0 CALL EAX

使用Metasploit的nasm_shell.rb取得opcode

metasploit framework提供的使用Ruby寫的script,可用來產生opcode
notion image
notion image
與前述mona.py產生的opcode比較稍短,紅框處為不一樣的地方
與前述mona.py產生的opcode比較稍短,紅框處為不一樣的地方
💡
原則上opcode越短越好
執行結果與前述mona產生的opcode一致
執行結果與前述mona產生的opcode一致

使用NASM取得opcode

直接寫組語的code,並利用nasm組譯器編譯成二進位的檔案,再透過工具讀取二進位檔案並轉換成字元陣列的形式。
利用nasm將shellcode001.nasm編譯成二進位檔
notion image
利用HxD查看binary file,就可以看到我們所寫的opcode
notion image
作者寫了一個小工具類似metasploit裡的generic/none編碼器,用來將bin二進位檔案轉換成c++ 的字元陣列格式。
執行結果
執行結果
重複將產出的opcode改成TestShellcode,產出結果會與前面一樣。
💡
作者最推薦使用nasm產opcode,因產出bin二進位檔案,可同步比較記憶體中的shellcode是否完整。

討論Shellcode v1

前面使用的shellcode會有下列幾個問題:
  1. 預先假設執行環境是Windows的cmd.exe
  1. 含有NULL字元(\x00)
  1. 使用絕對記憶體0x77C1186A(msvcrt.printf)、0x77c09e7e(msvcrt.exit)
  1. 預先假設函式printf、exit一定會被call,換句話說預先假設msvcrt.dll一定會被載入到記憶體中

Q1 - 利用AllocConsole()API來產生Console

因為目前shellcode均使用cmd.exe來呼叫執行,故先將問題簡化成確保我們執行shellcode均有console可以讓我們執行。

Q2 - 排除Bad Characters(本章不會提到)

Q3Q4 - dll被載入到的記憶體位址不同而改變

printf、exit function均在msvcrt.dll中,當msvcrt.dll載入的記憶體位址不同,其中的function位址也會不同,目前實驗環境語系是中文環境

Compiled With Dev-C++

Immunity Debugger →View→Executable Modules
Immunity Debugger →View→Executable Modules
WinDbg
WinDbg
上圖不管是使用immunity debugger或是用WinDbg可以看到載入4個模組到記憶體中,其中TestShellcode.exe的基底位址是00400000,大小長度在00006000,另外msvcrt.dll記憶體基底位址在77BE0000,大小長度在00058000(語系設定中文環境),從中可以看到0x77C1186A(printf)、0x77C09E7E(exit)function是在msvcrt.dll裡面。

Compiled with VS C++ Express

這裡使用VC++ 2010 Express進行編譯,VC++ 2010預設都會有2組編譯和連結設定檔案,一組是Debug;另一組是Release版。
notion image
notion image
預設Build Solution是Debug版,Debug版本內含偵錯偵錯處理
notion image
notion image
從下圖可以看到Debug version載入的是MSVCR100D.dll,base address在10200000,長度為00172000
Immunity Debugger open debug version
Immunity Debugger open debug version
WinDbg open Debug version
WinDbg open Debug version
編譯成Release版,此版本編譯出來的執行檔會經過最佳化處理
notion image
notion image
notion image
下圖看到release版本載入的MSVCR100.dll,base address在78AA0000,長度在000BF000
Immunity Debugger open release version
Immunity Debugger open release version
WinDbg open release version
WinDbg open release version
若是我們將前面的shellcode使用VC++編譯
notion image
debug version
debug version
EIP=77C1186A是我們LAB中msvcrt.dll,printf function的位址,因為VC++編譯出的執行檔並未載入故導致找不到該位址,故程式執行到這裡會造成異常終止
debug version
debug version
release version
release version
notion image
結論
  • function 位址會隨著載入的dll位址變化而不同
  • 不一定會載入所想要的dll,故也不一定能夠呼叫到想要的function
  • solution 3 - 需要動態決定記憶體位址(base address+offset相對位址)
  • solution 4 - 如果記憶體中沒有想要的dll,就在shellcode中自己載入
💡
【坑】回頭看

透過PEB手法找到Kernel32.dll的基底位址 - 探索OS內部

因為實驗環境windows xp sp3微軟不更新了,為了要讓Windbg可以搜尋的到符號文件,參考
在Window中Process會有一個資料結構PEB(Process Environment Block),而Thread本身也會有TEB(Thread Environment Block)
!peb
!peb
!teb
!teb

Deep dive in the _TEB structure

在Windbg使用dt ntdll!_TEB 來查看teb資料結構如下,其真正名字是_TEB ,型別為C語言的結構(struct),也可看到下圖也有輸出其offset

TEB中offset 0x000成員NtTib

從下圖可知在_TEB中第一個成員_NT_TIB結構中第一個成員ExceptionList(offset 0x00)是一個指向_EXCEPTION_REGISTRATION_RECORD結構的32位元指標,主要負責例外處理的註冊動作(後面會在細講)。
另一個在_NT_TIB結構中最後一個成員Self(offset 0x18)指向自己的32位元指標

TEB中offset 0x030成員ProcessEnvironmentBlock


從Kernel32.dll基底位址找到LoadLibraryA函式位址 - 攀爬PE結構


函式名稱的hash值


Shellcode v2


Metasploit attack shellcode - Hello World Message box


Windows 32 bit vs Windows 64 bit


Reference

  • Windows Internals