Headline
Microsoft Warbird and PMP Security Research
This paper provides an in-depth technical explanation, illustration, and verification of discovered attacks affecting PlayReady on Windows 10 / 11 x64 that pertain to Warbird deficiencies, content key sniffer operation, magic XOR keys discovery, white-box crypto attack, and complete client identity compromise attacks.
# 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.)
- all messages are signed semi-anonymously (as MSRC), as a result one has
- 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 processbinaries\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 ofwbsetup -s
command)tests\trace
sample trace logs and scriptstests\xor_key
tests conducted with respect to XOR key value for various versions of PlayReady
library and Windows OStests\web_soap_licenses
tests illustrating web browser exploitation scenariotests\sniffdata
license sniffer outputs conducted for various VOD platforms and Live TVtests\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+ VODtests\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 devicestests\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 LStools\mspr_toolkit
MSPR Toolkittools\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 identitiestools\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<Cmd*> 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
- WB segment descriptors, which usually describe encrypted data located in
.data
or.rdata
sections - 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..,..<b.~.U..
0x00000090: af 70 02 d3 7d 45 76 59 bd 6b fd ac bf 95 46 c7 .p..}EvY.k....F.
0x000000a0: 43 da ee 1f 6a 26 c4 f1 5e ab 81 4f 35 33 71 28 C...j&..^..O53q(
0x000000b0: c5 f0 05 e2 79 cd d0 65 20 bf cb 35 58 1a 88 32 ....y..e ..5X..2
0x000000c0: b6 64 28 3c 3c 3a f4 7d 03 bf d1 eb c8 50 15 84 .d(<<:.}.....P..
0x000000d0: b9 78 f8 58 7f bc 9a 74 f3 ff a9 5e d0 04 4b a9 .x.X...t...^..K.
0x000000e0: a3 22 e1 d9 6f 90 11 c6 11 c2 39 7b f9 11 8f 91 ."..o.....9{....
0x000000f0: dd a3 a5 dd e2 64 2f 04 02 7a a6 c8 3f 0c 8d cb .....d/..z..?...
Arbitrary decryption operation is shown below:
wret> 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 [email protected]..
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 throughrcx
pointer - subroutine arguments are put into a contiguous block (they are serialized),
as a result they may occupy different offsets (with respect torcx
for
different subroutines, pointers might not be aligned)
- top of the stack doesn’t contain the usual return address (
- the decrypted subroutine call is executed through the APC (Asynchronous
Procedure Call) mechanism (the call toPspSetContextThreadInternal
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 ..&[email protected]......
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.<ko.>.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 [email protected].|$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 [email protected]...+-.
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:
- 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 - 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 ofNtQuerySystemInformation
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 ofGetCurrentPackageId
,
GetCurrentApplicationUserModelId
andGetCurrentPackageFamilyName
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:
- does nothing for segment encrypt / decrypt operations (no need to as these
got decrypted as part of the setup process) - 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
visibleSupressActHandler
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 theRequestAccess
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:
- 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 toNtQuerySystemInformation
used by it can be intercepted (break up of thesemi-atomic invocation chain
/ possibility of the hooking), - 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<IMFMediaBuffer> 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 [email protected]
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 [email protected]
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<n.r/.L....b.73
0050: 4a b1 fe 09 0c 2f ad 70 b6 b6 4a 0f 77 7a c0 24 J..../.p..J.wz.$
0060: fc c7 1c f6 aa ec 6b 1d b1 ef dc 95 ec cb 73 42 ......k.......sB
0070: af 99 9f d7 b1 3c 23 4f 5f 8d 50 60 7e 17 4d 74 .....<#O_.P...Mt
stored to zlicense.blob
It’s decryption revealed XOR values associated with content key:
wret> 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 [email protected]
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<n.r/.L....b.73
0x00000050: 4a b1 fe 09 0c 2f ad 70 b6 b6 4a 0f 77 7a c0 24 J..../.p..J.wz.$
0x00000060: fc c7 1c f6 aa ec 6b 1d b1 ef dc 95 ec cb 73 42 ......k.......sB
0x00000070: af 99 9f d7 b1 3c 23 4f 5f 8d 50 60 7e 17 4d 74 .....<#O_.P`~.Mt
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.
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
- The name of the output file is
xorkey.txt'. Corresponding
gentest.batscript 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"[email protected]>.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 [email protected]
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 ..{[email protected]
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 [email protected]>.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 [email protected].#
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 [email protected].#..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 ..<Ngr...Uq,..%?
0060: 8f f8 88 eb 6e 0c e8 85 6f df 1f 2f 1f e1 bc 96 ....n...o../....
0070: b6 49 9d 0d 5e c3 f3 0e b7 9a 6a a7 2e fd 50 b7 .I..^.....j...P.
0080: 27 21 13 80 ff 64 66 08 ce 78 8a 29 66 03 5c 18 '!...df..x.)f...
0090: bc f6 f6 02 92 8f 8c 92 d6 b4 47 e1 ac a2 d1 f8 ..........G.....
00a0: 46 4d 3c b2 4e 00 92 b4 a1 af eb 68 4b ee a4 6e FM<.N......hK..n
stored to rounds.dat
Finally, we use the WRET toolkit and setup it with some PlayReady library
corresponding to Windows 10 system:
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>
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:
- the private ECC decryption is deobfuscated, as a result “obfuscated ECC
key” form is obtained of size 0x20 - 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 - 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:
- a nibble of the “mapped ECC key” tells, which precalc ECC point to use
as a starting point for license blob processing (ECC decryption), - 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 - 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 ..<W.*.<.gW.F...
0040: c1 3f 41 85 3e e5 15 b9 59 53 f1 1f 75 e1 0a 07 .?A.>...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 [email protected]
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 ..<W.*.<.gW.F...
0x00000040: c1 3f 41 85 3e e5 15 b9 59 53 f1 1f 75 e1 0a 07 .?A.>...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 [email protected]
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 [email protected]
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 [email protected]
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 [email protected]
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):