顯示具有 Windows 標籤的文章。 顯示所有文章
顯示具有 Windows 標籤的文章。 顯示所有文章

MiniFilter InstanceSetupCallback is not called?

一般來說,MiniFilter 的 InstanceSetupCallback 會在 filter manager 把 minifilter attache 到 volume 後呼叫。如果沒有的話,可以檢查一下 minifilter 的 INF 是否把 instance flags 設定成 0x00000001 了:

  • 0x00000000: Allow all attachments
  • 0x00000001: Suppress automatic attachments

如果你的 INF 不是自己從頭寫的,而是參考 Windows Minispy 改來的,哪很有可能你沒有收到 InstanceSetupCallback() 的原因就是你忘了改掉 flags 值為 0。

;Instances specific information.
DefaultInstance    = "Scanner Instance"
Instance1.Name     = "Scanner Instance"
Instance1.Altitude = "265000"
Instance1.Flags    = 0x0          ; Allow all attachments

Written with StackEdit.

LLVM for Windows Visual Studio 2010/2012

Chandler Caruth 在 GoingNative 2013 上宣布了一個好消息,LLVM 3.4 (開發中)正在如火如荼地移植到 Windows 上,目前初步的成果已經可以使用 Visual Studio 2010/2012 作為 IDE 了,對於習慣 Visual Studio 的人來講應該是個好消息。

alpha 版本可以在這邊下載到:

  • SVN Snapshot: http://llvm.org/builds/
  • 設定上也非常簡單,在 Project > Properties > General > Platform Toolset 改成 LLVM-vs2010 即可。
    llvm_on_windows

試玩了一下發現:

WinDbg Tips: Automatically Break into the Target Computer

適用時機

  • 進行非常早期的 kernel debugging ,希望在 kernel 完成初始化後,立即中斷。
  • 追蹤 boot process 、kernel loader 、休眠啟動(resume from hibernation)。透過適當 BCDEdit 設定,也可用來 debug BootMgr, Winload.exe, WinResume.exe 等程式

說明

在 WinDbg 裡頭又稱作:Change Post-Reboot Break State 。目前共有三種模式可供切換:

No break
除非使用者按下 CTRL+Break 手動觸發一個 break event ,否則不會中斷 target 的運作。預設模式。
Break on reboot
當 target 上的 kernel 完成初始化後,立即中斷。 Break on first module load
當 target 上的 kernel 完成第一個 module 的載入,立即中斷。

語法

CTRL+ALT+K

範例

automatic break

應用

透過 BCDEdit 開啟 boot manager 的 debug :

bcdedit /set {bootmgr} bootdebug on

接著,在 WinDbg 中設定 Break on reboot ,重開機後,我們就可以看到 WinDbg 停在 bootmgr 上了。

bootmgr

感謝

會有這篇文章,是因為上了張銀奎老師的課,張銀奎老師不用多做介紹,他是软件调试[2]的作者,也常在高端调试[3]出沒(應該也是該網站的建立者?!),三天的課程非常精實,把 Windows Internals 用 WinDbg 方式再講一次,受益匪淺 :)

Reference

  1. CTRL+K (Change Post-Reboot Break State) (Windows Debuggers): http://msdn.microsoft.com/zh-tw/library/windows/hardware/ff540326%28v=vs.85%29.aspx
  2. 软件调试:http://advdbg.org/books/swdbg/
  3. 高端调试:http://advdbg.org/

Visual C++ Directories/Inherited Properties Changes

前言

在 Visual C++ 裡頭,C++ Directories 定義了 Visual C++ 這個 IDE 要去哪些路徑下找尋 header files/library files/sources files ,在 2005/2008 裡頭,全域的設定可以在 Tools > Options > Projects and Solutions > VC++ Directories 下找到。

image

圖一、VC++ 2005 下的 VC++ Directories Dialog

這個 dialog 在 VC++ 2010 不復存在了,取而代之的是請使用者使用 property sheet 以及 per-project 的設定。

image

如果好奇為什麼做了這樣的更動,可以參考 [2] 的文章,主要是因為:

  1. 這樣全域的設定太過強大,當使用者有許多 projects 在同一台機器上 build 時,容易混淆出錯。
  2. 因為這個全域的設定檔,是存放在 %LOCALAPPDATA%\Microsoft\VisualStudio\8.0\VCComponents.dat (8.0 為 VC++ 2005,9.0 則是 2008) 這個檔案裡頭,而不是跟著每個專案檔。因此當使用者使用 source control system 時,不會把設定跟著同步到 server 上,check-out 的人也因此無法正確建置專案了。
  3. VCComponents.dat 是給 VCBuild.exe 讀的 INI 檔,而 VC++ 2010 開始改用 MSBuild.exe 來作為統一的 build utility [3] ,因此竟然要改,就連同格式一起換掉吧。

按編:這三個理由實在都很說不過去,不過既然這是 Visual C++ team 的決定,我們也只能接受了 :~

UI 變動

從 2010 之後,變動後的 VC++ Directories 不再放在 Tools > Options > Projects and Solutions > VC++ Directories ,而是以 per-project 方式存在,使用者可以:

  • 對 project 按右鍵,選擇 properties > Configuration Properties > VC++ Directories
  • 或是,從工具列:Project > <your-project-name> properties > Configuration Properties > VC++ Directories

選取 VC++ Directories 。

image

很快地,妳/你會發現,新的 dialog 跟原有的沒什麼差別,而實際上也是,只是現在的變更都是 per-project 的,不再對所有的專案有效,因此若是妳/你想將 Boost library 作為所有 VC++ project 都會使用的函式庫,就沒辦法從這裡完成;也或是,妳/你更新了 Boost 版本,每個已經使用 Boost 又想升級的 projects ,就得手動來更改。

檔案位置與格式變化

說完了使用上的變動,接著是檔案位置的更動,存放的位置從:

%LOCALAPPDATA%\Microsoft\VisualStudio\8.0\VCComponents.dat

換到了:

%LOCALAPPDATA%\Microsoft\MSBuild\v4.0\

下,並且以 Microsoft.Cpp.<Platform>.user 命名:

  • Microsoft.Cpp.ARM.user
  • Microsoft.Cpp.Itanium.user
  • Microsoft.Cpp.Win32.user
  • Microsoft.Cpp.x64.user

image

新的檔案格式稱作:Property sheet ,附檔名 props 。有了這個檔案,我們還是有機會可以做到 2005 & 2008 時的全域設定。先前提到了:為了迎接 MSBuild 的來臨,VC++ 將 INI 格式變成了 XML 格式,所以客製化上還是很簡單的,任選一個 platform 的檔案打開:

image

所以一種修改方式,就是直接用編輯器修改 XML ,看想修改哪些就可以改,值得一提的是,以往在 2005 & 2008 裡頭,路徑名不可以是 Visual C++ 裡頭的 Macro ,現在反倒是可以了,因為 IDE 是將檔案中的值讀入後才做解析。另外一種方式則是透過 Visual C++ 內建的 Property Manager 修改,我們放到下一節介紹。

Inherited Values

wdk803

當 Visual C++ 將 property sheet 讀入後,會當做 Inherited values ,對於當前 project 來說,是不可編輯的,我們必須繞到 props 檔去,或是使用 property manager 編輯。property manager 可以從工具列 > View > Property Manager 找到:

image

叫起 Property Manager 後,可以看到目前 project configuration 所涵蓋的 platform 的設定。 Double click 後,可以看到跳出一個視窗,

image

跳出來的視窗長得很像 project properties ,不過仔細看會發現 Configuration 和 Platform 已經被鎖死成對應的 property sheet 檔所代表的設定,接著,直接編輯,然後存檔,這樣就完成了修改全域 property sheet 了。

VC++ 2010 & VC++2012

值得注意的是,property sheet 放在 MSBuild 的 local app data 下,而目前 2010 和 2012 所使用的 MSBuild 都是 4.0 [4],因此一旦我們修改了 property sheet ,可是會同時影響到 2010 跟 2012 的 ... Orz ...

Reference

  1. Inherited Properties and Property Sheets: http://blogs.msdn.com/b/vsproject/archive/2009/06/23/inherited-properties-and-property-sheets.aspx
  2. VC++ Directories: http://blogs.msdn.com/b/vsproject/archive/2009/07/07/vc-directories.aspx
  3. VCBuild vs. C++ MSBuild on the Command Line: http://blogs.msdn.com/b/vcblog/archive/2010/01/11/vcbuild-vs-c-msbuild-on-the-command-line.aspx
  4. MSBuild Toolset: http://msdn.microsoft.com/en-us/library/bb383796.aspx

VC++ 2012 Can't Build Driver Project

這幾天終於趁著聽課的空檔,把電腦裝了 VC++ 2012 ,這版的一大特色:整合了 device driver 的開發。很快地試玩了一下它的 project wizard :

wdk801

結果,很不幸地失敗 ...

1>C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h(10920): fatal error C1003: error count exceeds 100; stopping compilation

再看一下 Error list:

Error    1    error C2220: warning treated as error - no 'object' file generated    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\sal.h    2884
Error    4    error C2054: expected '(' to follow '_IRQL_requires_same_'    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1897
Error    5    error C2085: 'EXCEPTION_ROUTINE' : not in formal parameter list    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1903
Error    6    error C2085: 'EXCEPTION_ROUTINE' : not in formal parameter list    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1905
Error    7    error C2143: syntax error : missing ';' before '*'    C:\Program Files (x86)\Windows Kits\8.0\Include\shared\ntdef.h    1905
Error    8    error C2143: syntax error : missing ')' before 'constant'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384
Error    9    error C2143: syntax error : missing '{' before 'constant'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384
Error    10    error C2059: syntax error : '<Unknown>'    C:\Program Files (x86)\Windows Kits\8.0\Include\KM\wdm.h    10384

真的是很 ooxx 。問題都是發生在 sal.h, ntdef.h wdm.h 等等基本的 header files 裡。

這類的問題通常是發生在 include 了錯誤的 header files ,所以回頭檢視 solution property : Project >  Properties > Configuration Properties > VC++ Directories 。

image

include directories 多了很多路徑,其他像是 Executable Directories 、Library Directories 也有一樣的問題。嗯嗯,VC++ 2012 匯入了電腦上其他版本 VC++ 的設定,可能的原因之一:安裝完 2012 、第一次啟動時,我允許了 2012 去找尋舊版本的設定並匯入所導致。再點開 Include Directories 看一下:

image

