看到前輩們相關的文章,不太明白什么是句柄降權,于是專門去學習一下,過程有一點波折。
句柄降權
什么是句柄
當一個進程利用名稱來創建或打開一個對象時,將獲得一個句柄,該句柄指向所創建或打開的對象。以后,該進程無須使用名稱來引用該對象,使用此句柄即可訪問。這樣做可以顯著地提高引用對象的效率。句柄是一個在軟件設計中被廣泛使用的概念。例如,在C運行庫中,文件操作使用句柄來表示,每當應用程序創建或打開一個文件時,只要此創建或打開操作成功,則C運行庫返回一個句柄。以后應用程序對文件的讀寫操作都使用此句柄來標識該文件。而且,如果兩個應用程序以共享方式打開了同一個文件,那么,它們將分別得到各自的句柄,且都可以通過句柄操作該文件。盡管兩個應用程序得到的句柄的值并不相同,但是這兩個句柄所指的文件卻是同一個。因此,句柄只是一個對象引用,同一個對象在不同的環境下可能有不同的引用(句柄)值。
上文中的"對象"指的是內核對象,我們在R3中所使用的文件、進程、線程在內核中都有對應內核對象。應用層每次創建或打開進程、文件都會對相應的內核對象創建一個句柄。當多個進程同時打開一個文件時,該文件在內核中只會存在一個文件內核對象,但每個進程都有一個各自的文件句柄,每個句柄會增加內核對象的引用計數,只有當內核對象的引用計數為0時,內核對象才會釋放。
私有句柄表
eprocess
指向一個ObjectTable
,ObjectTbale
中存在TableCode
,這個指向的是這個進程的私有句柄表。同時ObjectTable
中還有一個HandleTableList
,這個是一個鏈表,通過HandleTableList
?成員遍歷得到所有進程的ObjectTable
地址
我們的目標是獲取到_object_header
結構體,這個結構體才是句柄的真正內容。但是不同版本系統下的取法不太一樣,win7是直接指向句柄,win10則需要做一些偏移,這些偏移google沒有資料,大多都是通過IDA靜態分析函數才能得到。
win中有一些根據_handle_table_entry獲取進程句柄的函數,我這里沒有做過多分析,直接使用前輩分析后的經驗。分析目標ntoskrnl.exe
下的ObpEnumFindHandleProcedure
函數,可以看到如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
可以在開頭部分看到(handle_table_entry->LowValue >> 16) & 0xFFFFFFFFFFFFFFF0ui64)
,這樣才能獲取到句柄內容,獲取到_object_header
.
但是我自己嘗試的時候沒有獲取到,直到我注意到帖子里面最后得到的值開頭都是0xffff
,這說明右移前面不是補充0,而是補充1
所以地址計算實際是:(handle_table_entry->LowValue >> 16) & 0xFFFFFFFFFFFFFFF0ui64) + 0xffff000000000000
得到的就是_OBJECT_HEADER
,這表示一個句柄頭,句柄體在body的位置,我系統版本的偏移是0x30,進程句柄的話就是_eprocess
結構體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
0x18位置的TypeIndex表示這個句柄對應的對象是一個什么類型的對象,比如文件、進程、線程等,0x30的Body就是便指向了該句柄對應的對象結構。若句柄對應的對象是一個進程對象那么0x30的位置存的就是對應進程對象的_EPROCESS
的結構,可以從這個結構便獲得進程名、進程ID等等信息。
所以在內核中有一個鏈表存放了每一個進程的私有句柄表。
TableCode句柄表
句柄表是以頁為單位,兩層句柄表則是第一層存放第二層的指針,一般只有系統進程才會打開那么多的句柄,惡意進程通常只有一層。win10的機器上tablecode是存放了一頁的handle_table_entry,每一個16字節,一頁大小是4k,所以一頁最多256個句柄。(32位系統的是一頁512個句柄)
怎么判斷句柄表有幾層?
TableCode的最后2個bit表示層數(有的文章說是3個bit,我也不確定),但是目前我看最多的也只有兩層,下面分別是0層和1層的情況。
?可以看到tablecode句柄表的內容不是句柄,而是_handle_table_entry
,這不是一個結構體,是一個union
從handle_table_entry到句柄還需要一些額外的計算變化,同時一個進程句柄的權限就標注在每個句柄對應的這個結構體當中
Windbg 調試
以手動的方式從一個eprocess內存看到他下面的句柄表
windbg以內核附加模式連接上虛擬機/真機后,首先我們需要一個EPROCESS結構體地址,使用!process 0 0
查看所有進程的基本信息
1 2 3 4 5 6 7 8 9 10 11 |
|
PROCESS的值就是EPROCESS的地址值, 我這里選用csrss.exe的PROCESS值ffffbf8eab1ea080
定位ObjectTable
的值
1 2 |
|
再進一步查看_HANDLE_TABLE結構體,定位TableCode的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
得到TableCode的值是0xffffd00b`16d58001,注意TableCode最后的一位是1,這表示有兩層頁表,第一層的值是指向的第二層的指針,所以先查看第一層句柄指針表
1 2 3 4 5 6 7 8 9 |
|
看到有3個二層句柄表,我們選用第一張表
1 2 3 4 5 6 7 8 9 |
|
可以看到從ffffd00b`16667010開始每16字節表示一個_handle_table_entry union體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
(handle_table_entry->LowValue >> 16) & 0xFFFFFFFFFFFFFFF0ui64)
獲取_object_header結構體,記得前面填充的要是1,計算一下值
1 2 3 |
|
0xbf8eaaee9430ffff
是ffffd00b16667010
的值,對應的就是handle_table_entry->LowValue
,得到地址0xffffbf8eaaee9430
也就是_object_header
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
可以看到整個object_header的內容,這只是句柄的頭部,句柄的內容還在0x30偏移的位置,由于我調試發現這一個進程句柄,所以直接用eprocess展示這個句柄內容。
1 2 3 4 5 6 7 8 9 |
|
這樣就通過進程的私有句柄表獲得了被打開句柄進程的信息。
判斷句柄類型
怎么樣從一個句柄頭(_object_header)判斷出這是一個進程句柄(process handle),文件句柄(file handle)還是設備句柄(device handle)
這個不同版本的系統判斷方法不一樣,win7/8/8.1是一樣的 win10則不同。網絡上大多都是win7, win8的我在這篇外網文章上才找到win10的判斷方法
win7/8/8.1
這幾個版本的_object_header結構大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
一般直接TypeIndex表示這個句柄的類型index。相同的類型這個值會相同,還可以進一步查看這個index在nt!object_type
這個表中的具體信息
?
上面就是Typeindex = 7
對應的意義,是進程句柄
win10
win10的Typeindex
就不一樣了,測試會發現,哪怕都是進程句柄這個Typeindex
的值也會不同。
需要將3個單字節的值異或起來,Typeindex ^ nt!ObHeaderCookie ^ 地址的第二個字節
?
最后得到的才是真的Typeindex
句柄降權/提權
一個句柄的權限,表示句柄擁有者對這個句柄的操作權限,權限有以下幾種
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
當我們通過OpenProcess
以多種權限申請打開進程時便將多種權限或運算就得到了我們想要的權限值,我們的目的是為了降低句柄擁有者對我們要保護的進程的操作權限,那最簡單暴力的方法便是把handle_table_entry->GrantedAccessBits
的值修改成我們設定的值,直接讓句柄擁有者對我們的進程操作權限被修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
代碼實現防止CE讀取進程內存
現在假設一個場景,我們打開一個記事本(notepad.exe),然后用CE去讀取這個進程的內存。我們的目標是保護這個記事本進程,讓降低CE中已經打開的記事本進程句柄權限,讓CE無法再繼續讀取內存。
首先CE打開目標進程
?
我這里直接寫死進程號了,使用PsLookupProcessByProcessId
獲取指定PID的eprocess
結構體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
然后開始寫ProtectProcessHandleByEprocess
函數,這個函數才是主要的邏輯,傳入指定eprocess
,然后遍歷鏈表所有的句柄表,匹配是否相同,如果相同則修改權限。
我首先定義兩個結構體,方便后面編程
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
然后給這兩個結構體定義了幾個方法
CheckHandleTableEntry
:檢查HANDLE_TABLE_ENTRY的值是否合法NewProcessHandleObject
:新建一個PROCESS_HANDLE_OBJECT結構體FreeProcessHandleObject
:釋放一個PROCESS_HANDLE_OBJECT結構體HandleEntryTable2ObjectHeader
: 計算單個handle_table_entry轉化成object_header地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
然后看一下ProtectProcessHandleByEprocess函數,傳入eprocess地址后,首先計算出來_object_table地址,然后計算出來HandleTableList地址
1 2 3 4 |
|
然后遍歷鏈表,我們把鏈表上每一個節點都創建一個PROCESS_HANDLE_OBJECT結構體,因為鏈表上每一個節點代表一個進程,每一個進程都有一張或者多張句柄表,我們先將鏈表上每一個節點的部分信息收集好后放在一個數組中,方便我們后續遍歷操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
然后開始遍歷這個數組,具體每一個進程都遍歷它的句柄表再來對比,關鍵邏輯在如下的FilterObjByEprocess
函數中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
下面看我們怎么遍歷一個進程節點的句柄表,也就是FilterObjByEprocess
函數。
這里我只考慮一層和兩次的句柄表,至于三層的不考慮。大部分惡意軟件都只有一層句柄表,CE有兩層。所以我們分兩種來處理,一種是只有一層句柄表的,一種是兩層句柄表的
FilterOneTableByEprocess
FilterTWOTabelByEprocess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
FilterOneTableByEprocess
需要傳入兩個參數,一個是需要保護的eprocess地址,一個是一層的句柄表tablecode,大致流程如下
檢查傳入的tablecode有沒有異常,有異常的跳過
tablecode其實就是_handle_table_entry數組,所以把所有_handle_table_entry轉換成對應的_object_header
然后提取每個_object_header的body對比是否等于我們的目標EPROCESS,如果等于表示找到了,就修改權限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
修改權限也很簡單,直接去掉內存讀和內存寫的權限
1 2 3 4 5 6 7 8 9 |
|
上面就是一層句柄的修改了,這樣改完CE還是能讀取內存,因為CE是兩層句柄表,所以要再處理一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
這樣整個代碼邏輯就完全了,下面放一下全部的代碼,分兩個文件,寫的比較難看。
- main.c
- header.c
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
header.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
|
結果
運行后,打印內容大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
然后此時CE已經不能查看內存了
但是這只是當前這個CE進程的,如果關掉這個CE進程,再打開新的CE并且Open記事本進程就又可以重新讀取了。因為在新的CE進程中,還沒有修改句柄表中記事本進程的權限,所以還需要對抗。
對抗句柄降權思路
反復修改權限
防護方是通過驅動來遍歷私有句柄表,然后修改攻擊者進程的私有句柄表中指向被保護進程的句柄屬性。
那我們也可以寫一個驅動來不停的修改我們自身進程的私有句柄權限,將句柄權限改成full control
斷鏈
防護方是通過遍歷私有句柄鏈表來查找攻擊方的私有句柄表,那我們可以將私有句柄從鏈表上斷掉,放置一個空/假的私有句柄表結構體或者直接讓我們的進程私有句柄表從鏈表上斷開
ObRegisterCallbacks 保護
實際上上述的句柄降權/提權都是針對callbacks保護來做的,很多廠商是使用這個微軟公開的API來保護自身進程句柄的權限.
ObRegisterCallbacks 例程為線程、進程和桌面句柄操作注冊一系列回調例程。也就是我們可以給我們進程句柄設定一個回調函數,當我們的進程句柄被NtOpenProcess打開后,就會執行這個回調函數。如果我們在回調函數中修改這個句柄的權限,那么任何進程獲取我們進程句柄將得到修改過后權限的句柄。
1 2 3 4 |
|
1 |
|
指向?OB_CALLBACK_REGISTRATION?結構的指針,該結構指定回調例程和其他注冊信息的列表。
1 |
|
指向變量的指針,該變量接收標識注冊的回調例程集的值。 調用方將此值傳遞給?ObUnRegisterCallbacks?例程,以取消注冊回調集。
我們看一下OB_CALLBACK_REGISTRATION結構體
1 2 3 4 5 6 7 |
|
其中OperationRegistration
參數是指向OB_OPERATION_REGISTRATION
結構的數組的指針。 每個結構指定?ObjectPreCallback?和?ObjectPostCallback?回調例程以及調用例程的操作類型。
ObjectPreCallback就是發生進程或線程句柄操作時,操作系統會調用?ObjectPreCallback?例程
ObjectPostCallback就是發生進程或線程句柄操作后,操作系統會調用?ObjectPostCallback?例程
所以我們的操作函數也就是放在這兩個數組當中。
ObRegisterCallbacks?例程使用此結構。 此例程的?CallBackRegistration?參數是指向包含?OB_CALLBACK_REGISTRATION?結構的緩沖區的指針,該結構后跟一個或多個?OB_OPERATION_REGISTRATION?結構的數組。
在傳遞給?ObRegisterCallback?的每個OB_OPERATION_REGISTRATION結構中,調用方必須提供一個或兩個回調例程。 如果此結構的?PreOperation?和?PostOperation?成員均為?NULL,則回調注冊操作將失敗。
為了保證注冊成功,我們可以在不用的操作上注冊一個空的函數。比如我要注冊Pre的,我也寫一個空的Post
?