├── img └── demo.gif ├── README.md ├── LICENSE └── etwbreaker.py /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbus-cert/etwbreaker/HEAD/img/demo.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # etwbreaker 2 | An IDA Plugin to statically find ETW events in a PE file and generate a Conditional Breakpoint to facilitate Security Research. 3 | 4 | ![Demo](img/demo.gif) 5 | 6 | ## How To Install? 7 | 8 | Just put the `etwbreaker.py` script in the `plugins` folder of IDA. 9 | 10 | ``` 11 | git clone git@github.com:Airbus-CERT/etwbreaker.git 12 | mklink "C:\\Program Files\\IDA Pro 7.4\\plugins\\etwbreaker.py" "etwbreaker\etwbreaker.py" 13 | ``` 14 | 15 | Launch your IDA and press `Ctrl-Shift-L` to activate it. 16 | 17 | ## How Does It Work? 18 | 19 | `ETWBreaker` try to find all references about ETW providers statically compiled into a Windows module. 20 | 21 | ### Manifest-based Provider 22 | 23 | `ETWBreaker` will try to find a resource name `WEVT_TEMPLATE`. This resource includes the ETW manifest for the module. 24 | Once we get all events available, we can compute a signature and try to find the associated symbol of the event to enrich analysis. 25 | Then we can also generate a conditional breakpoint to debug the module only once the target event is triggered. 26 | 27 | ### Tracelogging provider 28 | 29 | `Microsoft` recently added the `Tracelogging` API, that works over ETW but without manifests. 30 | Tracelogging encompasses its scheme directly into a special ETW field named `ExtendedData`. 31 | The Tracelogging API is a macro-based API, it means that schemes are generated at compilation and can be retrieved statically. 32 | Scheme data are contained in a bordered region for security purposes, and can be retrieved easily. 33 | 34 | But, to the contrary of manifest-based ETW, the link between event and provider is made at execution time, and all events have the same ID (0). 35 | This is why we list only providers in case of Tracelogging. 36 | 37 | ## SSTIC (Symposium sur la sécurité des technologies de l'information et des communications) 38 | 39 | This project is part of presentation made for [SSTIC](https://www.sstic.org/2020/presentation/quand_les_bleus_se_prennent_pour_des_chercheurs_de_vulnrabilites/) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /etwbreaker.py: -------------------------------------------------------------------------------- 1 | """ 2 | ETWBreaker is an IDA plugin that find all references about ETW into a module 3 | 4 | Currently ETWBreaker can work with Manifest-based ETW and Tracelogging. 5 | 6 | For Manifest-based ETW, ETWBreaker will parse module ressources to find and parse the manifest 7 | and show all possible events handled by the current module. 8 | 9 | It tries to parse also all tracelogging events, which are more hidden than Manifest-based ones. 10 | 11 | In the end ETWBreaker can generate a conditional breakpoint to dynamically analyze events. 12 | """ 13 | 14 | import idaapi 15 | import idautils 16 | import ida_dbg 17 | import ida_name 18 | import idc 19 | import sys 20 | import struct 21 | import textwrap 22 | from io import BytesIO 23 | from PyQt5 import QtCore, QtWidgets, QtGui 24 | from typing import List, Tuple 25 | 26 | __author__ = "Airbus CERT" 27 | 28 | 29 | class ETWBreakerException(Exception): 30 | """ 31 | Base exception for all exception of ETW breaker 32 | """ 33 | def __init__(self, message): 34 | super().__init__(message) 35 | 36 | 37 | class ETWBreakerWevtTemplateNotFound(ETWBreakerException): 38 | """ 39 | The WEVT_TEMPLATE ressource was not found 40 | """ 41 | def __init__(self): 42 | super().__init__("WEVT_TEMPLATE resource not found.") 43 | 44 | 45 | class ETWBreakerTLNotFound(ETWBreakerException): 46 | """ 47 | The tracelogging magic was not found 48 | """ 49 | def __init__(self): 50 | super().__init__("Trace logging not found") 51 | 52 | 53 | class ETWBreakerUnexpectedToken(ETWBreakerException): 54 | """ 55 | During parsing an unexpected token was found. 56 | Please open an issue on Github. 57 | """ 58 | def __init__(self, expected, found): 59 | super().__init__("Unexpected token. Expected %s, found %s"%(expected, found)) 60 | 61 | 62 | class Stream(BytesIO): 63 | """ 64 | A wrapper that is nicer to understand 65 | """ 66 | def read_u32(self) -> int: 67 | return struct.unpack(" int: 70 | return struct.unpack(" int: 73 | return struct.unpack(" int: 76 | return struct.unpack(" str: 162 | """ 163 | Try to find a symbol associated to the event 164 | 165 | This is based on the event header signature 166 | Most of then are into .rdata segment and some of them have a name 167 | """ 168 | pattern = struct.pack(" Channel: 214 | """ 215 | Try to find a channel from its identifier 216 | """ 217 | return next((channel for channel in self.channels if channel.identifier == identifier), None) 218 | 219 | 220 | class ManifestProvider(Provider): 221 | """ 222 | Convenient class use to identify Manifest based providers 223 | """ 224 | 225 | 226 | class TraceLoggingProvider(Provider): 227 | """ 228 | Convenient class use to identify TraceLogging providers 229 | """ 230 | 231 | 232 | def find_segment(name: str) -> List[Tuple[int, int]]: 233 | """ 234 | Try to find the segment from name 235 | 236 | :ivar name: name of segment 237 | :ret: Start ant end address 238 | """ 239 | result = [] 240 | for seg in idautils.Segments(): 241 | if idc.get_segm_name(seg) == name: 242 | result.append((idc.get_segm_start(seg), idc.get_segm_end(seg))) 243 | return result 244 | 245 | 246 | def find_wevt_template(start, end) -> Stream: 247 | """ 248 | This function try to retrieve the WEVT_TEMPLETE resource 249 | This resource start with the magic CRIM 250 | 251 | :ivar start: start address 252 | :ivar end: end address 253 | :ret: Stream use to parse Manifest based provider or raise an exception 254 | :raise: ETWBreakerWevtTemplateNotFound 255 | """ 256 | resource = idc.get_bytes(start, end - start) 257 | result = resource.find(b"CRIM") 258 | if result == -1: 259 | raise ETWBreakerWevtTemplateNotFound() 260 | 261 | return Stream(resource[result:]) 262 | 263 | 264 | def find_tracelogging_meta(start, end) -> Stream: 265 | """ 266 | Try to find ETW0 magic 267 | 268 | :ivar start: start address 269 | :ivar end: end address 270 | :ret: Stream use to parse tracelogging or None if not found 271 | """ 272 | data = idc.get_bytes(start, end - start) 273 | result = data.find(b"ETW0") 274 | if result == -1: 275 | raise ETWBreakerTLNotFound() 276 | 277 | return Stream(data[result:]) 278 | 279 | 280 | def parse_tracelogging_event(stream: Stream) -> Event: 281 | """ 282 | A tracelogging event is identified by its channel number_of_channel 283 | that are always 11. Actually we can't handle tracelogging event 284 | because the lonk between event and provider is made during code execution 285 | 286 | :ivar stream: current stream use to parse the event 287 | :ret: An event object for tracelogging 288 | """ 289 | channel = stream.read_u8() 290 | if channel != 11: 291 | raise ETWBreakerUnexpectedToken(11, channel) 292 | level = stream.read_u8() 293 | opcode = stream.read_u8() 294 | keyword = stream.read_u64() 295 | size = stream.read_u16() 296 | stream.read(size - 2) 297 | return Event(0, 0, channel, level, opcode, 0, keyword) 298 | 299 | 300 | def parse_tracelogging_provider(stream: Stream) -> Provider: 301 | """ 302 | Create a default provider for tracelogging 303 | It will add a default event for this provider 304 | Because in traclogging all event have the event id set to 0 305 | 306 | :ivar stream: current stream use to parse the provider 307 | :ret: A provider object for tracelogging 308 | """ 309 | guid = Guid(stream.read(16)) 310 | size = stream.read_u16() 311 | payload = stream.read(size - 2) 312 | name = payload[:payload.find(b"\x00")].decode("ascii") 313 | 314 | return TraceLoggingProvider(guid, [Event(0, 0, 11, 0, 0, 0, 0)], [Channel(11, name)]) 315 | 316 | 317 | def parse_tracelogging(stream: Stream) -> List[Provider]: 318 | """ 319 | Try to parse all tracelogging event and provider 320 | from an .rdata segmant 321 | 322 | Actually only provider are intersting. It's because the link 323 | between event and provider are made into the code dynamically. 324 | 325 | :ivar stream: current stream use to parse the event 326 | :ret: the list of all provider which are found 327 | """ 328 | magic = stream.read(4) 329 | if magic != b"ETW0": 330 | raise ETWBreakerUnexpectedToken(b"ETW0", magic) 331 | 332 | stream.read(12) 333 | providers = [] 334 | while True: 335 | type = stream.read_u8() 336 | if type == 6: 337 | parse_tracelogging_event(stream) 338 | elif type == 4: 339 | providers.append(parse_tracelogging_provider(stream)) 340 | elif type == 0: 341 | # padding 342 | continue 343 | else: 344 | print("Unknown Trace logging type %s, expect to be the end of trace logging block"%type) 345 | break 346 | return providers 347 | 348 | 349 | def parse_event_elements(stream: Stream) -> List[Event]: 350 | """ 351 | Parse an event element 352 | An event is defined by : 353 | * unique identifier 354 | * a channel 355 | * a set of keywords 356 | * a level 357 | 358 | :ivar stream: Input stream once read the EVNT magic and the size of the payload 359 | :ret: List of all event parsed 360 | """ 361 | number_of_event = stream.read_u32() 362 | stream.read(4) # padding 363 | 364 | events = [] 365 | for i in range(0, number_of_event): 366 | event_id = stream.read_u16() 367 | version = stream.read_u8() 368 | channel = stream.read_u8() 369 | level = stream.read_u8() 370 | opcode = stream.read_u8() 371 | task = stream.read_u16() 372 | keywords = stream.read_u64() 373 | message_identifier = stream.read_u32() 374 | template_offset = stream.read_u32() 375 | opcode_offset = stream.read_u32() 376 | level_offset = stream.read_u32() 377 | task_offset = stream.read_u32() 378 | stream.read(12) 379 | events.append(Event(event_id, version, channel, level, opcode, task, keywords)) 380 | return events 381 | 382 | 383 | def parse_channel_element(stream: Stream) -> List[Channel] : 384 | number_of_channel = stream.read_u32() 385 | result = [] 386 | for i in range(0, number_of_channel): 387 | unknown = stream.read_u32() 388 | offset = stream.read_u32() 389 | identifier = stream.read_u32() 390 | message_identifier = stream.read_u32() 391 | 392 | sub_stream = Stream(stream.getvalue()) 393 | sub_stream.read(offset) 394 | size = sub_stream.read_u32() 395 | name = sub_stream.read(size-4).decode("utf-16le") 396 | result.append(Channel(identifier, name)) 397 | return result 398 | 399 | 400 | def parse_event_provider(guid: Guid, stream: Stream) -> Provider: 401 | """ 402 | Parse an event provider 403 | An event provider is composed by a plenty of sort of element: 404 | * EVNT for event 405 | 406 | https://github.com/libyal/libfwevt/blob/master/libfwevt/fwevt_template.h 407 | 408 | :ivar guid: GUID of the provider 409 | :ivar stream: stream of the entire resource with offset set to the start of the provider 410 | """ 411 | magic = stream.read(4) 412 | if magic != b"WEVT": 413 | raise ETWBreakerUnexpectedToken(b"WEVT", magic) 414 | 415 | size = stream.read_u32() 416 | message_table_id = stream.read_u32() 417 | 418 | number_of_element = stream.read_u32() 419 | number_of_unknown = stream.read_u32() 420 | 421 | element_descriptor = [(stream.read_u32(), stream.read_u32()) for i in range(0, number_of_element)] 422 | unknown = [stream.read_u32() for i in range(0, number_of_unknown)] 423 | 424 | events = [] 425 | channels = [] 426 | for offset, _ in element_descriptor: 427 | stream.seek(offset) 428 | magic = stream.read(4) 429 | size = stream.read_u32() 430 | 431 | # Event declaration 432 | if magic == b"EVNT": 433 | events = parse_event_elements(stream) 434 | elif magic == b"CHAN": 435 | channels = parse_channel_element(stream) 436 | 437 | return ManifestProvider(guid, events, channels) 438 | 439 | 440 | def parse_manifest(stream: Stream) -> List[Provider]: 441 | """ 442 | An ETW Manifest is a binary serialized 443 | It start with CRIM magic 444 | 445 | Then list all providers 446 | For each providers we can parse GUID and Provider description 447 | 448 | """ 449 | magic = stream.read(4) 450 | if magic != b"CRIM": 451 | raise ETWBreakerUnexpectedToken(b"CRIM", magic) 452 | 453 | size = stream.read_u32() 454 | 455 | major_version = stream.read_u16() 456 | minor_version = stream.read_u16() 457 | 458 | number_of_provider_descriptor = stream.read_u32() 459 | 460 | # Read provider meta informations 461 | providers_descriptor = [(Guid(stream.read(16)), stream.read_u32()) for i in range(0, number_of_provider_descriptor)] 462 | 463 | # Parse providers 464 | providers = [] 465 | for guid, offset in providers_descriptor: 466 | stream.seek(offset) 467 | providers.append(parse_event_provider(guid, stream)) 468 | 469 | return providers 470 | 471 | 472 | def add_breakpoint(guid: Guid, event: Event): 473 | """ 474 | Add a software break point on ntdll!EtwEventWrite 475 | And set a condition on event id and event provider 476 | """ 477 | bpt = idaapi.bpt_t() 478 | bpt.set_sym_bpt("ntdll_EtwEventWrite", 0) 479 | bpt.condition = textwrap.dedent(""" 480 | import idaapi 481 | import idc 482 | 483 | rdx = idaapi.regval_t() 484 | idaapi.get_reg_val('RDX',rdx) 485 | event_id = int.from_bytes(idc.get_bytes(rdx.ival, 2), "little") 486 | 487 | rcx = idaapi.regval_t() 488 | idaapi.get_reg_val('RCX',rcx) 489 | provider_guid = idc.get_bytes((rcx.ival & 0xFFFFFFFFFFFF) + 0x20, 16) 490 | 491 | if event_id == %s and provider_guid == %s: 492 | print(f"[ETWBreaker] break on Provider {{%s}} EventId ({event_id})") 493 | return True 494 | else: 495 | return False 496 | """%(event.event_id, guid.raw, guid)) 497 | bpt.elang = "Python" 498 | idaapi.add_bpt(bpt) 499 | 500 | 501 | def delete_breakpoint(symbol: str): 502 | """ 503 | Delete the breakpoint set on ntdll_EtwEventWrite 504 | """ 505 | location = idaapi.bpt_location_t() 506 | location.set_sym_bpt(symbol) 507 | 508 | if idaapi.find_bpt(location, None): 509 | idaapi.del_bpt(location) 510 | 511 | 512 | class ETWResultsModel(QtCore.QAbstractTableModel): 513 | """ 514 | This class is QT class that help to view data from COM parsing 515 | """ 516 | COL_ID = 0x00 517 | COL_TYPE = 0x01 518 | COL_GUID = 0x02 519 | COL_CHANNEL = 0x03 520 | COL_SYMBOL = 0x04 521 | 522 | 523 | def __init__(self, providers: List[Provider], parent=None): 524 | super().__init__(parent) 525 | 526 | self._column_headers = { 527 | ETWResultsModel.COL_ID : 'Event ID', 528 | ETWResultsModel.COL_TYPE : 'Type', 529 | ETWResultsModel.COL_GUID : 'GUID', 530 | ETWResultsModel.COL_CHANNEL : 'Channel', 531 | ETWResultsModel.COL_SYMBOL : 'Symbol' 532 | } 533 | 534 | self._results = [] 535 | for provider in providers: 536 | self._results += [(provider, event) for event in provider.events] 537 | 538 | self._row_count = len(self._results) 539 | 540 | def flags(self, index): 541 | return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable 542 | 543 | def rowCount(self, index=QtCore.QModelIndex()): 544 | return self._row_count 545 | 546 | def columnCount(self, index=QtCore.QModelIndex()): 547 | return len(self._column_headers) 548 | 549 | def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole): 550 | """ 551 | Define the properties of the the table rows & columns. 552 | """ 553 | if orientation == QtCore.Qt.Horizontal: 554 | 555 | # the title of the header columns has been requested 556 | if role == QtCore.Qt.DisplayRole: 557 | try: 558 | return self._column_headers[column] 559 | except KeyError as e: 560 | pass 561 | 562 | # the text alignment of the header has beeen requested 563 | elif role == QtCore.Qt.TextAlignmentRole: 564 | 565 | # center align all columns 566 | return QtCore.Qt.AlignHCenter 567 | 568 | # unhandled header request 569 | return None 570 | 571 | def data(self, index, role=QtCore.Qt.DisplayRole): 572 | """ 573 | Define how Qt should access the underlying model data. 574 | """ 575 | # data display request 576 | if role == QtCore.Qt.DisplayRole: 577 | 578 | # grab for speed 579 | row = index.row() 580 | column = index.column() 581 | 582 | if column == ETWResultsModel.COL_GUID: 583 | return "{%s}"%(self._results[row][0].guid) 584 | elif column == ETWResultsModel.COL_ID: 585 | return self._results[row][1].event_id 586 | elif column == ETWResultsModel.COL_CHANNEL: 587 | event = self._results[row][1] 588 | return str(self._results[row][0].find_channel(event.channel) or "Unknown channel") 589 | elif column == ETWResultsModel.COL_TYPE: 590 | return self._results[row][0].__class__.__name__ 591 | elif column == ETWResultsModel.COL_SYMBOL: 592 | return self._results[row][1].find_symbol() or "No symbol" 593 | 594 | # font color request 595 | elif role == QtCore.Qt.ForegroundRole: 596 | return QtGui.QColor(QtCore.Qt.black) 597 | 598 | # unhandeled request, nothing to do 599 | return None 600 | 601 | 602 | class ETWResultsForm(idaapi.PluginForm): 603 | """ 604 | The Qt form use to display ETW table 605 | """ 606 | def __init__(self, providers: List[Provider]): 607 | 608 | super().__init__() 609 | self.providers = providers 610 | 611 | def OnCreate(self, form): 612 | """ 613 | Initialize the custom PyQt5 content on form creation. 614 | """ 615 | # Get parent widget 616 | self._widget = self.FormToPyQtWidget(form) 617 | self._init_ui() 618 | 619 | def show(self): 620 | """ 621 | Make the created form visible as a tabbed view. 622 | """ 623 | flags = idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WOPN_PERSIST 624 | return idaapi.PluginForm.Show(self, "ETW", flags) 625 | 626 | 627 | def _init_ui(self): 628 | """ 629 | Init ui of ETW table 630 | """ 631 | self._model = ETWResultsModel(self.providers, self._widget) 632 | self._table = QtWidgets.QTableView() 633 | 634 | # set these properties so the user can arbitrarily shrink the table 635 | self._table.setMinimumHeight(0) 636 | self._table.setSizePolicy( 637 | QtWidgets.QSizePolicy.Ignored, 638 | QtWidgets.QSizePolicy.Ignored 639 | ) 640 | 641 | self._table.setModel(self._model) 642 | 643 | # set a windbg breakpoint on double click 644 | self._table.doubleClicked.connect(self._ui_entry_double_click) 645 | 646 | # table selection should be by row, not by cell 647 | self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 648 | 649 | # more code-friendly, readable aliases 650 | vh = self._table.verticalHeader() 651 | hh = self._table.horizontalHeader() 652 | vh.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) 653 | 654 | # hide the vertical header themselves as we don't need them 655 | vh.hide() 656 | 657 | # Allow multiline cells 658 | self._table.setWordWrap(True) 659 | self._table.setTextElideMode(QtCore.Qt.ElideMiddle); 660 | self._table.resizeColumnsToContents() 661 | self._table.resizeRowsToContents() 662 | 663 | layout = QtWidgets.QGridLayout() 664 | layout.addWidget(self._table) 665 | self._widget.setLayout(layout) 666 | 667 | def _ui_entry_double_click(self, index): 668 | """ 669 | When user click on an event 670 | we send to windbg a special crafted debug command 671 | 672 | That will set a conditional breakpoint on ntdll!EtwEventWrite function 673 | with condition on function argument that match the eventid and the provider GUID 674 | """ 675 | event = self._model._results[index.row()][1] 676 | guid = self._model._results[index.row()][0].guid 677 | delete_breakpoint("ntdll_EtwEventWrite") 678 | add_breakpoint(guid, event) 679 | 680 | 681 | def PLUGIN_ENTRY(): 682 | return ETWBreaker() 683 | --------------------------------------------------------------------------------