百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

.NET程序的 GDI句柄泄露 的再反思

liebian365 2024-10-20 10:01 32 浏览 0 评论

一:背景

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 句柄泄露类型,这是一篇很好的参考资料,相信能从另一个角度给你提供一些灵感。

相关推荐

C++零基础入门学习指南(中篇)

目标:像拼装乐高一样理解程序模块,掌握内存管理核心技能...

“5 分钟 CMake 使用指南,解决我的 C++ 打包问题!”

...

Linux下跨语言调用C++实践

不同的开发语言适合不同的领域,例如Python适合做数据分析,C++适合做系统的底层开发,假如它们需要用到相同功能的基础组件,组件使用多种语言分别开发的话,不仅增加了开发和维护成本,而且不能确保多种语...

输入格式控制:C++程序中的数据接收与处理技巧

在C++编程中,输入输出是非常基本且重要的操作。尤其是输入部分,程序员通常需要从用户那里获取数据,并根据不同的输入格式进行处理。然而,用户的输入往往是多样化的,如何有效地控制输入格式,确保程序正确接收...

常见读写excel文件的库/类

在C++语言中读写EXCEL表格,有这几种方法:COM方式、ODBC方式、OLE方式、纯底层格式分析方式。Basicexcel使用方法:https://www.cnblogs.com/paullam/...

C++文档识别接口如何实现 高效办公

  数字化信息爆炸时代,办公效率的提升成为企业和个人的迫切需求。人工智能技术的飞速发展,为我们带来了前所未有的便利,翔云文档识别接口便是其中之一。  与传统的人工手动录入相比,文档识别接口优势显著。人...

C++如何生成Microsoft Word文档

...

超实用C++学习指南:语法要点、经典书籍、实战案例全汇总!

以下是为您整理的C++学习指南,综合了语法要点、资源推荐及实战方向,结合搜索结果和经典知识体系,帮助您系统学习:一、C++基础语法学习指南1.核心概念oC++是静态类型、编译式语言,支持面向对象和...

掌握C++文件读写,让代码更灵动!

文章改写指令通常涉及对原有文本进行调整、重组或重新表达,以保持或增强信息的准确性和可读性,同时可能改变风格、语气或目标受众。以下是一些具体的文章改写指令示例:·2.简化语言:→指令:将文章中的复杂词汇...

闲置宽带能换钱?P2P CDN、无线宝、赚钱宝到底靠不靠谱

无线宝类产品其实由来已久,无线宝类产品即与支付宝、余额宝、余利宝等货币基金毫无干系,与区块链“挖币”更存在本质的不同,而是一种利用家庭中的闲置宽带,通过流量来换取佣金的产品。无线宝类产品其实在过去几年...

攻略什么?闲置宽带还可以赚钱?

现在很多朋友在使用10Mbps、50Mbps甚至100Mbps的高速宽带,不过普通用户并不是长时间都需要这么高速的宽带。比如对于100Mbps的宽带用户,在日常浏览网页时,基本上2Mbps左右的带宽即...

明日学业水平考试开始报名 详细步骤都在这里

点击上面蓝字关注我们哦~日前,山东省教育考试院发布了《山东省2019年夏季学业水平考试报名考生操作说明》(点击文末阅读原文查看),明天就到了报名的时候了,详细的报名步骤、网上缴费流程、追加报考科目等...

瞄准用户上传带宽:HiWiFi 极路由 联合 迅雷 推出 “极赚钱”套餐

上次总理谈到宽带降价问题时,很多网友除了吐槽网速慢费用贵,还反映宽带网络的上下行速度不对等。比如说以前ADSL2M的宽带只有512Kbps的上行速度,现在升级到光纤网络之后,按理说技术上实现上下行...

揭秘P2P平台刷数据:交易额从100万到1200万

(作者:峰岭、刘珺、周娜)从默默无闻到万众瞩目,从“零数据”到“大数据”,从小众投资到大众理财,从个人借贷到企业借款,从个人信用到车、房、资产抵押……近两年来P2P行业以迅雷之速快速爆发,P2P平台也...

运营商让我签这个宽带违规使用告知函,我懵逼了

特么的是爱奇艺迅雷自己上传的p2p数据,btpt也会上传,直播也会上传,监控也会,传文件也会,到底他么的运营商你要干个啥啊,我不仅没捞着一分好处,夹在中间两头受气!真特么晦气这特么是谁弄的函?完全没搞...

取消回复欢迎 发表评论: