Saturday, April 30, 2011

WinDbg: CreateProcessAsUser 回傳 ACCESS_DENIED 之案例分析

最近手邊處理了一個案例。因為一些特殊需求,想要試試看在 Administrator 帳號下,呼叫 CreateProcessAsUser 建立在別的 Logon Session 的進程。這並不是一個典型的應用。你可以照著 MSDN 上的說明處理,幫 Administrator 加上需要的權限,看起來可以在 Console Session 上正常運作。

事情沒那麼簡單,在 Windows Server 2003 下面,CreateProcessAsUser 沒辦法在 Terminal Service 的 Logon Session 下面建立進程,永遠會得到一個 ACCESS_DENIED (5) 的錯誤碼。不過,MSDN 並沒有描述這個錯誤碼的原因。

看來,要有把手弄髒的心理準備。

首先,嘗試用 WinDbg 逐步執行看看是哪裡丟出了這個錯誤碼。結果發現,CreateProcessAsUser 裡面最終會用 CreateFile 去開一個 Terminal Server 的 Named Pipe,這個地方是讓 CreateProcessAsUser 呼叫失敗的點。

0:000> kvn
 # ChildEBP RetAddr  Args to Child              
00 002cce10 00525227 002cee90 c0000000 00000000 kernel32!CreateFileW (FPO: [Non-Fpo])
01 002ceef4 00515fd4 00000001 00000000 000003bc ADVAPI32!CreateRemoteSessionProcessW+0xbd (FPO: [Non-Fpo])
02 002cef44 0040433c 000003bc 00000000 002cf1a8 ADVAPI32!CreateProcessAsUserW+0xb0 (FPO: [Non-Fpo])

0:000> du 002cee90 
002cee90  "\\.\Pipe\TerminalServer\ozUBFyty"
002ceed0  "9LQoprqROq\1"

看來問題似乎是 Administrator 並沒有權限去讀寫這個 Named Pipe,於是乎,接下來的方向轉為確認這個 Name Piped 的 Security Context。

C:\>PsExec.exe -s c:\accesschk.exe \Pipe\TerminalServer\ozUBFyty9LQoprqROq\1

\\.\Pipe\TerminalServer\ozUBFyty9LQoprqROq\1
  RW NT AUTHORITY\SYSTEM
  RW NT AUTHORITY\LOCAL SERVICE
  RW NT AUTHORITY\NETWORK SERVICE
c:\accesschk.exe exited on 6BD2PFN1Z with error code 0.

結果發現 Administrator 果然不行開 Named Pipe。只有System、Local Service、和 Network Service 這幾個帳戶才有能力讀寫這個 Named Pipe。

在 Windows Server 2003 有 Terminal Service 的 System 帳戶下,呼叫 CreateProcessAsUser 在別的 Remote Logon Session 建立新進程,會發現新進程的 Parent Process 並不是我們自己的 Caller Process,而是 Winlogon.exe 這個系統進程。

這裡可以做個猜測,這個 Named Pipe 的作用就是讓 Caller Process 可以送出建立新進程的指令給 Winlogon.exe。打開 Process Explorer,確認一下果然發現 Named Pipe 是由 Winlogon.exe 所建立。


追到這邊,大概心裡有數了。在 Windows 2003 Server 上面,CreateProcessAsUser 是沒辦法跑在非系統內建的幾個重要帳戶裡面。Winlogon.exe 已經限制了 Named Pipe 的讀寫權限,若是沒有符合的帳戶,就是沒有辦法執行 CreateProcessAsUSer。

結論:好奇心除了可以殺死一隻貓,也可以殺掉你寶貴的時間。:P

[Updated]

  • CodeProject 裡有個小工具 RunAsEx,你可以用它來調整各種組合,實驗看看各種不同情境下的 CreateProcessAsUser,可以很方便的達到 PoC 的效果。

Saturday, April 23, 2011

WinDbg:手把手教你看C++ Exception

不管是用WinDbg打開一個Dump File或是Live Debug,如果遇到的是C++ Exception的話,WinDbg就會丟出C++ EH exception的訊息。

(abc.2dc): C++ EH exception - code e06d7363 (first/second chance not available)
eax=073bee80 ebx=03bb12f0 ecx=00000000 edx=00000000 esi=073bef08 edi=0000005c
eip=7c812afb esp=073bee7c ebp=073beed0 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
kernel32!RaiseException+0x53:
7c812afb 5e              pop     esi
0:055> kn
 # ChildEBP RetAddr  
