``` # Microsoft Warbird and PMP security research # (c) Security Explorations 2008-2024 Poland # (c) AG Security Research 2019-2024 Poland ``` "...If you decide to make it public..., stress the fact that it is not a security issue in PlayReady or any Microsoft technology; it's a security issue in the STB" - Microsoft, Nov 2022 # INTRODUCITON This package presents security analysis conducted with respect to Microsoft Warbird and Protected Media Path technologies. A more general description of the results obtained and their impact is described at project web page: ``` https://security-explorations.com/microsoft-warbird-pmp.html ``` This document provides a more in-depth technical explanation, illustration and verification of discovered attacks along toolsets used for their implementation. We hope this document provides both important contribution along a valuable perspective on the state of the art / security provided by PlayReady content security technology (vide the nature of the issues uncovered / verification of vendor's claims). For a more complete picture, you are encouraged to get familiar with our prior PlayReady research from 2022 (along associated technical doc): ``` https://security-explorations.com/microsoft-playready.html ``` ## DISCLAIMER The goal of this research is not to promote PayTV or VOD content piracy in any way. It is to both increase awareness and trigger more work at vendor's end to make PayTV / content piracy harder to accomplish. The results obtained indicate that there is much space for the improvement through better technical means. Information in this document is provided purely for educational purposes only. It is expressly forbidden to use them for any purposes that would violate any domestic or international laws. ## ON MICROSOFT / SHARING OF THE RESEARCH We believe the following is worth to mention regarding Microsoft. Please, keep in mind that we have 28 years of experience when it comes to dealing with various SW / HW vendors over security issues. - contact with Microsoft Security Response Center has been a questionable experience: * all messages are signed semi-anonymously (as MSRC), as a result one has the impression of no accountability and anonymity, the experience is more like talking to a customer service of a big retailer, not security contact of a major SW vendor (other vendors we dealt with used real names), * MSRC responses are not prompt, simple confirmation of a successful download / decryption of a research material takes ~3 days for the company, this doesn't look like 24/7 global team operations, this doesn't look like "we take security issues seriously" * MSRC can occasionally lose the topic (doesn't do the basic work that could clarify the context of a given message such as to ask internally employees CCed), this creates the impression that responses get redacted or are sent in a hurry / without due care * in 2019, Microsoft didn't respond to our inquiry, we didn't assume this was intentional until cease of a communication, which took place in early 2023 (as a follow up of our inquiries regarding PlayReady security, which hasn't been answered btw.) - Microsoft claims that it rewards security research, well the devil lies in a detail, more specifically: * `Microsoft Bounty Terms and Conditions` implicate commercial use with unknown payment terms, all non-negotiable and under Microsoft control * there are no categories for PlayReady / content protection (there has never been such categories), * our research didn't need to rely on any privilege elevation/kernel exploit (direct base for no bug stance) * Microsoft claimed no bug at its end in 2022 (with above, solid rationale for similar evaluation in 2024) * the company hasn't expressed / signaled any interest to discuss access to this research on a fair and commercial basis (regulating conditions of IP / know-how use, mutual agreement on a price, etc.) for the last 8 months, the company was well aware of research impact (PlayReady for Windows being broken to pieces) along the amount of work it required - Microsoft sort of played with us in 2022, which is hard to perceive in other terms than disrespect, company's claims regarding fixing and taking the issue seriously didn't reflect the reality, it took nearly 2 years to have the cert confirmed by Microsoft as compromised to be revoked, it's even hard to think of this action as revocation as the service got simply shut down, the company avoided to admit that PlayReady might provide little or no security upon client compromise, not much security improvements has been noticed since 2022 when we signaled the need for it (this research sort of proves it), this all built solid base for no trust to the company at our end. We decided to give Microsoft (a company consisting of 100,000+ SW engineers, with access to all the know-how, internal docs and source codes) approx. the same amount of time to fix / address the issues as it took us (a 1 man shop relying on binaries and public info only) to analyze and reverse engineer the technology, discover the issues, develop illustrating POCs and dedicated toolsets. We finally provided Microsoft with access to the complete research package comprising of a technical document, all toolsets with sources and test data (285MB ZIP file) on Nov 18, 2024 and free of any charge (exactly two weeks prior to the planned disclosure and as agreed with the company). But, Microsoft is only partly the winner here as its engineers likely failed to locate / address the root cause of the issues over the recent 8 months (no fixes / mitigations observed). That's quite a shame in our opinion. The other source for the shame lies in the nature of the issues and attacks described in this doc. ## PACKAGE DESCRIPTION The package comprises of the following tools: - `Warbird Reverse Engineering toolkit` Standalone toolkit making it possible to investigate Warbird protected binary files and facilitating their static and dynamic analysis. The toolkit makes it possible to perform dynamic analysis of arbitrary PlayReady functionality such as activation / individualization, license acquisition or license blob decryption (content key decryption), private ECC key discovery, XOR key discovery, white-box crypto key discovery, etc. - `Content key sniffer` The tool making it possible to extract plaintext values of content keys from Protected Media Path process (SW DRM on Windows 10 / 11 case). - `Test LS` Simple PlayReady License Server with a basic functionality to handle PlayReady individualization and license acquisition requests. - `MSPR Toolkit` MSPR toolkit with an update for client identity import, inspection and key exports, sniffer data import and dump along reverse engineering support for XOR key discovery and decryption of license server responses acquired with the use of a web browser (its built-in network monitoring functionality / developer console) ### DIRECTORY STRUCTURE The package follows directory structure described below: - `binaries` it contains base version of W10 PlayReady library used during reversing / analysis process - `binaries\dynamic` decrypted version of a base library, which has been setup for dynamic analysis (output of `wbsetup -r` command) - `binaries\static` decrypted version of the above base library, which has been setup for static analysis (output of `wbsetup -s` command) - `tests\trace` sample trace logs and scripts - `tests\xor_key` tests conducted with respect to XOR key value for various versions of PlayReady library and Windows OS - `tests\web_soap_licenses` tests illustrating web browser exploitation scenario - `tests\sniffdata` license sniffer outputs conducted for various VOD platforms and Live TV - `tests\win_sniffed_key_for_canalp_vod` exploit scenario illustrating the use of a key acquired by the sniffer to successfully decrypt the movie of Canal+ VOD - `tests\wincert_use_for_canalp_vod` exploit scenario illustrating the use of Windows based certificate / PlayReady identity issued for arbitrary VOD service to obtain access to movies (download and decryption) of Canal+ VOD service available to STB devices - `tests\sniffer_test_nov2024` test of the HW DRM disabling / MFPMP process attach / sniffer operation done in Nov 2024 - `tools\sniffer` content key sniffer - `tools\test_ls` Test LS - `tools\mspr_toolkit` MSPR Toolkit - `tools\wret_toolkit` Warbird Reverse Engineering toolkit Additionally, the following directories contain some helper data used by MSPR toolkit: - `tools\mspr_toolkit\cdm` CDM content with arbitrary real PlayReady identities - `tools\mspr_toolkit\decdata` some predefined ECC points (with specific signature / content key patterns for Point.X), which can be used to generate license blobs (with specific signature / license key) ## ASSUMPTIONS AND TERMS USED ACROSS THE DOC Most code snippets used in this document and illustrating implementation of PlayReady client library (`Windows.Media.Protection.PlayReady.dll`) are relying on its Windows 10 version depicted below (denoted as "sample prlib" throughout the doc): ``` wret> peinfo PEInfoCmd::run [PE file] name w10_prlib.dll path w10_prlib.dll size 10347408 base 180000000 sections: 15 symbols: 0 time: Thu Apr 29 11:08:30 2021 sha256: 0x00000000: e0 e7 ee c2 0e bf 12 2c b6 fb 13 ec f9 ff cd 94 .......,........ 0x00000010: 39 f3 86 06 0d 74 46 e3 be d8 b0 da e1 af 37 ac 9....tF.......7. ``` The focus on a given single client library should not be perceived in terms of any limitation. All libraries we tested follow the same implementation when it comes to core functionality pertaining to identity handling and license blobs decryption. All tests / analysis have been conducted in Windows 10/11 x64 environment. Addresses displayed by the WRET tool with respect to PlayReady image are RVAs - relative virtual addresses. WRET toolkit should be perceived in terms of a Proof of Concept. As such, it might not work in some cases / with respect to all Warbird binaries, it does not do proper cleanup with respect to Warbird / hooking setup. The way it implements some features are far from being perfect too. The following nomenclature is used with respect to extensions of identity files: - `*.enc.prv` Obfuscated private ECC key component used for license blob encryption - `*.enc.pub` Public ECC key component used for license blob encryption - `*.enc.plain` Plaintext value of a private ECC key component used for license blob encryption - `*.sig.prv` Obfuscated private ECC key component used for signing license requests - `*.sig.pub` Public ECC key component used for signing license requests - `*.sig.plain` Plaintext value of ECC key component used for signing license requests The plaintext content key value used in this document corresponds to the movie, which is not available any more in a target VOD platform (HTTP request for url denoting Manifest file was verified to return error, VOD search box for movie title didn't show the movie). ## TOOLS' BUILDING The tools requires the following software for building: - Java SE implementation such as Coretto from Amazon `https://aws.amazon.com/corretto/` - Microsoft’s Visual Studio Community Edition `https://visualstudio.microsoft.com/pl/vs/community/` Installation paths to the above software need to be setup in `tools\config.bat` file prior to the build process or tools usage: ``` set vcdir="c:\_SOFTWARE\Microsoft Visual Studio\2022\Community" set javadir="c:\_SOFTWARE\Coretto\jdk21.0.5_11" ``` Each tool can be built be running `build.bat` script residing in a tool's main directory: ``` c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\wret_toolkit>build ********************************************************************** ** Visual Studio 2022 Developer Command Prompt v17.6.1 ** Copyright (c) 2022 Microsoft Corporation ********************************************************************** [vcvarsall.bat] Environment initialized for: 'x64' Microsoft (R) Program Maintenance Utility Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. del out\test Could Not Find c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\wret_toolkit\out\test del obj\*.obj ml64.exe /c /Foobj\ src\asm\wb.asm src\asm\tramp.asm src\asm\policy.asm src\asm\comhelper.asm src\asm\appmodel.asm Microsoft (R) Macro Assembler (x64) Version 14.36.32532.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: src\asm\wb.asm Assembling: src\asm\tramp.asm Assembling: src\asm\policy.asm Assembling: src\asm\comhelper.asm Assembling: src\asm\appmodel.asm cl.exe /nologo /EHsc /GS /Zc:wchar_t /ZI /Gm- /Od /sdl /Zc:inline /fp:precise /D "_CONSOLE" /D "_CRT_SECURE_NO_WARNINGS" /WX- /Zc:forScope /RTC1 /Gd /MDd /FC /std:c++17 /c /Foobj\ src\*.cpp Aes.cpp App.cpp AppContainer.cpp Cmd.cpp ComLib.cpp Dbg.cpp DebugProc.cpp Dll.cpp DllMap.cpp DynCall.cpp Error.cpp FS.cpp ... Generating Code... Compiling... WinRT.cpp Generating Code... cl.exe /nologo /INCREMENTAL:NO /Gm- /Gy- /F 0x400000 /Feout\test.exe obj\*.obj bcrypt.lib kernel32.lib user32.lib ole32.lib WindowsApp.lib Aes.obj : warning LNK4075: ignoring '/EDITANDCONTINUE' due to '/OPT:ICF' specification c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\wret_toolkit> ``` ## TOOLS' EXECUTION Each tool can be executed by invoking `run.bat` script residing in a tool's main directory. The WRET and MSPR Toolkit tools work in a shell-like manner. As such they implement a set of custom commands. The sniffer tool can be provided with a single argument: ``` licsniff [-t | outfile] ``` where: - `-t` indicates that a stack dump should be performed for a target PMP process (for debugging purposes) - `outfile` denotes the target file where sniffed data such as content / white-box keys should be stored to The `test_ls` server by default listens on port 8080 and default host IP address. ## MSPR TOOLKIT COMMANDS The list of all commands implemented by the tool can be obtained with the use of `help` command. For each command, brief information about the arguments taken is displayed. Below, more details regarding commands and their arguments is given. The list is limited to the commands added to MSPR toolkit since 2022 (updated commands). - import license sniffer file ``` implicdata license_sniff_file ``` - show, export or decrypt license information for a given keuid ``` licdata [-k keyid][-e outfile][-r outfile][-c prvkey][-v][-l] ``` - show, export or decrypt license information for a given keyid ``` licdata [-k keyid][-e outfile][-r outfile][-c prvkey][-v][-l] ``` - generate license blob for a given identity or key, with explicit value at given pos ``` genlicense -k pubkey | -i identity [-v val][-p pos] ``` - geenrate intermediate decryption data (license blobs with predefined values) for given identity or public key ``` gendecdata -k pubkey | -i identity ``` - generate license blob with a special singature / content key pattern, use predefined ECC point file for that purpose ``` genzlicense -k pubkey | -i identity [-p pointfile][-o outfile] ``` - list identities or expost a given identity (its keys) to files ``` identity [identity] [-v][-e outfile] ``` - check a given private and public ECC key pair for match ``` checkkeypair prvky pubkey ``` - check if a given ECC point is on ECC curve (NIST P-256) ``` oncurve point ``` - calculate AES CMAC for given data and key ``` aescmac data key ``` - do bijection test ``` bijtest data key ``` - performul ECC MUL operation for a given point and val ``` eccmulp point val ``` - generate ECC key pair from for a fixed value of a private key ``` genfixedkey outfile kval ``` - decrypt HTTP SOAP license server response (such as the one acquired with the help of a web brower) ``` dechttplicense httprespfile [-r][-v] ``` - decode Base64 data ``` b64dec file [-o outfile] ``` ## WRET TOOLKIT COMMANDS Similarly to MSPR toolkit, WRET tool works in a shell-like fashion. The list of all commands implemented by the tool can be obtained with the use of `help` command: ``` # Warbird RE toolkit (WRET) # (c) Security Explorations 2016-2024 Poland # (c) AG Security Research 2019-2024 Poland wret> wret> help test test exit exit shell help print help information verbose set verbose printing image load image save save image images show loaded images dllmap show dll maps peinfo show various PE image information mdinfo show various Metadata image information wbinfo show various Warbird information wbdesc show Warbird segment information wbop do Warbird operation for address (encrypt, decrypt, lookup, restart) wbsetup setup Warbird for static or dynamic analysis cominfo show COM image information include load header file eccdata show ECC data information from image getlicense issue PlayReady license request declicense decrypt PlayReady license decsignkey decrypt signing key decenckey decrypt encryption key prinit initialize PlayReady prinfo show PlayReady information prsubs show PlayReady subroutines ektable show Expand Key table ekkey get content key from expanded key file xorkey show or set XOR key eccpctab show ECC precalc table pkdata set, restore or show private ECC Key data dmem dump memory wmem write memory smem save memory to file sstr search for string sstrw search for wide string wstr write string to memory wstrw write wide string to memory uuid show uuid representation trace trace calls ps show process list pid show process id debug debug process ptr show ptr info pkgs show app packages pkg set app package funcs show image functions fun set function attribute handler manage call handlers logcfg manage global logging configuration msprtrace manage MSPR tracing configuration syminfo show symbol information metadata load metadata lskey load license server key lsurl set license server url prlib set default PlayReady library start start process aeskey show AES key information aestecheck check AES table for bijection gmul multply operation in GF(2^8) space wret> ``` All implemented commands are defined in a global `_cmd_table` variable of the `Shell` class (`tools\wret_toolkit\src\Shell.cpp`): ``` vector Shell::_cmd_table={ new TestCmd("w[name=word,int]", "test"), new ExitCmd(NULL, "exit shell"), new HelpCmd("1[name=command,str]", "print help information"), new VerboseCmd("1[name=option,str]", "set verbose printing"), new ImageCmd("1[name=image,str]", "load image"), new SaveCmd("1[name=image,str]", "save image"), new ImagesCmd("a[name=all,flag]", "show loaded images"), new DllmapCmd("1[name=libname,str]", "show dll maps"), new PEInfoCmd("h[name=header,str]" "s[name=sections,flag]" "i[name=imports,flag]" "d[name=delayimports,flag]" "e[name=exports,flag]" "l[name=loadconfig,flag]" "x[name=exceptions,flag]" "L[name=libraries,flag]" "v[name=verbose,flag]", "show various PE image information"), new MDInfoCmd("h[name=header,str]" "s[name=stream,str]" "t[name=table,str]" "d[name=dump,str]" "v[name=verbose,flag]", "show various Metadata image information"), new WBInfoCmd("s[name=summary,flag]" "H[name=heapexecdesc,flag]" "S[name=segmentdesc,flag]" "d[name=descriptor,int]" "v[name=verbose,flag]", "show various Warbird information"), new WBDescCmd("H[name=heapexecdesc,int]" "S[name=segmentdesc,int]" "v[name=verbose,flag]", "show Warbird segment information"), new WBOpCmd("r[name=restart,flag]" "l[name=lookup,int]" "e[name=encrypt,int]" "d[name=decrypt,int]" "v[name=verbose,flag]", "do Warbird operation for address (encrypt, decrypt, lookup, restart)"), new WBSetupCmd("s[name=static,flag]" "r[name=runtime,flag]" "v[name=verbose,flag]", "setup Warbird for static or dynamic analysis"), ... ``` Details regarding each command implementation can be further investigated in the body of a designated class. # TECHNICAL DESCRIPTION A more detailed overview of the key problems encountered along exploitation techniques used is given below. ## WARBIRD DECRYPTION PRIMITIVES Binaries produced by Warbird are usually encrypted and its code obfuscated. These binaries can execute "encrypted" code too. Warbird binaries can be easily identified as they usually contain a special PE Image section named `?g_Encr`. Windows 10/11 contains several images with such sections of which PlayReady client library is a signature example: ``` wret> image w10_prlib.dll ImageCmd::run - Dll init w10_prlib.dll size 10347408 bytes base 180000000 wret> prlib w10_prlib.dll PRLib::run wret> peinfo -s PEInfoCmd::run [SECTIONS] * section: .text va 1000 size 6d6a00 char 60000020 * section: ?g_Encr va 6d8000 size a00 char 60000020 * section: ?g_Encr va 6d9000 size 800 char 60000020 * section: ?g_Encr va 6da000 size e400 char 60000020 * section: ?g_Encr va 6e9000 size 800 char 60000020 * section: ?g_Encr va 6ea000 size 600 char 60000020 * section: ?g_Encr va 6eb000 size d800 char 60000020 * section: ?g_Encr va 6f9000 size 200 char 60000020 * section: ?g_Encr va 6fa000 size 200 char 60000020 * section: .rdata va 6fb000 size 291200 char 40000040 * section: .data va 98d000 size b284 char c0000040 * section: .pdata va 999000 size 24400 char 40000040 * section: .didat va 9be000 size 200 char c0000040 * section: .rsrc va 9bf000 size 9600 char 40000040 * section: .reloc va 9c9000 size a600 char 42000040 ... ``` Warbird binaries can contain both data and code sections encrypted. Summary of the encryption rate used with respect to various sections for sample prlib can be seen below: ``` wret> wbinfo -s WBInfoCmd::run Warbird encrypted binary * section: .text va 1000 size 6d6a00 encr_size 580f8a (80%) * section: .rdata va 6fb000 size 291200 encr_size 12821d (45%) * section: .data va 98d000 size b284 encr_size 4aae (41%) ``` In order to support encrypted code and data, Warbird binaries embed data structures (WB segment descriptors) describing encrypted memory portions (segments) of Warbird image along some metadata needed for their handling (decryption or encryption). There are two such base structures 1) WB segment descriptors, which usually describe encrypted data located in `.data` or `.rdata` sections 2) WB heap execute segment descriptors, which describe encrypted code located in `.text` section WB segments descriptors used by sample prlib is shown below: ``` wret> wbinfo -S WBInfoCmd::run Warbird encrypted binary - wb segment desc_va: 0x6d8000 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 223): * [0000] encr_va 6b3d00 size 0000df section .text - wb segment desc_va: 0x6d8100 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 415): * [0000] encr_va 6b3a40 size 00019f section .text - wb segment desc_va: 0x6d8200 size 10c relocs_va: 0x9436f0 num 0x5197 segments (num 2, data size 2306): * [0000] encr_va 6b3120 size 000401 section .text * [0001] encr_va 6b3530 size 000501 section .text - wb segment desc_va: 0x6d830c size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 1590): * [0000] encr_va 656b50 size 000636 section .text - wb segment desc_va: 0x6d840c size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 1607): * [0000] encr_va 657190 size 000647 section .text - wb segment desc_va: 0x6d850c size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 3676): * [0000] encr_va 65a220 size 000e5c section .text - wb segment desc_va: 0x6d8618 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 3568): * [0000] encr_va 65b090 size 000df0 section .text - wb segment desc_va: 0x6d8718 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 213): * [0000] encr_va 6b3df0 size 0000d5 section .text - wb segment desc_va: 0x6d9000 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 260): * [0000] encr_va 6b3bf0 size 000104 section .text - wb segment desc_va: 0x6d9100 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 5246): * [0000] encr_va 6577e0 size 00147e section .text - wb segment desc_va: 0x6d9200 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 198): * [0000] encr_va 6b3050 size 0000c6 section .text - wb segment desc_va: 0x6d9300 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 24955): * [0000] encr_va 6509c0 size 00617b section .text - wb segment desc_va: 0x6d9400 size 100 relocs_va: 0x9436f0 num 0x5197 segments (num 1, data size 37303): * [0000] encr_va 6a19e0 size 0091b7 section .text - wb segment desc_va: 0x6d9500 size 10c relocs_va: 0x9436f0 num 0x5197 segments (num 2, data size 48910): * [0000] encr_va 152b70 size 00beec section .text * [0001] encr_va 7d1a20 size 000022 section .rdata - wb segment ... ``` WB heap execute descriptors for the same lib is listed below: ``` wret> wbinfo -H WBInfoCmd::run Warbird encrypted binary - wb heap exec desc_va: 0x3080 size f0 encr_code: va 0x6bacf0 size 0x17f - wb heap exec desc_va: 0x33c0 size f0 encr_code: va 0x123950 size 0x300 - wb heap exec desc_va: 0x3700 size f0 encr_code: va 0xce7d0 size 0x55c - wb heap exec desc_va: 0x3a40 size f0 encr_code: va 0x1abde0 size 0xd9 - wb heap exec desc_va: 0x3d80 size f0 encr_code: va 0x6c3d50 size 0x19d - wb heap exec desc_va: 0x40c0 size f0 encr_code: va 0x1acd10 size 0x17a - wb heap exec desc_va: 0x4400 size f0 encr_code: va 0x10c580 size 0x103 - wb heap exec desc_va: 0x4740 size f0 encr_code: va 0x1ba61c size 0x319 - wb heap exec desc_va: 0x4a80 size f0 encr_code: va 0x6bde70 size 0x1e0 - wb heap exec desc_va: 0x4dc0 size f0 encr_code: va 0x150100 size 0x106 - wb heap exec desc_va: 0x5100 size f0 encr_code: va 0x15ebb0 size 0x18a - wb heap exec desc_va: 0x5440 size f0 encr_code: va 0x6c0cf0 size 0x3a - wb heap exec desc_va: 0x5780 size f0 encr_code: va 0x1097e0 size 0x306 - wb heap exec desc_va: 0x5ac0 size f0 encr_code: va 0x14b9e0 size 0x1665 - wb heap exec desc_va: 0x5e00 size f0 encr_code: va 0x15ea70 size 0xdb - wb heap exec desc_va: 0x6140 size f0 encr_code: va 0x1a3b90 size 0x1c3 - wb heap exec desc_va: 0x6480 size f0 encr_code: va 0x1523b0 size 0x282 - wb heap exec desc_va: 0x67c0 size f0 encr_code: va 0x178be0 size 0x57c - wb heap exec desc_va: 0x6b00 size f0 encr_code: va 0x14e780 size 0x486 - wb heap exec desc_va: 0x6e40 size f0 encr_code: va 0x6be370 size 0x25b - wb heap exec desc_va: 0x7180 size f0 encr_code: va 0x1482b0 size 0x114 - wb heap exec desc_va: 0x74c0 size f0 encr_code: va 0x1b4560 size 0x7e ... ``` WB segment descriptors can group several memory chunks (segments), which is the case for WB descriptor 0x6d8200 depicted above. Information about target encrypted memory region and its size is denoted by the `encr_va` and `size` props. Further information about WB descriptor such as encryption key / Feistel symmetric algorithm rounds' data can be displayed too: ``` wret> wbdesc -S 0x6d8000 WBDescCmd::run Warbird encrypted binary - wb descriptor digest: 0x00000000: a5 23 6e 27 11 7f d3 e3 d6 8f 72 b5 91 5f ad 87 .#n'......r.._.. 0x00000010: 94 a1 6b ba c5 58 83 8b ce d8 2a ca 82 78 2c ba ..k..X....*..x,. size: 0x100 v1: 0x0 desc_va: 0x6d8000 relocs_va: 0x9436f0 num 0x5197 encr_va: 0x0 num 0x80000000 v2: 0x1 v3: 0x1 v4: 0x1 key: 0x82b8c764000b3d0f 0 Feistel round v1: 0x3 v2: 0x6b v3: 0xb2 v4: 0x11 1 Feistel round v1: 0x2 v2: 0xf5 v3: 0x33 v4: 0x34 2 Feistel round v1: 0xc v2: 0xfd v3: 0xef v4: 0x96 3 Feistel round v1: 0x1e v2: 0xf1 v3: 0x48 v4: 0xd3 4 Feistel round v1: 0x17 v2: 0xc9 v3: 0x15 v4: 0xf4 5 Feistel round v1: 0x4 v2: 0x67 v3: 0xa v4: 0xde 6 Feistel round v1: 0x1 v2: 0x4b v3: 0x5f v4: 0x6c 7 Feistel round v1: 0xf v2: 0xeb v3: 0x41 v4: 0xb1 8 Feistel round v1: 0xb v2: 0x5d v3: 0x7f v4: 0xf2 9 Feistel round v1: 0x13 v2: 0xa8 v3: 0xf6 v4: 0xcf ``` Similarly, WB heap execute descriptors can be inspected in a detail too: ``` wret> wbdesc -H 0x3080 WBDescCmd::run Warbird encrypted binary - wb descriptor digest: 0x00000000: 26 a5 94 8b d5 7a 6f a4 5d 9d f6 43 ad 45 e4 cf &....zo.]..C.E.. 0x00000010: a8 a8 aa 26 86 3f 72 f9 ce 45 97 30 67 cb 83 28 ...&.?r..E.0g..( size: 0xf0 v1: 0xffffff00 desc_va: 0xf0003080 relocs_va: 0x0 num 0xffff00c3 encr_va: 0xf06bacf0 num 0xf000017f v2: 0xf0000000 v3: 0xf0000000 v4: 0xf0000000 key: 0x843cdaecb328ce8a 0 Feistel round v1: 0x8 v2: 0x3f v3: 0xf v4: 0xb 1 Feistel round v1: 0x7 v2: 0x30 v3: 0x9 v4: 0xdb 2 Feistel round v1: 0x17 v2: 0x2e v3: 0x0 v4: 0x36 3 Feistel round v1: 0xf v2: 0x83 v3: 0x44 v4: 0xba 4 Feistel round v1: 0xe v2: 0x51 v3: 0x42 v4: 0x34 5 Feistel round v1: 0x1a v2: 0xa5 v3: 0x48 v4: 0x98 6 Feistel round v1: 0x1e v2: 0xd6 v3: 0x1 v4: 0xdd 7 Feistel round v1: 0xc v2: 0x46 v3: 0xc9 v4: 0x72 8 Feistel round v1: 0xa v2: 0x86 v3: 0x37 v4: 0x1c 9 Feistel round v1: 0x14 v2: 0xe v3: 0xe3 v4: 0xa3 ``` Support for Warbird segments is implemented at the kernel level through `NtQuerySystemInformation` system call (WB system call): ``` #define WB_SYSCALL_NUMBER 0x36 ``` and dedicated system information call: ``` #define WB_SYSTEM_INFORMATION_CLASS 0xb9 ``` A pointer to the following structure specifies arguments for target WB call: ``` typedef struct { DWORD64 op; union { DecryptEncryptionSegment_args_t decrypt; ReencryptEncryptionSegment_args_t reencrypt; RemoveProcess_args_t remove; ProcessStartup_args_t startup; }; } WarbirdCallArgs_t; ``` The following Warbird operations are supported: ``` typedef enum { WbDecryptEncryptionSegment=1, WbReEncryptEncryptionSegment, WbHeapExecuteCall, WbSetTrapFrame, WbInvalidOp1, WbInvalidOp2, WbRemoveWarbirdProcess, WbProcessStartup, WbProcessModuleUnload } wb_call_t; ``` It looks all data needed to handle encrypted WB segments are contained in a target segment itself. As such it should be possible to decrypt arbitrary WB segments as long as details of the Feistel algorithm implemented at kernel side is reverse engineered (such as on a non-Windows system). A more straightforward approach has been taken by us though as the kernel provides functionality to both decrypt and reencrypt WB segments. Such a decryption is illustrated below for sample WB segment: ``` - wb segment desc_va: 0x6f9000 size 10c relocs_va: 0x9436f0 num 0x5197 segments (num 2, data size 605): * [0000] encr_va 5a1b60 size 000087 section .text * [0001] encr_va 5a1bf0 size 0001d6 section .text ``` Below, content of its encrypted memory is shown (encrypted code): ``` wret> dmem 0x5a1bf0 DmemCmd::run va 5a1bf0 0x00000000: a3 ce 76 4c 0f 61 2b 24 3f 1e 5b 38 ef 7b 06 52 ..vL.a+$?.[8.{.R 0x00000010: f4 58 19 1b 32 ba 88 5c 89 44 6c d2 b9 9e a5 df .X..2..\.Dl..... 0x00000020: eb 3e dd 2c 7e 58 d3 1a e7 00 35 2e 01 fc 59 b1 .>.,~X....5...Y. 0x00000030: 79 95 e8 3f c0 9e 04 0a a8 6f 2b 63 8f c8 de ea y..?.....o+c.... 0x00000040: 6b f0 51 7e 5c 90 25 ee 29 3d 49 df 5e c8 4c 27 k.Q~\.%.)=I.^.L' 0x00000050: 40 7f 04 27 cd ef 0c d1 76 3a 49 af 91 f8 c9 91 @..'....v:I..... 0x00000060: 6f 09 b4 a7 b0 3e 1b 3b 08 09 d4 84 c0 f6 5d 71 o....>.;......]q 0x00000070: 06 c8 93 50 b7 93 fa 0d 52 fd 14 a1 21 7e 96 a4 ...P....R...!~.. 0x00000080: 70 8d 6d d4 84 2c d9 c3 3c 62 e9 7e 12 55 13 0f p.m..,.. wbop -d 0x5a1b60 -v WBOpCmd::run - wb segment digest: 0x00000000: 96 35 e0 86 ca d1 dc b2 7d 21 3b 7d 23 98 4c 48 .5......}!;}#.LH 0x00000010: 62 1e 12 36 cc d7 6f b9 2d 9b 46 e1 b0 c6 ee cd b..6..o.-.F..... desc_va: 0x6f9000 size 10c relocs_va: 0x9436f0 num 0x5197 key: 0x2ad255175333d38b segments (num 2, data size 605): * [0000] encr_va 5a1b60 size 000087 section .text flags 30c|READWRITE|WRITECOPY|GUARD|NOCACHE * [0001] encr_va 5a1bf0 size 0001d6 section .text flags 30c|READWRITE|WRITECOPY|GUARD|NOCACHE - WarbirdCallArgs * op: WbDecryptEncryptionSegment * segdesc: 7ffd2ca39000 * image_base1: 7ffd2c340000 * image_base2: 7ffd2c340000 * relocs: 7ffd2cc836f0 * relocs_num: 5197 - NtQuerySystemInformation res: 0 - segment status: CHANGED decrypted ``` It can be verified that the content of memory got changed as a result of the op (it got decrypted). ``` wret> dmem 0x5a1b60 DmemCmd::run va 5a1b60 0x00000000: 19 91 16 4e a3 bf 6a e0 1a 02 76 30 44 75 25 f4 ...N..j...v0Du%. 0x00000010: d0 40 37 2d 8c 85 95 88 78 d3 91 8f b0 38 c9 fc .@7-....x....8.. 0x00000020: b3 a5 ac 0d 65 df 0c b0 88 82 3c d7 8c 68 bd 7e ....e.....<..h.~ 0x00000030: 08 51 19 6e 48 2d 64 31 89 da ec 72 38 dc 5e c0 .Q.nH-d1...r8.^. 0x00000040: c1 24 71 4f e1 e9 3b da c3 8b 45 eb 1f ef 9f b1 .$qO..;...E..... 0x00000050: f7 55 18 53 47 ed 44 6f 62 71 2c 93 f9 79 af c9 .U.SG.Dobq,..y.. 0x00000060: 77 f6 cc d3 03 47 41 29 b1 51 21 35 44 1c 30 80 w....GA).Q!5D.0. 0x00000070: b4 ba e9 58 39 ea 91 65 4e 24 0c 55 43 b6 c3 02 ...X9..eN$.UC... 0x00000080: 94 6d 2d 39 39 6e 70 cc cc cc cc cc cc cc cc cc .m-99np......... 0x00000090: a3 ce 76 4c 0f 61 2b 24 3f 1e 5b 38 ef 7b 06 52 ..vL.a+$?.[8.{.R 0x000000a0: f4 58 19 1b 32 ba 88 5c 89 44 6c d2 b9 9e a5 df .X..2..\.Dl..... 0x000000b0: eb 3e dd 2c 7e 58 d3 1a e7 00 35 2e 01 fc 59 b1 .>.,~X....5...Y. 0x000000c0: 79 95 e8 3f c0 9e 04 0a a8 6f 2b 63 8f c8 de ea y..?.....o+c.... 0x000000d0: 6b f0 51 7e 5c 90 25 ee 29 3d 49 df 5e c8 4c 27 k.Q~\.%.)=I.^.L' 0x000000e0: 40 7f 04 27 cd ef 0c d1 76 3a 49 af 91 f8 c9 91 @..'....v:I..... 0x000000f0: 6f 09 b4 a7 b0 3e 1b 3b 08 09 d4 84 c0 f6 5d 71 o....>.;......]q ``` Decryption of heap execute segments is not that straightforward. Upon a WB system call, the following happens at kernel side: - a dynamic memory block gets allocated for a user process and encrypted code block is decrypted into it, the block gets marked as no-write (exec only) - registers and stack gets filled with arguments provided through WB system call structure, this is done in a special way though: * top of the stack doesn't contain the usual return address (`[rsp]`), but a pointer to data structure containing ptr to subroutine result and its arguments * the `rcx` is loaded with this pointer at the beginning of WB heap exec call * subroutine arguments do not follow the usual arguments passing convention, the arguments are references through `rcx` pointer * subroutine arguments are put into a contiguous block (they are serialized), as a result they may occupy different offsets (with respect to `rcx` for different subroutines, pointers might not be aligned) - the decrypted subroutine call is executed through the APC (Asynchronous Procedure Call) mechanism (the call to `PspSetContextThreadInternal` is used for the setup), queued APC is executed prior to user thread's resuming execution (prior to syscall return) - subroutine result (if any) gets stored to `[rcx]` memory location - upon call completion, return from WB system call is done. The above construction is done for the following reason: - to make traps / breakpoints setting difficult (target calls are executed at semi-random addresses) - hijacking of WB heap execution calls becomes problematic (there is ptr to args in place of return addr) - everything happens in one shot / semi-atomic way - the call is seen as if it has been executed by WB system call. We have found a way to decrypt heap execute segments though. One can simply do the following: - change the valid WB segment descriptor and point it to heap execute descriptor (mimic encrypted heap execute WB segment as if it was a pure WB segment), - compute the SHA256 digest to make any changes conducted to the WB segment look legitimate to the kernel - issue WbDecryptEncryptionSegment WB system call op. The implementation of this is shown below for the following heap exec segment: ``` - wb heap exec desc_va: 0x3080 size f0 encr_code: va 0x6bacf0 size 0x17f wret> wbop -d 0x6bacf0 -v WBOpCmd::run ORG CUSTDESC 0x00000000: fd bb 33 d3 a2 c0 1d 7d a9 36 be 94 f9 23 c2 68 ..3....}.6...#.h 0x00000010: 32 00 22 79 28 d3 28 dd bc bb 97 2b 25 36 96 85 2."y(.(....+%6.. 0x00000020: 00 01 00 00 00 00 00 00 00 81 6d 00 f0 36 94 00 ..........m..6.. 0x00000030: 97 51 00 00 00 00 00 00 00 00 00 80 01 00 00 00 .Q.............. 0x00000040: a1 00 00 00 00 00 00 00 85 42 bd 41 c6 55 87 6a .........B.A.U.j 0x00000050: 0c 00 00 00 69 00 00 00 67 00 00 00 d5 00 00 00 ....i...g....... 0x00000060: 13 00 00 00 92 00 00 00 de 00 00 00 dc 00 00 00 ................ 0x00000070: 01 00 00 00 81 00 00 00 11 00 00 00 39 00 00 00 ............9... 0x00000080: 11 00 00 00 17 00 00 00 45 00 00 00 2a 00 00 00 ........E...*... 0x00000090: 1d 00 00 00 60 00 00 00 21 00 00 00 0f 00 00 00 ....`...!....... 0x000000a0: 10 00 00 00 5f 00 00 00 a6 00 00 00 d8 00 00 00 ...._........... 0x000000b0: 16 00 00 00 1e 00 00 00 55 00 00 00 ef 00 00 00 ........U....... 0x000000c0: 07 00 00 00 ad 00 00 00 05 00 00 00 09 00 00 00 ................ 0x000000d0: 18 00 00 00 27 00 00 00 7d 00 00 00 dd 00 00 00 ....'...}....... 0x000000e0: 0e 00 00 00 d4 00 00 00 ca 00 00 00 4f 00 00 00 ............O... 0x000000f0: 01 00 00 00 0c 03 00 00 40 3a 6b 00 9f 01 00 00 ........@:k..... BEFORE DECRYPTION 6bacf0 0x00000000: 9d a4 f6 33 08 80 2c 6b 48 06 5d 65 47 e1 87 c2 ...3..,kH.]eG... 0x00000010: 73 4e db b7 85 dd d5 a8 94 86 3b 41 b7 5c f7 1e sN........;A.\.. 0x00000020: 76 1e a6 14 51 79 31 ab 1a 68 48 c5 e5 b4 ec 5e v...Qy1..hH....^ 0x00000030: de 5f da 99 7d 40 3f 5f fa e0 a6 fe 44 3b 38 fc ._..}@?_....D;8. 0x00000040: e6 e2 e4 ab 60 ab e1 74 1e 1b f6 60 0e 4a e2 a5 ....`..t...`.J.. 0x00000050: 94 2f ef 6d 84 84 4d 52 75 bf 4b ac 6a 0f ee 4d ./.m..MRu.K.j..M 0x00000060: 37 47 77 b4 3f 63 f7 03 23 5e da 8d be bc 2f 23 7Gw.?c..#^..../# 0x00000070: 5e ee b4 e9 13 fd 08 e9 7f 14 16 0e 49 03 cf 4a ^...........I..J 0x00000080: 67 8b cc ce 28 9a 6b bd 4c 23 8d 36 d0 9b 5d 26 g...(.k.L#.6..]& 0x00000090: 45 e6 cd 34 c3 87 76 17 29 b8 86 ab a8 e9 57 d2 E..4..v.).....W. 0x000000a0: bd da 3e 0b c2 d1 fa 56 5f 46 60 b1 4a e1 10 55 ..>....V_F`.J..U 0x000000b0: a6 47 9d 5b 63 df f2 e4 5e 92 99 35 44 73 00 f2 .G.[c...^..5Ds.. 0x000000c0: f1 50 5c d7 af fa cb 9e b3 7c 22 4f f4 d6 7e 14 .P\......|"O..~. 0x000000d0: f6 59 6d 8c 54 f0 86 af e4 3f 27 9d 01 22 3e 9a .Ym.T....?'..">. 0x000000e0: 23 81 30 33 a9 2e eb fd 9b 19 6f b0 4f d5 33 ff #.03......o.O.3. 0x000000f0: 19 99 fb 7a 95 56 8e ae 3c 97 ac 0e d0 ad fe 91 ...z.V..<....... 0x00000100: ec e1 26 1b e6 e5 40 70 b6 4a ac 8f 1e 11 be 00 ..&...@p.J...... 0x00000110: 50 47 8c 57 c1 fe e6 4d d8 26 be 23 ca b0 16 48 PG.W...M.&.#...H 0x00000120: 7b c5 59 a0 9f 59 81 3c 6b 6f cc 3e 07 6e 41 c9 {.Y..Y..nA. 0x00000130: 9a 33 2e b6 c9 ec 4b ca e5 0d 05 aa 43 d5 0f a0 .3....K.....C... 0x00000140: 40 d5 79 3b 8b 80 f4 6c 60 55 eb 00 dd 2a 31 66 @.y;...l`U...*1f 0x00000150: 92 b7 51 c7 1d f9 3f 60 5f 22 fe ee 3b 3b 05 21 ..Q...?`_"..;;.! 0x00000160: 85 bf 90 72 33 4e c1 4c 72 67 4f c7 31 b4 99 9f ...r3N.LrgO.1... 0x00000170: b1 20 ca 8a 09 5c ab 0d 4b da 8c 30 37 83 9f . ...\..K..07.. FAKE CUSTDESC 0x00000000: bd d8 c4 24 a8 2d 15 03 1f 95 96 c4 99 ad 0f 75 ...$.-.........u 0x00000010: 9a e2 74 64 3b f3 89 20 8c 78 eb a4 50 fd 68 fa ..td;.. .x..P.h. 0x00000020: 00 01 00 00 00 00 00 00 00 81 6d 00 f0 36 94 00 ..........m..6.. 0x00000030: 97 51 00 00 00 00 00 00 00 00 00 80 01 00 00 00 .Q.............. 0x00000040: 00 00 00 f0 00 00 00 00 8a ce 28 b3 ec da 3c 84 ..........(...<. 0x00000050: 08 00 00 00 3f 00 00 00 0f 00 00 00 0b 00 00 00 ....?........... 0x00000060: 07 00 00 00 30 00 00 00 09 00 00 00 db 00 00 00 ....0........... 0x00000070: 17 00 00 00 2e 00 00 00 00 00 00 00 36 00 00 00 ............6... 0x00000080: 0f 00 00 00 83 00 00 00 44 00 00 00 ba 00 00 00 ........D....... 0x00000090: 0e 00 00 00 51 00 00 00 42 00 00 00 34 00 00 00 ....Q...B...4... 0x000000a0: 1a 00 00 00 a5 00 00 00 48 00 00 00 98 00 00 00 ........H....... 0x000000b0: 1e 00 00 00 d6 00 00 00 01 00 00 00 dd 00 00 00 ................ 0x000000c0: 0c 00 00 00 46 00 00 00 c9 00 00 00 72 00 00 00 ....F.......r... 0x000000d0: 0a 00 00 00 86 00 00 00 37 00 00 00 1c 00 00 00 ........7....... 0x000000e0: 14 00 00 00 0e 00 00 00 e3 00 00 00 a3 00 00 00 ................ 0x000000f0: 01 00 00 00 0c 03 00 00 f0 ac 6b 00 7f 01 00 00 ..........k..... AFTER DECRYPTION 6bacf0 0x00000000: 48 8d 15 f9 ff ff ff 48 8b 0c 24 48 89 5c 24 10 H......H..$H.\$. 0x00000010: 48 89 6c 24 18 56 57 41 54 41 56 41 57 48 83 ec H.l$.VWATAVAWH.. 0x00000020: 60 4c 8b 79 18 48 8b ea 4c 8b 61 10 48 8b 79 08 `L.y.H..L.a.H.y. 0x00000030: 4c 8b 31 48 8b 5a f0 48 8d 05 92 57 a1 ff 48 8b L.1H.Z.H...W..H. 0x00000040: 72 f8 48 8b cf 48 8d 15 a4 b1 10 00 48 03 c3 48 r.H..H......H..H 0x00000050: 03 d3 ff d0 85 c0 75 41 48 8d 15 81 b1 10 00 48 ......uAH......H 0x00000060: 8b cf 48 8d 05 67 57 a1 ff 48 03 d3 48 03 c3 ff ..H..gW..H..H... 0x00000070: d0 85 c0 75 24 48 8d 15 6c 32 10 00 48 8b cf 48 ...u$H..l2..H..H 0x00000080: 8d 05 4a 57 a1 ff 48 03 d3 48 03 c3 ff d0 85 c0 ..JW..H..H...... 0x00000090: 75 07 bf 01 40 00 80 eb 54 48 89 7c 24 48 48 8d u...@...TH.|$HH. 0x000000a0: 05 6b 89 94 ff 48 03 c3 48 c7 44 24 30 03 00 00 .k...H..H.D$0... 0x000000b0: 00 48 89 44 24 38 48 8d 54 24 30 48 8d 84 24 90 .H.D$8H.T$0H..$. 0x000000c0: 00 00 00 4c 89 64 24 50 48 89 44 24 40 41 ba b9 ...L.d$PH.D$@A.. 0x000000d0: 00 00 00 48 8b c6 4c 89 7c 24 58 41 b8 30 00 00 ...H..L.|$XA.0.. 0x000000e0: 00 45 33 c9 0f 05 8b bc 24 90 00 00 00 48 8d 05 .E3.....$....H.. 0x000000f0: 2c 2b 2d 00 48 03 c3 48 8d 0d 22 2b 2d 00 48 39 ,+-.H..H.."+-.H9 0x00000100: 04 0b 74 47 85 ff 79 43 48 8d 05 11 2b 2d 00 48 ..tG..yCH...+-.H 0x00000110: 8b 04 03 f6 40 1c 08 74 32 48 8d 0d 00 2b 2d 00 ....@..t2H...+-. 0x00000120: 89 7c 24 20 48 8b 0c 0b 4c 8d 05 c9 31 10 00 4c .|$ H...L...1..L 0x00000130: 8d 15 1a fe a0 ff ba 0f 00 00 00 44 8b cf 4c 03 ...........D..L. 0x00000140: c3 4c 03 d3 48 8b 49 10 41 ff d2 4c 8d 5c 24 60 .L..H.I.A..L.\$` 0x00000150: 41 89 3e 49 8b 5b 38 48 8b c6 49 8b 6b 40 49 8b A.>I.[8H..I.k@I. 0x00000160: e3 41 5f 41 5e 41 5c 5f 5e 49 ba b9 00 00 00 00 .A_A^A\_^I...... 0x00000170: 00 00 00 48 33 d2 4d 33 c0 4d 33 c9 0f 05 c3 ...H3.M3.M3.... WB PROLOG FOUND decrypted ``` It's worth to note that some other issues could be potentially exploited for a successful WB heap execute calls' decryption / hijacking such as the following: 1) executing WB heap execute calls with all register content set to invalid values (invalid pointer values in particular), as a result of accessing memory through the invalid pointer a trap is raised (as early as possible), user thread is stopped and WB heap exec segment is in a decrypted state 2) reusing the cached (kernel side LRU list) WB heap exec segments, some early tests indicated same (predictable) segments' uses (user level adresses) for wb heap exec segments, this makes it possible to set debug breakpoint (such as INT3) at the start of the execution of a target heap exec code (stop execution of a decrypted heap exec code) ## STATIC AND DYNAMIC ANALYSIS SETUP Generic ability to decrypt any Warbird segment constitutes a base for whole image decryption. WRET tool contains support for such a decryption. It also makes it possible to prepare Warbird binaries for static or dynamic analysis. The following illustrates preparation of arbitrary PlayReady binary for static analysis: ``` wret> image w10_prlib.dll ImageCmd::run - Dll init w10_prlib.dll size 10347408 bytes base 180000000 wret> prlib w10_prlib.dll PRLib::run wret>\ wret> wbsetup -s WBSetupCmd::run Warbird encrypted binary ### setting up Warbird for static analysis - scanning for heap exec descriptors found: 981 - scanning for segment descriptors found: 37 - decrypting heap exec descriptors - decrypting segment descriptors - adjusting relocations total: 402 - adjusting code refs total: 1470 - adjusting data refs total: 0 wret> save w10_prlib_static.dll SaveCmd::run - saving image [w10_prlib_static.dll] ``` The following illustrates preparation of PlayReady binary for dynamic analysis: ``` wret> image w10_prlib.dll ImageCmd::run - Dll init w10_prlib.dll size 10347408 bytes base 180000000 wret> prlib w10_prlib.dll PRLib::run wret> wbsetup -r WBSetupCmd::run Warbird encrypted binary ### setting up Warbird for runtime analysys - scanning for heap exec descriptors found: 981 - scanning for segment descriptors found: 37 - decrypting heapexec descriptors - decrypting segment descriptors - locating ret syscalls total: 1023 - locating heapexec syscalls total: 911 (WB_MOV10_B9 880, WB_MOV10_REG 10, WB_LEA10 3, WB_SHARED 18) - adjusting code refs total: 1470 - adjusting data refs total: 2 - patching self LEAs total: 981 - patching ret syscalls - patching heapexec syscalls - patching wb call - patching antidebug call - patching App Policy - Dll init Windows.Media.dll size 7145640 bytes base 180000000 - patching App Model - Dll init Windows.Storage.ApplicationData.dll size 435248 bytes base 180000000 - disabling thread library calls wret> save w10_prlib_dynamic.dll SaveCmd::run - saving image [w10_prlib_dynamic.dll] wret> ``` Both processes are a little bit more complex due to the following: - WB heap execute calls can embed (issue) other WB system calls (recursive WB calls, nested calls make use of SYSCALL instead of `NtQuerySystemInformation` call, there is a dedicated WB SYSCALL used to mark return from the call) - the assumption that PlayReady runs as App Container, which requires some App Policy to be adjusted too (we mimic the code runs in App Container of MSEdge application through the instrumentation of `GetCurrentPackageId`, `GetCurrentApplicationUserModelId` and `GetCurrentPackageFamilyName` calls in particular). The static analysis changes offsets used by WB heap execute calls and denoting WB segments. The original code such as this one: ``` .text:00000001800CE770 public MSPRMFGetClassObject .text:00000001800CE770 MSPRMFGetClassObject proc near ; DATA XREF: .rdata:000000018073821C↓o .text:00000001800CE770 ; .rdata:off_180988FE8↓o ... .text:00000001800CE770 .text:00000001800CE770 arg_0 = dword ptr 8 .text:00000001800CE770 .text:00000001800CE770 mov r11, rsp .text:00000001800CE773 sub rsp, 58h .text:00000001800CE777 xor r9d, r9d ; ReturnLength .text:00000001800CE77A mov [r11-20h], rcx .text:00000001800CE77E lea rax, qword_180003080 <---- PTR to WB segment descriptor .text:00000001800CE785 mov [r11-18h], rdx .text:00000001800CE789 mov [r11-30h], rax .text:00000001800CE78D lea rdx, [r11-38h] ; SystemInformation .text:00000001800CE791 lea rax, [r11+8] .text:00000001800CE795 mov [r11-10h], r8 .text:00000001800CE799 lea r8d, [r9+30h] ; SystemInformationLength .text:00000001800CE79D mov qword ptr [r11-38h], 3 .text:00000001800CE7A5 mov ecx, 0B9h ; SystemInformationClass .text:00000001800CE7AA mov [r11-28h], rax .text:00000001800CE7AE call NtQuerySystemInformation .text:00000001800CE7B3 mov eax, [rsp+58h+arg_0] .text:00000001800CE7B7 add rsp, 58h .text:00000001800CE7BB retn .text:00000001800CE7BB MSPRMFGetClassObject endp .text:0000000180003080 qword_180003080 dq 0A46F7AD58B94A526h, 0CFE445AD43F69D5Dh, 0F9723F8626AAA8A8h .text:0000000180003080 ; DATA XREF: MSPRMFGetClassObject+E↓o .text:0000000180003080 dq 2883CB67309745CEh, 0FFFFFF00000000F0h, 0F0003080h, 0F06BACF0FFFF00C3h .text:0000000180003080 dq 0F0000000F000017Fh, 0FFFFFFFFF0000000h, 843CDAECB328CE8Ah .text:0000000180003080 dq 3F00000008h, 0B0000000Fh, 3000000007h, 0DB00000009h .text:0000000180003080 dq 2E00000017h, 3600000000h, 830000000Fh, 0BA00000044h .text:0000000180003080 dq 510000000Eh, 3400000042h, 0A50000001Ah, 9800000048h ``` is replaced with the following sequence, which has WB segment pointers resolved so that they point to actual code (decrypted one, provided as argument to `LEA` instruction): ``` .text:00000001800CE770 MSPRMFGetClassObject proc near ; DATA XREF: .rdata:000000018073821C↓o .text:00000001800CE770 ; .rdata:off_180988FE8↓o .text:00000001800CE770 .text:00000001800CE770 arg_0 = dword ptr 8 .text:00000001800CE770 .text:00000001800CE770 mov r11, rsp .text:00000001800CE773 sub rsp, 58h .text:00000001800CE777 xor r9d, r9d ; ReturnLength .text:00000001800CE77A mov [r11-20h], rcx .text:00000001800CE77E lea rax, sub_1806BACF0 <---- PTR to actual code executed by WB syscall .text:00000001800CE785 mov [r11-18h], rdx .text:00000001800CE789 mov [r11-30h], rax .text:00000001800CE78D lea rdx, [r11-38h] ; SystemInformation .text:00000001800CE791 lea rax, [r11+8] .text:00000001800CE795 mov [r11-10h], r8 .text:00000001800CE799 lea r8d, [r9+30h] ; SystemInformationLength .text:00000001800CE79D mov qword ptr [r11-38h], 3 .text:00000001800CE7A5 mov ecx, 0B9h ; SystemInformationClass .text:00000001800CE7AA mov [r11-28h], rax .text:00000001800CE7AE call NtQuerySystemInformation .text:00000001800CE7B3 mov eax, [rsp+58h+arg_0] .text:00000001800CE7B7 add rsp, 58h .text:00000001800CE7BB retn .text:00000001800CE7BB MSPRMFGetClassObject endp ``` The above allows to restore the code flow obscured through WB syscalls (such as call hierarchy, subroutine uses, etc.). For dynamic analysis, after decrypting all WB segments (data and code related), all invocations of WB system calls encountered in the code gets patched. This involves patching `NtQuerySystemInformation` call (patch of IAT entry) along the syscall opcode used for embedded WB calls. This also requires some instruction reordering / fitting due to the need to make place for the call instruction transferring execution to common WB glue code (2 bytes long `syscall` instruction is replaced with 5 bytes long `call` one). This faces some challenges due to insufficient space in target code blocks, shared code blocks (single WB invocations used by more than one code path), various registers uses, etc. Without going into the details, it is sufficient to say that the dynamic analysis is accomplished through a common WB call glue code, which: 1) does nothing for segment encrypt / decrypt operations (no need to as these got decrypted as part of the setup process) 2) setups / restores stack frame and registers for WB heap execute calls as if the call has been executed by the OS kernel (thus the "glue" name). Fragment of a main glue code and its "pass through" nature is shown below: ``` ... ;Warbird calls WbDecryptEncryptionSegment EQU 1 WbReEncryptEncryptionSegment EQU 2 WbHeapExecuteCall EQU 3 WbSetTrapFrame EQU 4 WbInvalidOp1 EQU 5 WbInvalidOp2 EQU 6 WbRemoveWarbirdProcess EQU 7 WbProcessStartup EQU 8 WbProcessModuleUnload EQU 9 ... asm_wb_glue_syscall proc frame OPTION PROLOGUE:NONE, EPILOGUE:NONE .endprolog ALLOC_SPARGS SAVE_REGS ... cmp rdx,0 je _heap_exec_call_ret cmp qword ptr [rdx],WbDecryptEncryptionSegment je _DecryptEncryptionSegment cmp qword ptr [rdx],WbReEncryptEncryptionSegment je _ReEncryptEncryptionSegment cmp qword ptr [rdx],WbHeapExecuteCall je _HeapExecuteCall cmp qword ptr [rdx],WbSetTrapFrame je _SetTrapFrame cmp qword ptr [rdx],WbInvalidOp1 je _InvalidOp1 cmp qword ptr [rdx],WbInvalidOp2 je _InvalidOp2 cmp qword ptr [rdx],WbRemoveWarbirdProcess je _RemoveWarbirdProcess cmp qword ptr [rdx],WbProcessStartup je _ProcessStartup cmp qword ptr [rdx],WbProcessModuleUnload je _ProcessModuleUnload _DecryptEncryptionSegment: DO_OK_RET _ReEncryptEncryptionSegment: DO_OK_RET _HeapExecuteCall: ;save rdx value (for return result store) ; push rdx sub rsp,GLUE_STACK_SIZE ;setup [rsp] to point to result ptr and args ; lea rax,qword ptr [rdx+10h] lea rax,(WB_HEAPEXEC_CALL ptr [rdx])._res_ptr mov qword ptr [rsp],rax ;load target sub addr mov rax,(WB_HEAPEXEC_CALL ptr [rdx])._sub_addr ;call target sub jmp rax _heap_exec_call_ret: ;skip ret addr pushed by the call to ret from heap exec handler add rsp,8 add rsp,GLUE_STACK_SIZE ;restore rdx value ; pop rdx DO_OK_RET _SetTrapFrame: DO_OK_RET _InvalidOp1: DO_OK_RET _InvalidOp2: DO_OK_RET _RemoveWarbirdProcess: DO_OK_RET _ProcessStartup: DO_OK_RET _ProcessModuleUnload: DO_OK_RET ``` The dynamic analysis makes it possible to trace execution of PlayReady library as a response to various actions such as activation / individualization or license request. It also makes it possible to call arbitrary code such as COM or WB subroutines in particular. ## COM information PlayReady implementation heavily relies on Component Object Model. While there are some public COM objects, some are not. WRET tool makes it possible to extract COM information from target binary: ``` wret> cominfo -m COMInfoCmd::run [COMModuleBase] - destructor_va cf980 - IncrementObjectCount_va cf6e0 - DecrementObjectCount_va cf6c0 - GetObjectCount_va cdb60 - GetFirstEntryPointer_va cdb70 - GetMidEntryPointer_va cdb80 - GetLastEntryPointer_va cdb90 - GetLock_va cdba0 - RegisterWinRTObject_va cf690 - UnregisterWinRTObject_va cf690 - RegisterCOMObject_va cf690 - UnregisterCOMObject_va cf690 ### COM classes ### WinRT classes * [Microsoft.Media.PlayReadyClient.NDStreamParserNotifier] * [Microsoft.Media.PlayReadyClient.NDDownloadEngineNotifier] * [Microsoft.Media.PlayReadyClient.NDStorageFileHelper] * [Microsoft.Media.PlayReadyClient.PRRemoteObjectFactory] * [Microsoft.Media.PlayReadyClient.PlayReadyITADataGenerator] * [com.microsoft.playready.ContentDecryptionModuleFactory] * [com.microsoft.playready.software.CdmFactory] * [com.microsoft.playready.hardware.CdmFactory] * [com.microsoft.playready.CdmFactory] * [Microsoft.Media.PlayReadyClient.PlayReadyWinRTTrustedInput] * [Microsoft.Media.PlayReadyClient.PlayReadyRevocationServiceRequestReactive] * [Microsoft.Media.PlayReadyClient.PlayReadyRevocationServiceRequest] * [Microsoft.Media.PlayReadyClient.PlayReadyMeteringReportServiceRequestReactive] * [Microsoft.Media.PlayReadyClient.PlayReadyMeteringReportServiceRequest] * [Microsoft.Media.PlayReadyClient.PlayReadyDomainLeaveServiceRequestReactive] * [Microsoft.Media.PlayReadyClient.PlayReadyDomainLeaveServiceRequest] ... ``` It can extract some information about COM interfaces and their factories too: ``` wret> cominfo -f ### WinRT factories [Microsoft.Media.PlayReadyClient.NDStreamParserNotifier-Factory] * initializer e3a40 * trust level BaseTrust * interfaces: if 6fd620 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [Microsoft.Media.PlayReadyClient.NDDownloadEngineNotifier-Factory] * initializer e3950 * trust level BaseTrust * interfaces: if 6fd660 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [Microsoft.Media.PlayReadyClient.NDStorageFileHelper-Factory] * initializer e3860 * trust level BaseTrust * interfaces: if 6fd6a0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [Microsoft.Media.PlayReadyClient.PRRemoteObjectFactory-Factory] * initializer e3770 * trust level BaseTrust * interfaces: if 6fd6e0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [Microsoft.Media.PlayReadyClient.PlayReadyITADataGenerator-Factory] * initializer e3680 * trust level BaseTrust * interfaces: if 6fd720 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [com.microsoft.playready.ContentDecryptionModuleFactory-Factory] * initializer e3590 * trust level BaseTrust * interfaces: if 6fd760 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [com.microsoft.playready.software.CdmFactory-Factory] * initializer e34a0 * trust level BaseTrust * interfaces: if 6fd7a0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [com.microsoft.playready.hardware.CdmFactory-Factory] * initializer e33b0 * trust level BaseTrust * interfaces: if 6fd7e0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [com.microsoft.playready.CdmFactory-Factory] * initializer e32c0 * trust level BaseTrust * interfaces: if 6fd820 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: [Microsoft.Media.PlayReadyClient.PlayReadyWinRTTrustedInput-Factory] * initializer e31d0 * trust level BaseTrust * interfaces: if 6fd860 * uuid 00000035-0000-0000-c000-000000000046 * off 0 instances: ... [Microsoft.Media.PlayReadyClient.PlayReadyContentHeader-Factory] * initializer e1580 * trust level BaseTrust * override interfaces: if 6fc1b0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 if 6fc178 * uuid d1239cf5-ae6d-4778-97fd-6e3a2eeadbeb * off 8 if 6fc130 * uuid cb97c8ff-b758-4776-bf01-217a8b510b2c * off 10 if 6fc0f8 * uuid d0d7d187-d16c-4165-b39c-b93c423e88e3 * off 20 if 6fc0b0 * uuid 7de8418e-efed-4eda-ab18-9ae5bf00a069 * off 28 * interfaces: if 6fe5c0 * uuid 00000035-0000-0000-c000-000000000046 * off 0 if 6fe588 * uuid d1239cf5-ae6d-4778-97fd-6e3a2eeadbeb * off 8 if 6fe540 * uuid cb97c8ff-b758-4776-bf01-217a8b510b2c * off 10 if 6fe508 * uuid d0d7d187-d16c-4165-b39c-b93c423e88e3 * off 20 if 6fe4c0 * uuid 7de8418e-efed-4eda-ab18-9ae5bf00a069 * off 28 instances: ``` ## LOGGING / TRACING AND HANDLERS WRET tool makes it possible to show the trace of executed subroutines (log their nesting, arguments used, etc.). For standard API calls, the arguments can be described through loading of header file information (more specificallt, preprocessed header). WRET makes it possible to either change or handle arbitrary subroutine calls in in a special way. This is accomplished through the idea of call handlers. The following handlers are currently supported: - `Logger` It makes it possible to log calls with various configuration options (with the possibility to select arbitrary arguments to show, select the types of calls to show in the trace such as WB calls, symbol / API calls (such as imports / delayed imports), any near calls, whether call nesting should be visible - `SupressActHandler` It makes it possible to suppress activation (make PlayReady believe that it is activated / individualization is not needed), this is important if security / reverse analysis is conducted in an offline manner (our company policy) - `SupressLicHandler` It makes it possible to suppress certain license server errors, so that the license response processing doesn't stop early (such as upon invalid signature, expired license, etc.) - `PrvKeyExtractor` The handler executed for the purpose of extracting private ECC encryption key. ### SAMPLE TRACE SETUP The following steps can be used to setup a sample trace for a PlayReady `getlicense` request communication with a test license server. Starting sample license server: ``` c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\test_ls>run ## Local HTTP Server # ## (c) SECURITY EXPLORATIONS 2018 Poland # ## http://www.security-explorations.com # ## (c) AG Security Research 2019-2022 Poland # ## http://www.agsecurityresearch.com # ## # loaded server key from file LICENSE SERVER ECC KEY - prv: efa20855ae243ce098b4f6382fb8b703fdb9f7c567f5c0e003c9c13eae6a9001 - pub: X: 1f3c71ab5aa90dcc1c192c99eb21b13454fa47fa107a8278e5344f4600613a1 Y: 59b0c0ad7fe7fcb02ea879a7c586ce616036e46defbc8fce6d54bda8dfe09fa2 HTTP server [class HTTPServer$SEServer] listening on: 8080 ``` Starting WRET toolkit: ``` c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\wret_toolkit>out\test.exe main argc: 1 container: none # Warbird RE toolkit (WRET) # (c) Security Explorations 2016-2024 Poland # (c) AG Security Research 2019-2024 Poland wret> ``` Setting up target PlayReady library for dynamic analysis (in another command shell window): ``` wret> image w10_prlib.dll ImageCmd::run - Dll init w10_prlib.dll size 10347408 bytes base 180000000 wret> prlib w10_prlib.dll PRLib::run wret> wbsetup -r WBSetupCmd::run Warbird encrypted binary ### setting up Warbird for runtime analysys - scanning for heap exec descriptors found: 981 - scanning for segment descriptors found: 37 - decrypting heapexec descriptors - decrypting segment descriptors - locating ret syscalls total: 1023 - locating heapexec syscalls total: 911 (WB_MOV10_B9 880, WB_MOV10_REG 10, WB_LEA10 3, WB_SHARED 18) - adjusting code refs total: 1470 - adjusting data refs total: 2 - patching self LEAs total: 981 - patching ret syscalls - patching heapexec syscalls - patching wb call - patching antidebug call - patching App Policy - Dll init Windows.Media.dll size 7145640 bytes base 180000000 - patching App Model - Dll init Windows.Storage.ApplicationData.dll size 435248 bytes base 180000000 - disabling thread library calls wret> ``` Loading ECC server key used by the license server (patching default `WMRMECC256PubKey` key): ``` wret> lskey ..\test_ls\ecc.key LSKey::run WMRMECC256PubKey at 7dc840 ``` Setting up license server url (to point to the url of the test license server setup above): ``` wret> lsurl http://192.168.56.1:8080 LSUrl::run ``` Suppressing PlayReady activation errors (for offline testing): ``` wret> handler -a supressact HandlerCmd::run wret> ``` Enabling logging functionality: ``` wret> handler -a logger HandlerCmd::run wret> ``` Setting up logging of return values and args with call nesting enabled (paddings): ``` wret> logcfg rets,pads,args flag rets val 10 flag pads val 40 flag args val 80 LogcfgCmd::run wret> ``` Loading preprocessed include file for API calls resolving: ``` wret> include inc.i ``` Setting tracing for import calls (for any CALL instruction additional `-t` argument could be used): ``` wret> trace -i TraceCmd::run - tracing: GetTraceLoggerHandle - tracing: GetTraceEnableFlags - tracing: RegisterTraceGuidsW - tracing: GetTraceEnableLevel - tracing: TraceMessage - tracing: UnregisterTraceGuids - tracing: LCMapStringW - tracing: GetCPInfo - tracing: GetOEMCP ... - trace call at 6d7295 to 63c650 - trace call at 6d7505 to cff60 - trace call at 6d7529 to cd640 - trace call at 6d758d to cd550 - trace call at 6d759e to cd550 - trace call at 6d7656 to d68c0 - trace call at 6d7884 to 213f84 total: locations 21014 calls 13861 wret> ``` Triggering the invocation of get license request with a log trace: ``` wret> getlicense GetLicenseCmd::run GET LICENSE 3 - ComLib init w10_prlib.dll * hlib: 7ffd3cf50000 * get_activation_factory: 7ffd3d01df50 w10_prlib.dll!cad55 : @wb_decrypt!6eb000 w10_prlib.dll!cdfae -> api-ms-win-core-synch-l1-1-0.dll!EnterCriticalSection(lpCriticalSection = 7ffd3d8e5e08) w10_prlib.dll!cdfae <- ret 0 w10_prlib.dll!cdfbb -> api-ms-win-core-winrt-string-l1-1-0.dll!WindowsGetStringRawBuffer(arg1 = 21fd3febb8,arg2 = 21fd3fe9c8,arg3 = 30,arg4 = 0,arg5 = 0,arg6 = 7ffd846d1f34) w10_prlib.dll!cdfbb <- ret 7ff694b9bc00 w10_prlib.dll!ce28f -> api-ms-win-core-synch-l1-2-0.dll!InitOnceExecuteOnce(InitOnce = 7ffd3d8e5670,InitFn = 7ffd3d01f9e0,Parameter = 0,Context = 0) w10_prlib.dll!ce28f <- ret 1 w10_prlib.dll!ce2a7 -> api-ms-win-core-winrt-string-l1-1-0.dll!WindowsIsStringEmpty(arg1 = 21fd3febb8,arg2 = 7ffd3d01f9e0,arg3 = 0,arg4 = 0,arg5 = 0,arg6 = 7ffd846d1f34) w10_prlib.dll!ce2a7 <- ret 0 w10_prlib.dll!ce2bc -> api-ms-win-core-winrt-string-l1-1-0.dll!WindowsStringHasEmbeddedNull(arg1 = 21fd3febb8,arg2 = 21fd3fe9c4,arg3 = 0,arg4 = 0,arg5 = 0,arg6 = 7ffd846d1f34) w10_prlib.dll!ce2bc <- ret 0 w10_prlib.dll!ce2d9 -> api-ms-win-core-winrt-string-l1-1-0.dll!WindowsGetStringRawBuffer(arg1 = 21fd3febb8,arg2 = 0,arg3 = 0,arg4 = 0,arg5 = 0,arg6 = 7ffd846d1f34) w10_prlib.dll!ce2d9 <- ret 7ff694b9bc00 w10_prlib.dll!63b89e -> api-ms-win-core-heap-l1-1-0.dll!HeapAlloc(hHeap = 138c4cc0000,dwFlags = 0,dwBytes = 50) w10_prlib.dll!63b89e <- ret 138c4cc0860 ... w10_prlib.dll!1ff104 -> 1c1f80(arg1 = 21fd3fe2e4,arg2 = 21fd3fddd0,arg3 = 138d2fe5918,arg4 = 138ca599068,arg5 = 138ca599614,arg6 = 0) w10_prlib.dll!1c202e -> 1c0dd0(arg1 = 21fd3fddd0,arg2 = 21fd3fdf30,arg3 = 21fd3fdd50,arg4 = 1250,arg5 = 21fd3fdcf0,arg6 = 170) w10_prlib.dll!1c202e <- w10_prlib.dll!1ff104 <- ret 0 w10_prlib.dll!1ff1ca -> 600700(arg1 = 21fd3fddd0,arg2 = 138ca599624,arg3 = fd3fe2c400001250,arg4 = 13800000021,arg5 = 138ca599614,arg6 = 0) w10_prlib.dll!600809 -> 5e0100(arg1 = 21fd3fddd0,arg2 = 138ca599624,arg3 = 7ffd3d11202e,arg4 = 1250,arg5 = 21fd3fe008,arg6 = 138ca599624) w10_prlib.dll!5e0173 -> 1c2070(arg1 = 21fd3fddd0,arg2 = 138ca599624,arg3 = 138ca599624,arg4 = 138ca599624,arg5 = 21fd3fdc20,arg6 = 138ca599624) w10_prlib.dll!1c20fc -> 1c13f0(arg1 = 138ca599624,arg2 = 138ca599624,arg3 = 21fd3fddd0,arg4 = 21fd3fddd0,arg5 = 21fd3fdba0,arg6 = 21fd3fdc80) w10_prlib.dll!1c20fc <- w10_prlib.dll!5e0173 <- ret 13800000000 w10_prlib.dll!600809 <- ret 0 w10_prlib.dll!6008f4 -> 5e0100(arg1 = 21fd3fddd0,arg2 = 138ca599634,arg3 = 7ffd3d11202e,arg4 = 1250,arg5 = 21fd3fe008,arg6 = 138ca599624) w10_prlib.dll!5e0173 -> 1c2070(arg1 = 21fd3fddd0,arg2 = 138ca599634,arg3 = 138ca599624,arg4 = 138ca599624,arg5 = 21fd3fdc20,arg6 = 13800000000) w10_prlib.dll!1c20fc -> 1c13f0(arg1 = 138ca599634,arg2 = 138ca599634,arg3 = 21fd3fddd0,arg4 = 21fd3fddd0,arg5 = 21fd3fdba0,arg6 = 21fd3fdc88) w10_prlib.dll!1c20fc <- w10_prlib.dll!5e0173 <- ret 13800000000 ... ``` The trace shows all WB calls. Heap execute calls are shown without annotation. Decryption / encryption calls are annotated with `@wb_` prefix: ``` w10_prlib.dll!ec879 : @wb_decrypt!6d9200 w10_prlib.dll!ec0f9 : @wb_decrypt!6d840c w10_prlib.dll!ec059 : @wb_reencrypt!6d840c w10_prlib.dll!ec9b9 : @wb_decrypt!6d9300 w10_prlib.dll!ec919 : @wb_reencrypt!6d9300 w10_prlib.dll!ec7d9 : @wb_reencrypt!6d9200 w10_prlib.dll!ebe79 : @wb_decrypt!6d8200 w10_prlib.dll!ebdd9 : @wb_reencrypt!6d8200 w10_prlib.dll!ec559 : @wb_reencrypt!6d9000 ``` It should be possible to have tracing work across multiple binaries loaded into a target process (such as `PlayReady.dll`, `mf.dll`, etc.). Sample trace log files along scripts can be found in `tests\trace` directory. It should be mentioned that occasionally, PlayReady identity could be wiped out as a result of detecting expired or invalid identity. In such cases, placing arbitrary copy of `hds` and `bla` files to PlayReady CDM directory helps avoid errors during tracing. See `prepare.bat` script for details. ## KEY SUBROUTINES The developed toolset made it possible to conduct an in-depth static and dynamic analysis of PlayReady operation with respect to some key functionality such as activation / individualization (`prinit` cmd) and license acquisition (`getlicense` cmd). General protocol operation has been reverse engineered by us in 2022 (CANAL+ STB device environment), which made things easier (we knew what artifacts to look for). As a result of the analysis, some key subroutines pertaining to cryptographic operations conducted on identity and license blobs have been discovered. What's worth to note is that their addresses could be established in a completely automatic fashion across various PlayReady library versions used in both Windows 10 and 11 environments: ``` wret> prsubs PRSubsCmd::run [w10_prlib.dll] * main obj [vtable 713148] - module init 215400 - decrypt license 217500 - sign data 217570 - decrypt domain key 2155b0 * decrypt license internal 6c6560 * get license decryptor 215600 * decrypt license blob 2228a0 * expand key 6509c0 * decrypt signing key 222650 ``` Actual implementation details can be found in source code (`prsubs` command implementation of WRET toolkit). The script `genprsubs.bat` located in `tests\xor_key` directory can automatically discover `prsubs` values for various versions of PlayReady libraries present in Windows 10 / 11 x64 systems across various builds from late 2022 till Nov 2024: ``` c:\_MNT\PROJECTS\_PROJECTS\MSPR\_MS.NOV.2024\WBPMP\tests\xor_key>genprsubs ### GENERATING PRSUBS w10\w10_22h2_aug22\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_aug24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_dec22\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_dec23\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_jul24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_jun23\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_jun24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_mar23\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_mar24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_nov24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_oct24\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_sep23\Windows.Media.Protection.PlayReady.dll w10\w10_22h2_sep24\Windows.Media.Protection.PlayReady.dll w11\w11_22h2_dec22\Windows.Media.Protection.PlayReady.dll w11\w11_22h2_mar23\Windows.Media.Protection.PlayReady.dll w11\w11_22h2_sep22\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_aug24\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_jul24\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_mar24\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_nov24\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_oct24\Windows.Media.Protection.PlayReady.dll w11\w11_23h2_sep24\Windows.Media.Protection.PlayReady.dll ``` The name of the output file is `prsubs.txt'. ## PRIVATE SIGNING KEY DISCOVERY License request issued to the license server requires the use of a private client identity key for signing the request. The trace of a license server revealed the subroutine that conducted decryption of that key (prsubs addr denoted by "decrypt signing key"): ``` .text:0000000180222650 agsr_decrypt_secret_key proc near ; DATA XREF: agsr_sign_data:loc_180217628↑o .text:0000000180222650 ; .rdata:000000018097A6E8↓o ... .text:0000000180222650 .text:0000000180222650 var_1E0 = qword ptr -1E0h .text:0000000180222650 var_1D8 = qword ptr -1D8h .text:0000000180222650 var_1D0 = qword ptr -1D0h .text:0000000180222650 var_1C8 = qword ptr -1C8h .text:0000000180222650 var_1C0 = qword ptr -1C0h .text:0000000180222650 var_1B8 = dword ptr -1B8h .text:0000000180222650 var_1B0 = byte ptr -1B0h .text:0000000180222650 var_1A0 = byte ptr -1A0h .text:0000000180222650 var_30 = qword ptr -30h .text:0000000180222650 var_28 = qword ptr -28h .text:0000000180222650 arg_0 = dword ptr 10h .text:0000000180222650 arg_8 = dword ptr 18h .text:0000000180222650 arg_10 = dword ptr 20h .text:0000000180222650 .text:0000000180222650 lea rdx, unk_180998298 .text:0000000180222657 mov rcx, [rsp+0] .text:000000018022265B push rbp .text:000000018022265C push rdi .text:000000018022265D push r12 .text:000000018022265F push r14 .text:0000000180222661 push r15 .text:0000000180222663 lea rbp, [rsp-0C0h] .text:000000018022266B sub rsp, 1C0h .text:0000000180222672 cmp dword ptr [rcx+10h], 20h ; ' ' ; arg2=encrypted key size .text:0000000180222676 mov r15, rdx .text:0000000180222679 mov r12, [rcx] ; res ptr .text:000000018022267C mov rax, rcx .text:000000018022267F mov rdi, [rdx-8] .text:0000000180222683 mov r14, [rdx-10h] .text:0000000180222687 .text:0000000180222687 loc_180222687: ; DATA XREF: .rdata:000000018097A6E8↓o .text:0000000180222687 ; .rdata:000000018097A6FC↓o ... .text:0000000180222687 mov [rsp+1E0h+var_28], rbx .text:000000018022268F mov ebx, 10h .text:0000000180222694 jz short loc_1802226A1 ; arg1 = encrypted key .text:0000000180222696 mov r8d, 80070057h .text:000000018022269C jmp loc_180222838 .text:00000001802226A1 ; --------------------------------------------------------------------------- .text:00000001802226A1 .text:00000001802226A1 loc_1802226A1: ; CODE XREF: agsr_decrypt_secret_key+44↑j .text:00000001802226A1 mov rcx, [rcx+8] ; arg1 = encrypted key .text:00000001802226A5 test rcx, rcx ``` Decryption of the signing key was thus straightforward. All that was required to accomplish that was to call that subroutine with an encrypted key provided as an argument. One needs to keep in mind that this was WB subroutine normally invoked through the `NtQuerySystemInformation` (this is manifested through `rcx` being loaded from stack top / ret addr location and arguments accessed through `rcx`). The following procedure has been used for decryption of the signing key and verification of the process. MSPR toolkit has been used to save public and encrypted keys for a given chosen identity: ``` c:\_MNT\PROJECTS\_PROJECTS\MSPR\MS_PKG\tools\mspr_toolkit>run # MS Play Ready / Canal+ VOD toolkit # (c) Security Explorations 2016-2019 Poland # (c) AG Security Research 2019-2022 Poland loaded cdn [CDN helper] loaded mspr [MS Play Ready toolkit] loaded vod [CANALP VOD toolkit] loaded cgaweb [CANALP CGAWeb toolkit] ``` Listing identities (as denoted by CDM pointed by CDN_DIR variable): ``` msprcp> identity 0C86330B0E98CD7C586F336088DAFA0E 4F72F3CBDC81C849F635AE556A73679F 902B255736B6E891F3AF30F98B0A5DBA D9A5C7A90F8DEA029AA8FB1C95887BE3 E82DFAE7A9DB21FC1ECF33C1DADC54B7 ``` Exporting client identity (signing and encryption keys): ``` msprcp> identity -e 0C86330B0E98CD7C586F336088DAFA0E 0C86330B0E98CD7C586F336088DAFA0E.enc.pub (public encryption key) 0C86330B0E98CD7C586F336088DAFA0E.enc.prv (private encryption key) 0C86330B0E98CD7C586F336088DAFA0E.sig.pub (public signing key) 0C86330B0E98CD7C586F336088DAFA0E.sig.prv (private signing key) ``` Now, the WRET toolkit can be used to decrypt the signing key: ``` wret> decsignkey ..\mspr_toolkit\0C86330B0E98CD7C586F336088DAFA0E.sig.prv -o ..\mspr_toolkit\0C86330B0E98CD7C586F336088DAFA0E.sig.plain DecSignKeyCmd::run keydata 0x00000000: d7 60 5c 71 57 a0 01 7c 58 e2 e7 79 a8 b1 12 55 .`\qW..|X..y...U 0x00000010: 1d 72 14 f0 d9 2c ef 04 6c cc 57 c1 2e 9b e3 b4 .r...,..l.W..... target 7ffd3d172650 wb res: 0 call res 0 output key 0x00000000: cc 55 f7 54 f7 62 4f e2 b0 07 d8 30 2a 34 80 8c .U.T.bO....0*4.. 0x00000010: ad e4 9b 6e 78 2b 8c de 1e c3 56 96 20 c4 24 f3 ...nx+....V. .$. saving to ..\mspr_toolkit\0C86330B0E98CD7C586F336088DAFA0E.sig.plain file wret> ``` We can verify whether the decrypted private key matches the public one: ``` msprcp> checkkeypair 0C86330B0E98CD7C586F336088DAFA0E.sig.plain 0C86330B0E98CD7C586F336088DAFA0E.sig.pub KEY CHECK: - prv: cc55f754f7624fe2b007d8302a34808cade49b6e782b8cde1ec3569620c424f3 - pub: X: 42b2a0ff381c34cc67063b50e12e0dde7449552938ef660c605c909f8cb04943 Y: fe7a81f2f675ab2905c3e2e996219b44a398b23645e4cd7cc9538bd3cd32bf7 KEY CHECK OK ``` It is worth to mention that the signing key decryption subroutine has been highly obfuscated (on purpose). This was completely irrelevant from a target security analysis though - there was no need to dive into the obfuscated code / no need to understand how it works. What was relevant was subroutine identification and its API functionality (input args with decrypted key -> some black box key processing -> output plaintext key). ## CONTENT KEY SNIFFER Analysis of the license request processing was not sufficient for us to reach a point where private client identity key could be used for license blob decryption. Thus, we needed to move our work to online environment and conduct some more in-depth analysis of MS Edge and PMP environment in order to find out PlayReady operation following license server response (how license blobs are decrypted and content keys obtained). As a result of the above, successful XMR license and content key sniffing could be achieved. All regardless of the protections implemented at OS kernel level such as protected process and `PEAuth`, which is responsible for guarding that PMP environment is authorized to process protected content. `PEAuth` is implemented in both kernel (`PEAuth` driver) and user land (calls from PlayReady library). It's worth to note that `PEAuth` driver can execute encrypted code too: ``` INIT:00000001C00C6582 lea rdx, unk_1C0031340 INIT:00000001C00C6589 mov cs:dword_1C003144C, 1 INIT:00000001C00C6593 lea rcx, byte_1C002EC30 ; -> ptr to encrypted segment descriptor INIT:00000001C00C6593 ; off 0x50 contains memory RVA: INIT:00000001C00C6593 ; .rdata:00000001C002EC80 db 8 INIT:00000001C00C6593 ; .rdata:00000001C002EC81 db 90h INIT:00000001C00C6593 ; .rdata:00000001C002EC82 db 3 INIT:00000001C00C6593 ; .rdata:00000001C002EC83 db 0 INIT:00000001C00C659A call agsr_decrypt_code? INIT:00000001C00C659F test eax, eax INIT:00000001C00C65A1 js short loc_1C00C65BB INIT:00000001C00C65A3 call loc_1C0039008 ; -> call into just decrypted memory INIT:00000001C00C65A8 lea rdx, unk_1C0031340 INIT:00000001C00C65AF lea rcx, byte_1C002EC30 INIT:00000001C00C65B6 call agsr_encrypt_code? ; reencrypt memory ``` In general `PEAuth` driver assists user code (PMP process) by periodically exchanging some crypto challenges with it. At the time of returning result to the calling process, it verifies whether it is protected: ``` PAGE:00000001C003B2CC loc_1C003B2CC: ; CODE XREF: agsr_check_and_set_ioctl_result+E0↑j PAGE:00000001C003B2CC mov eax, [r12+8] ; protected process flag (=1 dla protected) PAGE:00000001C003B2D1 cmp esi, 4 ; u mnie 1 PAGE:00000001C003B2D4 jz short loc_1C003B33F PAGE:00000001C003B2D6 test eax, eax PAGE:00000001C003B2D8 jnz short loc_1C003B2E6 ; -> jump for protected process PAGE:00000001C003B2DA PAGE:00000001C003B2DA loc_1C003B2DA: ; CODE XREF: agsr_check_and_set_ioctl_result+169↓j PAGE:00000001C003B2DA mov dword ptr [rbx+4], 0C00D7165h ; store result PAGE:00000001C003B2DA ; PAGE:00000001C003B2DA ; // PAGE:00000001C003B2DA ; // MessageId: MF_E_NON_PE_PROCESS PAGE:00000001C003B2DA ; // PAGE:00000001C003B2DA ; // MessageText: PAGE:00000001C003B2DA ; // PAGE:00000001C003B2DA ; // A non-PE process tried to talk to PEAuth.%0 PAGE:00000001C003B2DA ; // PAGE:00000001C003B2DA ; #define MF_E_NON_PE_PROCESS _HRESULT_TYPEDEF_(0xC00D7165L) ``` For non-protected processes, it returns error, which informs the PMP environment that an attempt is made to launch PlayReady DRM (process protected content) in an untrusted env. ### PMP PROCESS HIJACK Upon initialization of Protected Media Path (and PlayReady DRM), PMP process gets started by MS Edge. Process launch is conducted with `CREATE_PROTECTED_PROCESS` creation flags (from `mfcore.dll` and `mfpmp.exe`): ``` .text:000000018009E5FC jnz loc_180177651 .text:000000018009E602 test r14b, 1 ; jezeli r14b == 1, to mam flage .text:000000018009E602 ; MFPMPSESSION_UNPROTECTED_PROCESS .text:000000018009E602 ; .text:000000018009E602 ; flags for MFCreatePMPMediaSession function .text:000000018009E602 ; .text:000000018009E602 ; typedef enum MFPMPSESSION_CREATION_FLAGS { .text:000000018009E602 ; MFPMPSESSION_UNPROTECTED_PROCESS = 0x1, .text:000000018009E602 ; MFPMPSESSION_IN_PROCESS = 0x2 .text:000000018009E602 ; } ; .text:000000018009E606 jnz short loc_18009E617 ; -> go unprotected .text:000000018009E608 call agsr_check_protected_process_bypass ; check whether to start protected process .text:000000018009E608 ; (check for registry override) .text:000000018009E60D test eax, eax ; set protected process if eax==0 .text:000000018009E60F mov ecx, 40000h ; CREATE_PROTECTED_PROCESS!!!! .text:000000018009E614 cmovz esi, ecx ; dwCreationFlags .text:000000018009E617 ... .text:000000018009E7C4 and [rsp+380h+var_348], 0 .text:000000018009E7CA and [rsp+380h+var_350], 0 .text:000000018009E7D0 mov [rsp+380h+dwCreationFlags], esi ; dwCreationFlags .text:000000018009E7D4 and dword ptr [rsp+380h+var_360], 0 .text:000000018009E7D9 call cs:CreateProcessW ``` With MS Edge running with user privileges, process creation can be easily bypassed. What is needed for the purpose is the hijack of `CreateProcessW` call and clearing of the flag. ### PEAUTH BYPASS `PEAuth` checks are called both during the PMP environment initialization and also periodically (every 5000 decoded MPEG frames). Running PMP process as unprotected is not sufficient for PlayReady DRM operation (no license request / processing is done if insecure env is detected) for the reasons depicted above. There is however a way to bypass `PEAuth` checks and have PlayReady DRM run in full as part of the unprotected process. Initial `PEAuth` check is invoked from `IMFInputTrustAuthority::RequestAccess` call. The code that is part of ITA verification call inspects a value of some cached variable prior to the `PEAuth` check: ``` .text:0000000180152BDE .text:0000000180152BDE loc_180152BDE: ; CODE XREF: agsr_ITA_verify????+4E↑j .text:0000000180152BDE ; agsr_ITA_verify????+54↑j .text:0000000180152BDE xor eax, eax ; =0 .text:0000000180152BE0 mov [rsp+10h+arg_11F8], r12 .text:0000000180152BE8 mov esi, eax ; result = 0 (ok) .text:0000000180152BEA cmp [r14+0C8h], eax .text:0000000180152BF1 jnz short loc_180152C17 .text:0000000180152BF3 cmp [r14+0B8h], eax ; some special flag (trusted env / skip PEAuth ?) .text:0000000180152BFA jnz short loc_180152C17 .text:0000000180152BFC mov [rbp+10F0h+var_1058], eax ``` What's interesting in this variable is that it is set to 1 upon successful completion of the `PEAuth` check. In that context, the variable acts as a cached value for the `PEAuth` check. So, setting this variable to 1 prior to the check will simply skip it. Again, while ITA verification code is quite long and highly obfuscated, one doesn't need to analyze it at all. It is sufficient to find out that it signals an error at the time of detecting insecure (unprotected) PMP process and how to override it (skip it). Setting the `PEAuth` check variable described above requires that it is done at proper time and context (this object instance context). This is not that straightforward taking into account that ITA verify subroutine is available in plaintext form only for the time of its execution (the code is dynamically decrypted and reencrypted): ``` .text:00000001806BC641 js loc_1806BC331 ; -> error .text:00000001806BC647 lea rax, agsr_wb_decrypt_0 .text:00000001806BC64E add rax, rsi .text:00000001806BC651 call rax .text:00000001806BC653 test eax, eax .text:00000001806BC655 js loc_1806BC76A ; -> error .text:00000001806BC65B lea rax, agsr_ITA_verify???? ; sets [this+0xb8] to 1 if PEAuth OK .text:00000001806BC662 add rax, rsi .text:00000001806BC665 lea rcx, [r15-10h] ; this-10h .text:00000001806BC669 call rax .text:00000001806BC66B mov r12d, eax .text:00000001806BC66E lea rax, agsr_wb_reencrypt_0 .text:00000001806BC675 add rax, rsi ``` Additionally, the code is executed through WB heap execute call: ``` .text:000000018015F620 agsr_RequestAccess proc near ; DATA XREF: .rdata:0000000180710A08↓o .text:000000018015F620 ; .rdata:000000018073AF08↓o ... .text:000000018015F620 .text:000000018015F620 var_18 = dword ptr -18h .text:000000018015F620 arg_8 = dword ptr 10h .text:000000018015F620 .text:000000018015F620 mov r11, rsp .text:000000018015F623 sub rsp, 58h .text:000000018015F627 mov [r11-20h], rcx .text:000000018015F62B lea rax, agsr_RequestAccess_wrap .text:000000018015F632 mov [r11-30h], rax .text:000000018015F636 xor r9d, r9d ; ReturnLength .text:000000018015F639 lea rax, [r11+10h] .text:000000018015F63D mov qword ptr [r11-38h], 3 .text:000000018015F645 mov [r11-28h], rax .text:000000018015F649 mov ecx, 0B9h ; SystemInformationClass .text:000000018015F64E mov [rsp+58h+var_18], edx ; MFPOLICYMANAGER_ACTION Action .text:000000018015F652 lea rdx, [r11-38h] ; SystemInformation .text:000000018015F656 mov [r11-14h], r8 ; IMFActivate **ppContentEnablerActivate .text:000000018015F65A lea r8d, [r9+2Ch] ; SystemInformationLength .text:000000018015F65E call NtQuerySystemInformation .text:000000018015F663 mov eax, [rsp+58h+arg_8] .text:000000018015F667 add rsp, 58h .text:000000018015F66B retn .text:000000018015F66B agsr_RequestAccess endp ``` We have figured out the way to accomplish the patching though. As WB call above is done through `NtQuerySystemInformation`, one can hijack the IAT entry for it and conduct some processing upon detection of the target call location (from within `RequestAccess`). The overwrite of IAT along hijack of arbitrary code into target PMP process is possible (due to no process protection, this got disabled at the time of PMP process launch). Detection of a target call location is accomplished through the following: - code pattern match (same pattern is present for Windows 10 and 11 PlayReady binaries), - matching of the `MFPOLICYMANAGER_ACTION` argument (it should denote `PEACTION_PLAY`) - verification of a virtual methods table (vtable) pointer (whether vtable slot corresponding to the `RequestAccess` method corresponds to the WB code called) At this point it is worth to mention that the hijacking of WB code implemented by the sniffer demonstrates two potential weaknesses of Warbird / PMP: 1) virtual methods table calls constituting a gap in the encrypted call chains, vtable slots usually point to plaintext code wrappers that invoke WB code (setup WB args and invoke WB syscall), a call to `NtQuerySystemInformation` used by it can be intercepted (break up of thesemi-atomic invocation chain / possibility of the hooking), 2) no additional memory protection for the PlayReady image upon memory load (protected process is not enough) It's worth to mention that similar bypass should work with respect to periodic `PEAuth` check conducted every 5000 samples. The code below indicates that there are some instance variables checked prior to the call that denote the code runs in a trusted environment: ``` .text:0000000180179960 agsr_check_pe_trusted proc near ; DATA XREF: agsr_LongRunning_DecryptSample:loc_180180F16↓o .text:0000000180179960 ; agsr_DoProcessSample+DA↓o ... .text:0000000180179960 .text:0000000180179960 var_78 = dword ptr -78h .text:0000000180179960 var_70 = dword ptr -70h .text:0000000180179960 var_68 = qword ptr -68h .text:0000000180179960 var_60 = qword ptr -60h .text:0000000180179960 var_58 = qword ptr -58h .text:0000000180179960 var_50 = qword ptr -50h .text:0000000180179960 var_48 = qword ptr -48h .text:0000000180179960 var_38 = qword ptr -38h .text:0000000180179960 arg_0 = dword ptr 8 .text:0000000180179960 arg_8 = dword ptr 10h .text:0000000180179960 arg_10 = dword ptr 18h .text:0000000180179960 .text:0000000180179960 lea rdx, unk_180998298 .text:0000000180179967 mov rcx, [rsp+0] .text:000000018017996B push rbx .text:000000018017996C push rbp .text:000000018017996D push rsi .text:000000018017996E push rdi .text:000000018017996F push r12 .text:0000000180179971 push r15 .text:0000000180179973 sub rsp, 68h .text:0000000180179977 mov r8, [rcx+10h] ; arg2 - ptr to invoke counter .text:000000018017997B mov r15, rdx .text:000000018017997E mov rdi, [rcx+8] ; this .text:0000000180179982 mov r12, [rcx] .text:0000000180179985 .text:0000000180179985 loc_180179985: ; DATA XREF: .rdata:000000018096E54C↓o .text:0000000180179985 ; .rdata:000000018096E55C↓o ... .text:0000000180179985 mov [rsp+98h+var_38], r14 .text:000000018017998A mov rbp, [rdx-8] .text:000000018017998E xor ebx, ebx .text:0000000180179990 mov rsi, [rdx-10h] .text:0000000180179994 mov [rsp+98h+arg_0], ebx .text:000000018017999B test r8, r8 .text:000000018017999E jnz short loc_1801799AA ; -> ok .text:00000001801799A0 mov ebx, 80070057h .text:00000001801799A5 jmp loc_180179ADD .text:00000001801799AA ; --------------------------------------------------------------------------- .text:00000001801799AA .text:00000001801799AA loc_1801799AA: ; CODE XREF: agsr_check_pe_trusted+3E↑j .text:00000001801799AA mov eax, [r8] .text:00000001801799AD cmp eax, 1388h ; =5000 dec .text:00000001801799B2 jbe loc_180179AD8 ; PE security every 5000 invocation .text:00000001801799B8 mov [r8], ebx ; clear counter .text:00000001801799BB cmp [rdi+0BC0h], ebx .text:00000001801799C1 jz short loc_1801799D2 .text:00000001801799C3 .text:00000001801799C3 loc_1801799C3: ; CODE XREF: agsr_check_pe_trusted+B7↓j .text:00000001801799C3 mov dword ptr [rdi+0BF0h], 1 ; trusted pe flag ? .text:00000001801799CD jmp loc_180179ADD ``` Finally, it's worth to mention that the analysis of the `PEAuth` was made much easier thanks to `Media Foundation Samples' and the code for `clearkeyStoreCDM`, which seemed to be a mirror of actual CDM used by PlayReady: ``` .text:0000000180199860 agsr_VerifyState?? proc near ; DATA XREF: sub_180179960+C2↑o .text:0000000180199860 ; .rdata:0000000180970E4C↓o ... .text:0000000180199860 .text:0000000180199860 var_258 = qword ptr -258h .text:0000000180199860 var_250 = qword ptr -250h .text:0000000180199860 var_248 = qword ptr -248h .text:0000000180199860 var_240 = qword ptr -240h .text:0000000180199860 var_238 = qword ptr -238h .text:0000000180199860 var_230 = dword ptr -230h .... .text:0000000180199C01 lea rax, [r12+10h] .text:0000000180199C06 mov r8d, 10h .text:0000000180199C0C lea r9, [rbp+170h+var_1C8+4] .text:0000000180199C10 sub r9, rax .text:0000000180199C13 .text:0000000180199C13 loc_180199C13: ; CODE XREF: sub_180199860+3C5↓j .text:0000000180199C13 movzx ecx, byte ptr [r9+rax] .text:0000000180199C18 cmp cl, [rax] .text:0000000180199C1A ja short loc_180199C3A .text:0000000180199C1C jb short loc_180199C3A .text:0000000180199C1E inc rax .text:0000000180199C21 sub r8, 1 .text:0000000180199C25 jnz short loc_180199C13 .text:0000000180199C27 mov eax, [r12+0Ch] .text:0000000180199C2C mov ecx, 0C00D715Fh ; // .text:0000000180199C2C ; // MessageId: MF_E_GRL_VERSION_TOO_LOW .text:0000000180199C2C ; // .text:0000000180199C2C ; // MessageText: .text:0000000180199C2C ; // .text:0000000180199C2C ; // The current GRL on the machine does not meet the minimum version requirements.%0 .text:0000000180199C2C ; // .text:0000000180199C2C ; #define MF_E_GRL_VERSION_TOO_LOW _HRESULT_TYPEDEF_(0xC00D715FL) ``` the original code is shown below: ``` HRESULT Cdm_clearkey_PEAuthHelper::VerifyState() { HRESULT hr = S_OK; PEAUTH_MSG oMsg = { 0 }; PEAUTH_MSG_RESPONSE oRsp = { 0 }; // // Obtain the current state of the protected environment from the kernel // IF_FAILED_GOTO( _GenerateKernelInquiryMessage( m_fRequireTrustedKernel, m_dwMinimumGRLVersionRequired, &oMsg ) ); IF_FAILED_GOTO( this->TransmitPEAuthMessage( &oMsg, sizeof( oMsg ), &oRsp, sizeof( oRsp ) ) ); IF_FAILED_GOTO( _VerifyResponseMessageStatus( m_fRequireTrustedKernel, m_fRequireTrustedUsermode, &oRsp ) ); IF_FAILED_GOTO( _VerifyBinarySigning( oRsp.ResponseBody.Status.ResponseFlag, m_fBlockTestSignatures ) ); ... ``` The published samples made it possible to follow the ITA path and discover the symbolic names of many calls by simply comparing the asm code with available C++ sources: ``` .text:000000018018E40D lea rax, [rcx+0B8h] .text:000000018018E414 mov rcx, rax .text:000000018018E417 mov [rbp+20h+var_98], rax .text:000000018018E41B call agsr_CloneSample ; IF_FAILED_GOTO( _CloneSample( pSample, &spSample ) ); .text:000000018018E420 mov rsi, qword ptr [rsp+120h+var_E8] ; spSample .text:000000018018E425 mov r14d, eax .text:000000018018E428 test eax, eax .text:000000018018E42A js loc_18018EAD4 ; -> error .text:000000018018E430 mov rax, [rsi] .text:000000018018E433 mov rbx, [rax+148h] .text:000000018018E43A mov rcx, rbx .text:000000018018E43D call cs:__guard_check_icall_fptr ; addr 7ffbba0ee443 (18e443) -> target 7ffbf4c55400 (3acf5400) 180015400 ; __int64 __fastcall CMFSample::ConvertToContiguousBuffer(CMFSample *__hidden this, struct IMFMediaBuffer **) .text:000000018018E43D ; .text:000000018018E43D ; IF_FAILED_GOTO( spSample->ConvertToContiguousBuffer( &spOutputBuffer ) ); .text:000000018018E443 lea rdx, [rsp+120h+var_C8] ; ComPtr spOutputBuffer; .text:000000018018E448 mov rcx, rsi .text:000000018018E44B call rbx .text:000000018018E44D mov r14d, eax .text:000000018018E450 test eax, eax .text:000000018018E452 js loc_18018EAD4 ; -> error .text:000000018018E458 mov rbx, [rsp+120h+var_C8] ; spOutputBuffer .text:000000018018E45D mov rax, [rbx] .text:000000018018E460 mov r14, [rax+18h] .text:000000018018E464 mov rcx, r14 .text:000000018018E467 call cs:__guard_check_icall_fptr ; addr 7ffbba0ee46d (18e46d) -> target 7ffbba172150 (212150) .text:000000018018E467 ; .text:000000018018E467 ; IF_FAILED_GOTO( spOutputBuffer->Lock( &pbOutputWeakRef, nullptr, &cbOutputWeakRef ) ); ??? .text:000000018018E467 ; .text:000000018018E467 ; Lock .text:000000018018E467 ; ( .text:000000018018E467 ; [out] BYTE **ppbBuffer, .text:000000018018E467 ; [out] DWORD *pcbMaxLength, .text:000000018018E467 ; [out] DWORD *pcbCurrentLength .text:000000018018E467 ; ) .text:000000018018E46D lea r9, [rsp+120h+var_F0] ; cbOutputWeakRef .text:000000018018E472 xor r8d, r8d ; null .text:000000018018E475 lea rdx, [rsp+120h+var_B8] ; pbOutputWeakRef .text:000000018018E47A mov rcx, rbx .text:000000018018E47D call r14 ; -> 180212150 ``` ### CONTENT KEY EXTRACTION WB syscall hijack (`NtQuerySystemInformation` IAT entry hijack) made it possible to investigate WB calls (callstack), their arguments, stack and memory content of PMP process during license acquisition (runtime PlayReady operation in arbitrary VOD service environment such as Canal+ Online, Netflix, HBO Max, Amazon Prime or Sky Showtime). Data and logs retrieved could be both tuned and compared with decrypted static binary image. As a result, WB call location has been selected as including the following information on the stack: - XMR license blob - license blob signature - content key This is illustrated below (callstack layout done with respect to sub `0x215600`: ``` 7e f5 18 92 41 cf ba fb 47 a0 5b c2 97 b7 4c 6d -> var_5B0 (content key blob, size 0x20) 3e b5 52 69 e8 78 ae bf 99 5d bd 89 76 f4 a2 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 72 07 00 00 00 00 00 00 ad e3 a6 29 fa 7f 00 00 30 d7 d7 be 72 00 00 00 e8 dc d7 be 72 00 00 00 a6 93 88 c4 dc 01 00 00 10 00 00 00 00 00 00 00 <-- license signature data (addr, size), 24 92 88 c4 dc 01 00 00 76 01 00 00 00 00 00 00 <-- license xmr data (addr,size) 90 d7 d7 be 72 00 00 00 8f 03 00 00 00 00 00 00 30 a0 88 c4 dc 01 00 00 3d 74 6d ed f9 7f 00 00 f7 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e0 dc d7 be 72 00 00 00 4f 03 00 00 00 00 00 00 a0 5d 00 00 00 00 00 00 80 b9 88 c4 dc 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 a0 5d 00 00 00 00 00 00 20 a0 88 c4 dc 01 00 00 8c d1 ae 6c c3 c6 8e fe 9f 02 5b 3c cd 7d 4b 4b -> white-box AES rounds (expand key) 7a 36 9a 99 d8 1a dc 1e 66 9f e8 2c b1 d4 13 95 8e 18 01 c0 f3 6b f0 3f 18 b3 6b ed 98 8f 08 33 e2 9d 50 36 97 70 26 8f 09 45 cb e4 17 4c 45 51 f8 94 fc 29 e9 62 5c 20 66 a1 11 42 f7 6b d2 95 08 96 3c 4e 67 72 e6 e8 87 55 71 2c f6 b8 25 3f 8f f8 88 eb 6e 0c e8 85 6f df 1f 2f 1f e1 bc 96 b6 49 9d 0d 5e c3 f3 0e b7 9a 6a a7 2e fd 50 b7 27 21 13 80 ff 64 66 08 ce 78 8a 29 66 03 5c 18 bc f6 f6 02 92 8f 8c 92 d6 b4 47 e1 ac a2 d1 f8 46 4d 3c b2 4e 00 92 b4 a1 af eb 68 4b ee a4 6e 70 00 00 00 00 00 00 00 7f 00 00 00 00 00 00 00 a0 10 00 00 00 00 00 00 20 a0 88 c4 dc 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7f 00 00 00 00 00 00 00 a0 5d 00 00 00 00 00 00 a7 c1 73 1b 23 ad b4 07 20 95 c4 62 9d 2b 57 6f -> license blob data (var_3C0) 41 e3 e0 c6 33 2d af 65 68 aa ae 3b b0 56 7f c5 2b 1e 8e f9 5e 0b 2b aa 18 8b d3 bc 48 a1 e3 e3 11 7c 77 15 5a 25 39 2a 49 b9 12 ce d8 31 1b ad fe 6a 9d 21 36 9b 6d 8e 69 25 af c2 d5 45 32 90 54 5a e8 c2 04 e5 57 5d e1 8c 16 9b 34 21 4d 6c 9b 9c 0b 65 fc 74 4f d2 2b 69 13 19 49 d0 bb d8 69 8b c7 8a ff 3f 77 91 a8 b1 3d 88 b6 9d b6 40 98 44 cb cf bf cf b0 64 15 fe ae 24 b0 eb b4 c7 -> encrypted prkf key (var_340) 28 0c 98 98 22 16 bc 8a a8 28 bd 36 1d 7a ef 9a 0b a0 e8 a2 e8 58 26 c2 6c fe 53 fe 69 19 cb fa f6 38 4e e0 0d 4b f5 cf 24 f5 21 18 6f 27 15 8d 8d 00 00 00 00 00 00 00 a1 5b a7 29 fa 7f 00 00 ``` The above callstack corresponded to the WB segment decryption call invoked from the following location: ``` .text:0000000180215D2C loc_180215D2C: ; CODE XREF: agsr_get_license_decryptor+720↑j .text:0000000180215D2C cmp [rbp+5A0h+arg_10], 0 ; =0 (ustawiane wyzej) .text:0000000180215D33 jnz short loc_180215D9B ; u mnie 0 .text:0000000180215D35 lea rax, agsr_decrypt_segment ; TUTAJ MOGE ZROBIC HIJACK CALL'A!!!! .text:0000000180215D35 ; aby odczytac ECC key .text:0000000180215D35 ; .text:0000000180215D35 ; decrypt .text:0000000180215D35 ; - wb segment .text:0000000180215D35 ; desc_va: 0x6d8100 size 100 .text:0000000180215D35 ; relocs_va: 0x9436f0 num 0x5197 .text:0000000180215D35 ; segments (num 1, data size 415): .text:0000000180215D35 ; * [0000] encr_va 6b3a40 size 00019f section .text .text:0000000180215D3C add rax, r14 .text:0000000180215D3F call rax .text:0000000180215D41 test eax, eax .text:0000000180215D43 js loc_180215C89 ; -> error .text:0000000180215D49 mov ebx, [rsp+6A0h+var_650] ; u mnie 0 .text:0000000180215D4D lea rax, [rbp+5A0h+var_5B0] ; plaintext key (ECC secret) ? rbp-10h ?? .text:0000000180215D51 mov r9d, dword ptr [rbp+5A0h+var_550+8] ; arg4 = license xmr size ? .text:0000000180215D55 xor ecx, ecx .text:0000000180215D57 mov r8, qword ptr [rbp+5A0h+var_550] ; arg3 = license xmr ? .text:0000000180215D5B cmp r13w, 5 ; decryptor type ? .text:0000000180215D60 mov [rsp+6A0h+var_670], ebx ; arg7 = u mnie 0 .text:0000000180215D64 mov [rsp+6A0h+var_678], rax ; arg6 = plaintext key (ECC secret) ? ``` The actual call to WB syscall has been conducted (hijacked) at this location: ``` .text:00000001800EBD1C mov eax, cs:dword_18098F428 .text:00000001800EBD22 lea rdx, [r11-48h] ; SystemInformation .text:00000001800EBD26 mov [r11-28h], rcx .text:00000001800EBD2A mov ecx, 0B9h ; SystemInformationClass .text:00000001800EBD2F mov [r11-20h], rax .text:00000001800EBD33 call cs:__imp_NtQuerySystemInformation .text:00000001800EBD39 bts eax, 1Ch .text:00000001800EBD3D .text:00000001800EBD3D loc_1800EBD3D: ; CODE XREF: agsr_decrypt_segment+21↑j ``` Initially, the hijacking proceeded by comparing the low 16-bit value of the return address: ``` is_decrypt_location: ; mov rdx,qword ptr [rsp+SAVED_REG_RIP+8] ; and edx,0ffffh ; cmp edx,0bd39h ``` In order to make sniffer code portable across various PlayReady library versions and Windows 10 / 11 systems, another, completely automatic and address independent approach has been used that relied on the following: - heuristic check for rbp "validity" - match of wb call args size - match of wb call type - some stack content verification as expected to be present at target call location (license xmr / signature ptrs, signature size) The above was sufficient to match on a target WB call. Upon matching, the sniffer proceeds with arbitrary data dump of data contained on the callstack comprising of license, signature and content keys. It also takes into account potential shift of the data on the stack occurring on some systems, which is due to different local variable layout (it does so with respect to content key blob). ## HW DRM DISABLING On Windows platforms with HW DRM capability, the attack (content key sniffing) can still proceed as this feature can be easily disabled by setting the following registry entry: ``` HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\PlayReady\Troubleshooter ``` with value `DisableHWDRM` DWORD val = `1` Upon HWDRM disabling, PlayReady DRM proceeds as in Windows 10 without HW support. More specifically, client identities get created and are maintained in local file system (`*.bla` files). ## MAGIC XOR KEYS DISCOVERY The analysis conducted with respect to license blob decryption provided strong hints that content key acquired by the sniffer (and denoted by `var_5B0` above) is actually the content key blob. Investigation of code paths using that blob revealed that decryptor instance used it to build AES rounds table. The AES rounds table was further used by the core AES decryptor subroutine for MPEG A/V segments decryption. Yet, the content key blob in its form present in memory and acquired by the sniffer didn't work (we failed to decrypt target MPEG movies with it, we tried various combinations, byte reversals, etc.). We started to suspect that content key blob is maintained in PMP in some other form. It could be permutation, affine / algebraic transform or some obfuscation. We didn't know the real content key value, which could provide some hints with respect to how content key blob bytes corresponds to it. Thinking more about it we figured out that we didn't need any real content key values at all. We could generate these on our own. PlayReady identities stored in Windows CDM carry public components for both signing and encryption key. One could use the public encryption component to build a custom license carrying a specific content key. This is what we did. So, we generated arbitrary license blob for a given PlayReady identity (using its public encryption key), so that a content key was known to have byte 0 at pos 0: ``` msprcp> genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 0 -p 0 pubkey 0000: cb 27 6f 9f 9f 76 46 64 54 23 19 ef 9c c7 69 0f .'o..vFdT#....i. 0010: 9c 3b e3 75 8b d3 78 2a 8d 03 fb a8 bf 9e 1c 6d .;.u..x*.......m 0020: f7 10 1c 69 94 2c 4d 07 d9 68 8b 61 09 85 bb d3 ...i.,M..h.a.... 0030: 4e e8 58 20 e2 0c c9 bc a9 a8 1e b7 f6 59 65 7d N.X..........Ye} content key 0000: 19 3f fc 9e 78 e2 35 05 9c 2f 5e 15 f1 6a 3a ed .?..x.5../^..j:. 0010: 00 1a 31 8d ea 33 7a c3 67 57 e7 e6 26 4a bd 00 ..1..3z.gW..&J.. license 0000: be 70 c2 2b 49 ff 69 b4 b4 e5 ec db f4 93 2d d8 .p.+I.i.......-. 0010: a2 42 de 3a 8e 20 38 bd 9e be 00 d5 12 b2 7d 14 .B.:..8.......}. 0020: d4 18 30 ab 3b 96 7e 23 23 fd 81 fd c6 f8 98 7e ..0.;..##....... 0030: 90 7b 78 02 22 1a 21 6b c6 00 13 a1 6a 5c 85 c5 .{x.".!k....j... 0040: 19 8d aa 46 4e 58 d8 a1 c1 6b 9c 6b 9b 02 6e 1e ...FNX...k.k..n. 0050: 83 b4 e4 32 0a 4f 18 28 93 2d 32 0d 8a 04 b8 63 ...2.O.(.-2....c 0060: 85 aa bf 2e af a3 ae 36 9f 8b db c5 06 8f bf 0d .......6........ 0070: 8a 8e 95 be 49 db ab 95 e6 c5 55 18 f2 08 c5 6d ....I.....U....m stored to license.blob ``` We then tried to decrypt the license blob with the use the key subroutine we located for that purpose (decrypt license blob indicated by `prsubs` cmd): ``` wret> declicense ..\mspr_toolkit\license.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: be 70 c2 2b 49 ff 69 b4 b4 e5 ec db f4 93 2d d8 .p.+I.i.......-. 0x00000010: a2 42 de 3a 8e 20 38 bd 9e be 00 d5 12 b2 7d 14 .B.:. 8.......}. 0x00000020: d4 18 30 ab 3b 96 7e 23 23 fd 81 fd c6 f8 98 7e ..0.;.~##......~ 0x00000030: 90 7b 78 02 22 1a 21 6b c6 00 13 a1 6a 5c 85 c5 .{x.".!k....j\.. 0x00000040: 19 8d aa 46 4e 58 d8 a1 c1 6b 9c 6b 9b 02 6e 1e ...FNX...k.k..n. 0x00000050: 83 b4 e4 32 0a 4f 18 28 93 2d 32 0d 8a 04 b8 63 ...2.O.(.-2....c 0x00000060: 85 aa bf 2e af a3 ae 36 9f 8b db c5 06 8f bf 0d .......6........ 0x00000070: 8a 8e 95 be 49 db ab 95 e6 c5 55 18 f2 08 c5 6d ....I.....U....m target 7ffd3d1728a0 wb res: 0 call res 0 output key 0x00000000: be 44 10 7a e9 4a 4b b9 44 8d 98 3d a7 db 5f 5e .D.z.JK.D..=.._^ 0x00000010: 3a c8 66 a1 12 b8 a0 20 ba 90 7c 0e 30 f4 8c 85 :.f.... ..|.0... ``` The output indicated content key value 0x3a for pos 0. We did the same for other values corresponding to pos 0 such as 1: ``` genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 1 -p 0 ``` The output indicated content key value 0x3b at pos 0. ``` wret> declicense ..\mspr_toolkit\license.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: 2c b2 7a 58 fa a0 b2 67 ba 1e 72 9d 54 2f 75 bc ,.zX...g..r.T/u. 0x00000010: 93 82 66 4e 02 92 16 07 0b 16 c6 04 de 32 a5 94 ..fN.........2.. 0x00000020: 29 52 e6 8f 1b 08 65 e2 ad 16 9a 8c d5 a3 42 df )R....e.......B. 0x00000030: b0 6e c6 61 f6 46 c5 b9 31 13 d3 a4 a9 7d 55 a5 .n.a.F..1....}U. 0x00000040: 79 c2 65 b4 9f 21 4b 53 85 35 4c 26 fc 4d 87 81 y.e..!KS.5L&.M.. 0x00000050: 9f fb 55 3e f6 3d 40 6d 74 85 cf b3 f6 c1 2e 8d ..U>.=@mt....... 0x00000060: 57 32 3e c9 77 c8 53 d6 74 b8 df ba 02 45 9d c2 W2>.w.S.t....E.. 0x00000070: ee 5d 52 b7 f7 23 81 dd 23 bd dc 5b ba d1 16 d0 .]R..#..#..[.... target 7ffd3c6528a0 wb res: 0 call res 0 output key 0x00000000: 56 1b e7 af 17 c1 ca 55 bb b8 f7 dc 1a d2 c5 97 V......U........ 0x00000010: 3b c0 85 03 97 db 75 a5 b5 83 73 19 13 4d 16 24 ;.....u...s..M.$ ``` We tested other values and here is where things started to gen interesting. No matter what content key value was used for pos 0, the output obtained indicated it was simply a result of a XOR operation with value 0x3a. The other interesting observation was that the function used to calculate the output byte for a content key at pos 0 was a bijection (XOR). So, we tested values at other positions: ``` genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 0 -p 1 genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 1 -p 1 genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 2 -p 1 ... genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 0 -p 2 genlicense -i 0C86330B0E98CD7C586F336088DAFA0E -v 1 -p 2 ... ``` For all positions, we ended up with a bijection and a XOR function with some constant value. The only difference was in a constant value - it was different for every key pos. So, finally we generated a license with a content key having all bytes set to zero: ``` msprcp> genzlicense -i 0C86330B0E98CD7C586F336088DAFA0E -p decdata\key_zero.dat pubkey 0000: cb 27 6f 9f 9f 76 46 64 54 23 19 ef 9c c7 69 0f .'o..vFdT#....i. 0010: 9c 3b e3 75 8b d3 78 2a 8d 03 fb a8 bf 9e 1c 6d .;.u..x*.......m 0020: f7 10 1c 69 94 2c 4d 07 d9 68 8b 61 09 85 bb d3 ...i.,M..h.a.... 0030: 4e e8 58 20 e2 0c c9 bc a9 a8 1e b7 f6 59 65 7d N.X..........Ye} content key 0000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO.MICROSOFT! 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ license 0000: fe c1 77 07 03 01 0c d1 3b 75 54 24 3f d2 cf 53 ..w.....;uT$?..S 0010: c7 21 68 a6 da f9 72 ab 6c 9d b5 9a 0b 10 88 40 .!h...r.l......@ 0020: fe 91 90 55 12 0f 0c 80 04 88 c1 80 87 67 68 3c ...U.........gh< 0030: 32 4f 1b 46 1d eb 6c e1 d1 f0 9e ea 99 09 30 49 2O.F..l.......0I 0040: 59 3c 6e a9 72 2f 8e 4c 9c 0b e0 f2 62 85 37 33 Y declicense ..\mspr_toolkit\zlicense.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: fe c1 77 07 03 01 0c d1 3b 75 54 24 3f d2 cf 53 ..w.....;uT$?..S 0x00000010: c7 21 68 a6 da f9 72 ab 6c 9d b5 9a 0b 10 88 40 .!h...r.l......@ 0x00000020: fe 91 90 55 12 0f 0c 80 04 88 c1 80 87 67 68 3c ...U.........gh< 0x00000030: 32 4f 1b 46 1d eb 6c e1 d1 f0 9e ea 99 09 30 49 2O.F..l.......0I 0x00000040: 59 3c 6e a9 72 2f 8e 4c 9c 0b e0 f2 62 85 37 33 Y....3....{..1. 0x00000010: 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 :.W,..........1. ``` The following XOR key value (magic sequence) was obtained: ``` 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 ``` By following the same approach across various PlayReady library and Windows OS versions (10 and 11), we found out that there are only two such magic key sequences used across Windows OS versions released since 2022 (one for Windows 10, the other for Windows 11). The sequence for Windows 11 was the following: ``` 81 73 8a db 5b 4b b4 22 3f aa d8 9c bb 45 9b ec ``` The script `genxorkey.bat` located in `tests\xor_key` directory can discover XOR key values automatically for various versions of PlayReady libraries present in Windows 10 / 11 x64 systems across various builds from late 2022 till Nov 2024. The name of the output file is `xorkey.txt'. Corresponding `gentest.bat` script decrypts the license containing special signature / content key sequence (`decdata\key_zero.dat`). It produces `test.txt` output file for each library. ### CONTENT KEYS / MAGIC XOR KEYS VERIFICATION Knowing that content key blob obtained by the license sniffer was a bijection (XOR functions with a predefined constant for each byte pos), we could now try to obtain the plaintext value of the actual content key. We imported sniffed data for arbitrary movie from Canal+ Online VOD collection: ``` msprcp> implicdata ..\..\tests\sniffdata\vod\w11\canalp_xxxxxxxxxxxxx.dat LICENSE * key id: d4710165a17e7f4ab6f0973e9bb8c723 * content key: 04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx * key SHA256: c37da9b9500501a429ed038a6bf037df079a69aefdbcee873f2414089d51882b ``` We exported its license blob to file: ``` msprcp> licdata -k d4710165a17e7f4ab6f0973e9bb8c723 -e sniff.blob license blob 0000: 3f a0 a4 df 3a a5 4b 9b 21 19 4e 76 04 c3 49 4c ?...:.K.!.Nv..IL 0010: 6d 56 e6 8a b8 b1 5c 4d 93 83 06 70 95 19 e2 de mV.....M...p.... 0020: 65 22 f3 40 37 7e 4a 3e 60 56 ff 46 be 85 37 6e e".@7.J>.V.F..7n 0030: 04 6b 8d af 37 e2 6b f2 fb fd 00 b9 cc cf a8 33 .k..7.k........3 0040: e8 07 bb 65 d3 3b 58 99 42 38 a1 7a 74 b6 39 f6 ...e.;X.B8.zt.9. 0050: 4e 13 53 ae 71 ec 44 67 59 7b 10 99 6a 10 a0 f8 N.S.q.DgY{..j... 0060: 2b 78 a0 b0 85 19 9e c7 d9 ed b9 54 e4 e1 b4 2e +x.........T.... 0070: 9c 70 0e 87 ac 86 7b a1 7c 03 a6 f7 99 5b d4 45 .p....{.|....[.E stored to sniff.blob msprcp> ``` The license data indicated it was encrypted to the identity key starting with `34 87 66 73` bytes: ``` msprcp> licdata -k d4710165a17e7f4ab6f0973e9bb8c723 -v ... attr: 002a ECCDeviceKey data 0000: 00 01 00 40 34 87 66 73 af da 57 db 05 60 7a 56 ...@4.fs..W...zV 0010: 93 31 05 44 b9 79 c2 4d 20 95 b3 5f 99 da d5 8d .1.D.y.M..._.... 0020: f4 ae e5 af e7 60 33 06 ec 03 84 dc 90 74 ec e6 ......3......t.. 0030: 05 08 01 ea b2 b8 bd 47 2a ba c0 6c a8 98 14 6b .......G*..l...k 0040: ed 29 40 ab .)@. ... ``` So, in order to decrypt the license we needed to use the W11 CDM identity that has this key: ``` msprcp> set CDM_DIR cdm\w11 msprcp> identity 318780A3793C675A09F6871E67DA5817 49CB836D40F3C68E496382B6F26B035D 5B75F8180A95751793D99A4E3BCF1E28 92B7A487F4BBA29A87CC845EA86CCBAC 9D83557F9740AAFD4F8F594279D970F1 msprcp> identity 5B75F8180A95751793D99A4E3BCF1E28 [5B75F8180A95751793D99A4E3BCF1E28] SIGN IdentityInfo pubkey 0000: 78 1f a2 d6 09 98 78 88 07 ce bd 27 c8 f5 83 60 x.....x....'.... 0010: 0b 1c 7b 46 ad ee f1 db b3 4e 15 88 f6 40 cc 31 ..{F.....N...@.1 0020: ea 3c ea 06 5d 5f 59 74 f1 34 4c e2 ae a6 b0 bb .<..]_Yt.4L..... 0030: ea 96 70 ea 0a c5 3f 94 a1 c1 b8 ec 9e d6 93 1c ..p...?......... prvkey 0000: 97 f2 1f bf e9 eb 6f 1b 56 45 61 45 20 63 3e af ......o.VEaE.c>. 0010: af 40 ca 47 10 b2 34 3e 60 49 0d 1f 18 b2 56 33 .@.G..4>.I....V3 ENCRYPT IdentityInfo pubkey 0000: 34 87 66 73 af da 57 db 05 60 7a 56 93 31 05 44 4.fs..W...zV.1.D 0010: b9 79 c2 4d 20 95 b3 5f 99 da d5 8d f4 ae e5 af .y.M..._........ 0020: e7 60 33 06 ec 03 84 dc 90 74 ec e6 05 08 01 ea ..3......t...... 0030: b2 b8 bd 47 2a ba c0 6c a8 98 14 6b ed 29 40 ab ...G*..l...k.)@. prvkey 0000: 5d ae 07 b2 02 c1 28 0b d5 86 04 9f 2e ee a9 00 ].....(......... 0010: 42 de 00 b3 25 ae 82 1d b6 22 c9 42 80 af 5f ed B...%....".B.._. 0020: 08 85 aa 4b 33 cd b9 2c 69 a5 2b dc 2f 72 1c 6a ...K3..,i.+./r.j 0030: 99 b9 76 cc 57 99 93 70 c4 eb 10 af 50 e4 ba 09 ..v.W..p....P... 0040: 92 . ``` After exporting the identity keys: ``` msprcp> identity -e 5B75F8180A95751793D99A4E3BCF1E28 5B75F8180A95751793D99A4E3BCF1E28.enc.pub (public encryption key) 5B75F8180A95751793D99A4E3BCF1E28.enc.prv (private encryption key) 5B75F8180A95751793D99A4E3BCF1E28.sig.pub (public signing key) 5B75F8180A95751793D99A4E3BCF1E28.sig.prv (private signing key) ``` we tried to decrypt the sniffed license blob, this time we needed to use some PlayReady library corresponding to Windows 11 OS: ``` wret> prlib w11_oct24.dll PRLib::run wret> wbsetup -r WBSetupCmd::run Warbird encrypted binary ### setting up Warbird for runtime analysis - scanning for heap exec descriptors found: 985 - scanning for segment descriptors found: 37 - decrypting heapexec descriptors - decrypting segment descriptors - locating ret syscalls total: 1042 - locating heapexec syscalls total: 919 (WB_MOV10_B9 892, WB_MOV10_REG 7, WB_LEA10 5, WB_SHARED 15) - adjusting code refs total: 1487 - adjusting data refs total: 2 - patching self LEAs total: 985 - patching ret syscalls - patching heapexec syscalls - patching wb call - patching antidebug call - patching App Policy - Dll init Windows.Media.dll size 7145640 bytes base 180000000 - patching App Model - Dll init Windows.Storage.ApplicationData.dll size 435248 bytes base 180000000 - disabling thread library calls wret> wret> wret> declicense ..\mspr_toolkit\sniff.blob -k 5B75F8180A95751793D99A4E3BCF1E28.enc.prv DecLicenseCmd::run DECRYPT LICENSE main obj lea at 2bde10 main obj vtable 43b4d8 lea at 2bdfa7 lea va 2c1920 lea at 2c1995 lea va 2c0050 syscall va 2c04a7 lea candidate at 2c0479 lea candidate va 2cba40 syscall va 2c061e lea candidate at 2c05d6 lea candidate va 2cc170 sub_candidate e9600 size 63cf keydata 0x00000000: 5d ae 07 b2 02 c1 28 0b d5 86 04 9f 2e ee a9 00 ].....(......... 0x00000010: 42 de 00 b3 25 ae 82 1d b6 22 c9 42 80 af 5f ed B...%....".B.._. 0x00000020: 08 85 aa 4b 33 cd b9 2c 69 a5 2b dc 2f 72 1c 6a ...K3..,i.+./r.j 0x00000030: 99 b9 76 cc 57 99 93 70 c4 eb 10 af 50 e4 ba 09 ..v.W..p....P... 0x00000040: 92 . license 0x00000000: 3f a0 a4 df 3a a5 4b 9b 21 19 4e 76 04 c3 49 4c ?...:.K.!.Nv..IL 0x00000010: 6d 56 e6 8a b8 b1 5c 4d 93 83 06 70 95 19 e2 de mV....\M...p.... 0x00000020: 65 22 f3 40 37 7e 4a 3e 60 56 ff 46 be 85 37 6e e".@7~J>`V.F..7n 0x00000030: 04 6b 8d af 37 e2 6b f2 fb fd 00 b9 cc cf a8 33 .k..7.k........3 0x00000040: e8 07 bb 65 d3 3b 58 99 42 38 a1 7a 74 b6 39 f6 ...e.;X.B8.zt.9. 0x00000050: 4e 13 53 ae 71 ec 44 67 59 7b 10 99 6a 10 a0 f8 N.S.q.DgY{..j... 0x00000060: 2b 78 a0 b0 85 19 9e c7 d9 ed b9 54 e4 e1 b4 2e +x.........T.... 0x00000070: 9c 70 0e 87 ac 86 7b a1 7c 03 a6 f7 99 5b d4 45 .p....{.|....[.E target 7ffd3c9cc170 wb res: 0 call res 0 output key 0x00000000: 24 5a 65 ef a8 c8 07 9c 08 1e 4e 8f 85 97 6d 85 $Ze.......N...m. 0x00000010: 85 14 8f 9e 4b b8 c0 7e 7b 30 fe fd db 0f 08 8a ....K..~{0...... XOR(ed) signature key 0x00000000: 24 5a 65 ef a8 c8 07 9c 08 1e 4e 8f 85 97 6d 85 $Ze.......N...m. XOR(ed) content key 0x00000000: 85 14 8f 9e 4b b8 c0 7e 7b 30 fe fd db 0f 08 8a ....K..~{0...... ``` The output key `85 14 8f 9e` visible above is the content key blob. In order to find the actual content key value, we need to set the XOR key, which is zero by default: ``` wret> xorkey XORKeyCmd::run [none] 0x00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` So, we set the magic XOR key value to correspond to Windows 11 OS: ``` wret> xorkey w11 XORKeyCmd::run [w11] 0x00000000: c3 cd 57 69 1e ff 15 6c 95 d9 46 d5 34 12 1d fb ..Wi...l..F.4... 0x00000010: 81 73 8a db 5b 4b b4 22 3f aa d8 9c bb 45 9b ec .s..[K."?....E.. ``` and decrypt the license again: ``` wret> declicense ..\mspr_toolkit\sniff.blob -k 5B75F8180A95751793D99A4E3BCF1E28.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 5d ae 07 b2 02 c1 28 0b d5 86 04 9f 2e ee a9 00 ].....(......... 0x00000010: 42 de 00 b3 25 ae 82 1d b6 22 c9 42 80 af 5f ed B...%....".B.._. 0x00000020: 08 85 aa 4b 33 cd b9 2c 69 a5 2b dc 2f 72 1c 6a ...K3..,i.+./r.j 0x00000030: 99 b9 76 cc 57 99 93 70 c4 eb 10 af 50 e4 ba 09 ..v.W..p....P... 0x00000040: 92 . license 0x00000000: 3f a0 a4 df 3a a5 4b 9b 21 19 4e 76 04 c3 49 4c ?...:.K.!.Nv..IL 0x00000010: 6d 56 e6 8a b8 b1 5c 4d 93 83 06 70 95 19 e2 de mV....\M...p.... 0x00000020: 65 22 f3 40 37 7e 4a 3e 60 56 ff 46 be 85 37 6e e".@7~J>`V.F..7n 0x00000030: 04 6b 8d af 37 e2 6b f2 fb fd 00 b9 cc cf a8 33 .k..7.k........3 0x00000040: e8 07 bb 65 d3 3b 58 99 42 38 a1 7a 74 b6 39 f6 ...e.;X.B8.zt.9. 0x00000050: 4e 13 53 ae 71 ec 44 67 59 7b 10 99 6a 10 a0 f8 N.S.q.DgY{..j... 0x00000060: 2b 78 a0 b0 85 19 9e c7 d9 ed b9 54 e4 e1 b4 2e +x.........T.... 0x00000070: 9c 70 0e 87 ac 86 7b a1 7c 03 a6 f7 99 5b d4 45 .p....{.|....[.E target 7ffd3c9cc170 wb res: 0 call res 0 output key 0x00000000: 24 5a 65 ef a8 c8 07 9c 08 1e 4e 8f 85 97 6d 85 $Ze.......N...m. 0x00000010: 85 14 8f 9e 4b b8 c0 7e 7b 30 fe fd db 0f 08 8a ....K..~{0...... signature key 0x00000000: e7 97 32 86 b6 37 12 f0 9d c7 08 5a b1 85 70 7e ..2..7.....Z..p~ content key 0x00000000: 04 67 05 45 10 f3 74 5c 44 9a 26 61 60 4a 93 66 .g.E..t\D.&a`J.f wret> ``` Now, the real content key is revealed (`04 67 05 45`...). A proof and confirmation that this key is real is available in tests directory (`win_sniffed_key_for_canalp_vod`). It contains a log that showcases the import of a license data (and content key) contained by the sniffer to MSPR toolkit for a successful movie sequence download and decryption. The other confirmation that sniffed content keys are real ones can be obtained with the help of license signatures and crypto: ``` msprcp> licdata -k d4710165a17e7f4ab6f0973e9bb8c723 -v LICENSE * key id: d4710165a17e7f4ab6f0973e9bb8c723 * signature key: e7xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx * key SHA256: ab7ea6600bee5e9d150051bbcf74ae0f64971d23f0d8fa102a261860c473a36e * content key: 04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx * key SHA256: c37da9b9500501a429ed038a6bf037df079a69aefdbcee873f2414089d51882b ... XMR SIGNATURE 0000: ed 47 b2 51 7d 1a 5f 25 9e f1 59 d7 7a 91 58 f8 .G.Q}._%..Y.z.X. calculated cmac 0000: ed 47 b2 51 7d 1a 5f 25 9e f1 59 d7 7a 91 58 f8 .G.Q}._%..Y.z.X. ##### SIGNATURE KEY / AES-CMAC OK msprcp> ``` The crypto check works as following: - plaintext value of a digital signature key encrypted through ECC is extracted from a Protected Media Path process (this explains several versions of the sniffer .dat files), - the extracted signature key is used to calculate the AES-CMAC value of a binary license XMR blob - the calculated signature value is checked against the signature appended at the end of the issued license - correct AES-CMAC value implicates correct signature key (and correct content key) The above mechanism is also used by Microsoft to verify the correctness of decrypted content keys received from a license server. It relies on the fact that signature key is part of the same encrypted license blob as content key. Thus, successful extraction of a signature key implicates successful extraction of a content key. Finally, it is worth to note that sniffer tool has been updated to "deobfuscate" the content key blobs harvested from the stack with the use of a magic XOR key values (provided as config in `sniffer\decdata\xorkey.w10` and `sniffer\decdata\xorkey.w11`). As such, the sniffer should be deemed as extracting actual (plaintext) content key values. ## WHITEBOX CRYPTO BYPASS The initial (XOR key) attack, is limited to the narrow time window. The sniffer needs to extract content key from the stack at a given code execution context. The content keys gets cleared upon return from a target subroutine. There is however a data structure, which is not imposed by said limits. These are the white-box data structures in a form of AES round keys. They need to be present for the whole time of a movie decryption / playback (core AES decryption subroutine uses these). PlayReady creates a license decryptor object as a follow up of a license blob decryption and successful signature verification. The license blob decryptor doesn't hold the content key (AES key) in clear form - it maintains it through the form of white-box crypto AES round keys. The goal of white-box crypto is to make reconstruction of a secret-key difficult / not possible. This is illustrated by the constructor code below: ``` .text:0000000180222D50 sub_180222D50 proc near ; DATA XREF: agsr_construct_license_decryptor_internal+CE↓o .text:0000000180222D50 ; .pdata:00000001809AF98C↓o .text:0000000180222D50 .text:0000000180222D50 arg_0 = qword ptr 8 .text:0000000180222D50 arg_8 = qword ptr 10h .text:0000000180222D50 arg_10 = qword ptr 18h .text:0000000180222D50 arg_18 = qword ptr 20h .text:0000000180222D50 .text:0000000180222D50 lea rdx, unk_180998298 .text:0000000180222D57 mov rcx, [rsp+0] .text:0000000180222D5B mov [rsp+arg_0], rbx .text:0000000180222D60 mov [rsp+arg_8], rbp .text:0000000180222D65 mov [rsp+arg_10], rsi .text:0000000180222D6A mov [rsp+arg_18], rdi .text:0000000180222D6F push r14 .text:0000000180222D71 push r15 .text:0000000180222D73 movzx ebp, word ptr [rcx+10h] ; decryptor type .text:0000000180222D77 mov rsi, rdx .text:0000000180222D7A mov r9, [rcx+1Eh] .text:0000000180222D7E mov r10d, ebp ; decryptor type .text:0000000180222D81 mov r11d, [rcx+1Ah] .text:0000000180222D85 mov r8, [rcx+12h] ; src data to copy into decryptor .text:0000000180222D89 mov rbx, [rcx+8] ; this ... .text:0000000180222E02 mov edx, 0B0h ; size 0xb0 .text:0000000180222E07 lea r9, [rbx+14h] ; target to copy data to .text:0000000180222E07 ; decryptor offset+0x14, data size 0xb0 (AES round keys) .text:0000000180222E0B nop dword ptr [rax+rax+00h] .text:0000000180222E10 .text:0000000180222E10 loc_180222E10: ; CODE XREF: sub_180222D50+D2↓j .text:0000000180222E10 movzx eax, byte ptr [r8] .text:0000000180222E14 lea r8, [r8+1] .text:0000000180222E18 mov [r9], al .text:0000000180222E1B lea r9, [r9+1] .text:0000000180222E1F add edx, 0FFFFFFFFh .text:0000000180222E22 jnz short loc_180222E10 ``` The white-box crypto attack becomes fairly easy to implement upon the knowledge of the actual content key and resulting AES round keys. The AES round keys can be obtained with the use of a prsubs call to "expand key" code. We found out the relation between content key bytes and initial white-boxed AES round key (key round 0) by verifying what value is generated by the expand key subroutine for a given pos as a response to given content key byte (at the same pos). Finding out the answer to the above required to run "expand key" subroutine 16*256 times (16 is the length of AES content key, 256 is the possible value count for each pos). As a result, we obtained 16 tables, each was 256 bytes in size. These can be seen with the use of `ektable` (expand key table) command: ``` wret> ektable -v EKTableCmd::run [w11_oct24.dll] * pos 0 0x00000000: 60 d4 13 a7 86 32 f5 41 b7 03 c4 70 51 e5 22 96 `....2.A...pQ.". 0x00000010: d5 61 a6 12 33 87 40 f4 02 b6 71 c5 e4 50 97 23 .a..3.@...q..P.# 0x00000020: 11 a5 62 d6 f7 43 84 30 c6 72 b5 01 20 94 53 e7 ..b..C.0.r.. .S. 0x00000030: a4 10 d7 63 42 f6 31 85 73 c7 00 b4 95 21 e6 52 ...cB.1.s....!.R 0x00000040: 82 36 f1 45 64 d0 17 a3 55 e1 26 92 b3 07 c0 74 .6.Ed...U.&....t 0x00000050: 37 83 44 f0 d1 65 a2 16 e0 54 93 27 06 b2 75 c1 7.D..e...T.'..u. 0x00000060: f3 47 80 34 15 a1 66 d2 24 90 57 e3 c2 76 b1 05 .G.4..f.$.W..v.. 0x00000070: 46 f2 35 81 a0 14 d3 67 91 25 e2 56 77 c3 04 b0 F.5....g.%.Vw... 0x00000080: bf 0b cc 78 59 ed 2a 9e 68 dc 1b af 8e 3a fd 49 ...xY.*.h....:.I 0x00000090: 0a be 79 cd ec 58 9f 2b dd 69 ae 1a 3b 8f 48 fc ..y..X.+.i..;.H. 0x000000a0: ce 7a bd 09 28 9c 5b ef 19 ad 6a de ff 4b 8c 38 .z..(.[...j..K.8 0x000000b0: 7b cf 08 bc 9d 29 ee 5a ac 18 df 6b 4a fe 39 8d {....).Z...kJ.9. 0x000000c0: 5d e9 2e 9a bb 0f c8 7c 8a 3e f9 4d 6c d8 1f ab ]......|.>.Ml... 0x000000d0: e8 5c 9b 2f 0e ba 7d c9 3f 8b 4c f8 d9 6d aa 1e .\./..}.?.L..m.. 0x000000e0: 2c 98 5f eb ca 7e b9 0d fb 4f 88 3c 1d a9 6e da ,._..~...O.<..n. 0x000000f0: 99 2d ea 5e 7f cb 0c b8 4e fa 3d 89 a8 1c db 6f .-.^....N.=....o * pos 1 0x00000000: ec 58 9f 2b 0a be 79 cd 3b 8f 48 fc dd 69 ae 1a .X.+..y.;.H..i.. 0x00000010: 59 ed 2a 9e bf 0b cc 78 8e 3a fd 49 68 dc 1b af Y.*....x.:.Ih... 0x00000020: 9d 29 ee 5a 7b cf 08 bc 4a fe 39 8d ac 18 df 6b .).Z{...J.9....k 0x00000030: 28 9c 5b ef ce 7a bd 09 ff 4b 8c 38 19 ad 6a de (.[..z...K.8..j. 0x00000040: 0e ba 7d c9 e8 5c 9b 2f d9 6d aa 1e 3f 8b 4c f8 ..}..\./.m..?.L. 0x00000050: bb 0f c8 7c 5d e9 2e 9a 6c d8 1f ab 8a 3e f9 4d ...|]...l....>.M 0x00000060: 7f cb 0c b8 99 2d ea 5e a8 1c db 6f 4e fa 3d 89 .....-.^...oN.=. 0x00000070: ca 7e b9 0d 2c 98 5f eb 1d a9 6e da fb 4f 88 3c .~..,._...n..O.< 0x00000080: 33 87 40 f4 d5 61 a6 12 e4 50 97 23 02 b6 71 c5 3.@..a...P.#..q. 0x00000090: 86 32 f5 41 60 d4 13 a7 51 e5 22 96 b7 03 c4 70 .2.A`...Q."....p 0x000000a0: 42 f6 31 85 a4 10 d7 63 95 21 e6 52 73 c7 00 b4 B.1....c.!.Rs... 0x000000b0: f7 43 84 30 11 a5 62 d6 20 94 53 e7 c6 72 b5 01 .C.0..b. .S..r.. 0x000000c0: d1 65 a2 16 37 83 44 f0 06 b2 75 c1 e0 54 93 27 .e..7.D...u..T.' 0x000000d0: 64 d0 17 a3 82 36 f1 45 b3 07 c0 74 55 e1 26 92 d....6.E...tU.&. 0x000000e0: a0 14 d3 67 46 f2 35 81 77 c3 04 b0 91 25 e2 56 ...gF.5.w....%.V 0x000000f0: 15 a1 66 d2 f3 47 80 34 c2 76 b1 05 24 90 57 e3 ..f..G.4.v..$.W. ... ``` While white-box crypto in use by PlayReady doesn't rely on a simple XOR as it was the case for content key obfuscation, it is still of questionable strength. The problem lies in the fact that tables obtained for each content key pos (ektables) are all bijections (1 to 1 mappings, each table contains unique 256 values). This makes discovery of a content key from white-box structures maintained by the decryptor straightforward. One just needs the first white-box AES round key value and reverse lookup table built from ektables described above. The illustration of content key discovery from white-box crypto structures is shown below. We first decrypt arbitrary sniffed license, but this time also issue a call to expand key in order to obtain the corresponding white-box AES crypto structure: ``` wret> declicense ..\mspr_toolkit\sniff.blob -k 5B75F8180A95751793D99A4E3BCF1E28.enc.prv -e -o ekey.dat ... target 7ffd3d2ec170 wb res: 0 call res 0 output key 0x00000000: 24 5a 65 ef a8 c8 07 9c 08 1e 4e 8f 85 97 6d 85 $Ze.......N...m. 0x00000010: 85 14 8f 9e 4b b8 c0 7e 7b 30 fe fd db 0f 08 8a ....K..~{0...... signature key 0x00000000: e7 97 32 86 b6 37 12 f0 9d c7 08 5a b1 85 70 7e ..2..7.....Z..p~ content key 0x00000000: 04 67 05 45 10 f3 74 5c 44 9a 26 61 60 4a 93 66 .g.E..t\D.&a`J.f 7ffd3d109600 expand key expandkey res: 0 expanded key 0x00000000: 7e e3 f8 8f 31 f4 d8 1d 1e c1 6a 92 4c be 7a e5 ~...1.....j.L.z. 0x00000010: 0d 50 a6 75 53 be 04 e9 26 dc 96 d2 cc 05 92 23 .P.uS...&......# 0x00000020: a7 be 78 39 64 c8 37 c3 be 4f c8 ae d2 a1 c1 23 ..x9d.7..O.....# 0x00000030: 1d 8b 2a 7a 61 5b 05 a1 c7 0c d5 17 0d b5 0c 2c ..*za[........., 0x00000040: 88 32 d7 b6 f1 71 ca 0f 2e 65 07 00 3b c8 13 34 .2...q...e..;..4 0x00000050: c1 67 90 e0 28 0e 42 f7 1e 73 5d ef 3d a3 56 c3 .g..(.B..s].=.V. 0x00000060: a1 88 ad a4 91 9e f7 4b 97 f5 b2 bc b2 4e fc 67 .......K.....N.g 0x00000070: 16 ec 8b 98 9f 6a 64 cb 10 87 ce 6f ba d1 2a 10 .....jd....o..*. 0x00000080: 03 09 52 12 84 7b 2e c1 8c e4 f8 b6 2e 2d ca be ..R..{.......-.. 0x00000090: be 9b 9b f8 71 de b0 71 a0 5f 78 ea 8b 7f e4 6f ....q..q._x....o 0x000000a0: 2a 21 ec 62 d4 9a 40 66 dd d3 5d de c7 62 b0 7a *!.b..@f..]..b.z saving to ekey.dat file wret> ``` Then, we make use of the predefined ektable in order to obtain the content key value corresponding to the initial round key (EK key): ``` wret> ekkey ekey.dat EKKeyCmd::run EK key 0x00000000: 7e e3 f8 8f 31 f4 d8 1d 1e c1 6a 92 4c be 7a e5 ~...1.....j.L.z. 7ffd3d109600 plaintext key 0x00000000: 04 67 05 45 10 f3 74 5c 44 9a 26 61 60 4a 93 66 .g.E..t\D.&a`J.f wret> ``` One can notice that value `7e` treated as index of EK table (pos 0) yields value `04`. Similarly value `e3` for EK table (pos 1) yields `67`. This confirms the ability to discover plaintext value of a content key from AES white-box crypto (more specifically from AES round key 0). What's worth to mention is that the white-box crypto attack seems to work for PlayReady binaries corresponding to same Windows OS version (not all binaries were tested, but Win 11 binaries from Sep 2024, Aug 2024 and Oct 2024 all worked fine though). Finally, we can check if the white-box AES key extracted by the sniffer can be deciphered. This time, we do the test on Windows 10. The test is conducted with respect to license data obtained for the same movie available in CANAL+ VOD (`XXXXX XXXXXXXX`). First, we import the sniffed license data to the MSPR toolkit: ``` msprcp> implicdata ..\..\tests\sniffdata\vod\w10\canalp_xxxxxxxxxxxxx.dat LICENSE * key id: d4710165a17e7f4ab6f0973e9bb8c723 * content key: 04xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx * key SHA256: c37da9b9500501a429ed038a6bf037df079a69aefdbcee873f2414089d51882b ``` Then we import the AES rounds harvested by the sniffer to output file: ``` msprcp> licdata -k d4710165a17e7f4ab6f0973e9bb8c723 -r rounds.dat rounds key 0000: 8c d1 ae 6c c3 c6 8e fe 9f 02 5b 3c cd 7d 4b 4b ...l......[<.}KK 0010: 7a 36 9a 99 d8 1a dc 1e 66 9f e8 2c b1 d4 13 95 z6......f..,.... 0020: 8e 18 01 c0 f3 6b f0 3f 18 b3 6b ed 98 8f 08 33 .....k.?..k....3 0030: e2 9d 50 36 97 70 26 8f 09 45 cb e4 17 4c 45 51 ..P6.p&..E...LEQ 0040: f8 94 fc 29 e9 62 5c 20 66 a1 11 42 f7 6b d2 95 ...).b..f..B.k.. 0050: 08 96 3c 4e 67 72 e6 e8 87 55 71 2c f6 b8 25 3f .. image w10_prlib.dll ImageCmd::run - Dll init w10_prlib.dll size 10347408 bytes base 180000000 wret> prlib w10_prlib.dll PRLib::run wret> wbsetup -r WBSetupCmd::run Warbird encrypted binary ### setting up Warbird for runtime analysys - scanning for heap exec descriptors found: 981 - scanning for segment descriptors found: 37 - decrypting heapexec descriptors - decrypting segment descriptors - locating ret syscalls total: 1023 - locating heapexec syscalls total: 911 (WB_MOV10_B9 880, WB_MOV10_REG 10, WB_LEA10 3, WB_SHARED 18) - adjusting code refs total: 1470 - adjusting data refs total: 2 - patching self LEAs total: 981 - patching ret syscalls - patching heapexec syscalls - patching wb call - patching antidebug call - patching App Policy - Dll init Windows.Media.dll size 7145640 bytes base 180000000 - patching App Model - Dll init Windows.Storage.ApplicationData.dll size 435248 bytes base 180000000 - disabling thread library calls wret> ``` We setup the XOR key for Windows 10 system too prior to setting up EK tables: ``` wret> xorkey w10 XORKeyCmd::run [w10] 0x00000000: a7 7b ec e4 91 a8 7e bc d8 a2 c6 28 56 b1 65 b3 .{....~....(V.e. 0x00000010: 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 :.W,..........1. ``` We use `ekkey` command to obtain the plaintext value of the content key from the whitebox crypto data file: ``` wret> ekkey ..\mspr_toolkit\rounds.dat EKKeyCmd::run EK key 0x00000000: 8c d1 ae 6c c3 c6 8e fe 9f 02 5b 3c cd 7d 4b 4b ...l......[<.}KK plaintext key 0x00000000: 04 67 05 45 10 f3 74 5c 44 9a 26 61 60 4a 93 66 .g.E..t\D.&a`J.f ``` We obtained the content key, which was verified through movie decryption and XMR crypto signature to be the correct one. It's worth to mention that ektable bijection made the algebraic transformation in use by the core AES decryption subroutine irrelevant (no need to analyse) too: ``` .text:00000001806B3F55 pcmpgtb xmm5, xmm3 ; ###################### .text:00000001806B3F55 ; ##### GMUL 2 .text:00000001806B3F55 ; ###################### .text:00000001806B3F55 ; .text:00000001806B3F55 ; xmm5 ffffffff000000ffffffff000000 .text:00000001806B3F59 pand xmm5, xmm6 ; xmm5 1b1b1b1b0000001b1b1b1b000000 .text:00000001806B3F5D paddb xmm3, xmm3 ; xmm3 ca8e4206da9e5216dc985410cc8844 .text:00000001806B3F61 test r13, r13 ; obfuscated content key .text:00000001806B3F64 pxor xmm4, xmm4 ; =0 .text:00000001806B3F68 pxor xmm3, xmm5 ; xmm3 d195591dda9e5216c7834f0bcc8844 .text:00000001806B3F68 ; xmm3 = iv[0-f] gmul 2 #2 .text:00000001806B3F6C jz loc_1806B498E ; -> error .text:00000001806B3F72 pcmpgtb xmm4, xmm3 ; ###################### .text:00000001806B3F72 ; ##### GMUL 2 .text:00000001806B3F72 ; ###################### .text:00000001806B3F72 ; .text:00000001806B3F72 ; xmm4 ffff0000ffff00ffff0000ffff00 .text:00000001806B3F76 xor eax, eax ; =0 .text:00000001806B3F78 pand xmm4, xmm6 ; xmm4 1b1b00001b1b001b1b00001b1b00 .text:00000001806B3F7C paddb xmm3, xmm3 ; xmm3 a22ab23ab43ca42c8e069e16981088 .text:00000001806B3F80 movdqu xmm0, xmmword ptr [r13+0A0h] ; obfuscated content key .text:00000001806B3F80 ; round key 10 ??? .text:00000001806B3F80 ; key[a0-af] .text:00000001806B3F89 pxor xmm5, xmm5 ; =0 .text:00000001806B3F8D inc eax ; =1 .text:00000001806B3F8F pxor xmm3, xmm4 ; xmm3 b931b23aaf27a42c951d9e16830b88 .text:00000001806B3F8F ; xmm3 = iv[0-f] gmul 2 #3 .text:00000001806B3F93 movd xmm7, eax ; =1 (iv increment) .text:00000001806B3F97 pcmpgtb xmm5, xmm3 ; ###################### .text:00000001806B3F97 ; ##### GMUL 2 .text:00000001806B3F97 ; ###################### .text:00000001806B3F97 ; ``` ### OFFLINE IMPLEMENTATION We verified the content of ektables used across PlayReady binaries available for Windows 10 and 11 (`tests\xor_key\genektable.bat` script). The tests revealed that there are only two such tables in use, one for Windows 10 and the other for Windows 11 systems identified by the following hashes: * EK table sha256 (Windows 10) 0x00000000: dd c2 07 9b d3 4d 62 87 01 19 54 7a 56 5c 0c cc .....Mb...TzV\.. 0x00000010: 35 b3 70 95 1b 5b 3a 4b 0e f8 19 d8 20 65 72 ec 5.p..[:K.... er. * EK table sha256 (Windows 11) 0x00000000: 43 50 4b aa dd 25 53 09 74 fb 17 c0 c6 fd 69 34 CPK..%S.t.....i4 0x00000010: 40 59 fb 1f a9 1f 28 ab d2 2e 68 26 7b 78 25 7d @Y....(...h&{x%} This implicates potential offline implementation of white-box AES key rounds decryption upon their harvesting from PMP process memory (implementation that uses two ektables, each 16*256 in size and that does not require dedicated tools such as WRET toolkit - Warbird protection is irrelevant for the attack). ## PRIVATE ENCRYPTION KEY DISCOVERY So far, it has been shown that content keys could be successfully extracted from the Protected Media Path (memory of the PMP process) either directly (magic XOR key value) or indirectly (white-box crypto AES rounds). The only secret that remained unbroken so far was a private encryption identity key. Below, details regarding its successful compromise are given. ### LICENSE BLOB DECRYPTION IMPLEMENTATION Tthe main license decryption subroutine is denoted by prsubs command ("decrypt license blob"). It is a WB heap execute subroutine that takes the following arguments (accessed through `rcx`): ``` .text:00000001802228CE ; args: .text:00000001802228CE ; 1) op - 0 in our case (1 is for decryptor type==5?) .text:00000001802228CE ; 2) ptr to encrypted key (ECC encryption key) .text:00000001802228CE ; 3) encrypted key size (0x41 size) .text:00000001802228CE ; 4) binary license blob (0x80 size) .text:00000001802228CE ; 5) output for plaintext secret key ? (size 0x20) ``` The subroutine operation could be described as following: 1) the private ECC decryption is deobfuscated, as a result "obfuscated ECC key" form is obtained of size 0x20 2) the nibbles of a "obfuscated ECC key" (0x40 of them) are used as index to a "bijection" table, as a result each nibble is mapped to a new value (from table) and "mapped ECC key" is obtained 3) the actual license decryption relies on some obfuscated ECC crypto implementation, instead of using the private ECC key for MULTIPLY OP, the nibble of the mapped ECC key is used as an index to ECC precalc table that contains some precalc ECC data The above process is even more complex as there are multiple subroutines and multiple bijection tables, which can be used to process the key in step 2: ``` wret> pkdata PKDataCmd::run [PrvKey data] * internal decrypt 2b7cd0 bijection 8828a0 * internal decrypt 3375b0 bijection 8a2620 * internal decrypt 3b3be0 bijection 8c1ff0 * internal decrypt 3b7910 bijection 8e2190 * internal decrypt 523390 bijection 9023b0 * internal decrypt 5289d0 bijection 922430 ``` Decision which subroutine / bijection table is used depends on the format of encrypted ECC private key. The bijection is applied to each nibble of the key in a separate way (there are separate bijection subtables for each of the 0x40 nibbles). Finally, the bijection tables were just helper tables (they were not used directly as a map table). Sample ECC point precalc tables used in step 3) can be inspected with the use of the `eccpctab` command: ``` wret> eccpctab ..\mspr_toolkit\sniff.blob ECCPrecalcTabCmd::run [ECC point] 0x00000000: 3f a0 a4 df 3a a5 4b 9b 21 19 4e 76 04 c3 49 4c ?...:.K.!.Nv..IL 0x00000010: 6d 56 e6 8a b8 b1 5c 4d 93 83 06 70 95 19 e2 de mV....\M...p.... 0x00000020: 65 22 f3 40 37 7e 4a 3e 60 56 ff 46 be 85 37 6e e".@7~J>`V.F..7n 0x00000030: 04 6b 8d af 37 e2 6b f2 fb fd 00 b9 cc cf a8 33 .k..7.k........3 [precalc point 0] 0x00000000: 07 c7 29 6f 15 99 3b b1 bb 9e 44 5a 44 c8 72 0b ..)o..;...DZD.r. 0x00000010: 83 ac 30 a7 f8 13 df c9 9d 03 15 0d 8c d2 bd b9 ..0............. 0x00000020: 00 00 00 00 2f 3a 24 6c 63 bc 09 e0 dd b1 73 62 ..../:$lc.....sb 0x00000030: 36 6a 4f c6 d9 89 f5 e6 25 38 a4 6d f5 ae 44 80 6jO.....%8.m..D. 0x00000040: 17 7a 7a 41 00 00 00 00 .zzA.... [precalc point 1] 0x00000000: 8d b8 f5 93 a7 1c 76 d4 03 5d d8 17 5d 20 1c f7 ......v..]..] .. 0x00000010: ed ab 24 81 af 0f a6 55 b1 03 e9 09 22 34 7f 02 ..$....U...."4.. 0x00000020: 00 00 00 00 0f 55 4a 1b 46 a6 9d 0e 8c 43 d9 54 .....UJ.F....C.T 0x00000030: 3b 50 c9 44 19 d4 95 2b 58 1f 5c 58 13 d5 2e 1f ;P.D...+X.\X.... 0x00000040: 3c 8b e5 e2 00 00 00 00 <....... ... ``` To make things even more obscure, what looked like ECC points weren't them (or were not available in a plaintext form): ``` msprcp> oncurve pcpoint.dat ECC point: pcpoint.dat X: 7c7296f15993bb1bb9e445a44c8720b83ac30a7f813dfc99d03150d8cd2bdb9 Y: 2f3a246c63bc09e0ddb17362366a4fc6d989f5e62538a46df5ae4480177a7a41 ERROR: point is not on curve! ``` ### KEY DISCOVERY At first, we though that dissecting the high level ECC ops (such as add and multiply) along their use with respect to the encrypted license blob and precalc point is to reveal details of the crypto algorithm in use and potentially match is with the ECC decrypt operations such as the one in use by our code: ``` public static ECPoint decrypt(ECPoint encrypted[],BigInteger prv) { ECPoint point1=encrypted[0]; ECPoint point2=encrypted[1]; ECPoint tmp=point1.op_multiply(prv); ECPoint negpoint=tmp.op_neg(); ECPoint plaintext=point2.op_add(negpoint); return plaintext; } ``` But, we started to obtain some nested expressions during the analysis process: ``` .text:0000000180335C5D test r15d, r15d .text:0000000180335C60 jz short loc_180335CB6 .text:0000000180335C62 lea rax, [rbp+610h+var_C0] ; OP2( OP( OP2 ( OP(P0), OP(P0) )), OP2( OP( OP2 ( OP(P0), OP(P0) )), OP2( OP(P0), OP2( OP(P0), OP( OP2 ( OP(P0), OP(P0) )))))) .text:0000000180335C69 mov [rsp+710h+var_6C8], rax .text:0000000180335C6E lea r9, unk_1808A56E0 .text:0000000180335C75 mov [rsp+710h+var_6D0], rbx .text:0000000180335C7A lea rax, unk_1808A2B10 OP2( OP( OP2 ( OP(P0), OP(P0) )), OP2( OP( OP2 ( OP(P0), OP(P0) )), OP2( OP(P0), OP2( OP(P0), OP( OP2 ( OP(P0), OP(P0) )))))) change OP(P) = 2P OP2(P,Q) = P+Q ``` This didn't look like a good / promising approach, it looked quite tedious too. Instead of diving / analyzing highly obfuscated code, bijections and custom ECC crypto, we decided to take another approach. The following key observations were used for that: 1) a nibble of the "mapped ECC key" tells, which precalc ECC point to use as a starting point for license blob processing (ECC decryption), 2) while the bijection transformation was done separately for each nibble of the "mapped ECC key", the output nibble value was always in the range 0-f 3) discovering plaintext representation of the "mapped ECC key" required the knowledge of the nibble mapping. In order to find the nibble mapping, we decided to do the following: We generated ECC key with a private key denoted by a constant value set for all of its nibbles: ``` msprcp> genfixedkey key_01.pub 0x01 ECC key - prv: 1111111111111111111111111111111111111111111111111111111111111111 - pub: X: 217e617f0b6443928278f96999e69a23a4f2c152bdf6d6cdf66e5b80282d4ed Y: 194a7debcb97712d2dda3ca85aa8765a56f45fc758599652f2897c65306e5794 stored to key_01.pub ``` We then generated a license blob with a special signature / content key sequence (easy to spot / detect): ``` msprcp> genzlicense -k key_01.pub -o lic_key_01.blob pubkey 0000: 02 17 e6 17 f0 b6 44 39 28 27 8f 96 99 9e 69 a2 ......D9('....i. 0010: 3a 4f 2c 15 2b df 6d 6c df 66 e5 b8 02 82 d4 ed :O,.+.ml.f...... 0020: 19 4a 7d eb cb 97 71 2d 2d da 3c a8 5a a8 76 5a .J}...q--.<.Z.vZ 0030: 56 f4 5f c7 58 59 96 52 f2 89 7c 65 30 6e 57 94 V._.XY.R..|e0nW. content key 0000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO.MICROSOFT! 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ license 0000: fb 38 63 c4 7d 48 33 83 76 25 a7 32 fe 77 6a 06 .8c.}H3.v%.2.wj. 0010: bf be b7 c2 f2 75 de 6c 8e 04 6a 3e 94 1d bb de .....u.l..j>.... 0020: d6 31 aa ee a8 9b 51 ed e0 8c 26 19 6d bd 51 65 .1....Q...&.m.Qe 0030: ab 1a 3c 57 ba 2a 00 3c fd 67 57 16 46 a6 18 f6 .....YS..u... 0050: ec c6 4a 24 6e 48 71 c2 35 af 3e 47 83 7d db c1 ..J$nHq.5.>G.}.. 0060: c8 0b 58 0e 88 2e 13 54 b6 50 e1 e5 73 d5 90 de ..X....T.P..s... 0070: 23 e3 94 01 dc 65 3d 11 db 80 63 85 04 2f 2f 03 #....e=...c..//. stored to lic_key_01.blob ``` In the next step, we setup the bijection tables, so that they implicated the use of ECC precalc point at given predefined index (0 in our case): ``` wret> pkdata -k 0x00 PKDataCmd::run setting up bijection for key: 0x00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` Finally, we tried to decrypt the known license blob with the use of an unknown private key: ``` wret> declicense decdata\lic_key_01.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: fb 38 63 c4 7d 48 33 83 76 25 a7 32 fe 77 6a 06 .8c.}H3.v%.2.wj. 0x00000010: bf be b7 c2 f2 75 de 6c 8e 04 6a 3e 94 1d bb de .....u.l..j>.... 0x00000020: d6 31 aa ee a8 9b 51 ed e0 8c 26 19 6d bd 51 65 .1....Q...&.m.Qe 0x00000030: ab 1a 3c 57 ba 2a 00 3c fd 67 57 16 46 a6 18 f6 .....YS..u... 0x00000050: ec c6 4a 24 6e 48 71 c2 35 af 3e 47 83 7d db c1 ..J$nHq.5.>G.}.. 0x00000060: c8 0b 58 0e 88 2e 13 54 b6 50 e1 e5 73 d5 90 de ..X....T.P..s... 0x00000070: 23 e3 94 01 dc 65 3d 11 db 80 63 85 04 2f 2f 03 #....e=...c..//. target 7ffd3c6528a0 wb res: 0 call res 0 output key 0x00000000: ef 3e a0 a8 de 88 33 f5 9b f0 89 7b 19 f7 31 92 .>....3....{..1. 0x00000010: 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 :.W,..........1. signature key 0x00000000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO MICROSOFT! content key 0x00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` The decryption worked fine (special signature / content key sequence was obtained). The side effect of the above was the knowledge that: - value 0x0 of the "mapped ECC key" nibble corresponded to real nibble value 0x01 The more interesting case to illustrate is for ECC key generated with a private key denoting nibble value 0x2: ``` msprcp> genfixedkey key_02.pub 0x02 ECC key - prv: 2222222222222222222222222222222222222222222222222222222222222222 - pub: X: d65a93977caa3d1b081852ff57a79e465f1660577304baead505dd3a48589cf3 Y: 50185e895372df6221ea3a137557e473fddb6755f05bd507c3c533fce9c91285 stored to key_02.pub ``` Again, license blob with a special signature / content key sequence was generated: ``` msprcp> genzlicense -k key_02.pub -o lic_key_02.blob pubkey 0000: d6 5a 93 97 7c aa 3d 1b 08 18 52 ff 57 a7 9e 46 .Z..|.=...R.W..F 0010: 5f 16 60 57 73 04 ba ea d5 05 dd 3a 48 58 9c f3 _..Ws......:HX.. 0020: 50 18 5e 89 53 72 df 62 21 ea 3a 13 75 57 e4 73 P.^.Sr.b!.:.uW.s 0030: fd db 67 55 f0 5b d5 07 c3 c5 33 fc e9 c9 12 85 ..gU.[....3..... content key 0000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO.MICROSOFT! 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ license 0000: 01 61 c3 d6 9a 09 33 14 7d 11 b9 85 53 7e ca f9 .a....3.}...S... 0010: e2 cf 69 46 d3 08 a8 67 0b 01 66 83 3b 22 34 3c ..iF...g..f.;"4< 0020: e8 74 56 ad 7e f9 bd 1a 3d 85 6f 4a a7 be 47 2c .tV.....=.oJ..G, 0030: ae 2d 85 5b 6c 1f 36 8e 36 c1 b4 1d ba 6d 7a 58 .-.[l.6.6....mzX 0040: c4 a1 29 fc 6b 59 28 4a 6e a2 f2 c5 58 13 04 60 ..).kY(Jn...X... 0050: 52 b6 b2 f5 72 61 62 90 f3 f9 2b 99 ae d1 6a 40 R...rab...+...j@ 0060: e0 48 0e d3 06 f9 c5 16 f7 e9 21 69 93 f1 cc 69 .H........!i...i 0070: d4 f6 25 29 e1 d1 6f 8b de 3a e9 44 ca 6b f3 7b ..%)..o..:.D.k.{ stored to lic_key_02.blob ``` And an attempt to decrypt the known license blob with the use of an unknown private key was made starting with a bijection table implicating the use of ECC precalc point at index 1: ``` wret> pkdata -k 0x01 PKDataCmd::run setting up bijection for key: 0x00000000: 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 ................ 0x00000010: 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 ................ wret> declicense decdata\lic_key_02.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: 01 61 c3 d6 9a 09 33 14 7d 11 b9 85 53 7e ca f9 .a....3.}...S~.. 0x00000010: e2 cf 69 46 d3 08 a8 67 0b 01 66 83 3b 22 34 3c ..iF...g..f.;"4< 0x00000020: e8 74 56 ad 7e f9 bd 1a 3d 85 6f 4a a7 be 47 2c .tV.~...=.oJ..G, 0x00000030: ae 2d 85 5b 6c 1f 36 8e 36 c1 b4 1d ba 6d 7a 58 .-.[l.6.6....mzX 0x00000040: c4 a1 29 fc 6b 59 28 4a 6e a2 f2 c5 58 13 04 60 ..).kY(Jn...X..` 0x00000050: 52 b6 b2 f5 72 61 62 90 f3 f9 2b 99 ae d1 6a 40 R...rab...+...j@ 0x00000060: e0 48 0e d3 06 f9 c5 16 f7 e9 21 69 93 f1 cc 69 .H........!i...i 0x00000070: d4 f6 25 29 e1 d1 6f 8b de 3a e9 44 ca 6b f3 7b ..%)..o..:.D.k.{ target 7ffd3c6528a0 wb res: 0 call res 0 output key 0x00000000: e3 f4 3b d3 77 18 ec 2f 75 88 94 a4 df 00 06 61 ..;.w../u......a 0x00000010: d9 47 af 25 07 90 23 b7 9c 8c 30 fa e3 61 4a 73 .G.%..#...0..aJs signature key 0x00000000: 44 8f d7 37 e6 b0 92 93 ad 2a 52 8c 89 b1 63 d2 D..7.....*R...c. content key 0x00000000: e3 95 f8 09 ff 1b f9 54 41 4b ab 12 f5 df 7b f6 .......TAK....{. ``` This time, the special sequence hasn't been reveled. This means, that value 0x1 of the "mapped ECC key" nibble did not correspond to real nibble value 0x1. This should be expected (we found out that real nibble value 0x1 corresponds to nibble 0x0 present in obfuscated private key). The tests conducted for bijections set to nibble values 2-d (through `pkdata` command) were not successful. But, value 0xe triggered the match: ``` wret> pkdata -k 0x0e PKDataCmd::run setting up bijection for key: 0x00000000: ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ................ 0x00000010: ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ee ................ wret> declicense decdata\lic_key_02.blob -k 0C86330B0E98CD7C586F336088DAFA0E.enc.prv DecLicenseCmd::run DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: 01 61 c3 d6 9a 09 33 14 7d 11 b9 85 53 7e ca f9 .a....3.}...S~.. 0x00000010: e2 cf 69 46 d3 08 a8 67 0b 01 66 83 3b 22 34 3c ..iF...g..f.;"4< 0x00000020: e8 74 56 ad 7e f9 bd 1a 3d 85 6f 4a a7 be 47 2c .tV.~...=.oJ..G, 0x00000030: ae 2d 85 5b 6c 1f 36 8e 36 c1 b4 1d ba 6d 7a 58 .-.[l.6.6....mzX 0x00000040: c4 a1 29 fc 6b 59 28 4a 6e a2 f2 c5 58 13 04 60 ..).kY(Jn...X..` 0x00000050: 52 b6 b2 f5 72 61 62 90 f3 f9 2b 99 ae d1 6a 40 R...rab...+...j@ 0x00000060: e0 48 0e d3 06 f9 c5 16 f7 e9 21 69 93 f1 cc 69 .H........!i...i 0x00000070: d4 f6 25 29 e1 d1 6f 8b de 3a e9 44 ca 6b f3 7b ..%)..o..:.D.k.{ target 7ffd3c6528a0 wb res: 0 call res 0 output key 0x00000000: ef 3e a0 a8 de 88 33 f5 9b f0 89 7b 19 f7 31 92 .>....3....{..1. 0x00000010: 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 :.W,..........1. signature key 0x00000000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO MICROSOFT! content key 0x00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` Our knowledge of the nibble mapping could be updated with a new value: - "deobfuscated ECC key" nibble 0x0 -> real nibble 0x1 - "deobfuscated ECC key" nibble 0xe -> real nibble 0x2 The whole process has been repeated for the remaining nibble values. The following presents the final nibble mapping obtained (Windows 10 OS case): ``` 01 -> 00 02 -> 0e 03 -> 0c 04 -> 07 05 -> 0b 06 -> 05 07 -> 09 08 -> 06 09 -> 04 0a -> 02 0b -> 0d 0c -> 03 0d -> 0a 0e -> 0f 0f -> 08 ``` There is one mapping missing from it for value 0, which is due to the inability to generate ECC point for a zero private key. It can be easily figured out though: ``` 00 -> 01 (the remaining / unused value) ``` Now, we can replace nibbles of the "mapped ECC key" with real values (run it through the mapping): ``` obfuscated ECC key 0x00000000: f9 dc f8 b9 5a c4 5e 4e 35 c3 b3 46 4c af d9 8a ....Z.^N5..FL... 0x00000010: 1a f5 ed 9e ab ea 5b 5a 8f 49 71 82 70 e7 0d 52 ......[Z.Iq.p..R mapped ECC key 0x00000000: 99 17 ad 43 0c 16 69 a5 12 e3 50 d0 64 0d d2 a5 ...C..i...P.d... 0x00000010: 75 95 d2 b5 8f 07 58 35 f3 3e df 44 c3 03 db ca u.....X5.>.D.... plaintext ECC key 0x00000000: 77 04 db 9c 13 08 87 d6 0a 2c 61 b1 89 1b ba d6 w........,a..... 0x00000010: 46 76 ba 56 fe 14 6f c6 ec c2 be 99 3c 1c b5 3d Fv.V..o.....<..= ``` The obtained plaintext value of private ECC encryption key is correct as indicated by the crypto check: ``` msprcp> checkkeypair ..\wret_toolkit\0C86330B0E98CD7C586F336088DAFA0E.enc.plain ..\wret_toolkit\0C86330B0E98CD7C586F336088DAFA0E.enc.pub KEY CHECK: - prv: 7704db9c130887d60a2c61b1891bbad64676ba56fe146fc6ecc2be993c1cb53d - pub: X: cb276f9f9f764664542319ef9cc7690f9c3be3758bd3782a8d03fba8bf9e1c6d Y: f7101c69942c4d07d9688b610985bbd34ee85820e20cc9bca9a81eb7f659657d KEY CHECK OK ``` The WRET toolkit makes it possible to automatically find the value of a private encryption key with the use of the technique depicted above. The following commands can be issued to accomplish that: ``` wret> image w10_prlib.dll wret> prlib w10_prlib.dll wret> wbsetup -r wret> xorkey w10 wret> handler -a prvkeyextractor wret> decenckey 0C86330B0E98CD7C586F336088DAFA0E.enc.prv -o 0C86330B0E98CD7C586F336088DAFA0E.enc.plain ... DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: 51 20 b1 9b 2d 44 ff 68 87 6d eb 04 97 24 7f 0f Q ..-D.h.m...$.. 0x00000010: 36 6c 78 27 fb 78 f6 05 4c 0c a5 11 e5 26 1b f3 6lx'.x..L....&.. 0x00000020: 6c b5 90 63 68 25 d2 5b 7a 34 29 4f e2 ec dd 29 l..ch%.[z4)O...) 0x00000030: dc 55 fe 57 ce 4b 23 9f da a0 c9 0c 00 41 95 b4 .U.W.K#......A.. 0x00000040: 08 23 fb c9 91 e1 d6 3e 10 1f 3e 52 81 85 dc 83 .#.....>..>R.... 0x00000050: c2 17 99 4d 5a 7d 51 bf 03 3c ce 87 9f 01 80 e2 ...MZ}Q..<...... 0x00000060: 9e ef c1 98 ac a6 15 1f 9f 8c dc ee 5e 99 21 27 ............^.!' 0x00000070: 95 76 de b4 a5 ba 5e d6 ae d7 94 48 8c 73 15 4e .v....^....H.s.N target 7ffd3c6528a0 PrvKeyExtractor: decsub va 3375b0 wb res: 0 call res 0 output key 0x00000000: ef 3e a0 a8 de 88 33 f5 9b f0 89 7b 19 f7 31 92 .>....3....{..1. 0x00000010: 3a d2 57 2c f8 8b da e3 dd c7 9b e8 16 be 31 85 :.W,..........1. signature key 0x00000000: 48 45 4c 4c 4f 20 4d 49 43 52 4f 53 4f 46 54 21 HELLO MICROSOFT! content key 0x00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ [BIJECTION] 1 -> 0 0 -> 1 a -> 2 c -> 3 9 -> 4 6 -> 5 8 -> 6 4 -> 7 f -> 8 7 -> 9 d -> a 5 -> b 3 -> c b -> d 2 -> e e -> f [MAP] 0 -> 1 1 -> 0 2 -> a 3 -> c 4 -> 9 5 -> 6 6 -> 8 7 -> 4 8 -> f 9 -> 7 a -> d b -> 5 c -> 3 d -> b e -> 2 f -> e restoring original bijections DECRYPT LICENSE keydata 0x00000000: 4c 33 c6 8e 0e f1 b6 f1 0c d5 31 6b 40 94 aa 68 L3........1k@..h 0x00000010: 32 cc 68 1b 00 3b fc 65 8b c4 3c e3 cb 62 de fc 2.h..;.e..<..b.. 0x00000020: 11 ef 51 7b 92 73 a1 84 24 ac 71 33 cf 76 d3 05 ..Q{.s..$.q3.v.. 0x00000030: 44 2d 4e 12 79 3f 3f 09 7a 4e 4d 51 ac 78 a7 3c D-N.y??.zNMQ.x.< 0x00000040: 6b k license 0x00000000: 51 20 b1 9b 2d 44 ff 68 87 6d eb 04 97 24 7f 0f Q ..-D.h.m...$.. 0x00000010: 36 6c 78 27 fb 78 f6 05 4c 0c a5 11 e5 26 1b f3 6lx'.x..L....&.. 0x00000020: 6c b5 90 63 68 25 d2 5b 7a 34 29 4f e2 ec dd 29 l..ch%.[z4)O...) 0x00000030: dc 55 fe 57 ce 4b 23 9f da a0 c9 0c 00 41 95 b4 .U.W.K#......A.. 0x00000040: 08 23 fb c9 91 e1 d6 3e 10 1f 3e 52 81 85 dc 83 .#.....>..>R.... 0x00000050: c2 17 99 4d 5a 7d 51 bf 03 3c ce 87 9f 01 80 e2 ...MZ}Q..<...... 0x00000060: 9e ef c1 98 ac a6 15 1f 9f 8c dc ee 5e 99 21 27 ............^.!' 0x00000070: 95 76 de b4 a5 ba 5e d6 ae d7 94 48 8c 73 15 4e .v....^....H.s.N target 7ffd3c6528a0 PrvKeyExtractor: decsub va 3375b0 wb res: 0 call res 0 output key 0x00000000: 24 18 81 4e 85 b3 05 88 20 26 6e ba d9 cd 3b af $..N.... &n...;. 0x00000010: 12 4f 4e 16 6d af 41 de 46 2e 74 0a 0a 17 71 7d .ON.m.A.F.t...q} signature key 0x00000000: 83 63 6d aa 14 1b 7b 34 f8 84 a8 92 8f 7c 5e 1c .cm...{4.....|^. content key 0x00000000: 28 9d 19 3a 95 24 9b 3d 9b e9 ef e2 1c a9 40 f8 (..:.$.=......@. obfuscated ECC key 0x00000000: f9 dc f8 b9 5a c4 5e 4e 35 c3 b3 46 4c af d9 8a ....Z.^N5..FL... 0x00000010: 1a f5 ed 9e ab ea 5b 5a 8f 49 71 82 70 e7 0d 52 ......[Z.Iq.p..R mapped ECC key 0x00000000: 99 17 ad 43 0c 16 69 a5 12 e3 50 d0 64 0d d2 a5 ...C..i...P.d... 0x00000010: 75 95 d2 b5 8f 07 58 35 f3 3e df 44 c3 03 db ca u.....X5.>.D.... plaintext ECC key 0x00000000: 77 04 db 9c 13 08 87 d6 0a 2c 61 b1 89 1b ba d6 w........,a..... 0x00000010: 46 76 ba 56 fe 14 6f c6 ec c2 be 99 3c 1c b5 3d Fv.V..o.....<..= saving to 0C86330B0E98CD7C586F336088DAFA0E.enc.plain file ``` It's worth to mention that a use of a brute force approach to find out the nibble mapping would require checking 16! (20922789888000) combinations as there are 16 possible values for the nibble that are to be mapped (ordered): val 0 - 16 combinations val 1 - 15 combinations left (val 0 already assigned) val 2 - 14 combinations left (val 0 and 1 already assigned) ... val 15 - 1 value left This makes the presented approach much more practical (and faster). ## UNVERIFIED ISSUES There has been some potential issues, which hasn't been investigated by us in a detail, but could be relevant. ### AES DECRYPT HOOKING The main AES decrypt subroutine is easy to detect due to rather uncommon use of XMM registers and constants related to GF(2^8) multiplication: ``` ... .text:00000001806B3F18 mov ebx, 1B1B1B1Bh .text:00000001806B3F1D mov r15d, r9d ; data size / 16 (number of 128bit words) .text:00000001806B3F20 movd xmm6, ebx .text:00000001806B3F24 lea r12, agsr_round_decryption_table .text:00000001806B3F2B test r8, r8 ; data .text:00000001806B3F2E movdqu xmm2, xmmword ptr [rdx] .text:00000001806B3F32 jz loc_1806B498E ; -> error .text:00000001806B3F38 pshufd xmm6, xmm6, 0 .text:00000001806B3F3D movdqa xmm3, xmm2 .text:00000001806B3F41 pcmpgtb xmm4, xmm3 .text:00000001806B3F45 pand xmm4, xmm6 .text:00000001806B3F49 paddb xmm3, xmm3 .text:00000001806B3F4D pxor xmm5, xmm5 .text:00000001806B3F51 pxor xmm3, xmm4 .text:00000001806B3F55 pcmpgtb xmm5, xmm3 .text:00000001806B3F59 pand xmm5, xmm6 .text:00000001806B3F5D paddb xmm3, xmm3 .text:00000001806B3F61 test r13, r13 ; obfuscated content key ... ``` The decrypted data seems to be XORed with a semi-random stream though (result of some digest) - it is called by us `preparation data`. It may potentially make harvesting decrypted MPEG samples more difficult, but not impossible: ``` .text:000000018062466F js short loc_1806246EB ; -> error .text:0000000180624671 mov r8, r15 ; data .text:0000000180624674 lea rcx, [rbp+2E0h+var_190] ; preparation data .text:000000018062467B mov edx, r12d ; data size .text:000000018062467E call agsr_prepare_data_for_decryption ; prepare data buf for decryption ? .text:0000000180624683 test esi, esi .. .text:00000001805A1BF0 agsr_prepare_data_for_decryption proc near .text:00000001805A1BF0 ; CODE XREF: sub_180624300+37E↓p .text:00000001805A1BF0 ; sub_180624740+2FE↓p ... .text:00000001805A1BF0 ... .text:00000001805A1C80 loc_1805A1C80: ; CODE XREF: agsr_prepare_data_for_decryption+170↓j .text:00000001805A1C80 mov ebx, [rdi] ; load from data .text:00000001805A1C82 lea eax, [r10+1] .text:00000001805A1C86 movzx r10d, al .text:00000001805A1C8A movzx r9d, byte ptr [r10+rcx] ; there are 16 such loads from memory .text:00000001805A1C8F lea eax, [r9+r11] .text:00000001805A1C93 movzx r11d, al .text:00000001805A1C97 movzx eax, byte ptr [r11+rcx] .text:00000001805A1C9C mov [r10+rcx], al .text:00000001805A1CA0 mov [r11+rcx], r9b .text:00000001805A1CA4 movzx eax, byte ptr [r10+rcx] .text:00000001805A1CA9 add eax, r9d .text:00000001805A1CAC movzx eax, al .text:00000001805A1CAF movzx edx, byte ptr [rax+rcx] .text:00000001805A1CB3 lea eax, [r10+1] .text:00000001805A1CB7 movzx r10d, al .text:00000001805A1CBB xor ebx, edx ; XOR with preparation data ``` There are at least two potential hijack locations such as PATCH GUARD call and license decryptor virtual method (for obtaining ptr to AES key rounds): ``` .text:0000000180624687 mov rbx, [rsp+3E0h+var_3A0] ; license decryptor .text:0000000180624687 ; vtable 180713238 ??? .text:000000018062468C mov [rsp+3E0h+var_390], 0 .text:0000000180624695 mov rax, [rbx] .text:0000000180624698 mov rdi, [rax+28h] .text:000000018062469C mov rcx, rdi .text:000000018062469F call cs:__guard_check_icall_fptr ; addr 7ffbba5846a5 (6246a5) -> target 7ffbba182e80 (222e80) .text:00000001806246A5 lea rdx, [rsp+3E0h+var_390] ; .text:00000001806246AA mov rcx, rbx .text:00000001806246AD call rdi ; -> 180222e80 (license decyrptorn virtual method) .text:00000001806246AD ; .text:00000001806246AD ; get content key ptr ? returns ptr to buf size 0xb0!!! .text:00000001806246AD ; key buf change leads to bad decryption ``` ### WHITEBOX AES ROUNDS ACCESS WITHOUT ANY HOOKING (FROM MEMORY) The white-box AES key rounds in use by PlayReady, while relying on arbitrary (obfuscated) algebraic transformation must still adhere to similar rules as standard AES key rounds: ``` RoundKey* next() { if (_idx0) { unsigned int w3=_k[3]^_k[2]; unsigned int w2=_k[2]^_k[1]; unsigned int w1=_k[1]^_k[0]; unsigned int w0=_k[0]^SubWord(RotWord(w3,FALSE),(unsigned char*)aes_sbox)^(RCon(_idx)<<24); return new RoundKey(w0,w1,w2,w3,_idx-1); } else return NULL; } ``` Thus, a more in-depth analysis of expand key code along GF(2^8) operations used by the core AES decryption subroutine should reveal the relations between consecutive key rounds. Knowledge of such relations could be used to search for the whitebox AES rounds structure in PMP process memory without the need for any hooking (AES key rounds data could be potentially harvested directly from process memory, either through attach, memory dump or from within kernel, making Warbird protections irrelevant too). ### RSA KEYS IN MFCORE.DLL AND MF.DLL There is an encrypted RSA key in MFCORE.dll, which could be of a sensitive nature due to the fact that it is stored in an encrypted form (public modulus does not need to be hidden): ``` wret> image mfcore.dll ImageCmd::run - Dll init mfcore.dll size 4116864 bytes base 180000000 wret> wbinfo -s WBInfoCmd::run Warbird encrypted binary - scanning for heap exec descriptors found: 43 - scanning for segment descriptors found: 2 * section: .text va 1000 size 318000 encr_size 923f (1%) * section: .rdata va 31b000 size 67000 encr_size 2bc (0%) * section: .data va 382000 size 29540 encr_size 10 (0%) wret> wbinfo -S WBInfoCmd::run Warbird encrypted binary - scanning for segment descriptors found: 2 - wb segment desc_va: 0x319000 size 100 relocs_va: 0x0 num 0x0 segments (num 1, data size 16): * [0000] encr_va 3ab198 size 000010 section .data - wb segment desc_va: 0x31a000 size 100 relocs_va: 0x0 num 0x0 segments (num 1, data size 700): * [0000] encr_va 346ff0 size 0002bc section .rdata wret> dmem 0x346ff0 DmemCmd::run va 346ff0 0x00000000: eb 47 12 16 9f b4 7a 05 bc 9c fc 8d da e2 b4 b0 .G....z......... 0x00000010: 7f 87 0a a4 d5 7f 5e 61 a8 b8 d8 06 77 3f f6 57 ......^a....w?.W 0x00000020: 3d 31 18 9d d5 ab 7c ea 4c ad 7f 57 d9 f0 eb c3 =1....|.L..W.... 0x00000030: fe 9e 55 ba 42 fe 67 6d 99 4f ce 68 65 8c f7 76 ..U.B.gm.O.he..v 0x00000040: 06 eb b7 1e 41 55 53 65 dd 0d a7 e3 fa 31 39 8b ....AUSe.....19. 0x00000050: f6 33 d9 ec e0 7d 16 a8 8c 9d 0b 3c 19 27 fe ed .3...}.....<.'.. 0x00000060: fd 55 24 9b 8a 16 2e 7d 21 d0 29 45 04 4d 51 58 .U$....}!.)E.MQX 0x00000070: 21 8a 78 ab 5d e7 c8 fd f1 b0 07 66 ee a3 a2 4b !.x.]......f...K 0x00000080: 81 5d c6 5b 85 cf 3d 46 e4 5d 2f 1f 3d 78 3a d6 .].[..=F.]/.=x:. 0x00000090: 08 4f ba ba 59 13 0f 9a 04 38 35 87 c3 ab ad 48 .O..Y....85....H 0x000000a0: 42 8e a1 35 4f 6f 9d 5f ac b6 ea 21 b2 92 30 2e B..5Oo._...!..0. 0x000000b0: 46 30 97 e5 e2 45 8f f8 09 78 ad ff 9b 49 4f a0 F0...E...x...IO. 0x000000c0: 40 e4 c4 96 9f f5 1f 46 69 3f 2c c0 b3 e4 17 de @......Fi?,..... 0x000000d0: 8a b3 6c f2 1d ca a9 26 32 e3 b9 cc 0a 72 6d d7 ..l....&2....rm. 0x000000e0: 13 aa 4e b6 ba 54 5f 62 87 e9 39 37 50 4e 63 59 ..N..T_b..97PNcY 0x000000f0: 72 a1 4a 72 26 bf d5 9f a8 c5 7b 88 67 fe 42 d7 r.Jr&.....{.g.B. wret> wbop -d 0x346ff0 -v WBOpCmd::run - scanning for heap exec descriptors found: 43 - scanning for segment descriptors found: 2 - wb segment digest: 0x00000000: 94 71 a7 b5 c7 c4 e7 37 fb 3e 25 2d b8 56 63 f5 .q.....7.>%-.Vc. 0x00000010: 4c 05 f7 9d ec eb 02 b5 ca 4e e7 b9 e3 39 3f 02 L........N...9?. desc_va: 0x31a000 size 100 relocs_va: 0x0 num 0x0 key: 0x7a9a3b5a451d5ca segments (num 1, data size 700): * [0000] encr_va 346ff0 size 0002bc section .rdata flags 2|READONLY - WarbirdCallArgs * op: WbDecryptEncryptionSegment * segdesc: 7fff4a75a000 * image_base1: 7fff4a440000 * image_base2: 7fff4a440000 * relocs: 7fff4a440000 * relocs_num: 0 - NtQuerySystemInformation res: 0 - segment status: CHANGED decrypted wret> dmem 0x346ff0 -m DmemCmd::run va 346ff0 mapped addr 7fff4a786ff0 0x00000000: 52 53 41 32 88 00 00 00 00 04 00 00 7f 00 00 00 RSA2............ 0x00000010: 01 00 01 00 e5 8d b1 2f fb 50 9e 95 a3 2e 55 39 ......./.P....U9 0x00000020: 46 89 41 42 5a f1 1d 94 06 38 d9 58 d2 e0 6c 3b F.ABZ....8.X..l; 0x00000030: d9 6b 97 ee 53 96 c1 69 98 a3 36 93 74 bf db 67 .k..S..i..6.t..g 0x00000040: 61 c4 23 d3 45 35 00 10 49 01 61 99 03 b3 b4 5d a.#.E5..I.a....] 0x00000050: 9a dd cb 14 4a 6d 66 f8 a4 ed c1 5b 86 cc ff 68 ....Jmf....[...h 0x00000060: ed ac f9 ae dd 3d 8f ff a4 1c 8e 77 38 2f 74 76 .....=.....w8/tv 0x00000070: 26 fc c9 4a d4 7a 71 d2 76 74 6e 7d cc b7 d1 4b &..J.zq.vtn}...K 0x00000080: 1d 42 19 8a 2d b2 95 91 d6 e6 af f0 13 12 11 11 .B..-........... 0x00000090: 6f da b8 b3 00 00 00 00 00 00 00 00 85 84 ea e7 o............... 0x000000a0: a1 f1 b8 e0 29 eb 54 3f 79 02 49 7b 1b 3e 69 72 ....).T?y.I{.>ir 0x000000b0: 6d da e2 5d 30 dc 3f 2e 57 4d 58 66 e3 04 4e d5 m..]0.?.WMXf..N. 0x000000c0: 18 0b 14 e4 7b 36 dc dc e2 ab d8 5c 89 1c c8 23 ....{6.....\...# 0x000000d0: 0d c5 8f be 02 0d 3d 93 e8 54 70 e4 00 00 00 00 ......=..Tp..... 0x000000e0: e1 51 a1 43 ed a9 80 b0 2a 00 35 eb 59 6a ce 50 .Q.C....*.5.Yj.P 0x000000f0: ce 3c 27 67 23 8d ed cb 09 b7 41 d3 b5 e7 da 9e .<'g#.....A..... wret> ``` Another RSA key is hidden in `mf.dll` library: ``` wret> image mf.dll ImageCmd::run - Dll init mf.dll size 571608 bytes base 180000000 wret> wbinfo -S WBInfoCmd::run Warbird encrypted binary - scanning for segment descriptors found: 2 - wb segment desc_va: 0x59000 size 100 relocs_va: 0x0 num 0x0 segments (num 1, data size 700): * [0000] encr_va 71400 size 0002bc section .data - wb segment desc_va: 0x5a000 size 100 relocs_va: 0x0 num 0x0 segments (num 1, data size 16): * [0000] encr_va 711e0 size 000010 section .data wret> wbop -d 0x71400 WBOpCmd::run - scanning for heap exec descriptors found: 48 - scanning for segment descriptors found: 2 decrypted wret> dmem 0x71400 -m DmemCmd::run va 71400 mapped addr 7fff4e4d1400 0x00000000: 52 53 41 32 88 00 00 00 00 04 00 00 7f 00 00 00 RSA2............ 0x00000010: 01 00 01 00 8f 03 46 93 10 87 42 87 a4 a5 e9 af ......F...B..... 0x00000020: 1a ba c1 ee 40 cf 99 e6 e8 de 9f 04 b7 21 1f ee ....@........!.. 0x00000030: ec 9d 1b fa 58 fc 31 ce a0 23 f6 75 4a df 57 02 ....X.1..#.uJ.W. 0x00000040: 93 ad 40 5b f9 a9 bf 71 5e 31 96 f5 74 47 2e 52 ..@[...q^1..tG.R 0x00000050: ca 8c 41 fe 7d 3e ae 3b f5 d5 60 1c c6 30 7d f6 ..A.}>.;..`..0}. 0x00000060: 86 46 f0 10 1a c6 24 84 a9 6c f9 59 80 8d 65 e3 .F....$..l.Y..e. 0x00000070: f1 a0 10 1b 59 f5 80 55 1d f6 bb 68 be 33 2a 59 ....Y..U...h.3*Y 0x00000080: e0 46 15 f9 a4 1f 58 b1 35 16 5f e0 13 d8 cd 5f .F....X.5._...._ 0x00000090: 91 f4 b4 e0 00 00 00 00 00 00 00 00 1d 99 12 34 ...............4 0x000000a0: 31 49 19 f1 bf 6d 63 d1 26 c6 43 27 d0 00 44 2a 1I...mc.&.C'..D* 0x000000b0: e4 09 29 e0 38 49 a3 4d 75 16 18 ad 02 27 41 39 ..).8I.Mu....'A9 0x000000c0: 79 e0 aa 94 51 ac a4 e1 d0 38 8b 85 0f 57 69 6e y...Q....8...Win 0x000000d0: ff 2e d7 8e 47 ae b9 fe 13 94 00 f2 00 00 00 00 ....G........... 0x000000e0: 9b 5b 0e 50 9e d0 8e 35 33 16 ed 82 93 b4 de 5f .[.P...53......_ 0x000000f0: 64 5d c7 b4 f0 eb 1b ec db 72 08 88 75 f3 20 ad d].......r..u. . ``` We haven't investigated the purpose of these keys. They could be used for ITA nodes verification though. ### REGISTRY KEY FOR PROTECTED PROCESS BYPASS MFPMP process can be configured to run as unprotected process (`mfcore.dll`): ``` .text:00000001801780B6 ; START OF FUNCTION CHUNK FOR agsr_check_protected_process_bypass .text:00000001801780B6 .text:00000001801780B6 loc_1801780B6: ; CODE XREF: agsr_check_protected_process_bypass+4B↑j .text:00000001801780B6 ; DATA XREF: .pdata:00000001803AC914↓o ... .text:00000001801780B6 mov rcx, [rsp+38h+hKey] ; hKey .text:00000001801780BB lea rax, [rsp+38h+cbData] .text:00000001801780C0 mov [rsp+38h+lpcbData], rax ; lpcbData .text:00000001801780C5 lea r9, [rsp+38h+Type] ; lpType .text:00000001801780CA lea rax, [rsp+38h+Data] .text:00000001801780CF xor r8d, r8d ; lpReserved .text:00000001801780D2 lea rdx, aNoprotectedpro ; "NoProtectedProcess" .text:00000001801780D9 mov [rsp+38h+phkResult], rax ; lpData .text:00000001801780DE call cs:RegQueryValueExW .text:00000001801780E5 nop dword ptr [rax+rax+00h] .text:00000001801780EA nop .text:00000001801780EB jmp loc_18009EABD .text:00000001801780F0 ; --------------------------------------------------------------------------- .text:00000001801780F0 .text:00000001801780F0 loc_1801780F0: ; CODE XREF: agsr_check_protected_process_bypass+59↑j .text:00000001801780F0 call cs:RegCloseKey .text:00000001801780F7 nop dword ptr [rax+rax+00h] .text:00000001801780FC nop .text:00000001801780FD jmp loc_18009EACB .text:00000001801780FD ; END OF FUNCTION CHUNK FOR agsr_check_protected_process_bypass ``` This can be accomplished by setting the following registry entry: ``` HKEY_LOCAL_MACHINE\Software\Microsoft\Windows Media Foundation\PEAuth ``` with value `NoProtectedProcess` DWORD val = `1` Running MFPMP as unprotected process constitutes a potential base for arbitrary function hooking / patching resulting in a `PEAuth` bypass that could still make protected media processing possible. ### PEAUTH DEBUGGING SUPPORT There are data / code snippets in `mfcore.dll` indicating debugger could be attached to PMP process upon successful verification of debugger credentials: ``` .rdata:00000001803391C8 dw 433Dh ; Data3 .rdata:00000001803391C8 db 0BDh, 0F7h, 79h, 0CEh, 68h, 0D8h, 0ABh, 0C2h; Data4 .rdata:00000001803391D8 aGetdebcredpath db 'GetDebCredPath',0 ; DATA XREF: sub_1800B7C88:loc_1800B7CFF↑o .rdata:00000001803391E7 align 10h .rdata:00000001803391F0 ; const WCHAR aSystemCurrentc .rdata:00000001803391F0 aSystemCurrentc: ; DATA XREF: sub_1800B7C88+9E↑o .rdata:00000001803391F0 ; sub_1800F0794+6E↑o .rdata:00000001803391F0 text "UTF-16LE", 'System\CurrentControlSet\Services\PEAuth',0 .rdata:0000000180339242 align 8 .rdata:0000000180339248 aValidatedbgcre db 'ValidateDbgCred',0 ; DATA XREF: sub_1800D61AC:loc_1800D62A2↑o .rdata:0000000180339248 ; sub_1800D61AC+67E↑o ... .rdata:0000000180339258 a13614131110526 db '1.3.6.1.4.1.311.10.5.26',0 .rdata:0000000180339258 ; DATA XREF: sub_1800D61AC+291↑o .rdata:0000000180339270 ; const WCHAR aDatapath .rdata:0000000180339270 aDatapath: ; DATA XREF: sub_1800B7C88+113↑o .rdata:0000000180339270 ; sub_1800F0794+D4↑o .rdata:0000000180339270 text "UTF-16LE", 'DataPath',0 .rdata:0000000180339282 align 8 .rdata:0000000180339288 aDebcredBin: ; DATA XREF: sub_1800B7C88+193↑o .rdata:0000000180339288 text "UTF-16LE", '\debcred.bin',0 .rdata:00000001803392A2 align 8 .rdata:00000001803392A8 a1361413111056 db '1.3.6.1.4.1.311.10.5.6',0 ``` This alone creates a potential for yet another `PEAuth` bypass (through fake credentials, bypass of debug credentials check, etc.) and execution of PMP in an insecure manner (under debugger control, which is not possible by default). # SUMMARY A significant effort has been made by Microsoft to make reversing and analysis of PlayReady operation hard. Microsoft aimed to implement state of the art code protection to mitigate security threats related to client side based security through crypto, code integrity, auth checks, white-box crypto, code obfuscation and kernel level support. Yet, a successful and complete (from an identity and content key security point of view) compromise could be achieved. This was primarly due to the following: - the possibility to decrypt Warbird binaries, same decryption mechanism used across all Windows kernels, secret key embedded in binaries (all data needed for decryption), encrypted blobs using SHA256 for integrity check instead of ECC/RSA digital signatures, same mechanism used for code execution segments, possibility to decrypt binaries in offline mode - no immutability of the Warbird binaries, Warbird binaries should be treated as special ones (should be immutable, they are not as memory protections could be changed, patching of code, including encrypted heap exec segments could be done), - many parts of Warbird code being present in memory in decrypted form (not being reencrypted), which makes hooking, code search / pattern matching possible - client identity once established is valid and can be abused (not many changes to identity over time), it is questionable if identity should be maintained on user systems (it might be safer to consider storage in the cloud or at license server end, this would require change of a protocol though), - the use of known crypto (instead of custom one) makes things easier, everythingrelated to known crypto becomes helpful (state of the art docs, info about efficient algorithms, transformation to arbitrary spaces, etc.) obfuscating known crypto algorithm is no protection (no need to reverse engineer obfuscated code, but to steal keys -> easier target), - wrong assumptions about obfuscated code, it doesn't need to be analyzed line by line, higher-level functionality blocks are only needed even if these are still black boxes (input and output args along the knowledge of a general black box function could be enough) - domain keys don't seem to matter (private identity encryption key seem enough) - inconsistent Warbird execution flow, occasional `NtQuerySystemInformation` calls invoked from within heap executed (encrypted code), this breaks the "integrity" of wb code execution path, this is primary manifested through virtual methods dispatch (virtual method calls are not protected), the "integrity" and "atomicity" (as observed through WB syscall), of a Warbird call can be "broken" (arbitrary code injection into flow / code hooking) - warbird segment descriptors always "visible", this makes patching code easy (with wb decryption functionality), this is regardless of the fact that wb code gets decrypted / reencrypted on the fly - the use of bijections (XOR key, white-box crypto AES rounds, private ECC encryption key nibbles), bijections at byte / nibble level, time windows where obfuscated keys can be harvested from memory - the way code is generated provides help / hints during reversing, highly obfuscated code denotes most sensitive (important) code parts, code functionality follows the API based approach, the analysis doesn't need to reveal what black-box functions do exactly, but to only select those that are responsible for the core functionality (`prsubs`), local variables are used for the most sensitive keys, these get always cleared before subroutines' return (`memset` size such as 0x20 provides extra hint about algorithm / key type) - there are lots of Microsoft published material that helps a lot in reversing such as PlayReady samples, developer docs, APIs, include files, for instance, Media Foundation Samples' code for `clearkeyStoreCDM` seem to be a mirror of the actual CDM used by PlayReady (it was instrumental to discover `ITA verify` / `PEAuth` bypasses and core decryption subroutine), global values of error codes provides hints with respect to functions' arguments, subroutines' functionality, etc. - there are occasional PDB files available for Media Foundation Framework libraries such as `mfps.dll`, `mfpmp.dll`, `mfcore.dll`, `windows.media.dll`, which could be perceived in terms of some inconsistency with respect to PlayReady security and the way secrecy of the implementation is implemented and/or maintained - weak Protected Media Path process, protected media process is started in an insecure way (from user controlled / unprivileged web browser process), cached security check value for `PEAuth` process allows for `PEAuth` bypass, - built in debug (?) functionality allows for HW DRM disabling It's also worth to emphasize that no security vulnerability (such as privilege elevation or kernel compromise) needed to be exploited to successfully demonstrate PlayReady DRM compromise in the environment of Windows OS. This was regardless of integrating / implementing Protected Media Path at OS kernel side (`PEAuth` driver, protected process functionality) for security reasons. Finally, embedding the functionality into Windows OS that makes it possible to make reversing / analysis of code difficult could act as a two-edge sword. More specifically, it could constitute a challenge / risk to A/V software if abused by malware (this SW has no ability to inspect encrypted code).