發現這些多餘的路徑都是來自 Inherited values 。這裡,我試了兩種解決方式:

  1. 解法一
    取消 Inherit from parent or project defaults ,然後重新把需要的路徑一個一個加回去,也就是手動把
    $(ProjectDir)、$(CRT_INC_PATH)、$(ddk_INC_PATH)、$(KIT_SHARED_INC_PATH) 加上去。手動作這件事除了麻煩外,還只能用在這個 project 上,也就是一旦我開了新的 driver solution 或是在當下 solution 下新增新的 driver project 都得重做一次。
  2. 解法二
    比較一勞永逸, VC++ 把這個 Inherited Values 定義在 C:\Users\<user-name>\AppData\Local\Microsoft\MSBuild\v4.0 下的檔案裡頭,檔案是根據不同 configuration 而命名:
    Microsoft.Cpp.ARM.user.props
    Microsoft.Cpp.Itanium.user.props
    Microsoft.Cpp.Win32.user.props
    Microsoft.Cpp.x64.user.props
    *.props 是 XML 格式,把不用的 path 都清除乾淨,只留下 default value 就可以了。
    <?xml version="1.0" encoding="utf-8"?>
    <Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        <ExecutablePath>$(ExecutablePath)</ExecutablePath>
        <IncludePath>$(IncludePath)</IncludePath>
        <ReferencePath>$(ReferencePath)</ReferencePath>
        <LibraryPath>$(LibraryPath)</LibraryPath>
        <SourcePath>$(SourcePath)</SourcePath>
        <ExcludePath>$(ExcludePath)</ExcludePath>
      </PropertyGroup>
    </Project>

    這個方法是比較一勞永逸,不過有個大缺點:不同版本的 Visual C++ 會共用這個檔案。像我的電腦修改了後, VC++ 2010 的 VC++ project 的預設值也會受到影響,但已經產生的、既有 project 則不受影響,算是不幸中的大幸。

Windows Debugging 2 – Kernel Debugging with WinDbg and VMware

Windows Vista 之後已經不再將開機選項儲存於 boot.ini 中,而是改以 BCD (Boot Configuration Data) 儲存,因此以前透過編輯 boot.ini 啟動 Windows kernel debugging 的方法也得更新一下。這裡我們透過 bcdedit.exe 來編輯 BCD ,讓 Windows 啟動後可以進入 debug mode 。

VMware 設定

Debuggee 部分,和以前一樣使用 named piped 來替 guest OS 模擬 serial port (COM port)。

  1. 進入 guest OS 設定。 com port
  2. 新增硬體。
    com port 2
  3. 選擇 Serial Port 裝置。
    com port 3
  4. 選擇 named pipe 來模擬我們的 serial port。
    com port 4
  5. 替 named pipe 取一個名字,這個名字等會會被 WinDbg 使用。
    com port 5
  6. 最後別忘了把 Yield CPU on Poll 選項勾起。
    com port 6
  7. 完成新增後,請留意 VMware 替我們新增的 Serial Port 的編號,以上圖為例是:Serial Port 2 

Windows Boot 設定

  1. 使用 administrator 權限開啟一個 cmd。
    start cmd
  2. 透過 BCDEdit 編輯開機選項,使用 COM port (serial port)來做為 debugger 和 debuggee 間的溝通,debugport 的編號請根據 VMware 的 Serial Port 設定而定:
    bcdedit /dbgsettings serial debugport:2
  3. 複製現有的開機選項到 DebugEntr:
    bcdedit /copy {current} /d DebugEntry
    執行完該指令後,會出現一個 ID ,代表我們要複製出來的開機選項,該 ID 在每台電腦上都不太一樣。
  4. 調整開機選樣的順序:
    bcdedit /displayorder {current} {ID}
  5. 打開 debug 選項:
    bcdedit /debug {ID} ON
  6. 設定成預設開機選項:
    bcdedit /default {ID}
    bcdedit edit

 

Enable Debug Print

Vista之後,DbgPrintEx()、vDbgPrintEx()、vDbgPrintExWithPrefix()、KdPrintEx() 等選擇性的輸出函式預設是關閉的,要啟動的話,需要到 registry 上設定對應的 log level [3]:

到 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter 下新增 DEFAULT=dword:0000ffff
實際數值,依照需求而定,0000ffff 足以應付大多數用途。

Debug Print Filter

使用

在 bcdedit 設定完成後,重開機,便會看到新的開機選項 DebugEntry:

boot 1

選擇 DebugEntry 後,我們便可以在 debugger 的機器上的 cmd 執行下面指令來進行 kernel debugging,com_1 就是我們當初在 VMware guest OS 裡頭設定的名字:

windbg -b -k com:pipe,port=\\.\pipe\com_1,resets=0

停用驅動程式簽章增強(Disable Driver Signature Enforcement)

Vista 之後的 64-bit OS ,Microsoft 都會要求 drivers 必須有 WHQL 的認證才能啟動。這對於一般使用者來說可能沒有影響,但對於有在開發、測試 drivers 的人就會是個問題。

最簡單的方式是,開機時,使用 F8 進入進階模式:
boot 1
並選擇(停用驅動程式簽章增強)Disable Driver Signature Enforcement。
boot 2

Test Signing

若是 drivers 有 test sign ,那麼我們還可以使用 BCDEdit 來允許 testing sign driver 的載入[4]:
bcdedit /set TESTSIGNING ON
設定完成後,需要重開機。

要驗證 test sign 是否成功,可以使用 bcdedit 檢查,或是觀察桌面右下角的文字說明:

test signing

Reference

  1. Boot Parameters to Enable Debugging
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff542279%28v=vs.85%29.aspx
  2. Kernel Debugging in Windows Vista
    http://msdn.microsoft.com/en-us/windows/hardware/gg487520
  3. Reading and Filtering Debugging Messages
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff551519%28v=vs.85%29.aspx
  4. The TESTSIGNING Boot Configuration Option
    http://msdn.microsoft.com/en-us/library/windows/hardware/ff553484%28v=vs.85%29.aspx

See Also

  1. Windows Debugging – Kernel Debugging with WinDbg and VMware
    http://keikoblog.blogspot.com/2009/03/windows-debugging-kernel-debugging-with.html

WinDbg 實驗: Command Line Arguments 的傳遞

一個 C/C++ 程式可以透過 main() 的 argument list 取得 client 端輸入的 command line arguments:

int main( int argc, char* argv[] ) { ... }

如果好奇這是如何地從無到有,可以寫一段程式碼來 trace 。

先準備好 sample code:

#include <iostream>
using namespace std;

int main( int argc, char* argv[] )
{   // 程式的內容不是重點
    cout << "hello world";
    cin.get();
    return 0;
}

透過 VC++ 或是 WinDbg 在 main() 設定 breakpoint 來追蹤:

  1. Compiler: VC++ 2005
  2. OS: Windows XP
  3. 在 cmd 中執行 CrtDemo05 test

image

0:000> bp CrtDemo05!main
*** WARNING: Unable to verify checksum for CrtDemo05.exe

0:000> bl
 0 e 004377d0     0001 (0001)  0:**** CrtDemo05!main

0:000> g
Breakpoint 0 hit
eax=00383088 ebx=7ffda000 ecx=00383028 edx=00000001 esi=010df742 edi=010df6f2
eip=004377d0 esp=0012ff58 ebp=0012ffb8 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
CrtDemo05!main:
004377d0 55              push    ebp

0:000> k
ChildEBP RetAddr  
0012ff54 0044a203 CrtDemo05!main [f:\src\_experiment\crtdemo05\crtdemo05\main.cpp @ 6]
0012ffb8 00449fbd CrtDemo05!__tmainCRTStartup+0x233 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 327]
0012ffc0 7c816d4f CrtDemo05!mainCRTStartup+0xd [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 196]
0012fff0 00000000 kernel32!BaseProcessStart+0x23

進入 main() 之前的兩個函式都是 CRT 的一部分,負責完成基本但必要的初始化,舉凡 Global variables、Heap、I/O 等等都屬於這個範疇。這樣一來,身處 main() 後頭的我們才能順利工作。所以照這情勢看來,想知道 argument list 怎麼來,就得去 trace 這兩個 functions ,幸運的, VC++ CRT 的 source code 是隨著安裝程式散發的,通常就在:

C:\Program Files\Microsoft Visual Studio 8\VC\crt

視安裝路徑而定。也可以 double click VC++ 的 Call Stack 進入程式碼裡頭。

如果想利用 debugger 追蹤或試驗 CRT ,不妨把 CRT link 改成 debug mode 並且是 static link 。這可以省下一些時間、增加 tracing 時的可讀性和便利性,因為像是 debug 下的 dynamic link 會使用 ILT ,它所引入的間接性,會讓定位 functions 或 symbols 徒增額外的時間,造成實驗的不便。

VC++ 有所謂的 Microsoft C++ ,其程式進入點有自己的一套方式來定義,至少在名稱上,就可以找到四種:

  1. main
  2. wmain
  3. WinMain
  4. wWinMain

不過它們使用的 CRT 是同一份程式碼,並且使用了跟 TCHAR.h 相同的手法來區別,但確實由 WPRFLAG 這個 macro 所控制凡;凡是見到 t、_t、__t 開頭的名稱,都有機會透過它來替換掉,不過 __tmainCRTStartup() 可說是例外。下表節錄與 command line 相關的 t、_t、_t 開頭 symbols:

 

ansi/console

ansi/GUI

wide/console

wide/GUI

_tmainCRTStartup

mainCRTStartup

WinMainCRTStartup

wmainCRTStartup

wWinMainCRTStartup

_tcmdln

_acmdln

_acmdln

_wcmdln

_wcmdln

_targv

__argv

__argv

__wargv

__wargv

GetCommandLineT()

GetCommandLineA

GetCommandLineA

__crtGetCommandLineW

__crtGetCommandLineW

_tsetargv()

_setargv

_setargv

_wsetargv

_wsetargv

我們先從 ansi 版的 main() ,也是 C/C++ Standard 所描述的 main() 開始:

mainCRTStartup()

mainCRTStartup() 基本上是一個 forward function :

int _tmainCRTStartup( void )
{
    __security_init_cookie();
    return __tmainCRTStartup();
}

__tmainCRTStartup()

就像前面提到的:__tmainCRTStartup() 就是正港的 __tmainCRTStartup() 沒有 ansi, wide 的替換,很快地就可以梳理出跟 command line 相關的程式:

int __tmainCRTStartup( void )
{
    // ...

    __try {
        // ...
    
        /* get wide cmd line info */
        _tcmdln = (_TSCHAR *)GetCommandLineT();

        /* get wide environ info */
        _tenvptr = (_TSCHAR *)GetEnvironmentStringsT();

        if ( _tsetargv() < 0 )
            _amsg_exit(_RT_SPACEARG);
        if ( _tsetenvp() < 0 )
            _amsg_exit(_RT_SPACEENV);

        // ...

    #ifdef _WINMAIN_
        lpszCommandLine = _twincmdln();
        mainret = _tWinMain( (HINSTANCE)&__ImageBase,
                             NULL,
                             lpszCommandLine,
                             StartupInfo.dwFlags & STARTF_USESHOWWINDOW
                                  ? StartupInfo.wShowWindow
                                  : SW_SHOWDEFAULT
                            );
    #else  /* _WINMAIN_ */
        _tinitenv = _tenviron;
        mainret = _tmain(__argc, _targv, _tenviron);
    #endif  /* _WINMAIN_ */

        // ...
    }
    __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) {
        // ...
    }

    return mainret;
}

從上面的程式碼可以發現,重點就在於兩個 functions 上:

  1. GetCommandLineT()
  2. _tsetargv()

但是當我們將 breakpoint 設在 GetCommandLineT() 時,卻無法 step in 。這時可以切換到 assembly 模式,透過 WinDbg diassembly window 或是 WinDbg 的 uf 幫助來持續追蹤:

0:000> uf __tmainCRTStartup
...
CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]:
  300 0044a184 ff1564924b00    call    dword ptr [CrtDemo05!_imp__GetCommandLineA (004b9264)]
  300 0044a18a a3a4814b00      mov     dword ptr [CrtDemo05!_acmdln (004b81a4)],eax
  303 0044a18f e8a89efeff      call    CrtDemo05!ILT+55(___crtGetEnvironmentStringsA) (0043403c)
  303 0044a194 a3d45e4b00      mov     dword ptr [CrtDemo05!_aenvptr (004b5ed4)],eax
  305 0044a199 e87a9ffeff      call    CrtDemo05!ILT+275(__setargv) (00434118)
  305 0044a19e 85c0            test    eax,eax
  305 0044a1a0 7d0a            jge     CrtDemo05!__tmainCRTStartup+0x1dc (0044a1ac)
...

在 Windows 裡頭, _imp、__imp 開頭的 name mangling 代表: symbol 代表是從 DLL export 出的 ,可見 CrtDemo05!_imp__GetCommandLineA 存在與另外一個 DLL 之中。 dereference 該位址便可以找到真正的 symbol 。

0:000> dd 004b9264
004b9264  7c812c8d 7c93043d 7c812851 7c9305d4
004b9274  7c80aa49 7c80a0c7 7c809cad 7c832e2b

由於 CrtDemo05 裡頭使用 call 來呼叫這個位址,想必這個位址會是一段可以執行的指令,使用 uf 來查找:

0:000> uf 7c812c8d 
kernel32!GetCommandLineA:
7c812c8d a1f435887c      mov     eax,dword ptr [kernel32!BaseAnsiCommandLine+0x4 (7c8835f4)]
7c812c92 c3              ret

原來是落在 kernel32.dll 這個基礎函式庫裡頭,而且發現它的實做相當簡單:僅僅是從 kernel32!BaseAnsiCommandLine+0x4 位址讀取而已。為了驗證,我們以 dereference 該位址看是否真的儲存了要傳遞給 CrtDemo05 的 arguments 。

0:000> dd 7c8835f4
7c8835f4  00151ee0 000a0008 7c80e300 ffff02ff
7c883604  00000001 000a0008 7c831874 000a0008

接著

0:000> da 00151ee0 
00151ee0  "C:\test\CrtDemo05.exe test"

若是覺得 dd 兩層的 dereference 有點麻煩,針對字串可以使用 dp* ,

kd> dpa 7c8835f4 L1
7c8835f4  001522f8 "CrtDemo05.exe"

無論是哪種方式,都驗證了我們的想法── kernel32!GetCommandLineA() 是取得 command line 的函式,並且有著極簡單的實做──讀取一個預先填好值的 variable。

wmain() 版本也是相同的:

0:000> uf CrtDemo05!__tmainCRTStartup
...
CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]:
  300 0044a194 e8ad9efeff      call    CrtDemo05!ILT+65(___crtGetCommandLineW) (00434046)
  300 0044a199 a3e4824b00      mov     dword ptr [CrtDemo05!_wcmdln (004b82e4)],eax
  303 0044a19e e8439ffeff      call    CrtDemo05!ILT+225(___crtGetEnvironmentStringsW) (004340e6)
  303 0044a1a3 a3d85e4b00      mov     dword ptr [CrtDemo05!_wenvptr (004b5ed8)],eax
  305 0044a1a8 e845b1feff      call    CrtDemo05!ILT+4845(__wsetargv) (004352f2)
  305 0044a1ad 85c0            test    eax,eax
  305 0044a1af 7d0a            jge     CrtDemo05!__tmainCRTStartup+0x1db (0044a1bb)
...

函式,你可能會好奇為什麼 wide 版本並不是呼叫 _imp__GetCommandLineW() ,其實它被包裝在 ___crtGetCommandLineW() 裡頭。

0:000> uf 00434046
...
CrtDemo05!__crtGetCommandLineW+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\aw_com.c @ 52]:
   52 0046336f ff1530934b00    call    dword ptr [CrtDemo05!_imp__GetCommandLineW (004b9330)]
   52 00463375 85c0            test    eax,eax
   52 00463377 740c            je      CrtDemo05!__crtGetCommandLineW+0x25 (00463385)
...

並且同樣地,可以使用如同 ansi 版本的方式,去 trace :

0:000> dd 004b9330
004b9330  7c816cfb 7c80c6cf 7c801eee 7c873d83
004b9340  7c81abe4 7c80180e 7c810da6 7c80cd58

0:000> uf 7c816cfb 
kernel32!GetCommandLineW:
7c816cfb a10430887c      mov     eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)]
7c816d00 c3              ret

0:000> dd 7c883004
7c883004  0002062c 00000000 7c809784 7c80979d
7c883014  7c8097d2 7c8097e7 7c8097b7 00000000

0:000> du 0002062c 
0002062c  "C:\test\CrtDemo05.exe test"

使用 Kernel Debugging

或許你會好奇為什麼 trace command 如何填入一個 process 需要動要到 kernel debug ?其實原因很簡單,因為當 user-mode debug 無法滿足時,以這次的參數傳遞來看,當我們利用 windbg 載入一個 program 時,program 需要執行到某個狀態後, user-mode debugger 才能介入,此時 arguments 已經設置完成。

Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: C:\test\CrtDemo05.exe
Symbol search path is: srv*C:\sym_cache*http://msdl.microsoft.com/download/symbols;C:\test
Executable search path is: 
ModLoad: 00400000 004bb000   CrtDemo05.exe
ModLoad: 7c920000 7c9b5000   ntdll.dll
ModLoad: 7c800000 7c91d000   C:\WINDOWS\system32\kernel32.dll
(7d8.568): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd8000 ecx=00000001 edx=00000002 esi=00251f48 edi=00251eb4
eip=7c921230 esp=0012fb20 ebp=0012fc94 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c921230 cc              int     3

0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00 0012fb1c 7c95edc0 7ffdf000 7ffd8000 00000000 ntdll!DbgBreakPoint (FPO: [0,0,0])
01 0012fc94 7c941639 0012fd30 7c920000 0012fce0 ntdll!LdrpInitializeProcess+0xffa (FPO: [5,89,4])
02 0012fd1c 7c92eac7 0012fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183 (FPO: [Non-Fpo])
03 00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7

0:000> da 00151ee0 
00151ee0  "C:\test\CrtDemo05.exe test"

注意到沒?當 CrtDemo05 整支程式還在被 OS 的 loader 所初始化時,在 __tmainCRTStartup 還未執行到之前,arguments 已經填入了。因此這個例子需要透過 kernel debugging 來進行更早期的追蹤。 kernel debugging 的方式有許多種,使用 VMWare 算是便利的方法之一,可以參考 Windows Debugging – Kernel Debugging with WinDbg and VMWare 設定環境。

Catch Me If You Can

為了捕捉 kernel32!BaseUnicodeCommanLine 以及 kernel32!BaseAnsiCommandLine 是如何被填寫的?我們可以透過 WinDbg 的 break on access ,不過很快就會遇到第一個問題:

kd> x kernel32!BaseUnicodeCommandLine
7c885730 kernel32!BaseUnicodeCommandLine = <no type information>
7c883000 kernel32!BaseUnicodeCommandLine = <no type information>

遇到兩個同名的 symbols ,理論上這是件會造成 symbol resolve 上 ambiguous 的事。雖然暫時無解,但幸運的是,根據之前追蹤結果:

7c816cfb a10430887c mov eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)]

我們可以直接選擇 7c885730

kd> ba r4 7c883004
kd> g
Breakpoint 0 hit
nt!MiCopyOnWrite+0x148:
0008:805222bc f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
kd> k
ChildEBP RetAddr  
b22bad00 8051d019 nt!MiCopyOnWrite+0x148
b22bad4c 805406ec nt!MmAccessFault+0x9f9
b22bad4c 7c93a100 nt!KiTrap0E+0xcc
0012f938 7c93d8a0 ntdll!__security_init_cookie_ex+0x31
0012f944 7c93d83a ntdll!LdrpInitSecurityCookie+0x2f
0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x124
0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6
0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18
0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a
0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183
00000000 00000000 ntdll!KiUserApcDispatcher+0x7

這一次還不是我們想要的 stack:

kd> g
Breakpoint 0 hit
kernel32!_BaseDllInitialize+0x20f:
7c817ea8 ff157c10807c    call    dword ptr [kernel32!_imp__RtlUnicodeStringToAnsiString (7c80107c)]
kd> bl
 0 e 7c883004 r 4 0001 (0001) kernel32!BaseUnicodeCommandLine+0x4
kd> k
ChildEBP RetAddr  
0012f918 7c9211a7 kernel32!_BaseDllInitialize+0x20f
0012f938 7c93cbab ntdll!LdrpCallInitRoutine+0x14
0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x344
0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6
0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18
0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a
0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183
00000000 00000000 ntdll!KiUserApcDispatcher+0x7

這次 stack 看起來成功率比較高。 disassembly 一段 breakpoint 前的程式碼,使用 WinDbg 的 disassembly 視窗或是指令都可以做到:

kd> ub 7c817ea8 L10
kernel32!_BaseDllInitialize+0x1cc:
...
7c817e7f 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c817e85 8b4030          mov     eax,dword ptr [eax+30h]
7c817e88 8b4010          mov     eax,dword ptr [eax+10h]
7c817e8b 8b4840          mov     ecx,dword ptr [eax+40h]
7c817e8e 6a01            push    1
7c817e90 890d0030887c    mov     dword ptr [kernel32!BaseUnicodeCommandLine (7c883000)],ecx
7c817e96 8b4044          mov     eax,dword ptr [eax+44h]
7c817e99 680030887c      push    offset kernel32!BaseUnicodeCommandLine (7c883000)
7c817e9e 68f035887c      push    offset kernel32!BaseAnsiCommandLine (7c8835f0)
7c817ea3 a30430887c      mov     dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)],eax

解讀 fs:[00000018h]

可以發現,kernel32!BaseUnicodeCommandLine 是由 ecx 決定,而 ecx 則是由 fs:[00000018h] 所決定,這部份的解讀,需要一些的隱藏知識:在 x86 上Windows user mode 將每個 thread 的 TEB (Thread Environment Block),存放在 fs:[0];而在 kernel mode 中, fs:[0] 則是存放 KPCR (Process Control Region) 。

kd> dt _TEB
ntdll!_TEB
   +0x000 NtTib            : _NT_TIB
   +0x01c EnvironmentPointer : Ptr32 Void
   +0x020 ClientId         : _CLIENT_ID
   +0x028 ActiveRpcHandle  : Ptr32 Void
   +0x02c ThreadLocalStoragePointer : Ptr32 Void
   +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
   ...

整個 ntdll!_TEB 有點龐大,不過根據 ub 的結果,fs:[00000018h] 落在第一個欄位 ── _NT_TIB (Thread Information Block)裡頭,再進行一次 dt :

kd> dt _NT_TIB
ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 StackBase        : Ptr32 Void
   +0x008 StackLimit       : Ptr32 Void
   +0x00c SubSystemTib     : Ptr32 Void
   +0x010 FiberData        : Ptr32 Void
   +0x010 Version          : Uint4B
   +0x014 ArbitraryUserPointer : Ptr32 Void
   +0x018 Self             : Ptr32 _NT_TIB

那為何不直接使用 fs:[0] 而要選擇 fs:[00000018h] ?因為 fs:[0] 並非 process 的 virtual address :

kd> dd fs:0
003b:00000000  0012fa30 00130000 0012f000 00000000
003b:00000010  00001e00 00000000 7ffdd000 00000000
003b:00000020  00000778 00000128 00000000 00000000

因此使用上,往往透過 fs:[00000018h] 來指向正確 TEB 或說 TIB 的位址。

重新解讀 ub

7c817e7f 64a118000000    mov     eax,dword ptr fs:[00000018h]
7c817e85 8b4030          mov     eax,dword ptr [eax+30h]
7c817e88 8b4010          mov     eax,dword ptr [eax+10h]
7c817e8b 8b4840          mov     ecx,dword ptr [eax+40h]

上述組語在知道 fs:[0] 的意義後,便可尋著指令進行 dt 的解碼,並寫出對應的虛擬碼:

_TEB            teb = fs:[00000018h];
UNICODE_STRING  kernel32!BaseUnicodeCommandLine = 
    teb.ProcessEnvironmentBlock->ProcessParameters.CommandLine;

到了這個步驟,我們已經知道即使是 kernel32!_BaseDllInitialize() 這般低階的初始化,也僅僅只是將 PEB 中的某個變數值讀出,那麼 PEB 中的值又是給負責填寫呢?還記得 Win32 API 中的 CreateProcess() 嗎?它或許是有嫌疑的一份子。

CreateProcess()

kd> dt _RTL_USER_PROCESS_PARAMETERS
ntdll!_RTL_USER_PROCESS_PARAMETERS
   +0x000 MaximumLength    : Uint4B
   +0x004 Length           : Uint4B
   +0x008 Flags            : Uint4B
   +0x00c DebugFlags       : Uint4B
   +0x010 ConsoleHandle    : Ptr32 Void
   +0x014 ConsoleFlags     : Uint4B
   +0x018 StandardInput    : Ptr32 Void
   +0x01c StandardOutput   : Ptr32 Void
   +0x020 StandardError    : Ptr32 Void
   +0x024 CurrentDirectory : _CURDIR
   +0x030 DllPath          : _UNICODE_STRING
   +0x038 ImagePathName    : _UNICODE_STRING
   +0x040 CommandLine      : _UNICODE_STRING
   +0x048 Environment      : Ptr32 Void
   +0x04c StartingX        : Uint4B
   +0x050 StartingY        : Uint4B
   +0x054 CountX           : Uint4B
   +0x058 CountY           : Uint4B
   +0x05c CountCharsX      : Uint4B
   +0x060 CountCharsY      : Uint4B
   +0x064 FillAttribute    : Uint4B
   +0x068 WindowFlags      : Uint4B
   +0x06c ShowWindowFlags  : Uint4B
   +0x070 WindowTitle      : _UNICODE_STRING
   +0x078 DesktopInfo      : _UNICODE_STRING
   +0x080 ShellInfo        : _UNICODE_STRING
   +0x088 RuntimeData      : _UNICODE_STRING
   +0x090 CurrentDirectores : [32] _RTL_DRIVE_LETTER_CURDIR

目前我們已經知道,一個程式的 command line 儲存在 _RTL_USER_PROCESS_PARAMETERS 這個 strcut 裡頭,所以要做的是,找到何時會去填寫,方法有兩種:

  1. Trace CreateProcess
  2. 尋找有關 ProcessParameter 的 function 。

我們使用方法 2. 來加速:

可以使用 WinDbg 的 x 指令:

kd> x *!*processparameter*
7c819f9e kernel32!BasePushProcessParameters = <no type information>
7c801488 kernel32!_imp__RtlCreateProcessParameters = <no type information>
7c801484 kernel32!_imp__RtlDestroyProcessParameters = <no type information>
7c9432ec ntdll!RtlDestroyProcessParameters = <no type information>
7c950ea3 ntdll!RtlCheckProcessParameters = <no type information>
7c9433c1 ntdll!RtlCreateProcessParameters = <no type information>

這幾個 functions 看起來都有機會,其中以 BasePushProcessParameters() 的名稱看來最有可能是 creater 建立 createe process parameter 的函式,為了驗證這想法,將 WinDbg 目前的 context 切換到 cmd.exe 下,並設定 breakpoint 來驗證:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...
PROCESS 8200d7e8  SessionId: 0  Cid: 0194    Peb: 7ffdf000  ParentCid: 0608
    DirBase: 08940240  ObjectTable: e1533828  HandleCount:  33.
    Image: cmd.exe

kd> .process /r /P /p 8200d7e8

kd> bp 7c819f9e

kd> g
Breakpoint 1 hit
kernel32!BasePushProcessParameters:
7c819f9e 68c0020000      push    2C0h

kd> k
ChildEBP RetAddr  
0013f028 7c8199bc kernel32!BasePushProcessParameters
0013fa88 7c80235e kernel32!CreateProcessInternalW+0x184e
0013fac0 4ad031dd kernel32!CreateProcessW+0x2c
0013fc20 4ad02db0 cmd!ExecPgm+0x22b
0013fc54 4ad02e0e cmd!ECWork+0x84
0013fc6c 4ad05f9f cmd!ExtCom+0x40
0013fe9c 4ad013eb cmd!FindFixAndRun+0xcf
0013fee0 4ad0bbba cmd!Dispatch+0x137
0013ff44 4ad05164 cmd!main+0x216
0013ffc0 7c816d4f cmd!mainCRTStartup+0x125
0013fff0 00000000 kernel32!BaseProcessStart+0x23

stack 目前看來是支持我們的猜測, cmd.exe 透過 CreateProcess()  來建立 CrtDemo05.exe ,並且呼叫 BasePushProcessParameters() 來初始化 process parameter。在這邊,我們也可以切換到 CrtDemo05.exe 下,確定一下它目前的建立狀態,以瞭解它仍是否仍在建立中而非建立完成:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
...

PROCESS 8200d7e8  SessionId: 0  Cid: 0194    Peb: 7ffdf000  ParentCid: 0608
    DirBase: 08940240  ObjectTable: e1533828  HandleCount:  35.
    Image: cmd.exe

PROCESS 82476d00  SessionId: 0  Cid: 00c4    Peb: 7ffd4000  ParentCid: 0194
    DirBase: 089402c0  ObjectTable: e1d3bf68  HandleCount:  36.
    Image: conime.exe

PROCESS 8244e980  SessionId: 0  Cid: 05b4    Peb: 7ffdc000  ParentCid: 0400
    DirBase: 08940300  ObjectTable: e16e0700  HandleCount: 123.
    Image: wuauclt.exe

PROCESS 8244a460  SessionId: 0  Cid: 0224    Peb: 7ffd5000  ParentCid: 0194
    DirBase: 08940380  ObjectTable: e17871a0  HandleCount:   1.
    Image: CrtDemo0

看到 Image name 被截斷,有點訝異,不過可能是因為 kernel 在填寫時被我們中斷了。現在來看看 PEB 裡頭的 command line 是否已經準備好了:

kd> !peb 7ffd5000
PEB at 7ffd5000
error 1 InitTypeRead( nt!_PEB at 7ffd5000)...

kd> .process /r /P /p 8244a460 
Implicit process is now 8244a460
.cache forcedecodeuser done
Loading User Symbols
PEB is paged out (Peb.Ldr = 7ffd500c).  Type ".hh dbgerr001" for details
kd> dt _PEB 7ffd5000
nt!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0 ''
   +0x003 SpareBool        : 0 ''
   +0x004 Mutant           : 0xffffffff Void
   +0x008 ImageBaseAddress : 0x00400000 Void
   +0x00c Ldr              : (null) 
   +0x010 ProcessParameters : (null) 
   +0x014 SubSystemData    : (null) 
   +0x018 ProcessHeap      : (null) 
   +0x01c FastPebLock      : (null) 
   ...

試了兩種方法,都無法正確讀取 PEB ,這正和我們的預期:表示 CrtDemo05.exe 正在被建立中,其 PEB 似乎也在混沌之中。緊接著回到 cmd.exe 的 stack 上,接下來的事就需要點耐心,由於目前只知道 BasePushProcessParameters() 跟 _RTL_USER_PROCESS_PARAMETERS 有關,但是找不到可以設立 breakpoint 的地方,根據 assembly 一步步追蹤是個方法,不過直接閱讀 assembly 來找尋跟 ProcessParameters 相關的指令或許會更快,一個很快速的想法是使用關鍵字來縮小範圍,這裡若是使用 uf 來 diassembly ,可能會遇上一些麻煩,因為 CreateProcessInternalW() 被 OMAP 最佳化過,無法依照 mapping 到記憶體上的 layout 顯示:

kd> uf CreateProcessInternalW
Flow analysis was incomplete, some code may be missing
kernel32!CreateProcessInternalW+0x308:
...
...

當遇到這種狀況,我們得放棄 diassembly 整個 function ,而是直接指明記憶體位址:

kd> x kernel32!CreateProcessInternalW
7c8191eb kernel32!CreateProcessInternalW = 

kd> u 7c8191eb L600
kernel32!CreateProcessInternalW:
...
; ProcessParameter
; ebp-234h: _RTL_USER_PROCESS_PARAMETERS*
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                        ; pProcessParameters
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]
7c81a12a 33d2            xor     edx,edx                    ; edx = 0
7c81a12c 3bc2            cmp     eax,edx

...
...
7c81a538 ff158414807c    call    dword ptr [kernel32!_imp__RtlDestroyProcessParameters (7c801484)]

幸運的找到:

7c9432ec ntdll!RtlDestroyProcessParameters
7c9433c1 ntdll!RtlCreateProcessParameters

利用兩個 functions ,我們可以縮小可疑範圍,7c81a11d ~ 7c81a538 給了我們一個暗示:ebp-234h 是個 local variable ,並且傳遞至 RtlCreateProcessParameters() ,當結束後進行是否為 NULL 的比較,那麼 ebp-234h 基本上就很有機會就是一個 pointer point to _RTL_USER_PROCESS_PARAMETERS。另外在向上回溯,可以找找看是否還有其他參數被傳遞至 RtlCreateProcessParameters() :

7c81a0d7 ffd6            call    esi
7c81a0d9 8d85b4fdffff    lea     eax,[ebp-24Ch]
7c81a0df 50              push    eax                    ; parameter10
7c81a0e0 8d859cfdffff    lea     eax,[ebp-264h]
7c81a0e6 50              push    eax                    ; parameter9
7c81a0e7 8d85a4fdffff    lea     eax,[ebp-25Ch]
7c81a0ed 50              push    eax                    ; parameter8
7c81a0ee 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0f4 50              push    eaxe                   ; parameter7
7c81a0f5 ffb570fdffff    push    dword ptr [ebp-290h]   ; parameter6
7c81a0fb 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a101 50              push    eax                    ; parameter5
7c81a102 f7db            neg     ebx
7c81a104 1bdb            sbb     ebx,ebx
7c81a106 8d8578fdffff    lea     eax,[ebp-288h]
7c81a10c 23d8            and     ebx,eax
7c81a10e 53              push    ebx                    ; parameter4
7c81a10f 8d85acfdffff    lea     eax,[ebp-254h]
7c81a115 50              push    eax                    ; parameter3
7c81a116 8d858cfdffff    lea     eax,[ebp-274h]
7c81a11c 50              push    eax                    ; parameter2
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                    ; parameter1
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]

看到了 10 個參數被傳遞至 RtlCreateProcessParameters() ,我們可以選擇 diassembly 它看是否有與 command line 相關的操作,或是繼續搜尋其他 7c81a11d ~ 7c81a538 內 ebp-234h 的操作,這裡有一個或許可行的快速篩選法,因為 CommandLine 位於 _RTL_USER_PROCESS_PARAMETERS offset 40 bytes 的地方:

kd> dt _RTL_USER_PROCESS_PARAMETERS
ntdll!_RTL_USER_PROCESS_PARAMETERS
   ...
   +0x040 CommandLine      : _UNICODE_STRING
   ...

我們便找尋 40h 的關鍵字,而搜尋結果的確有幾筆與 40h 有關的操作,但幸運地,都不是和 CommandLine 有關的。於是乎,把焦點放回 RtlCreateProcessParameters():

kd> x ntdll!RtlCreateProcessParameters
7c9433c1 ntdll!RtlCreateProcessParameters

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c9435e1 6a04            push    4                          ; Protect
7c9435e3 6800100000      push    1000h                      ; AllocationType
7c9435e8 8d45cc          lea     eax,[ebp-34h]
7c9435eb 50              push    eax                        ; RegionSize
7c9435ec 53              push    ebx                        ; ZeroBits
7c9435ed 8d45e4          lea     eax,[ebp-1Ch]
7c9435f0 50              push    eax                        ; BaseAddress
7c9435f1 6aff            push    0FFFFFFFFh                 ; ProcessHandle
7c9435f3 e8e69efeff      call    ntdll!ZwAllocateVirtualMemory (7c92d4de)
...

由於 ZwAllocateVirtualMemory() 是公開的 API ,所以很快地可以對參數進行 mapping ,馬上就發現 local vaiable ebp-1ch 存放的就是 allocate 的結果,並且在稍後就有一個 +40h 的操作:

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94369a 40              inc     eax
7c94369b 40              inc     eax
7c94369c 50              push    eax
7c94369d 8b45e4          mov     eax,dword ptr [ebp-1Ch]    ; pBase
7c9436a0 83c040          add     eax,40h                    ; pBase + 40 = CommandLine
7c9436a3 57              push    edi
7c9436a4 50              push    eax
7c9436a5 8d45d8          lea     eax,[ebp-28h]
7c9436a8 50              push    eax
7c9436a9 e8ab000000      call    ntdll!RtlpCopyProcString (7c943759)
...

這和 CommandLine 的操作看似吻合,但我們仍需要找到更直接的證據,首先可以使用動態的證據,也就是設定 breakpoint 去證明,我們將 breakpoint 先設定在 add eax,40h:

kd> bp 7c9436a0

kd> g
Breakpoint 2 hit
ntdll!RtlCreateProcessParameters+0x2f5:
7c9436a0 83c040          add     eax,40h

kd> dd ebp-1c
0013ed00  00380000 0013ecd4 0013edf8 0013f018
0013ed10  7c92ee18 7c943748 00000000 0013f028

kd> dd 00380040 
00380040  00000000 00000000 00010000 00000000
00380050  00000000 00000000 00000000 00000000

接著,執行數個指令後,等到 RtlpCopyProcString() 執行完成後:

kd> dd 00380040 
00380040  00260024 003805c0 00010000 00000000
00380050  00000000 00000000 00000000 00000000

kd> du 003805c0
003805c0  "CrtDemo05.exe test"

再者,我們可以來個靜態分析:

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94370b 8b45e4          mov     eax,dword ptr [ebp-1Ch]
7c94370e 6689988a000000  mov     word ptr [eax+8Ah],bx
7c943715 ff75e4          push    dword ptr [ebp-1Ch]
7c943718 e8f6fbffff      call    ntdll!RtlDeNormalizeProcessParams (7c943313)
7c94371d 8b4d08          mov     ecx,dword ptr [ebp+8]
7c943720 8901            mov     dword ptr [ecx],eax
...

很快地就有一個可疑的證物: ebp+8 也就是 RtlCreateProcessParameters() 的第一個參數,它被 eax 給賦值。雖然 eax 在第一行被指定為 ebp-1Ch ,看似符合我們的假設,但因為中間有個 RtlDeNormalizeProcessParams() 的呼叫,不能掉以輕心,必須進去瞧瞧:

kd> uf RtlDeNormalizeProcessParams
Flow analysis was incomplete, some code may be missing
ntdll!RtlDeNormalizeProcessParams:
7c943313 8bff            mov     edi,edi
7c943315 55              push    ebp
7c943316 8bec            mov     ebp,esp
7c943318 8b4508          mov     eax,dword ptr [ebp+8]
7c94331b 85c0            test    eax,eax
7c94331d 7478            je      ntdll!RtlDeNormalizeProcessParams+0x84 (7c943397)
...

幸運的, RtlDeNormalizeProcessParams() 沒有可觀的實作,也僅在 function 開頭將 eax 給予 ebp+8 所代表的位址;而 ebp+8 也正就是 ebp-1Ch,其餘的對應操作都是 read 。確定了 RtlDeNormalizeProcessParams() 不會更改 ebp-1Ch 後,就可以放心回到 RtlCreateProcessParameters():

kd> u 7c9433c1 L200
ntdll!RtlCreateProcessParameters:
...
7c94345b 8b7d18          mov     edi,dword ptr [ebp+18h]
...
7c943685 e8cf000000      call    ntdll!RtlpCopyProcString (7c943759)
7c94368a 668b07          mov     ax,word ptr [edi]
7c94368d 663b4702        cmp     ax,word ptr [edi+2]
7c943691 0f84f79a0200    je      ntdll!RtlCreateProcessParameters+0x2e9 (7c96d18e)
7c943697 0fb7c0          movzx   eax,ax
7c94369a 40              inc     eax
7c94369b 40              inc     eax
7c94369c 50              push    eax
7c94369d 8b45e4          mov     eax,dword ptr [ebp-1Ch]    ; pBase
7c9436a0 83c040          add     eax,40h                    ; pBase + 40 = CommandLine
7c9436a3 57              push    edi
7c9436a4 50              push    eax
7c9436a5 8d45d8          lea     eax,[ebp-28h]
7c9436a8 50              push    eax
7c9436a9 e8ab000000      call    ntdll!RtlpCopyProcString (7c943759)

可以發現,可疑的參數有:

  1. 7c94369a                 inc eax
    7c94369b                 inc eax
    7c94369c                  push eax
  2. 7c94345b                 mov edi,dword ptr [ebp+18h]
    7c9436a3                 push edi
  3. 7c9436a5                 lea eax,[ebp-28h]
    7c9436a8                 push eax

為了找出實際的賦值給 ebp-1Ch+40h 的對象,這邊可以使用 breakpoint 去檢驗,分別是:

kd> bp 7c94369c
kd> g
Breakpoint 5 hit
ntdll!RtlCreateProcessParameters+0x2f1:
7c94369c 50              push    eax
kd> r
eax=00000026 ebx=00000000 ecx=00380034 edx=00000000 esi=00000208 edi=0013edac
eip=7c94369c esp=0013ecd4 ebp=0013ed1c iopl=0         nv up ei pl nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000203

kd> dpu 0013edac+4 L1
0013edb0  0015adc8 "CrtDemo05.exe test"

kd> bp 7c9436a8
kd> g
Breakpoint 6 hit
kd> dd ebp-28h
0013ecf4  003805c0 00000000 00000634 00380000
0013ed04  0013ecd4 0013edf8 0013f018 7c92ee18

kd> dpu 003805c0
003805c0  00000000

透過 breakpoint 的實驗,就可以知道 edi 才是賦值給 ebp-1Ch+40h 的來源,而且還是 RtlCreateProcessParameters() 的第五個參數。所以可以反推 edi 是怎麼來的,並一路反推,便可得到 command line 最初的來源。

kd> uf kernel32!CreateProcessW
kernel32!CreateProcessW:
7c802332 8bff            mov     edi,edi
7c802334 55              push    ebp
7c802335 8bec            mov     ebp,esp
7c802337 6a00            push    0                      ; parameter12
7c802339 ff752c          push    dword ptr [ebp+2Ch]    ; lpProcessInformation
7c80233c ff7528          push    dword ptr [ebp+28h]    ; lpStartupInfo
7c80233f ff7524          push    dword ptr [ebp+24h]    ; lpCurrentDirectory
7c802342 ff7520          push    dword ptr [ebp+20h]    ; lpEnvironment
7c802345 ff751c          push    dword ptr [ebp+1Ch]    ; dwCreationFlags
7c802348 ff7518          push    dword ptr [ebp+18h]    ; bInheritHandles
7c80234b ff7514          push    dword ptr [ebp+14h]    ; lpThreadAttributes
7c80234e ff7510          push    dword ptr [ebp+10h]    ; lpProcessAttributes
7c802351 ff750c          push    dword ptr [ebp+0Ch]    ; lpCommandLine
7c802354 ff7508          push    dword ptr [ebp+8]      ; lpApplicationName
7c802357 6a00            push    0                      ; parameter1
7c802359 e88d6e0100      call    kernel32!CreateProcessInternalW (7c8191eb)
7c80235e 5d              pop     ebp
7c80235f c22800          ret     28h

kd> x kernel32!CreateProcessInternalW
7c8191eb kernel32!CreateProcessInternalW = <no type information>

kd> u 7c8191eb L600
kernel32!CreateProcessInternalW:
...
7c819214 8b4510          mov     eax,dword ptr [ebp+10h]    ; lpCommandLine
7c819217 8985e0f8ffff    mov     dword ptr [ebp-720h],eax
...
7c819958 8b8de0f8ffff    mov     ecx,dword ptr [ebp-720h]   ; lpCommandLine
...
7c819964 ffb56cf9ffff    push    dword ptr [ebp-694h]    ; parameter13
7c81996a ffb500f7ffff    push    dword ptr [ebp-900h]    ; parameter12
7c819970 8a85b7f8ffff    mov     al,byte ptr [ebp-749h]
7c819976 f6d8            neg     al
7c819978 1bc0            sbb     eax,eax
7c81997a 83e002          and     eax,2
7c81997d 50              push    eax                    ; parameter11
7c81997e ff751c          push    dword ptr [ebp+1Ch]    ; parameter10
7c819981 8b85f4f6ffff    mov     eax,dword ptr [ebp-90Ch]
7c819987 0b4520          or      eax,dword ptr [ebp+20h]
7c81998a 50              push    eax                     ; parameter9
7c81998b 8d8560f7ffff    lea     eax,[ebp-8A0h]
7c819991 50              push    eax                     ; parameter8
7c819992 ffb5b0f8ffff    push    dword ptr [ebp-750h]    ; parameter7
7c819998 51              push    ecx                     ; parameter6
7c819999 ffb550f7ffff    push    dword ptr [ebp-8B0h]    ; parameter5
7c81999f ffb5e4f8ffff    push    dword ptr [ebp-71Ch]    ; parameter4
7c8199a5 ffb5b8f7ffff    push    dword ptr [ebp-848h]    ; parameter3
7c8199ab ffb594f9ffff    push    dword ptr [ebp-66Ch]    ; parameter2
7c8199b1 ffb534f7ffff    push    dword ptr [ebp-8CCh]    ; parameter1
7c8199b7 e8e2050000      call    kernel32!BasePushProcessParameters (7c819f9e)

kernel32!BasePushProcessParameters:
...
7c819fd3 8b4d1c          mov     ecx,dword ptr [ebp+1Ch]    ;
7c819fd6 898d5cfdffff    mov     dword ptr [ebp-2A4h],ecx   ; srcCmdLine
...
7c81a045 8d85d0fdffff    lea     eax,[ebp-230h]
7c81a04b 50              push    eax
7c81a04c 8d858cfdffff    lea     eax,[ebp-274h]
7c81a052 50              push    eax
7c81a053 ffd6            call    esi
7c81a055 ffb55cfdffff    push    dword ptr [ebp-2A4h]   ; srcCmdLine
7c81a05b 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a061 50              push    eax                    ; commandLine
7c81a062 ffd6            call    esi                    ; RtlInitUnicodeString
...
7c81a0cf 50              push    eax
7c81a0d0 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0d6 50              push    eax
7c81a0d7 ffd6            call    esi
7c81a0d9 8d85b4fdffff    lea     eax,[ebp-24Ch]
7c81a0df 50              push    eax                        ; runtimeData
7c81a0e0 8d859cfdffff    lea     eax,[ebp-264h]
7c81a0e6 50              push    eax                        ; shellInfo
7c81a0e7 8d85a4fdffff    lea     eax,[ebp-25Ch]
7c81a0ed 50              push    eax                        ; desktopInfo
7c81a0ee 8d8594fdffff    lea     eax,[ebp-26Ch]
7c81a0f4 50              push    eax                        ; windowTitle
7c81a0f5 ffb570fdffff    push    dword ptr [ebp-290h]       ; environment
7c81a0fb 8d8584fdffff    lea     eax,[ebp-27Ch]
7c81a101 50              push    eax                        ; commandLine, parameter5
7c81a102 f7db            neg     ebx
7c81a104 1bdb            sbb     ebx,ebx
7c81a106 8d8578fdffff    lea     eax,[ebp-288h]
7c81a10c 23d8            and     ebx,eax
7c81a10e 53              push    ebx                        ; currentDir
7c81a10f 8d85acfdffff    lea     eax,[ebp-254h]
7c81a115 50              push    eax                        ; dllPath
7c81a116 8d858cfdffff    lea     eax,[ebp-274h]
7c81a11c 50              push    eax                        ; imagePath
; ProcessParameter
; ebp-234h: _RTL_USER_PROCESS_PARAMETERS*
7c81a11d 8d85ccfdffff    lea     eax,[ebp-234h]
7c81a123 50              push    eax                        ; pProcessParameters
7c81a124 ff158814807c    call    dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]

最後,我們還沒來得及關心 creater 是如何的把 createe 的 PEB 建立完成,其實就在 RtlCreateProcessParameters() 稍後,透過 NtWriteVirtualMemory() 完成。

7c81a1eb 8b35f413807c    mov     esi,dword ptr [kernel32!_imp__NtWriteVirtualMemory (7c8013f4)]
...
7c81a38b 6a04            push    4                          ; Protect
7c81a38d bb00100000      mov     ebx,1000h
7c81a392 53              push    ebx                        ; AllocationtType
7c81a393 8d85c8fdffff    lea     eax,[ebp-238h]
7c81a399 50              push    eax                        ; RegionSize
7c81a39a 52              push    edx                        ; ZeroBits
7c81a39b 8d85c4fdffff    lea     eax,[ebp-23Ch]
7c81a3a1 50              push    eax                        ; BaseAddress
7c81a3a2 ffb580fdffff    push    dword ptr [ebp-280h]       ; hNewProcessHandle
7c81a3a8 8b3d9011807c    mov     edi,dword ptr [kernel32!_imp__NtAllocateVirtualMemory (7c801190)]
7c81a3ae ffd7            call    edi
7c81a3b0 898574fdffff    mov     dword ptr [ebp-28Ch],eax
7c81a3b6 8b85c8fdffff    mov     eax,dword ptr [ebp-238h]
7c81a3bc 898558fdffff    mov     dword ptr [ebp-2A8h],eax
7c81a3c2 83bd74fdffff00  cmp     dword ptr [ebp-28Ch],0
7c81a3c9 0f8c04950200    jl      kernel32!BasePushProcessParameters+0x504 (7c8438d3)
7c81a3cf 8b8dccfdffff    mov     ecx,dword ptr [ebp-234h]
7c81a3d5 8901            mov     dword ptr [ecx],eax
7c81a3d7 f6452b10        test    byte ptr [ebp+2Bh],10h
7c81a3db 0f85fa940200    jne     kernel32!BasePushProcessParameters+0x51d (7c8438db)
7c81a3e1 f6452b20        test    byte ptr [ebp+2Bh],20h
7c81a3e5 0f85ff940200    jne     kernel32!BasePushProcessParameters+0x52d (7c8438ea)
7c81a3eb f6452b40        test    byte ptr [ebp+2Bh],40h
7c81a3ef 0f8504950200    jne     kernel32!BasePushProcessParameters+0x53d (7c8438f9)
7c81a3f5 6a00            push    0                          ; nBytesWritten
7c81a3f7 8b85ccfdffff    mov     eax,dword ptr [ebp-234h]
7c81a3fd ff7004          push    dword ptr [eax+4]          ; nBytesToWrite    : pProcParameters->Length
7c81a400 50              push    eax                        ; Buffer           : pProcParameters
7c81a401 ffb5c4fdffff    push    dword ptr [ebp-23Ch]       ; BaseAddress      :
7c81a407 ffb580fdffff    push    dword ptr [ebp-280h]       ; hNewProcessHandle:
                                 ; NtWriteVirtualMemory( *(ebp-280h), *(ebp-23Ch), eax, ???, 0 )
7c81a40d ffd6            call    esi                    ; _imp__NtWriteVirtualMemory

網路上可以找到人家 reverse 過的 RtlCreateProcessParameters() prototype 可以加速 trace 的速度:

NTSTATUS RtlCreateProcessParameters( 
    PRTL_USER_PROCESS_PARAMETERS *ProcessParameters,
    PUNICODE_STRING     ImagePathName,
    PUNICODE_STRING     DllPath,
    PUNICODE_STRING     CurrentDirectory,
    PUNICODE_STRING     CommandLine,
    PWSTR               Environment, // Not sured
    PUNICODE_STRING     WindowTitle,
    PUNICODE_STRING     DesktopInfo,
    PUNICODE_STRING     ShellInfo,
    PUNICODE_STRING     RuntimeData );

不過如果想要硬派的自己來也是可以的,可以試試看這篇文章的作法 : )

Summary

image

Interrupt Handling in Windows —— part 1: Using WinDbg

透過 WinDbg 的 !idt 指令,我們可以看到 IDT 的內容:

0: kd> !idt

Dumping IDT:

37:    806e6864 hal!PicSpuriousService37
3d:    806e7e2c hal!HalpApcInterrupt
41:    806e7c88 hal!HalpDispatchInterrupt
50:    806e693c hal!HalpApicRebootService
62:    81bbe6f4 atapi!IdePortInterrupt (KINTERRUPT 81bbe6b8)
63:    8191a974 USBPORT!USBPORT_InterruptService (KINTERRUPT 8191a938)
             portcls!CKsShellRequestor::`scalar deleting destructor'+0x26 (KINTERRUPT 8197cbb0)
73:    81962bec USBPORT!USBPORT_InterruptService (KINTERRUPT 81962bb0)
82:    81bd4bec atapi!IdePortInterrupt (KINTERRUPT 81bd4bb0)
83:    81be1ae4 SCSIPORT!ScsiPortInterrupt (KINTERRUPT 81be1aa8)
93:    81965204 i8042prt!I8042KeyboardInterruptService (KINTERRUPT 819651c8)
a3:    81964044 i8042prt!I8042MouseInterruptService (KINTERRUPT 81964008)
b1:    81be423c ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 81be4200)
b2:    81795bec serial!SerialCIsrSw (KINTERRUPT 81795bb0)
b4:    818765d4 NDIS!ndisMIsr (KINTERRUPT 81876598)
c1:    806e6ac0 hal!HalpBroadcastCallService
d1:    806e5e54 hal!HalpClockInterrupt
e1:    806e7048 hal!HalpIpiHandler
e3:    806e6dac hal!HalpLocalApicErrorService
fd:    806e75a8 hal!HalpProfileInterrupt
fe:    806e7748 hal!HalpPerfInterrupt

單純的 !idt 只會輸出 ntoskrnl.exe 之外的 interrupt handler ,若想看到全部 IDT 項目,則須加上 -a:

!idt -a

More

0: kd> !idt

Dumping IDT:

37:    806e6864 hal!PicSpuriousService37
3d:    806e7e2c hal!HalpApcInterrupt
41:    806e7c88 hal!HalpDispatchInterrupt
50:    806e693c hal!HalpApicRebootService
62:    81bbe6f4 atapi!IdePortInterrupt (KINTERRUPT 81bbe6b8)
63:    8191a974 USBPORT!USBPORT_InterruptService (KINTERRUPT 8191a938)
             portcls!CKsShellRequestor::`scalar deleting destructor'+0x26 (KINTERRUPT 8197cbb0)
73:    81962bec USBPORT!USBPORT_InterruptService (KINTERRUPT 81962bb0)

一般的情況下,!idt 已經給予足夠的資訊了,以上面的 IDE 介面來說:

  1. 它的 interrupt number 是 62
  2. ISR 是位於 atapi module 的 IdePortInterrupt

不過好奇心驅使,可以發現:當去查詢 atapi!IdePortInterrupt 的位置時,卻不是 0x81bbe6f4 、也不是 0x81bbe6b8

0: kd> x atapi!IdePortInterrupt
f980467e atapi!IdePortInterrupt = 

而是 0xf980467e ,加上一個 IDT entry 竟然有這麼多 addresses ,真是詭異。所以我們可以懷疑一下,!idt 這個指令的輸出是經過加工的。不過必須找到一些更基礎的資訊來支持這個懷疑。在 x86 處理器的 protected mode 中, IDT 是由 IDTR (Interrupt Descriptor Table Register)所紀錄,所以來看看 registers 的狀況如何:

0: kd> r
eax=00000001 ebx=ffdff980 ecx=80554780 edx=000003f8 esi=00000003 edi=19d73a7c
eip=8052c5ec esp=805523b0 ebp=805523c0 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202
nt!RtlpBreakWithStatusInstruction:
8052c5ec cc              int     3

什麼?沒有 IDTR ,原來要打 r idtr

0: kd> r idtr
idtr=8003f400

知道 IDT 的位置後,必須算出適當的位移量來找到 atapi!IdePortInterrupt ,參考一下 Intel 的文件:Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3A: System Programming Guide 5.10 可以知道 IDT entry —— interrupt gate 的格式:

InterruptObject06

所以,atapi!IdePortInterrupt 等於:

image

0: kd> dd 8003f400+0x8*0x62
8003f710  0008e6f4 81bb8e00 0008a974 81918e00
8003f720  00081d28 80548e00 00081d32 80548e00
8003f730  00081d3c 80548e00 00081d46 80548e00

跟 IDT Entry 第一個 address 是符合的,所以可以確定它是 interrupt handler 的 entry point:

62:    81bbe6f4 atapi!IdePortInterrupt (KINTERRUPT 81bbe6b8)

那麼該怎麼從這 0x81bbe6f4 entry point 到 atapi!IdePortInterrupt  去執行呢?線索就在 KINTERRUPT 81bbe6b8 上, Windows 透過 interrupt object 來管理系統上的 interrupts ,driver programmers 必須向 interrrupt object 註冊自己的 ISR ,那麼 interrupt handling flow 才會有機會執行到這個 ISR 。Windows Internals chapter 3 有針對 interrupt 作詳細的說明。透過 dt 指令可以看到存放在 0x81bbe6b8 的 kinterrupt 變數。

0: kd> dt nt!_kinterrupt 81bbe6b8
   +0x000 Type             : 22
   +0x002 Size             : 484
   +0x004 InterruptListEntry : _LIST_ENTRY [ 0x81bbe6bc - 0x81bbe6bc ]
   +0x00c ServiceRoutine   : 0xf980467e     unsigned char  atapi!IdePortInterrupt+0
   +0x010 ServiceContext   : 0x81bbd030 
   +0x014 SpinLock         : 0
   +0x018 TickCount        : 0xffffffff
   +0x01c ActualLock       : 0x81bbe91c  -> 0
   +0x020 DispatchAddress  : 0x80546660     void  nt!KiInterruptDispatch+0
   +0x024 Vector           : 0x162
   +0x028 Irql             : 0x5 ''
   +0x029 SynchronizeIrql  : 0x5 ''
   +0x02a FloatingSave     : 0 ''
   +0x02b Connected        : 0x1 ''
   +0x02c Number           : 0 ''
   +0x02d ShareVector      : 0 ''
   +0x030 Mode             : 1 ( Latched )
   +0x034 ServiceCount     : 0
   +0x038 DispatchCount    : 0xffffffff
   +0x03c DispatchCode     : [106] 0x56535554

注意到沒有?放在 nt!_kinterrupt+0x03c 位置的 DispatchCode 其實就是 IDT Eetry。

0: kd> .formats 0x81bbe6b8+0x03c
Evaluate expression:
  Hex:     81bbe6f4
  Decimal: -2118392076
  Octal:   20156763364
  Binary:  10000001 10111011 11100110 11110100
  Chars:   ....
  Time:    ***** Invalid
  Float:   low -6.90244e-038 high -1.#QNAN
  Double:  -1.#QNAN

既然確定了 0x56535554 就是 entry point ,那麼可以反組譯一下 DispatchCode 來確定是否會執行到 atapi!IdePortInterrupt

0: kd> u 81bbe6f4 81bbe6f4+0x106*0x4
; Setup Trap Frame
81bbe6f4 54              push    esp
81bbe6f5 55              push    ebp
81bbe6f6 53              push    ebx
81bbe6f7 56              push    esi
81bbe6f8 57              push    edi
81bbe6f9 83ec54          sub     esp,54h
81bbe6fc 8bec            mov     ebp,esp
81bbe6fe 89442444        mov     dword ptr [esp+44h],eax
81bbe702 894c2440        mov     dword ptr [esp+40h],ecx
81bbe706 8954243c        mov     dword ptr [esp+3Ch],edx
81bbe70a f744247000000200 test    dword ptr [esp+70h],20000h
81bbe712 0f852b010000    jne     81bbe843
81bbe718 66837c246c08    cmp     word ptr [esp+6Ch],8
81bbe71e 7423            je      81bbe743
81bbe720 8c642450        mov     word ptr [esp+50h],fs
81bbe724 8c5c2438        mov     word ptr [esp+38h],ds
81bbe728 8c442434        mov     word ptr [esp+34h],es
81bbe72c 8c6c2430        mov     word ptr [esp+30h],gs
81bbe730 bb30000000      mov     ebx,30h
81bbe735 b823000000      mov     eax,23h
81bbe73a 668ee3          mov     fs,bx
81bbe73d 668ed8          mov     ds,ax
81bbe740 668ec0          mov     es,ax
81bbe743 648b1d00000000  mov     ebx,dword ptr fs:[0]
81bbe74a 64c70500000000ffffffff mov dword ptr fs:[0],0FFFFFFFFh
81bbe755 895c244c        mov     dword ptr [esp+4Ch],ebx
81bbe759 81fc00000100    cmp     esp,10000h
81bbe75f 0f82b6000000    jb      81bbe81b
81bbe765 c744246400000000 mov     dword ptr [esp+64h],0
81bbe76d fc              cld
81bbe76e 8b5d60          mov     ebx,dword ptr [ebp+60h]
81bbe771 8b7d68          mov     edi,dword ptr [ebp+68h]
81bbe774 89550c          mov     dword ptr [ebp+0Ch],edx
81bbe777 c74508000ddbba  mov     dword ptr [ebp+8],0BADB0D00h
81bbe77e 895d00          mov     dword ptr [ebp],ebx
81bbe781 897d04          mov     dword ptr [ebp+4],edi
81bbe784 64f60550000000ff test    byte ptr fs:[50h],0FFh
81bbe78c 750d            jne     81bbe79b
; Pass KInterrupt object to KiInterruptDispatch
81bbe78e bfb8e6bb81      mov     edi,81BBE6B8h ; 0x81bbe6b8 是我們的 interrupt object
81bbe793 e9c87e98fe      jmp     nt!KiInterruptDispatch (80546660); 0x80546660 interrupt object 中的 DispatchAddress 一欄

如果我們再追進 nt!KiInterruptDispatch :