00 073beed0 78158e89 kernel32!RaiseException+0x53
01 073bef08 647050a6 msvcr80!_CxxThrowException+0x46 [f:\dd\vctools\crt_bld\self_x86\crt\prebuild\eh\throw.cpp @ 161]

如果丟出來的exception不是標準函式庫裡面的exception object的話,用!analyze -v也沒辦法幫你對到exception的資訊。這時候,你可以試著按照以下的步驟操作,找出exception class。

0:055> .exr -1
ExceptionAddress: 7c812afb (kernel32!RaiseException+0x00000053)
   ExceptionCode: e06d7363 (C++ EH exception)
  ExceptionFlags: 00000001
NumberParameters: 3
   Parameter[0]: 19930520
   Parameter[1]: 073bef28
   Parameter[2]: 647261ac

首先,先用".exr -1"印出目前EXCEPTION_RECORD的資訊。根據_CxxThrowException丟出的例外結構,Parameter[0]會是一個0x19930520的Magic Number,表示這是一個C++ exception。而Parameter[1]會是exception object address。至於Parameter[2],存放了_s__ThrowInfo結構的address,裡面會有C++ exception class的資訊,我們需要先看看exception object到底是什麼class type。

0:055> dt msvcr80!_s__ThrowInfo 647261ac
   +0x000 attributes       : 0
   +0x004 pmfnUnwind       : 0x64705cf0     void  testDLL!TestDbException::~TestDbException+0
   +0x008 pForwardCompat   : (null) 
   +0x00c pCatchableTypeArray : 0x647261a0 _s__CatchableTypeArray

知道class type後,就可以傾印出exception object的資訊,看看到底是什麼原因丟出C++ exception。

0:055> dt testDLL!TestDbException 073bef28  
   +0x000 __VFN_table : 0x64722524 
   +0x004 m_nType         : 1
   +0x008 m_iErrorCode     : 0n-536570191

參考資料:

Thursday, April 14, 2011

Googlemock: Mock Object 應用於 C++ RAII 實例

在 Unit Test 中,Mocking 的技巧是用假的元件去取代受測元件所依賴的外部元件,稱為 Mock Object。測試者藉由控制 Mock Object 去可以改變受測元件的內部流程、或是驗證受測元件與外部元件的互動行為。

在 C++ 當中,常常慣用 RAII 的技巧,讓 Object 在進入 Function/Block 的範圍內獲取資源,離開範圍內釋放資源。舉例來說:
void example()
{
  File file("output.log");
  file.write("test");
}
在這種情況下,通常都會在測試程式中另外實作一組假的 File。不過,因為 Object 生成的時機是在進入受測的函式中,沒有機會用 ON_CALL 去控制 Mock Object 的行為。

所幸,你可以用下面的方式將 File 的實作,交給另外一個擁有相同介面的類別。這麼一來,我們還是可以控制這個新的類別。
// MockFile 是真正使用 Googlemock 的類別
class MockFile
{
public:
  MOCK_METHOD0(write, void(
    char *szLine));
};

MockFile *g_pMockFile = NULL;

// 將 File::write 導到 MockFile 上
File::write(char *szLine) 
{ 
  g_pMockFile->write(szLine); 
};

TEST_F(TestSuite, test_file_write)
{
  MockFile mockFile;
  EXPECT_CALL(mockFile, write(_));

  // 設定給 File::write() 使用這個 Mock Object
  g_pMockFile = &mockFile;

  example();
}
如此一來,就可以使用 Googlemock 幫我們取代掉 RAII 技巧所使用的類別。

Monday, April 11, 2011

NacoOS期末作業

[本來想要把另一個已經廢棄已久的 Blog 關掉,發現有兩篇文章被大量閱讀。看起來應該是對某些人還是有些幫助,我決定將他們轉錄到這邊來。感謝讀者的支持。]

期末專題(NachOS)有兩個作業-
  1. Implement system call "Sleep"
  2. Implement SJF scheduling
