.NET程式的 GDI控制程式碼洩露 的再反思

2023-07-25 12:00:44

一:背景

1. 講故事

上個月我寫過一篇 如何洞察 C# 程式的 GDI 控制程式碼洩露 文章,當時用的是 GDIView + WinDbg 把問題搞定,前者用來定位洩露資源,後者用來定位洩露程式碼,後面有朋友反饋兩個問題:

  • GDIView 統計不準怎麼辦?
  • 我只有 Dump 可以統計嗎?

其實那篇文章也聊過,在 x64 或者 wow64 的程式裡,在使用者態記憶體段中有一個 GDI Shared Handle Table 控制程式碼表,這個表中就統計了各自控制程式碼型別的數量,如果能統計出來也就回答了上面的問題,對吧。

32bit 程式的 GDI Shared Handle Table 段是沒有的,即 _PEB.GdiSharedHandleTable = NULL


0:002> dt ntdll!_PEB GdiSharedHandleTable 01051000
  +0x0f8 GdiSharedHandleTable : (null)

有了這些前置基礎,接下來就可以開挖了。

二:挖 GdiSharedHandleTable

1. 測試程式碼

為了方便測試,我來造一個 DC控制程式碼 的洩露。


    internal class Program
    {

        [DllImport("Example_20_1_5", CallingConvention = CallingConvention.Cdecl)]
        extern static void GDILeak();

        static void Main(string[] args)
        {
            try
            {
                GDILeak();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            Console.ReadLine();
        }
    }

然後就是 GDILeak 的 C++ 實現程式碼。


extern "C"
{
	_declspec(dllexport) void GDILeak();
}

void GDILeak()
{
    while (true)
    {
        CreateDCW(L"DISPLAY", nullptr, nullptr, nullptr);

        auto const gdiObjectsCount = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS);
        std::cout << "GDI objects: " << gdiObjectsCount << std::endl;

        Sleep(10);
    }
}

程式跑起來後,如果你是x64的程式那沒有關係,但如果你是 32bit 的程式一定要生成一個 Wow64 格式的 Dump,千萬不要抓它的 32bit dump,否則拿不到 GdiSharedHandleTable 欄位也就無法後續分析了,那如何生成 Wow64 格式的呢?我推薦兩種方式。

  • 使用64bit工作管理員(系統預設)生成

  • 使用 procdump -64 -ma QQ.exe 中的 -64 引數

這裡我們採用第一種方式,截圖如下:

2. 分析 GdiSharedHandleTable

使用偽暫存器變數提取出 GdiSharedHandleTable 欄位,輸出如下:


0:000> dt ntdll!_PEB GdiSharedHandleTable @$peb
   +0x0f8 GdiSharedHandleTable : 0x00000000`03560000 Void

接下來使用 !address 找到這個 GdiSharedHandleTable 的首末地址。


0:000> !address 0x00000000`03560000

Usage:                  Other
Base Address:           00000000`03560000
End Address:            00000000`036e1000
Region Size:            00000000`00181000 (   1.504 MB)
State:                  00001000          MEM_COMMIT
Protect:                00000002          PAGE_READONLY
Type:                   00040000          MEM_MAPPED
Allocation Base:        00000000`03560000
Allocation Protect:     00000002          PAGE_READONLY
Additional info:        GDI Shared Handle Table


Content source: 1 (target), length: 181000

上一篇我們聊過每新增一個GDI控制程式碼都會在這個表中增加一條 GDICell,輸出如下:


typedef struct {
	PVOID64 pKernelAddress;
	USHORT wProcessId;
	USHORT wCount;
	USHORT wUpper;
	USHORT wType;
	PVOID64 pUserAddress;
} GDICell;

這個 GDICell 有兩個資訊比較重要。

  • wProcessId 表示程序 ID
  • wType 表示控制程式碼型別。

理想情況下是對 控制程式碼型別 進行分組統計就能知道是哪裡的洩露,接下來的問題是如何找呢?可以仔細觀察結構體, wProcessId 和 wType 的偏移是 3USHORT=6byte,我們在記憶體中找相對偏移不就可以了嗎?接下來在記憶體中搜尋這塊


0:000> ~.
.  0  Id: 101c.4310 Suspend: 0 Teb: 00000000`009bf000 Unfrozen
      Start: Example_20_1_4_exe!wmainCRTStartup (00000000`00d4ffe0)
      Priority: 0  Priority class: 32  Affinity: fff

0:000> s-w 03560000 036e1000 101c
00000000`03562060  101c 0000 af01 0401 0b00 0830 0000 0000  ..........0.....
00000000`035782a0  101c ff1d ffff ffff 0000 0000 1d0f 010f  ................
00000000`0357c688  101c 0000 3401 0401 0160 0847 0000 0000  .....4..`.G.....
...
00000000`035a5f98  101c 0000 0801 0401 0dc0 08a1 0000 0000  ................
00000000`035a5fb0  101c 0000 0801 0401 0c60 08a1 0000 0000  ........`.......
00000000`035a5fc8  101c 0000 0801 0401 0840 08a1 0000 0000  ........@.......
00000000`035a5fe0  101c 0000 0801 0401 0b00 08a1 0000 0000  ................

從卦中可以看到,當前有1029個 GDICell 結構體,接下來怎麼鑑別每一條記錄上都是什麼型別呢?其實這裡是有列舉的。

  1. DC = 0x01
  2. Region = 0x04
  3. Bitmap = 0x05
  4. Palette =0x08
  5. Font =0x0a
  6. Brush = 0x10
  7. Pen = 0x30

即 GDIView 中的 紅色一列

到這裡我們可以通過肉眼觀察 + F5 檢索,可以清晰的看到1029 個控制程式碼物件,其中 1028 個是 DC 物件,其實這就是我們洩露的,截圖如下:

3. 指令碼處理

如果大家通讀會發現這些都是固定步驟,完全可以寫成比如 C++ 和 Javascript 的格式指令碼,在 StackOverflow 上還真有這樣的指令碼。


$$ Run as: $$>a<DumpGdi.txt
$$ Written by Alois Kraus 2016
$$ uses pseudo registers r0-5 and r8-r14

r @$t1=0
r @$t8=0
r @$t9=0
r @$t10=0
r @$t11=0
r @$t12=0
r @$t13=0
r @$t14=0
$$ Increment count is 1 byte until we find a matching field with the current pid
r @$t4=1

r @$t0=$peb
$$ Get address of GDI handle table into t5
.foreach /pS 3 /ps 1 ( @$GdiSharedHandleTable { dt ntdll!_PEB GdiSharedHandleTable @$t0 } ) { r @$t5 = @$GdiSharedHandleTable }

$$ On first call !address produces more output. Do a warmup
.foreach /pS 50 ( @$myStartAddress {!address  @$t5} ) {  }


$$ Get start address of file mapping into t2
.foreach /pS 4 /ps 40 ( @$myStartAddress {!address  @$t5} ) { r @$t2 = @$myStartAddress }
$$ Get end address of file mapping into t3
.foreach /pS 7 /ps 40 ( @$myEndAddress {!address  @$t5} ) { r @$t3 = @$myEndAddress }
.printf "GDI Handle Table %p %p", @$t2, @$t3

.for(; @$t2 < @$t3; r @$t2 = @$t2 + @$t4) 
{
  $$ since we walk bytewise through potentially invalid memory we need first to check if it points to valid memory
  .if($vvalid(@$t2,4) == 1 ) 
  {
     $$ Check if pid matches
     .if (wo(@$t2) == @$tpid ) 
     { 
        $$ increase handle count stored in $t1 and increase step size by 0x18 because we know the cell structure GDICell has a size of 0x18 bytes.
        r @$t1 = @$t1+1
        r @$t4 = 0x18
        $$ Access wType of GDICELL and increment per GDI handle type
        .if (by(@$t2+6) == 0x1 )  { r @$t8 =  @$t8+1  }
        .if (by(@$t2+6) == 0x4 )  { r @$t9 =  @$t9+1  }
        .if (by(@$t2+6) == 0x5 )  { r @$t10 = @$t10+1 }
        .if (by(@$t2+6) == 0x8 )  { r @$t11 = @$t11+1 }
        .if (by(@$t2+6) == 0xa )  { r @$t12 = @$t12+1 }
        .if (by(@$t2+6) == 0x10 ) { r @$t13 = @$t13+1 }
        .if (by(@$t2+6) == 0x30 ) { r @$t14 = @$t14+1 }
     } 
  } 
}

.printf "\nGDI Handle Count      %d", @$t1
.printf "\n\tDeviceContexts: %d", @$t8
.printf "\n\tRegions:        %d", @$t9
.printf "\n\tBitmaps:        %d", @$t10
.printf "\n\tPalettes:       %d", @$t11
.printf "\n\tFonts:          %d", @$t12
.printf "\n\tBrushes:        %d", @$t13
.printf "\n\tPens:           %d", @$t14
.printf "\n\tUncategorized:  %d\n", @$t1-(@$t14+@$t13+@$t12+@$t11+@$t10+@$t9+@$t8)

最後我們用指令碼跑一下,哈哈,是不是非常清楚。


0:000> $$>a< "D:\testdump\DumpGdi.txt"
GDI Handle Table 0000000003560000 00000000036e1000
GDI Handle Count      1028
	DeviceContexts: 1028
	Regions:        0
	Bitmaps:        0
	Palettes:       0
	Fonts:          0
	Brushes:        0
	Pens:           0
	Uncategorized:  0

三:總結

如果大家想從 DUMP 檔案中提取 GDI 控制程式碼洩露型別,這是一篇很好的參考資料,相信能從另一個角度給你提供一些靈感。

圖片名稱