0: kd> u nt!KiInterruptDispatch L50
nt!KiInterruptDispatch:
80546660 64ff05c4050000  inc     dword ptr fs:[5C4h]
80546667 8bec            mov     ebp,esp
80546669 8b4724          mov     eax,dword ptr [edi+24h]
8054666c 8b4f29          mov     ecx,dword ptr [edi+29h]
8054666f 50              push    eax
80546670 83ec04          sub     esp,4
80546673 54              push    esp
80546674 50              push    eax
80546675 51              push    ecx
80546676 ff1590904d80    call    dword ptr [nt!_imp__HalBeginSystemInterrupt (804d9090)]
8054667c 0bc0            or      eax,eax
8054667e 7440            je      nt!KiInterruptDispatch+0x60 (805466c0)
80546680 83ec0c          sub     esp,0Ch
80546683 833d6c56568000  cmp     dword ptr [nt!PPerfGlobalGroupMask (8056566c)],0
8054668a c745f400000000  mov     dword ptr [ebp-0Ch],0
80546691 7541            jne     nt!KiInterruptDispatch+0x74 (805466d4)
80546693 8b771c          mov     esi,dword ptr [edi+1Ch]
80546696 f00fba2e00      lock bts dword ptr [esi],0
8054669b 722b            jb      nt!KiInterruptDispatch+0x68 (805466c8)
8054669d 8b4710          mov     eax,dword ptr [edi+10h]
805466a0 50              push    eax
805466a1 57              push    edi
; 注意到了嗎?di 是 interrupt ojbect; edi+0Ch 是欄位 ServiceRoutine 也就是 0xf980467e atapi!IdePortInterrupt+0
805466a2 ff570c          call    dword ptr [edi+0Ch] 

終於,水落石出了!最後用 interrupt object 做一個總結:

image

  1. 當 interrupt 發生時,CPU 根據 interrupt number 到 IDT 描述找到對應的 DispatchCode
  2. DispatchCode 產生 trap frame ,然後呼叫對應的 kernel interrupt handler (nt!KiInterruptDispatch),並將 interrupt object 當作參數傳入
  3. Kernel interrupt handler 轉而呼叫對應的 ISR

那 !idt 輸出的格式就是:

Interrupt Number: IDT Entry Code Kernel/User Defined ISR (Corresponding interrupt object address)

是吧!再看一次:

0: kd> !idt

Dumping IDT:

37:    806e6864 hal!PicSpuriousService37
3d:    806e7e2c hal!HalpApcInterrupt
41:    806e7c88 hal!HalpDispatchInterrupt
50:    806e693c hal!HalpApicRebootService
62:    81bbe6f4 atapi!IdePortInterrupt (KINTERRUPT 81bbe6b8)
63:    8191a974 USBPORT!USBPORT_InterruptService (KINTERRUPT 8191a938)
             portcls!CKsShellRequestor::`scalar deleting destructor'+0x26 (KINTERRUPT 8197cbb0)
73:    81962bec USBPORT!USBPORT_InterruptService (KINTERRUPT 81962bb0)
82:    81bd4bec atapi!IdePortInterrupt (KINTERRUPT 81bd4bb0)
83:    81be1ae4 SCSIPORT!ScsiPortInterrupt (KINTERRUPT 81be1aa8)
93:    81965204 i8042prt!I8042KeyboardInterruptService (KINTERRUPT 819651c8)
a3:    81964044 i8042prt!I8042MouseInterruptService (KINTERRUPT 81964008)
b1:    81be423c ACPI!ACPIInterruptServiceRoutine (KINTERRUPT 81be4200)
b2:    81795bec serial!SerialCIsrSw (KINTERRUPT 81795bb0)
b4:    818765d4 NDIS!ndisMIsr (KINTERRUPT 81876598)
c1:    806e6ac0 hal!HalpBroadcastCallService
d1:    806e5e54 hal!HalpClockInterrupt
e1:    806e7048 hal!HalpIpiHandler
e3:    806e6dac hal!HalpLocalApicErrorService
fd:    806e75a8 hal!HalpProfileInterrupt
fe:    806e7748 hal!HalpPerfInterrupt

: )

相關閱讀

  1. Windows Debugging – Kernel Debugging with WinDbg and VMWare

Reference

  1. Windows Internals chapter 3
  2. Intel® 64 and IA-32 Architectures Software Developer's Manuals: http://www.intel.com/products/processor/manuals/

Invalid Parameter Handler and Security Enhancements in the CRT

在 VC++ 的 CRT 中,除了標準的 CRT 之外,Microsoft 也加入安全性的檢查和報錯機制,許多原有的 functions 都被加上 _s 的後贅字。舉例來說:strcpy_s(), wcscpy_s(), _mbscpy_s() 各別是 strcpy(), wcscpy(), mbscpy() 的 security enhanced 版。這些 Security enhancement 的重點在於:

  1. Parameter validation
  2. Sized buffers
  3. Null termination
  4. Enhanced error reporting
  5. Filesystem security
  6. Windows security
  7. Format string syntax checking

針對上述項目的檢查與 enforcement ,security-enhanced CRT 一旦發現問題,是不會嘗試去修正或繞過的,錯了就是錯了,舉例來說:wcscpy_s() 在 copy 記憶體時,一旦發現 destination buffer 比 source buffer 小,它並不會自動進行 truncation ,參考下面這個從 MSDN 來的例子:

errno_t wcscpy_s(
   wchar_t *strDestination,
   size_t sizeInWords,
   const wchar_t *strSource 
);
strDestination sizeInBytes, sizeInWords strSource Return value Contents of strDestination
NULL any any EINVAL not modified
any any NULL EINVAL not modified
any 0, or too small any ERANGE not modified

從表中可以看到,一旦 destination 和 source 有不正確的配對,那麼就不會對 destination 進行動,自然也就沒有把字串 truncate 後複製過去的結果發生。因此如果 truncation 是期望的結局,那就必須改用 strncpy_s(), _strncpy_s_l(), wcsncpy_s(), _wcsncpy_s_l(), _mbsncpy_s(), _mbsncpy_s_l() 這類原本就有 truncate 語意的函式。所以了,引用一下 MSDN 總結:

... the secure functions do not prevent or correct security errors; rather, they catch errors when they occur. They perform additional checks for error conditions, and in the case of an error, they invoke an error handler (see Parameter Validation).

沒錯,security enhancement 強調的是參數使用前的檢查以及可錯誤發生後的處理,不是幫忙修正錯誤。

Parameter Validation

Parameter validation 機制會檢查傳入 _s functions 的參數,檢查包括:

  1. Checking pointers for NULL
  2. Checking that integers fall into a valid range
  3. Checking that enumeration values are valid
  4. Validate that the buffer is large enough before writing to it

當 CRT 發現 invalid parameter 時,它會呼叫 invalid parameter handler 來處理。 CRT 有預設的 invalid parameter handler ,它會呼叫 Dr. Watson 產生 dump ,然後跳出一個 Error report 程式詢問使用者要不要上傳這個錯誤報告。而程式若是以 debug mode 編譯,則是會跳出 assertion。

drwtsn32

Fig 1. Dr. Watson (不是這個華生?也不是裘德洛?)

WER

Fig 2. Windows Error Reporting 對話方塊

AssertionFig 3. Debug Mode 下跳出的 assertion failed 視窗

Where have all the Exceptions gone?

const wchar_t*  src = L"Hello";
wchar_t         dest[ 5 ];

__try {
    wcscpy_s( dest, _countof( dest ), src );
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
    // Error handle...
}

在某些程式裡頭會看到像上面那樣的片段程式碼,看起來似乎四平八穩:我們指定了 dest 和其大小,若是 src 長度超過了 dest , wcscpy_s() 透過 exception 報錯,我們就可以透過 SEH 來處理。但程式一執行,ㄜ,好像 crash 了… 不過現在我們已經可以可能的問題是什麼:若是程式沒有置換過 invalid parameter handler ,那麼是丟不出 exception 的,程式的 crash 便是可預期的結果。

Install my own Invalid Parameter Handler

/*!
 * \param[in] expression argument expression
 * \param[in] function   the name of the CRT function that received the invalid argument
 * \param[in] file       file name in the CRT source
 * \param[in] line       line in file
 * \param[in] pReserved  reserved
 */
void _invalid_parameter(
    const wchar_t* expression,
    const wchar_t* function, 
    const wchar_t* file, 
    unsigned int   line,
    uintptr_t      pReserved
);

上述是 invalid parameter handler 的 prototype。我們很容易仿造一個自己的,延續之前 wcscpy_s() 的例子,我們就可以實作一個會丟出 exception 的 handler:

void myInvalidParameterHandler( const wchar_t* expression,
                                const wchar_t* function, 
                                const wchar_t* file, 
                                unsigned int line,
                                uintptr_t reserved )
{
    wcout << L"expr:" << expression << endl
          << L"func: " << function << endl
          << L"file:" << file << endl
          << L"line:" << line << endl;

    RaiseException( 0,          // exception code
                    0,          // continuable exception
                    0, NULL );  // no arguments
}

然後透過 _set_invalid_parameter_handler() 來註冊:

wchar_t   dest[ 5 ];
const wchar_t*  src = L"Hello";

_invalid_parameter_handler oldHandler = _set_invalid_parameter_handler( myInvalidParameterHandler );

_CrtSetReportMode( _CRT_ASSERT, 0 );

__try {
    wcscpy_s( dest, _countof( dest ), src );
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
    cerr << "exception ... " << GetExceptionCode() << endl;
}

_CrtSetReportMode() 在這裡可以幫助 debug mode 下取消 assertion failed 視窗的跳出。如果對於 wcscpy_s() 的 errno_t 的 error code 念念不忘,則可以修改 SEH 的 filter expression 從 EXCEPTION_EXECUTE_HANDLE 到 EXCEPTION_CONTINUE_EXECUTION。

__try {
    errno_t e = wcscpy_s( dest, _countof( dest ), src );

    if ( EINVAL == e ) {
        // ...
    }
    else if ( ERANGE == e ){
        // ...
    }
}
__except ( EXCEPTION_CONTINUE_EXECUTION ) {
}

PS: EINVAL, ERANGE 這類 pre-defined error code 定義於 ERRNO.H 裡頭。

Reference

  1. MSDN on Security Enhancements in the CRT: http://msdn.microsoft.com/en-us/library/8ef0s5kh.aspx
  2. MSDN on Parameter Validation: http://msdn.microsoft.com/en-us/library/ksazx244.aspx
  3. MSDN on _set_invalid_parameter_handler: http://msdn.microsoft.com/en-us/library/a9yf33zb.aspx
  4. MSDN on errno, _doserrno, _sys_errlist, and _sys_nerr: http://msdn.microsoft.com/en-us/library/t3ayayh1.aspx

MiniFilter InstanceSetupCallback is not called?

一般來說,MiniFilter 的 InstanceSetupCallback 會在 filter manager 把 minifilter attache 到 volume 後呼叫。如果沒有的話,可以檢查一下 minifilter 的 INF 是否把 instance fla...