紀錄一些過程,免得做報告時忘記了...
  1. Implement system call "Sleep"
    Implement Alarm.waitUtil(...) by InterruptHandler, getTime(). Then implement syscall sleep(...).
    Sleep(...)一定會不準,因為waitUtil(...)是以Real-Time為準,可是卻是以InterruptHandler實做(InterruptHandler天生就不準)。
  2. Implement SJF scheduling
    只 要動到nachos.threads這個package中的class就可以。以proj1的ThreadKernel為主,要產生Thread就去 KThread.selfTest()中修改。然後在nachos.threads底下增加一個SJFScheduler,模仿RoundRobin,只 需要修改RR中的FifoQueue.waitForAccess(...)。因為SJF需要Thread的單一執行時間,所以可以學習PingTest 用Loop模擬。最好是新增一個類似PingTest的類別,然後在裡面加入worktime之類的項目。
根據Berkeley的作業說明,只需要動到nachos.threads和nachos.userprog就可以,其他的package就假設都是正確的就好了。

NachOS安裝心得

[本來想要把另一個已經廢棄已久的 Blog 關掉,發現有兩篇文章被大量閱讀。看起來應該是對某些人還是有些幫助,我決定將他們轉錄到這邊來。感謝讀者的支持。]

Date: 2005/06/15

作業系統期末作業是以NachOS這個教學用的平台,體驗作業系統的運作。不過,安裝的過程卻一點也不順遂,所以我把一些安裝心得整理在這邊;方邊我自己參考,也幫助其他同樣遭遇到安裝問題的人。

目前我有找到四個NachOS版本,一個是最原始的,用C++實做的版本;另一個也是用C++實做的版本;最後兩個是跟隨其後,以Java來實做的版本。四種版本的架構都很類似,而我就是在安裝C++版本遇到挫折之後,轉而決定要來累積一些心得的。
NachOS 4.0 (C++ version) @UC Berkeley
NachOS 4.1 @Universität Karlsruhe
NachOS 5.0j @UC Berkeley
NachOS (Java version) @Rice University

安裝前的環境需求:
  1. cygwin-在MS Windows的環境下也可以跑Linux-Like Shell的模擬器。
  2. MIPS cross-compiler-用來把UserProgram編譯成在MIPS下跑的執行檔。NachOS是吃MIPS的coff格式的檔案。
  3. 把jdk/bin的目錄加入環境變數PATH。
NachOS 5.0j 安裝步驟:
  1. 把nachos-java-2005spring.tar.gz放在/opt底下。
    用tar zxvf nachos-java-2005spring.tar.gz把他解開。
  2. 把/nachos/Makefile用notepad打開來編輯。
    javac -source 1.3 -nowarn -classpath . -d . -sourcepath ../.. -g $< (加上這粗體字的參數) test: cd ../test ; make(從gmake改成make)
  3. 編輯/nachos/test/Makefile...
    GCCDIR = /xxx/
    在xxx處放上cross-compiler的所在目錄。
    CC=$(GCCDIR)mips-gcc
    其他的類推...
  4. cd /proj1
    make
    make test
    ../bin/nachos
  5. 接下來應該會看到一連串正常執行結果。
我遇到的問題:
  1. 為什麼跑proj3會出現unknown system call?
    因為NachOS只有實做一個的system call,其餘的system call要自己實做出來。nachos.userprog.UserProcess裡有handleSyscall(...)這個函式,要自己處理各式各樣的system call。
  2. 每次執行都要打../bin/nachos嗎?
    可以去編輯/etc/profile。去下找export PATH=...這一行。
    export PATH="/usr/local/bin:/usr/bin:/bin:/opt/nachos/bin:$PATH"

    增加/nachos/bin的目錄進去,以後在任何目錄都可以執行nachos這個執行檔了。
  3. MIPS指令看不懂怎麼辦?
    MIPS Instruction Set Reference
  4. 如何增加system call?
    首先找出/nachos/test/syscall.h,增加需要的syscall的宣告。
    /**
    * System call codes, passed in $r0 to tell the kernel which system call to do.
    */
    #define syscallHalt 0
    #define syscallExit 1
    #define syscallExec 2
    ...

    這邊加入syscall的id到時候會傳給NachOS的UserProcess處理。
    當然底下也要增加函式原型的宣告。
    /* Fork a thread to run a procedure ("func") in the *same* address space
    * as the current thread.
    */
    void threadFork(void (*func)());

    然後去編輯/nachos/test/start.s
    SYSCALLSTUB(threadFork, syscallThreadFork)

    在下面會找到類似上行的程式碼,就學著他的形式把剛才新增的syscall做一個對應。
    然後你就可以在Test Program中呼叫新的syscall了。
    (不過還是要在UserProcess中實做...)
  5. C++版本的真的不能用嗎?
    資工的工作站和cygwin都試過了,都會有Compile Error,還有清大OS的課程網頁也有說明。所以應該是說,可以用,但是要debug。
  6. 要怎麼看到MIPS的Assembly Code?
    gcc加上-S參數就可以。
  7. 用哪個Timer比較準?
    InterruptHandler大約每Stats.TimerTicks會invoke一次,不過不是很準,最好還是用getTime()來獲得系統時間。

