2013年10月17日 星期四

一種特別的參數傳遞技巧:直接嵌入立即資料於程式區段

相信有認真看過敝人曾經發過的幾篇呼叫慣例系列文章,應該已經很了解傳遞參數給函式背後
的基本原理,C 語言主要常用的不外乎就是 stdcall、cdecl 與 fastcall,在 x86-64 的環境裡面
也曾經提到過全部統一用 fastcall,可是參數在傳遞的過程中,讀者有沒有想過,是否能夠將
資料直接嵌入在程式區段中,而不需要宣告資料區段呢?這就是今天的主題,敝人將示範一種
特別的技巧,讓資料直接混合在程式區段內傳遞給一條簡單的 Windows API 作為示範。


今天要討論的示範程式很簡單,使用 MessageBox API 顯示一個小小的對話方塊
用 C 語言寫很簡單,原始碼如下:

#include <windows.h>

static TCHAR g_szText[] = _T("Hi! I'm the fasm program");

int APIENTRY WinMain(HINSTANCE,HINSTANCE,LPSTR,INT)
{
    MessageBox(
      HWND_DESKTOP,     // A handle to the owner windows of the message box
      g_szText,         // The message to be displayed.
      GetCommandLine(), // The dialog box title.
      MB_OK);           // The contents and behavior of the dialog box.
    return 0
 

這個程式執行結果如下:


現在把上面那段小小的 Win32 程式用 FASM 重寫,原始碼如下:

format PE GUI 4.0
entry start

include 'win32a.inc'

section '.text' code readable executable

start:
        push ebp         ; store old value of ebp
        mov  ebp,esp     ; create a stack frame
        push ebx esi edi ; store volatile registers

        invoke GetCommandLine

        push   MB_OK
        push   eax
        push   g_szText
        push   HWND_DESKTOP
        call   [MessageBox]

        pop  edi esi ebx
        leave
        invoke ExitProcess, 0

section '.data' data readable writeable

g_szText db "Hi! I'm the fasm program!"

section '.idata' import data readable writeable

library kernel,'kernel32.dll',                  \
        user,'user32.dll'

import kernel,                                  \
       GetCommandLine,'GetCommandLineA',        \
       ExitProcess, 'ExitProcess'

import user,                                    \
       MessageBox,'MessageBoxA'                  

讀者可以看到用 FASM 寫也是很簡單就可以完成一樣的功能,執行結果當然就跟剛剛上面
的圖一樣,只是還要自己手動寫 .idata 區段引入想要用的 API,讀者在這邊可以注意到吾
人改用了一條指令 leave,它其實跟另外兩條指令的組合等價:


傳統常用的寫法      新式寫法
mov esp,ebp           leave
pop  ebp

 
這是在函式的閉幕碼內常常出現的樣版,mov esp, ebp 可以先破壞函式內的區域變數堆疊
然後在恢復原本的 ebp,後來 x86 組合語言乾脆弄一條指令 leave,本身可讀性也必較好
新式的 Compiler 應該都會採用 leave,敝人自己在寫 FASM 也已經改用 leave。

然後讓我們來想想一個問題,有沒有可能直接把原本需要放在 .data 區段內的 g_szText
位址上的資料直接混合在 .text 區段內,也就是直接嵌入在程式區段裡面,要討論這個
問題之前,先回想一下 x86 組合語言指令 call 有甚麼特性。

還記得以前呼叫慣例系列文章有講過,call 不僅會把下一段返回後要執行的程式位址推入
堆疊中,它還有跳躍的功能,所以讀者發現了沒有,call 指令不光只是用在呼叫函式這種
正規用途 ( 也就是被呼叫函式內有要 ret 搭配 ),還可以拿來用於非正規用途,拿它投機取巧
把資料直接嵌入在程式區段,吾人下面先貼出不同處給讀者看看:

        push  MB_OK
        push  eax
        call  @F
        db    "Hi! I'm the fasm program!",00h
     @@:
        push  HWND_DESKTOP
        call  [MessageBox]

什麼!!資料竟然跟程式碼混合在一塊,這就是 call 指令的另一種妙用,少數用 call 指令而沒
有搭配 ret 的情況,因為 call 把下一段位址先推入堆疊啦,然後巧妙跨過資料而直接跳躍到
push HWND_DESKTOP,所以這種技巧不會讓 CPU 執行程式的時候誤判,不過顯然除錯器
Ollydbg 也知道這種技巧,當在下把編譯後的執行檔放入 Ollydbg 反組譯後出現下圖:


讀者點圖放大看可以發現,Ollydbg 很聰明的發現了吾人使用了嵌入式的資料直接
跟程式區段混合在一起,本篇主要就是介紹這種技巧給讀者知道參數的傳遞能夠用
call 指令巧妙的將資料嵌入在程式區段中,對於一些短的顯示字串或是常數,也許
直接用此技巧嵌入在程式碼區段中會比較方便,也不用去花腦袋思考變數命名的問題

不過這種技巧在 Ollydbg 內遇到那段 call 指令記得要用 step into,別用 step over,因為
CPU 在執行程式碼遇到 call 基本上就是認為要跳進某個函式,所以你得要用 into,假如
你用 over,那就會發現程式一路往下走就跑完了,因為程式永遠遇不到 ret 指令。

後記:

基於類似的理由,個人就對 FASM 提供的匿名標籤功能充滿敬意,它讓我不用在去思考
label 的名稱到底要怎麼令,反正 @F 就會往下找最近的 @@,而 @B 就會往回找最近的
@@,這個功能實在很方便,但是 FASM 要提供這種功能所實現的程式碼可就不簡單了。
對組譯器技術有興趣的讀者,可以專研 FASM Assembler 的原始碼,該組譯器是完全用純
x86 Assembly 完成,要閱讀它的程式碼基本上難度很高,我的過來經驗是最好要能閱讀
用 C 寫成的 NASM Assembler 的原始碼在去看 FASM Assembler 的原始碼,因為該作者
用組語大概已經達到神人境界,程式碼裡面會用到很多只有組合語言才能表達的特殊技巧
而且沒有對應的 C 概念可以表達,本文的示範就是其中一例,讀者在 C 語言中就很難辦到
組合語言這種可以偷機取巧用 call 指令指把資料混合在程式區段。當然有過 DOS 低階編程
的 LKK 們或許就覺得沒啥新鮮,在 DOS 中還可以直接操作節區,直接讓 ds 就等於當前的
cs 呢,在 DOS 下要怎麼搞都可以。

沒有留言:

張貼留言