Sunday, April 10, 2011

軟體測試:Unit Test 隨便談,從動機到工作流程改善

在軟體測試的領域裡,最能夠吸引開發者的測試系統大概就是 Unit Test。這難以假手於他人的 Best Practice,莫不提起開發者對於 Unit Test 既予高度的期待。大部分的討論主題都是在教你如何寫出容易維護的 Unit Test,或是提高 Testability 的程式設計技巧。但我認為更重要的是要討論 Unit Test 的動機,進而了解它對於專案開發效益,到底可以帶給開發團隊什麼樣的好處。

上圖說明了我們對於軟體開發過程的假設。在任何階段產生的臭蟲,如果不能及時被抓出來,維修的成本提昇的速度會隨著它潛伏的時間快速成長。舉個例子,Toyota 的煞車延遲事件,如果提前在研發或工廠中發現問題,純粹就只是一個工程的問題,而不會是對簿公堂,變成競爭者攻擊的話題,它所損失的絕對超過產品本身召回的金額。

那麼,如果可以越早的找到臭蟲,我們就可以降低已知問題的維修成本,進而降低產品推出後工程問題的風險。因此,對於自己的產品,我們佈下了天羅地網的測試系統,就是為了能夠及時的將臭蟲給抓出來,也確保問題不會重複發生(Safety Net)。

Coding 的階段來看,最早能夠貢獻的測試系統之一,就是 Unit Test。身為一個測試系統,其實目的和其他層次的測試系統是一樣的。不過因為它和 Source Code 能夠較為緊密結合,底下的效益是我認為可以彌補其他層次所不足:
  • Fast Feedback Loop:如果你在修改一段安裝檔的程式碼,好死不死每次安裝都要一個小時的時間。你會選擇每次修改之後就直接來一次整合測試,還是會先把修改部分的邏輯先獨立出來先測試完畢,然後再來整合測試?通過 Unit Test 先把無關的外部模組排除,模擬直接相關的外部模組,加速目前專注範圍的開發和除錯。
  • Early Defect Detection:小範圍獨立運作的程式碼,會比獨立運作的整個系統來的容易控制。如此一來,小範圍的待測物會有更多的機會可以模擬出整合測試不容易做出的測試條件。舉例來說,不同的 File System 列舉目錄下的檔案的規則可能都不相同,要測試如此的條件必須要有不同的真實環境,測試準備工作也需要比較多的時間去建置。通過 Unit Test,模擬難以由整合測試做出的測試案例,提前評估待測物的行為。
  • Safety Net for Developer:人是容易犯錯的動物,一個模組隨著時間的演進,可能會有許多的極端的測試案例被發現。若是案例可以盡可能的加入現有的 Unit Test,最終 Unit Test 可以變成一個檢核表,提前評估是否程式碼的修改影響任何測試案例。
對於 Defect Detection 和 Safety Net,注意我這裡講的是提前評估。最終來說它還是無法成為終極的測試銀彈,它能夠提供的還是有其限制。Unit Test 互動的都是我們設計好的測試鷹架(Test Scaffolding),無法代表真實系統整合後的運作情況;使用者介面也是一個 Unit Test 無法收服的領域。

我觀察到一些初入行的程式設計師,可能同時在多個模組上修改程式碼後,一口氣用手動的方式整合測試來觀察運作結果。通常的狀況就會變成:如果有問題,要花時間確認是哪個步驟出錯,修正程式碼,然後再重來一遍整合測試。或是看似沒有問題,是其實某個模組已經出錯,只是沒有明顯到出現在整合測試上面,這種臭蟲未來會更難以處理。

我認為,Unit Test 的策略非常適用於改善開發流程。先對所修改的個別地方做獨立測試,然後才是一輪整合測試。這樣的工作流程改善的是「尋找有問題的模組的時間」,並減少不必要的「整合測試運行的次數」。數據顯示,除錯常超過程式設計師工作時數 50% 以上,這樣的改善相信能夠影響工程師的生產力。

實務上,Unit Test 要怎麼做,要做到什麼樣的程度,其實不必太死忠於流程或是規範,矯枉過正,最後還是團隊受傷害。時常回頭看看動機,讓團隊決定怎麼做、怎麼評估,交由團隊做出承諾。

Friday, April 01, 2011

Win32程式設計: _beginthreadex or CreateThread?

先說結論,永遠先用_beginthreadex()來建立新的執行續,把它當作是你的 Code Convention。

為什麼?先從動機說起。

在 C Run-Time (CRT) 函式庫裡有許多函式在多執行續的環境下,會利用 Thread Local Storage (TLS) 去存取每個獨立執行續的資源,藉此達到 Thread-Safe。

舉例來說,errno 是檢查錯誤碼的變數,在多執行續的環境下,如果A()和B()兩個函式從不同的執行續改寫了 errno,那麼接下來的程式碼就無法確認 errno 來決定上面一個函式究竟是成功還是失敗。

為了解決這種問題,在多執行續的 CRT 下,errno 會被替換成 #define errno (*_errno())。_errno() 裡面做的事情就是去取出 TLS 裡面存放的 per-thread errno。這樣每一個執行續都有自己的 errno,不用擔心內容不正確的問題。

像是 errno 這樣的應用還有很多,它們都是存放在 _tiddata 這個結構中。_tiddata 又是放在 TLS 裡面。

再回來看_beginthreadex(),它比 CreateThread() 是多做了一些事情。最主要就是多初始一份 _tiddata 並且放在新的執行續的 TLS 中,讓 CRT 的函式可以使用這些 _tiddata 來達成 Thread-safe。

你可能會問,那...我的產品都已經釋出了,而且都是用CreateThread(),怎麼辦?先別擔心,先讓我們來看沒有正確使用 _beginthreadex() 會造成什麼影響。

  • _tiddata 沒有初始化?
    如果沒有初始化,在 CRT 的函式中只要是第一個使用到 _tiddata 的地方,還是會負責建立一份的。舉例來說,_errno() 裡面檢查到沒有 _tiddata,它會自己建立一份,再放回 TLS 中。所以你的 CRT 函式還是會正確運作的。
  • 記憶體洩漏?
    正常來說,_tiddata 只會在 _endthreadex() 呼叫之後才會被刪除掉。_beginthreadex() 產生出來的執行續在結束後會自動呼叫到 _endthreadex()。所以理論上 CreateThread() 產生的執行續就會有記憶體洩漏的問題。但是這點,Microsoft的工程師也幫你想到了,不管你是 Static-link 或是 Dynamic-link 多執行續的 CRT,CRT 會在收到 DLL_THREAD_DETACH 的時候再幫你檢查 _tiddata 然後把它刪除掉。

    不過這邊 Static-link 可能會有個問題。舉例來說,A.exe 裡面用了 errno 然後當下 malloc 了一份 _tiddata,然後 call-stack 進入到 B.dll,之後 B.dll 返回離開 call-stack 就會收到 DLL_THREAD_DETAH,B.dll 就會刪除這個 _tiddata,可是 A.exe 和 B.dll 是分別使用獨立的 Heap,這樣一刪除下去,恐怕會有 Heap Corruption 的機會。(純粹推理,還沒驗證過)
  • 還有其他的問題嗎?
    _beginthreadex() 還會呼叫 _fpclear() 和支援.NET AppDomain,前者和浮點數相關函數初始化有關,後者會將新的執行續在 Caller 相同的 AppDomain 上執行。
    (反組譯了一下我電腦上的 msvcr80.dll,_fpclear()其實什麼都沒做就返回了。)

如果是用 Multithread Dynamic-Link C Run-Time 看起來就沒什麼問題囉。(謎之聲:那個在 Production Code 用了 CreateThread 的人是誰呢? XD)


參考資料: