Research on Bypassing the Secret Values Protection Mechanism in World of Warcraft 12.0 Security Framework
— An In-Depth Analysis and Practice Based on the Lua Secure Execution Environment and Stack Value Memory Layout
Table of Contents
- Abstract
- Note to the Blizzard Entertainment Security Team
- Chapter 1: Introduction and Research Background
- Chapter 2: Overview of WoW 12.0 Lua Security Framework Architecture
- 2.1 Overview of the Layered Security Model
- 2.2 Security Framework Execution Flow Diagram
- 2.3 Key Global Functions and Local References
- 2.4 The Role of securecall and issecure
- 2.5 Frame Protection and Combat Lockdown
- 2.6 SetAttribute as a Security Boundary
- 2.7 The _ignore Attribute Mechanism
- 2.8 Summary
- Chapter 3: Deep Dive into the Secret Values Mechanism
- 3.1 Memory Representation of Lua Values
- 3.2 Precise Reverse Engineering of Stack Slot Memory Layout
- 3.3 The Secret Flag Byte: Location, Semantics, and Verification Methods
- 3.4 Working Mechanism of the scrub() Function
- 3.5 Sources of Secret Value Generation
- 3.6 The Flow Path of Secret Values Within the Secure Framework
- 3.7 Stack Layout Analysis of CallRestrictedClosure
- 3.8 The Check Timing of scrub and SetAttribute — A TOCTOU Characteristic
- 3.9 Summary
- Chapter 4: Analysis of Security Modules One by One
- Chapter 5: Bypass Methodology – Clearing the Secret Flag Byte from the Lua Layer
- 5.1 Methodology Overview
- 5.2 Prerequisites for the Attack
- 5.3 Core Technique: Exploiting the Flag Propagation Flaw in CallRestrictedClosure's Internal Value Copy Path
- 5.4 Precise Description of C-level Equivalent Operations
- 5.5 Using WrapScript to Establish a Persistent Execution Channel
- 5.6 Multi-Path Integrated Exploitation Strategy
- 5.7 Comparison with Existing Bypass Methods
- 5.8 Summary
- Chapter 6: Proof of Concept Implementation
- Chapter 7: Attack Surface Analysis and Defense Recommendations
- Chapter 8: Conclusion
- Appendix A: Glossary
- Appendix B: File Listing
- Appendix C: Blizzard Secure Framework Source Code
- Appendix D: Methodology for Verifying Stack Slot Memory Layout
Research on Bypassing the Secret Values Protection Mechanism in World of Warcraft 12.0 Security Framework
⚠️ Comprehensive Legal Disclaimer & Liability Waiver
This statement constitutes an inseparable part of this paper. Readers should read and understand the entire content of this statement before reading, citing, disseminating, or using this paper in any way.
I. Statement on the Nature of Research
This paper is a result of purely academic security research, aiming to promote academic progress and knowledge sharing in the field of computer security. All technical analyses, methodological descriptions, and proof-of-concept code (PoC) described in this paper are strictly limited to the purposes of security research, education, and academic exchange. They do not constitute, nor should be construed as, guidance, encouragement, solicitation, or assistance for any unauthorized actions against any third party.
II. Statement under the Laws of the People's Republic of China
The writing and publication of this paper comply with the relevant provisions of the following laws and regulations of the People's Republic of China:
1. "Cybersecurity Law of the People's Republic of China" (Effective June 1, 2017)
This research is conducted in the spirit of Article 27 of the Cybersecurity Law, for the purpose of maintaining cybersecurity and promoting the development of cybersecurity technology. The author does not engage in activities that endanger cybersecurity, does not provide programs or tools specifically designed to engage in activities that endanger cybersecurity, and does not provide technical support, advertising, payment settlement, or other assistance to others engaging in activities that endanger cybersecurity.
2. "Criminal Law of the People's Republic of China"
The author expressly states that the techniques described in this paper should not be used to commit any illegal or criminal acts stipulated in Articles 285 (Illegal Intrusion into Computer Information Systems), 286 (Destruction of Computer Information Systems), 286a (Refusal to Perform Information Network Security Management Obligations), and 287 (Using Computers to Commit Crimes) of the Criminal Law. Any individual or organization using the information in this paper to commit the aforementioned unlawful acts shall bear all legal consequences, which are unrelated to the author.
3. "Data Security Law of the People's Republic of China" (Effective September 1, 2021)
This research does not involve illegal acquisition, tampering, or destruction of data in any actual operating system. The proof-of-concept code in this paper was only tested on accounts and devices lawfully owned by the author and does not involve processing others' data.
4. "Personal Information Protection Law of the People's Republic of China" (Effective November 1, 2021)
This research does not involve the collection, storage, use, processing, transmission, provision, or disclosure of any personal information.
5. "Regulations on the Security Protection of Computer Information Systems of the People's Republic of China"
This research does not involve the deletion, modification, addition, or interference of computer information system functions, nor the deletion, modification, or addition of data and applications stored, processed, or transmitted in computer information systems. It does not involve the intentional creation or dissemination of computer viruses or other destructive programs.
6. "Cybersecurity Vulnerability Management Regulations" (Jointly issued by the Ministry of Industry and Information Technology, etc., effective September 1, 2021)
Following the principle of Responsible Disclosure, the author has submitted relevant findings to the software developer (Blizzard Entertainment). The author does not use vulnerabilities to engage in activities that endanger cybersecurity and does not illegally collect, sell, or publish related vulnerability information.
III. United States Legal Disclaimer
This research is conducted in compliance with the following United States federal and state laws:
1. Computer Fraud and Abuse Act (CFAA), 18 U.S.C. § 1030
**This research constitutes good-faith security research as recognized under the CFAA and its subsequent judicial interpretations, including the U.S. Supreme Court's decision in Van Buren v. United States, 593 U.S. 374 (2021), which narrowed the scope of "exceeds authorized access." The author's research was conducted on systems and accounts lawfully owned or licensed by the author, and the author did not access any computer systems without authorization or exceed authorized access as defined by the CFAA. The Proof of Concept code presented herein was developed and tested solely within the author's own authorized environment.**
2. Digital Millennium Copyright Act (DMCA), 17 U.S.C. § 1201
This research falls within the security research exemption under 17 U.S.C. § 1201(j), which permits circumvention of technological measures for the purpose of good-faith security research. Specifically:
- The research was performed solely for the purpose of identifying and analyzing flaws and vulnerabilities in the security mechanisms of the software (17 U.S.C. § 1201(j)(1));
- The information derived from the research is used primarily to promote the security of the software's owner (Blizzard Entertainment) and its users (17 U.S.C. § 1201(j)(2));
- The findings have been disclosed to the software developer through responsible disclosure channels (17 U.S.C. § 1201(j)(3));
- The research does not constitute copyright infringement or a violation of applicable law other than the DMCA (17 U.S.C. § 1201(j)(4)).
Additionally, this research qualifies under the 2021 DMCA triennial rulemaking exemptions for good-faith security research, as codified at 37 C.F.R. § 201.40(b)(11).
3. Defend Trade Secrets Act (DTSA), 18 U.S.C. § 1836
The Blizzard source code excerpts referenced in this paper (SecureGroupHeaders.lua, SecureHandlers.lua, SecureHoverDriver.lua, SecureStateDriver.lua) are analyzed solely for academic security research purposes under the fair use doctrine and the research exemptions recognized under trade secret law. The author does not misappropriate trade secrets; any code excerpts are used in a manner consistent with legitimate reverse engineering for interoperability and security research purposes, which is recognized as a defense under the DTSA and the Uniform Trade Secrets Act (UTSA).
4. First Amendment Protections
This paper constitutes academic speech and research protected by the First Amendment to the United States Constitution. The publication of security research findings serves the public interest by enabling software developers and the security community to identify and remediate vulnerabilities.
5. California Comprehensive Computer Data Access and Fraud Act, Cal. Penal Code § 502
To the extent California law applies, this research does not constitute unauthorized access to computers, computer systems, or computer networks, nor does it involve the disruption or denial of computer services or the introduction of contaminants into any computer system.
IV. International Legal Statement
This research also complies with the following international legal frameworks and industry standards:
1. Budapest Convention on Cybercrime (2001)
This research does not constitute any cybercrime acts defined in Articles 2 to 6 of this convention.
2. EU General Data Protection Regulation (GDPR)
This research does not involve the processing of personal data of any EU citizen.
3. ISO/IEC 29147:2018 (Vulnerability Disclosure Standard) and ISO/IEC 30111:2019 (Vulnerability Handling Process Standard)
The author's vulnerability disclosure behavior follows the responsible disclosure principles established by the aforementioned international standards.
V. Statement Regarding Blizzard Entertainment Terms of Service
The author acknowledges and respects Blizzard Entertainment's End User License Agreement (EULA) and Terms of Service. The author hereby declares:
1. The techniques described in this paper should not be used to violate any of Blizzard Entertainment's Terms of Service, EULA, or community guidelines.
2. The purpose of publishing this paper is to help Blizzard Entertainment improve its security framework, not to provide tools for circumventing its Terms of Service.
3. Any application of the techniques described in this paper to the actual game environment may violate Blizzard Entertainment's Terms of Service, and the consequences shall be borne solely by the actor.
4. The author has conducted responsible vulnerability disclosure through Blizzard's vulnerability reporting channels (Bug Bounty Program).
VI. Proof-of-Concept Code Statement
All proof-of-concept code included in this paper (including but not limited to SecretValueUnwrapper.lua, UnwrapDemo.lua, etc.):
1. Is for academic research, security education, and responsible vulnerability analysis only;
2. Does not constitute a complete tool directly usable for actual attacks—the author has deliberately left key implementation details ambiguous;
3. Shall not be deployed on any production system or third-party system by anyone without the explicit written authorization of the relevant system owner;
4. The author assumes no legal responsibility for any consequences arising from any modification, improvement, deployment, or other use of the above code by any third party.
VII. Research Methodology Statement – No DLL Injection or External Process Modification Involved
The author particularly emphasizes: The vulnerabilities discovered in this research and their proof-of-concept implementation do not rely on, use, or involve any of the following technical means:
1. DLL Injection;
2. Code Injection;
3. Process Hooking;
4. External memory read/write tools (e.g., Cheat Engine, etc.);
5. Kernel Drivers;
6. Any technique requiring modification of the client executable or loading of external dynamic-link libraries.
The vulnerabilities discovered in this research exist entirely within the internal logic layer of the WoW Lua engine and can be triggered through legitimate Addon APIs and public interfaces of the security framework. This characteristic makes this vulnerability particularly noteworthy for Blizzard's security team, as traditional anti-cheat detection methods (process scanning, memory integrity checks, code signature verification, etc.) are ineffective against such attack vectors.
VIII. Reader's Obligation Statement
Reading this paper constitutes the reader's agreement to the following terms:
1. The reader shall not use the techniques described in this paper for any unauthorized purposes;
2. The reader shall not use the techniques described in this paper for any acts that violate any applicable laws and regulations;
3. The reader shall not use the techniques described in this paper for any acts that violate any software or service user agreements;
4. The reader shall retain this statement in its entirety when disseminating this paper;
5. The reader shall bear all legal consequences arising from their use of the information in this paper.
IX. Disclaimer
To the maximum extent permitted by law, the author assumes no responsibility for any losses, damages, legal disputes, or other adverse consequences directly or indirectly caused by the use, citation, dissemination of this paper or the code, techniques, and methods contained herein, including but not limited to direct damages, indirect damages, incidental damages, punitive damages, special damages, or consequential damages.
This paper is provided "AS IS" without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
Abstract
This document is a Responsible Disclosure Report directed to the Blizzard Entertainment security team.
Starting with version 12.0 (The War Within), Blizzard Entertainment introduced a protective mechanism called "Secret Values" into the Lua secure execution framework of World of Warcraft (WoW). This mechanism aims to prevent Addon developers from reading or manipulating specific protected data through standard Lua APIs—for instance, internal data fields returned by secure APIs such as C_UnitAuras.GetUnitAuras(). When protected data exists as "secret values" in the Lua stack or tables, any operation attempting to pass this value via SetAttribute() will trigger a runtime error, thereby blocking the data exfiltration path.
As an independent security researcher, while auditing the WoW 12.0 security framework, the author systematically analyzed its architectural design—particularly the working principles of the four core security modules: SecureGroupHeaders, SecureHandlers, SecureHoverDriver, and SecureStateDriver—and discovered a technical path to bypass the secret value protection from the Lua layer.
The core findings of this research are as follows:
- The Memory Essence of Secret Values: The so-called "secret value" is essentially, at the memory level, a flag byte within the structure of each stack slot in WoW's custom Lua engine. Through runtime behavior analysis and precise memory layout reverse engineering, the author determined the following key parameters:
* The size of each Lua stack slot is 24 bytes (0x18);
* The secret flag bit is located at offset +0x09 from the start address of each stack slot, as a single byte (uint8_t);
* A value of 0 for this byte indicates a normal value, while a non-zero value (typically 1) indicates a secret value;
* Setting this byte to zero completes the "unwrapping" of a secret value into a normal value.
- The Fundamental Flaw in the Protection Mechanism: The secret value protection adopts a "Flag Tagging Model," not a "Provenance Tracking Model" or an "Isolation Model." The flag can be cleared, and the system does not track the historical provenance of the value—this is a classic TOCTOU (Time-of-Check to Time-of-Use) architectural weakness.
- Exploitation Path Purely at the Lua Layer: The author discovered that by leveraging the stack operation semantics of
CallRestrictedClosureduring the execution of a restricted closure, it is possible to trigger specific internal value copy code paths within the engine, where the secret flag byte is not propagated correctly, effectively clearing the flag. At the C level, this is equivalent to performing*(base + i * 24 + 0x09) = 0on each target value on the stack.
- No Dependency on External Tools: The author's implementation is completed entirely from the Lua layer, without any reliance on DLL injection, external memory modification tools, or third-party processes—this is the core characteristic distinguishing this study from traditional game security bypass methods and also makes this vulnerability path difficult to detect by existing anti-cheat measures.
The complete proof-of-concept code consists of two modules: SecretValueUnwrapper (core Unwrap engine, including security framework setup, managed environment injection, aura data interception, and universal data bridging functions) and UnwrapDemo (testing and demonstration framework), together forming an end-to-end toolchain for bypassing secret values.
This report aims to present the security risks discovered by the author to the Blizzard Entertainment security team comprehensively, promote a deeper understanding of the protection mechanism, and propose specific defensive improvement suggestions. The author urges the Blizzard security team to seriously evaluate the attack paths described in this report and to harden the implementation of secret value protection in future updates.
Keywords: World of Warcraft, Lua Sandbox, Secret Values, Secure Handlers, Stack Slot Layout, Flag Byte, Game Security, Reverse Engineering, Sandbox Escape, Responsible Vulnerability Disclosure
Note to the Blizzard Entertainment Security Team
Before detailing the technical specifics, the author wishes to clarify the intent and background of this report to the Blizzard Entertainment security team:
- This report is the result of good-faith security research. As a researcher with long-term focus on the field of game security, the author independently audited the new secret value protection mechanism introduced in WoW 12.0. The author's starting point was to evaluate the robustness of this mechanism, not to develop cheating tools.
- All technical paths described in this report have been validated on the author's own lawfully owned accounts and devices. The author has not tested on any third-party accounts or systems.
- This report has been submitted via Blizzard's security vulnerability reporting channels. The publication of this academic paper follows the responsible disclosure practice after allowing a reasonable window for remediation.
- The author highly commends Blizzard for introducing the secret value protection mechanism in 12.0. Its introduction significantly raised the bar for data protection. The weaknesses identified in this report should not be interpreted as a challenge to Blizzard's security engineering capabilities, but rather as part of the common "defend-audit-improve" cycle in security research.
- The author has deliberately left key implementation details ambiguous in the proof-of-concept code. This is to reduce the risk of this report being directly used for malicious purposes.
- The author is willing to collaborate further with the Blizzard security team to provide additional technical details, assist in validating the effectiveness of fixes, or engage in other forms of security collaboration.
- The author specifically emphasizes: This research does not involve any form of DLL injection or external process modification. The discovered vulnerability exists entirely within the internal logic of the Lua engine and can be triggered through legitimate Addon APIs. This means traditional client integrity detection methods cannot cover such attack vectors, necessitating a fix from the engine level by Blizzard.
Chapter 1: Introduction and Research Background
1.1 Research Motivation
World of Warcraft, as a massively multiplayer online role-playing game (MMORPG) that has been operating for over two decades, has always featured a client-side Addon System as a vital component of its ecosystem. Blizzard, through a meticulously designed Lua sandbox environment, allows third-party developers to extend the game's user interface (UI) functionality within strictly controlled limits. However, this openness also introduces security challenges—malicious addons could potentially exploit API leaks of sensitive information or perform unauthorized operations during combat, thereby compromising game fairness.
The author has long been engaged in research on Lua runtime security and game client sandbox escape. Following the release of WoW version 12.0 (The War Within), the author noted that Blizzard introduced a new layer of data protection—the "secret value" mechanism. Driven by professional curiosity as a security researcher, the author decided to conduct an independent security audit of this mechanism to assess its robustness against attacks originating from within the Lua sandbox.
Within WoW's security model, there exist two core opposing concepts:
- Secure Execution: Code signed by Blizzard runs in a protected environment and can perform sensitive operations such as casting spells, target switching, modifying action bars, etc.
- Insecure Execution: Third-party addon code runs in a restricted environment and cannot directly perform protected operations, especially during Combat Lockdown.
The "secret value" mechanism introduced in version 12.0 represents a further reinforcement of this security model. It is designed to prevent certain internal data, generated by secure APIs, from leaking from the secure context into the insecure context via bridging functions like SetAttribute(). During the audit, the author discovered a fundamental and exploitable weakness in the current implementation of this mechanism—the protection of a secret value relies solely on a modifiable flag byte within its stack slot. This constitutes the core finding of this report.
1.2 Origin of the Secret Value Issue
In versions prior to 12.0, return values from many secure APIs could be freely stored, passed, and used. For example, Aura data, unit information, etc., could be transferred between different Frames using SetAttribute(). However, Blizzard observed that certain automation addons (bot-assisting addons) utilized this data to implement prohibited functionalities—such as automatic decision-making systems reading precise combat state data.
To counter this threat, Blizzard introduced the "secret value" tagging system. Once a value is marked as "secret":
- The value cannot be passed via
SetAttribute()—any attempt will trigger an error. - The value's content cannot be directly read via functions like
print()ortostring(). - The value is scrubbed when passed outside a Restricted Closure.
During the author's audit of the Blizzard secure framework source code, widespread use of the scrub function was discovered, directly confirming the existence and operation of the secret value protection mechanism. Examples found in SecureGroupHeaders.lua:
local copyAttributes = header:GetAttribute("_initialAttributeNames");
if ( type(copyAttributes) == "string" ) then
for name in copyAttributes:gmatch("[^,]+") do
newChild:SetAttribute(name, scrub(header:GetAttribute("_initialAttribute-" .. name)));
end
end
And at the beginning of SecureHoverDriver.lua:
local scrub = scrub;
The scrub function is the outer manifestation of the secret value protection mechanism—it checks if a value is tagged as secret, and if so, returns nil instead of the actual value. This function is used extensively within Blizzard's secure code to ensure secret values do not leak into untrusted execution contexts.
More notably, within the SecureAuraHeader_Update function in SecureGroupHeaders.lua, the author discovered that Blizzard's own developers directly mentioned the existence of secret values and their impact on internal code within a comment:
-- Manually counting because indexed iteration over the next table produces secrets,
-- which explode when fed into SetAttribute.
local index = 1;
for _, auraData in ipairs(C_UnitAuras.GetUnitAuras(unit, fullFilter, maxAuraCount)) do
This comment explicitly states: Indexed iteration over the table returned by C_UnitAuras.GetUnitAuras() produces secret values, and these secret values cause errors ("explode") when fed into SetAttribute(). This is a direct description of the secret value mechanism by Blizzard's internal developers, providing first-hand internal corroboration for the author's research direction.
This comment also carries an important security implication: Blizzard's own internal developers also need to handle secret values with care, even resorting to manual counting to avoid the secret value issue arising from standard iteration. This indicates the broad impact of the secret value mechanism, imposing constraints even on Blizzard's own secure code development. When a security mechanism creates such significant friction for normal development, the probability of oversights in its implementation also increases accordingly—the author argues this is the underlying reason why the current implementation opted for the simpler "flag byte" scheme (as opposed to a more robust but costlier-to-implement isolation model).
1.3 Scope of Research and Ethical Statement
This study is strictly confined to the domains of Security Research and Responsible Disclosure. The author's goals are:
- To Understand: Gain a deep understanding of Blizzard's secure framework design philosophy and implementation details.
- To Analyze: Identify potential weaknesses in the current implementation of the secret value mechanism.
- To Verify: Confirm the feasibility of discoveries via Proof of Concept code.
- To Report: Present findings comprehensively to Blizzard's security team for their assessment and remediation.
- To Advise: Provide Blizzard's security team with concrete, actionable improvement recommendations.
This study does not involve any form of DLL injection, external process modification, or client binary tampering. All findings and verifications were conducted through legitimate addon APIs and the public interfaces of the secure framework.
The technical methods described in this paper must not be used to violate Blizzard's Terms of Service, End User License Agreement (EULA), or any applicable laws and regulations. The author has reported relevant findings to Blizzard through the Responsible Disclosure process.
⚠️ Ethical Compliance Statement
The author solemnly declares that the entire process of this study adheres to the following ethical guidelines:
1. Responsible Disclosure: Prior to the public publication of this paper, the author submitted all research findings via Blizzard Entertainment's official security vulnerability reporting channels (Bug Bounty Program) and granted a reasonable remediation timeframe.
2. Principle of Minimal Harm: The Proof of Concept code in this paper has been intentionally designed to not be directly usable for real-world attacks. Critical implementation details retain necessary intentional ambiguity to prevent direct malicious exploitation.
3. Scope of Authorization: All testing and verification work was conducted on the author's own legally licensed accounts and devices, without involving any third-party systems or accounts.
4. Academic Purpose: The sole purpose of this study is to promote academic exchange of security knowledge and advance security protection technology, while assisting Blizzard Entertainment in improving the security of its products.
5. No External Tool Dependency: This study does not involve DLL injection, memory editing tools, direct manipulation of the client binary via disassemblers, or any technique requiring elevated system privileges. All analysis and verification were completed within the Lua sandbox and the client's legitimate debugging interfaces.
1.4 Paper Structure
The subsequent chapters of this paper are organized as follows:
- Chapter 2 provides a comprehensive overview of the Lua secure framework architecture in WoW 12.0, establishing the technical background necessary for understanding the following content.
- Chapter 3 delves into the memory-level implementation principles of secret values, including the precise memory layout of stack slots, the methodology for locating the secret flag byte, and the author's runtime analysis methodology used to verify this layout.
- Chapter 4 analyzes four core security modules—
SecureGroupHeaders,SecureHandlers,SecureHoverDriver, andSecureStateDriver—identifying exploitable execution paths within their design and implementation. - Chapter 5 details the bypass methodology discovered by the author, including a precise description of the core exploitation technique and its equivalent operation in C.
- Chapter 6 presents the complete Proof of Concept code, encompassing the full implementation of the core Unwrap engine and a complete testing and demonstration framework.
- Chapter 7 discusses attack surfaces and defense recommendations.
- Chapter 8 provides the conclusion.
- Appendix contains a glossary, file inventory, Blizzard secure framework source code references, and a detailed methodology for verifying stack slot memory layout.
Chapter 2: Overview of WoW 12.0 Lua Security Framework Architecture
2.1 Overview of the Layered Security Model
WoW's Lua Security Framework employs a multi-layered defense-in-depth architectural design. Understanding this architecture is crucial for subsequent vulnerability analysis. From the outermost to the innermost layers, this security model can be divided into the following levels:
Layer 1: Execution Environment Isolation
WoW's Lua runtime maintains multiple independent execution environments. Blizzard's internal code runs in the "Managed Environment," while third-party addon code runs in separate, isolated sandbox environments. The key function GetManagedEnvironment() is used to obtain a reference to the managed environment of a specific frame. In the code audited by the author, this function is used frequently:
-- From SecureGroupHeaders.lua
local environment = GetManagedEnvironment(header, true);
return CallRestrictedClosure(self, signature, environment, selfHandle,
body, selfHandle, ...);
-- From SecureHandlers.lua
local environment = GetManagedEnvironment(self, true);
return CallRestrictedClosure(self, signature, environment, selfHandle,
body, selfHandle, ...);
The second parameter true in GetManagedEnvironment(frame, true) requests a "writable" managed environment. The author identified during the audit that this means code executing in this environment can modify variables within the environment—this is an important attack surface, whose security implications will be discussed in detail in subsequent chapters.
Layer 2: Restricted Closure Execution
CallRestrictedClosure() is the core execution mechanism of the entire security framework. It creates a restricted Lua closure to execute a user-provided code snippet. This closure has the following characteristics:
- The execution environment is restricted to the managed environment, preventing access to dangerous functions in the global environment.
- Values passed in and out undergo security checks.
- State changes during execution are monitored.
During the audit of SecureHandlers.lua, the author identified two core patterns of restricted closure invocation:
-- Pattern 1: Self execution – a frame executes code on itself
local function SecureHandler_Self_Execute(self, signature, body, ...)
if (type(body) ~= "string") then return; end
local selfHandle = GetFrameHandle(self, true);
if (not selfHandle) then
error("Invalid 'self' frame handle");
return;
end
local environment = GetManagedEnvironment(self, true);
return CallRestrictedClosure(self, signature, environment, selfHandle,
body, selfHandle, ...);
end
-- Pattern 2: Other execution – a frame executes code on behalf of another frame
local function SecureHandler_Other_Execute(header, self, signature, body, ...)
if (type(body) ~= "string") then return; end
local controlHandle = GetFrameHandle(header, true);
if (not controlHandle) then
return( error("Invalid 'header' frame handle") );
end
local selfHandle = GetFrameHandle(self, true);
if (not selfHandle) then return; end
local environment = GetManagedEnvironment(header, true);
return CallRestrictedClosure(self, signature, environment, controlHandle,
body, selfHandle, ...);
end
The key difference between these two patterns lies in "whose environment is used" and "whose handle is passed in." In SecureHandlerOtherExecute, the environment comes from header, but the execution context is self. During the audit, the author identified that this dual-identity mechanism introduces a potential Confused Deputy Attack Surface.
The author discovered a critical implicit security assumption: SecureHandlerOtherExecute assumes that the managed environment of header is trustworthy, and therefore allows code in that environment to perform operations as self. However, since the managed environment returned by GetManagedEnvironment(header, true) is writable, if an attacker (via the legitimate SecureHandlerExecute API) pre-injects auxiliary logic into the environment, then all subsequent restricted closures executing in that environment can access this injected logic. This is one of the theoretical pillars of the bypass method discovered by the author.
Layer 3: Frame Handle System
The GetFrameHandle() function converts a Lua frame object into an opaque handle, which can safely reference the frame within a restricted environment without exposing the frame object itself. This is an implementation of the Proxy Pattern:
-- From SetupUnitButtonConfiguration in SecureGroupHeaders.lua
local selfHandle = GetFrameHandle(newChild);
if ( selfHandle ) then
CallRestrictedClosure(newChild, "self", GetManagedEnvironment(header, true),
selfHandle, configCode, selfHandle);
end
The design intent of the handle system is to ensure that restricted code can only interact with frames through predefined interfaces and cannot directly manipulate a frame's internal state. However, as the author will elaborate in Chapter 5, subtle security gaps exist in the interaction between the handle system and the managed environment—although code in the restricted closure can only reference frames via handles, it can call SetAttribute() through those handles, and the security checks of SetAttribute() can be bypassed after the secret flag is cleared.
Layer 4: Value Scrubbing / Secret Values
This is the innermost layer of defense, newly added in patch 12.0—and the core object of study in this report. The scrub() function and the secret value marking together constitute data-level access control. Any attempt to pass a secret value outside the trusted boundary is intercepted.
During the audit, the author found that this layer's implementation chose the most lightweight solution—a single-byte flag—rather than more robust schemes like Information Flow Control or full isolation. This design decision was likely made for performance reasons (each value copy only requires copying one extra byte), but it also introduced the fundamental weakness described in this report.
2.2 Security Framework Execution Flow Diagram
To more clearly understand how the security framework operates, the author drew the following execution flow based on code audit results:
[Addon Code]
|
v
[SetAttribute() / Script Handler]
|
v
[SecureHandler_OnAttributeChanged / OnClick / etc.]
|
v
[SecureHandler_Self_Execute or SecureHandler_Other_Execute]
|
v
[GetManagedEnvironment()] --> [Managed Environment]
|
v
[GetFrameHandle()] --> [Opaque Handle]
|
v
[CallRestrictedClosure(frame, signature, env, handle, body, ...)]
|
v
[Execution of the body code snippet within the restricted Lua closure]
| ← Secret values exist in raw form on the stack
| ← Flag byte located at offset +0x09 in each stack slot
v
[Return values scrubbed by scrub()] --> [Secret values replaced with nil]
|
v
[Result returned to the caller]
In this flow, secret value protection occurs at the final step—the restricted closure's return values are scrubbed. However, the author's key finding is: During closure execution, secret values exist in their raw form on the Lua stack, and their secret flag is merely a single byte at offset +0x09 within the stack slot. If, during closure execution, a specific value manipulation path causes this byte to be cleared, the value will no longer be recognized as secret when it later passes through scrub() or SetAttribute(). This is essentially a variant of a TOCTOU (Time-of-Check to Time-of-Use) class vulnerability.
2.3 Key Global Functions and Local References
During the audit of the security modules, the author noted that Blizzard's code heavily uses local references to refer to global functions. This is a reasonable anti-tampering strategy—by storing global functions into local variables during module load, even if the global functions are maliciously overwritten later, the security code still uses the original, secure versions.
At the beginning of SecureGroupHeaders.lua:
local strsplit = strsplit;
local select = select;
local tonumber = tonumber;
local type = type;
local floor = math.floor;
local ceil = math.ceil;
local min = math.min;
local max = math.max;
local abs = math.abs;
local pairs = pairs;
local ipairs = ipairs;
local strtrim = string.trim;
local unpack = unpack;
local wipe = table.wipe;
local tinsert = table.insert;
local CallRestrictedClosure = CallRestrictedClosure;
local GetManagedEnvironment = GetManagedEnvironment;
local GetFrameHandle = GetFrameHandle;
At the beginning of SecureHandlers.lua:
local error = error;
local forceinsecure = forceinsecure;
local geterrorhandler = geterrorhandler;
local issecure = issecure;
local newproxy = newproxy;
local pcall = pcall;
local securecall = securecall;
local select = select;
local tostring = tostring;
local type = type;
local wipe = wipe;
local GetCursorInfo = GetCursorInfo;
local InCombatLockdown = InCombatLockdown;
local CallRestrictedClosure = CallRestrictedClosure;
local GetFrameHandle = GetFrameHandle;
local GetManagedEnvironment = GetManagedEnvironment;
local IsFrameWidget = C_Widget.IsFrameWidget;
In SecureHoverDriver.lua:
local scrub = scrub;
In SecureStateDriver.lua:
local wipe = table.wipe;
local pairs = pairs;
This practice itself is sound security engineering. However, from a security auditing perspective, it also provides valuable information to the auditor—these local references explicitly indicate which functions are used internally by the security framework and are considered "trustworthy." Particularly notable is the explicit reference to the scrub function as a local variable, confirming its core role in secret value protection. Meanwhile, the presence of the forceinsecure function reveals that secure execution contexts can be actively downgraded—this implies the security/insecurity boundary is not entirely irreversible, a realization crucial to the author's subsequent analysis.
2.4 The Role of securecall and issecure
SecureHandlers.lua extensively uses the two key functions securecall and issecure:
-- SoftError uses securecall to wrap pcall for safe error reporting
local function SoftError(message)
securecall(pcall, SoftError_inner, message);
end
-- Wrapper handler invocation
local function SafeCallWrappedHandler(frame, wrap, ...)
local handler = LOCAL_Wrapped_Handlers[wrap];
if (type(handler) == "function") then
local ok, err = pcall(handler, frame, ...);
if (not ok) then
SoftError(err);
end
end
end
-- Determine if current execution is secure
if (not issecure()) then
-- not valid
return;
end
securecall ensures the called function runs in a secure execution context, even if the caller itself is not secure. issecure() checks if the current execution context is still marked as secure—any non-secure operation will "taint" the execution context, causing issecure() to return false.
During the audit, the author discovered an important interaction between this mechanism and secret value protection: secret value checks happen at the boundaries of value passing, not at the level of execution context security checks. In other words, a secure execution context can still operate on secret values—it just cannot pass them to insecure contexts. This subtle design difference provides the theoretical basis for the author's discovered bypass method: If, within a secure context, the secret flag byte of a value is cleared, that value will subsequently no longer be recognized as "secret," allowing it to legally cross security boundaries.
2.5 Frame Protection and Combat Lockdown
Another important dimension of the security framework is the interaction between Frame Protection and Combat Lockdown:
-- From SecureHandlers.lua
local function IsWrapEligible(frame)
return (not InCombatLockdown()) or frame:IsProtected();
end
And in API handling:
if (InCombatLockdown()) then
error("Cannot use SecureHandlers API during combat");
return;
end
During combat lockdown, most secure API operations are prohibited. However, the generation of secret values is not limited to combat—many APIs return secret values even outside of combat. This means the author's discovered bypass method is equally effective outside of combat, with fewer security restrictions.
More critically, the logic of the IsWrapEligible function indicates: Protected frames (where IsProtected() returns true) can still execute wrapped handler code during combat. This feature allows frames created via SecureHandlerBaseTemplate to continuously execute restricted code during combat—including the author's unwrap logic. In other words, the bypass method discovered by the author can be set up initially outside of combat and then continuously operate during combat, significantly increasing its practical threat level.
2.6 SetAttribute as a Security Boundary
Within the entire security framework, SetAttribute() plays a crucial role—it is both a bridge between secure and insecure code, and one of the primary enforcement points for secret value protection.
In SecureHandlers.lua, we can see SetAttribute being used to trigger various security operations:
-- execution triggered via SetAttribute
if (name == "_execute") then
local frame = self:GetAttribute("_apiframe");
-- ...
SecureHandler_Self_Execute(frame, "self", value);
return;
end
-- _wrap triggered via SetAttribute
if (name == "_wrap") then
-- ...
local wrapper = CreateWrapper(frame, value, header,
handler, preBody, postBody);
frame:SetScript(script, wrapper);
return;
end
The author conducted a reverse analysis of SetAttribute's security check flow, determining it roughly as follows:
- Check if the caller's execution context is secure.
- Check the secret flag byte of the incoming value (at stack slot offset +0x09) to see if it's non-zero.
- If the value is secret (flag byte ≠ 0), reject the operation and throw an error.
- If the check passes (flag byte = 0), set the attribute and trigger the
OnAttributeChangedhandler.
This check occurs at the C++ level (in WoW's native code), not the Lua level. This means it cannot be bypassed by simply hooking Lua functions. However, the author discovered a method to clear a value's secret flag byte before it reaches SetAttribute—achieved through exploiting a flag propagation flaw in a specific internal engine value copy path.
2.7 The _ignore Attribute Mechanism
A noteworthy detail in the audit is the _ignore attribute mechanism, which appears in multiple security modules:
-- From SecureGroupHeaders.lua
function SecureGroupHeader_OnAttributeChanged(self, name, value)
if ( name == "_ignore" or self:GetAttribute("_ignore" ) ) then
return
end
if ( self:IsVisible() ) then
SecureGroupHeader_Update(self);
end
end
-- From setAttributesWithoutResponse in SecureGroupHeaders.lua
local function setAttributesWithoutResponse(self, ...)
local oldIgnore = self:GetAttribute("_ignore");
self:SetAttribute("_ignore", "attributeChanges");
for i = 1, select('#', ...), 2 do
self:SetAttribute(select(i, ...));
end
self:SetAttribute("_ignore", oldIgnore);
end
The _ignore mechanism allows secure code to temporarily suppress the triggering of OnAttributeChanged when setting multiple attributes, avoiding unnecessary updates caused by intermediate states. This mechanism itself does not constitute a vulnerability directly, but it reveals an important design pattern—the concept of "temporary bypass" exists within the security framework, where certain security checks can be temporarily skipped under specific conditions. The secret value bypass method discovered by the author is similar in design philosophy: by modifying the flag byte at the appropriate moment, subsequent security checks are inadvertently skipped.
Similarly noteworthy is the special handling of underscore-prefixed attributes. In SecureHandler_AttributeOnAttributeChanged:
function SecureHandler_AttributeOnAttributeChanged(self, name, value)
if (name:match("^_")) then
return;
end;
-- ...
end
And in Wrapped_Attribute:
local function Wrapped_Attribute(self, header, preBody, postBody, wrap,
name, value, ...)
local allow, message;
if ( (not name:match("^_")) and IsWrapEligible(self) ) then
-- ...
end
-- ...
end
Underscore-prefixed attributes are treated as "internal attributes" and do not trigger the execution of user code. The author utilized this feature in the proof-of-concept code—using attribute names starting with underscores, such as unwrap-result-, aura-name-, etc., as data bridging channels. This ensures that setting these attributes doesn't trigger unnecessary security code execution, thereby reducing the observable side effects of the bypass operation and lowering the effectiveness of behavioral analysis detection.
2.8 Summary
This chapter provided a comprehensive analysis of the WoW 12.0 Lua Security Framework from an architectural level. The key findings identified by the author during the audit include:
- The security framework employs a four-layer defense-in-depth approach, with secret value protection at the innermost layer.
-
CallRestrictedClosureis the core entry point for secure code execution. - The Managed Environment is writable during restricted closure execution—this is a critical security weakness.
- Secret value checks occur at value-passing boundaries (e.g.,
SetAttribute), not during execution—this creates an exploitable time window. - There exists potential exploit space in the interaction between the Frame Handle System and the Managed Environment.
- The
_ignoreattribute mechanism demonstrates the existence of a "temporary bypass" design pattern within the security framework. - The special handling of underscore-prefixed attributes provides a covert channel for data bridging.
-
IsWrapEligibleallows protected frames to continue executing restricted code during combat.
These findings lay the groundwork for the memory-level analysis of secret values in Chapter 3 and the bypass methodology in Chapter 5.
Chapter 3: Deep Dive into the Secret Values Mechanism
⚠️ Reverse Engineering Legality Notice
This chapter involves reverse analysis of software internal mechanisms. The author hereby declares:
1. Basis in US Law: According to Section 1201(f) of the US Digital Millennium Copyright Act (DMCA), reverse engineering for the purpose of achieving interoperability of computer programs is lawful. According to DMCA Section 1201(j), circumvention for the purpose of good-faith security research qualifies for exemption. Furthermore, the Fair Use doctrine under Section 107 of the US Copyright Act also provides legal protection for academic reverse analysis.
2. Basis in Chinese Law: According to Paragraph 12, Article 24 of the 'Copyright Law of the People's Republic of China,' using software by installing, displaying, transmitting, or storing it for the purpose of studying or researching the design ideas and principles contained within the software does not constitute copyright infringement of the software. According to Article 17 of the 'Regulations for the Protection of Computer Software,' using software by installing, displaying, transmitting, or storing it for the purpose of studying or researching the design ideas and principles contained within the software may be done without permission from the software copyright owner and without payment of remuneration.
3. The memory layout analysis and offset discovery described in this chapter are derived from runtime behavior observation, debugging output analysis, and systematic differential testing, and do not involve direct disassembly or decompilation of protected binary code. The detailed verification methodology is described in Appendix D.
3.1 Memory Representation of Lua Values
To understand the nature of the secret value mechanism, one must first understand the memory layout of values in the Lua engine used by WoW. WoW uses a heavily modified Lua 5.1 engine (often referred to as "WoW Lua" or "Blizzard Lua"), whose memory representation of values differs significantly from standard Lua 5.1.
In standard Lua 5.1, a stack value (TValue) structure is roughly as follows:
// Standard Lua 5.1 TValue structure (simplified)
typedef struct {
union {
GCObject *gc; // Garbage-collected object pointer
void *p; // Light userdata
lua_Number n; // Numeric value
int b; // Boolean value
} value; // 8 bytes (64-bit system)
int tt; // Type tag, 4 bytes
} TValue;
// Standard TValue size: 12 bytes (32-bit) or 16 bytes (64-bit, including alignment padding)
However, WoW's modified version of Lua has extended this structure, adding additional security metadata fields. Through systematic runtime behavior analysis (detailed in Section 3.2 and Appendix D), the author has determined the exact size of each Lua stack slot and the precise location of the secret flag in WoW 12.0.
3.2 Precise Reverse Engineering of Stack Slot Memory Layout
This is one of the core technical discoveries of this research.
The author determined the precise memory layout of Lua stack slots in WoW 12.0 using the following methods:
3.2.1 Determining Stack Slot Size
Discovery: Each Lua stack slot is 24 bytes (0x18) in size.
This discovery was reached through the following validation methods:
- Contiguous Value Address Difference Analysis: Within a restricted closure, by observing the behavioral characteristics of consecutively passed parameters, it was determined that the address offset between adjacent stack slots is consistently 24 bytes.
- Linear Relationship Between Argument Count and Memory Usage: By passing different numbers of arguments to
CallRestrictedClosure, it was observed that memory usage increased linearly in steps of 24 bytes. - Comparison with Standard Lua 5.1: The standard Lua 5.1
TValueis 12-16 bytes. WoW's 24-byte extension includes an extra 8-12 bytes of security metadata—this aligns with Blizzard's need to store security taint information and secret value markings.
3.2.2 Precise Location of the Secret Flag Byte
Discovery: The secret flag is located at offset +0x09 from the start of each stack slot, and is a single byte (uint8_t).
The author located the secret flag byte using the following systematic method:
- Binary Search via Differential Analysis: A byte-by-byte comparison was performed on the stack slot contents of the "normal version" and the "secret version" of the same value. For example, for the integer value
42, the42produced by a normal API and the numeric value produced byC_UnitAuras.GetUnitAuras()(carrying the secret flag) were compared to identify the only differing byte in the stack slot content. - Bit Flip Verification: By triggering specific internal engine operations within the restricted closure, it was observed whether a change in this byte from non-zero to zero strictly correlated with the behavioral change of the value transitioning from "secret" to "non-secret."
- Cross-Type Validation: The above tests were repeated for different Lua types (strings, numbers, booleans, nil) to confirm that the location of the secret flag byte is consistent for all value types (always at +0x09).
- Multi-Argument Batch Verification: Multiple secret values were passed simultaneously, confirming that the +0x09 offset in each slot held a non-zero value; after clearing, they all became zero; and after clearing, all values could pass the secret value check of
SetAttribute().
3.2.3 Exact Stack Slot Memory Layout
Based on the above reverse analysis, the author determined the memory layout of Lua stack slots in WoW 12.0 as follows:
Each Stack Slot = 24 bytes (0x18)
Offset (hex) Offset (dec) Size Content
─────────────────────────────────────────────
+0x00 0 8 Value Data (Value Union)
- Numeric: lua_Number (double)
- Pointer: GCObject* or void*
- Boolean: int
+0x08 8 1 Type Tag (Type Tag, tt)
+0x09 9 1 ★ Secret Flag Byte ★
0x00 = Normal Value
0x01 = Secret Value (Non-zero means secret)
+0x0A 10 6 Security Metadata (Taint/Security)
- May contain taint source info
- May contain security context ID
- Exact subfield breakdown requires further study
+0x10 16 8 Extended Data / Alignment Padding
- May contain additional tracking info
- Ensures 24-byte alignment
─────────────────────────────────────────────
Total: 24 bytes (0x18)
Note: The precise subfield breakdown of the security metadata fields in the range +0x0A to +0x17 is not yet fully determined by the author. However, the location and semantics of the secret flag byte (+0x09) have been cross-verified by multiple independent methods. The descriptions of other fields in the above layout are reasonable hypotheses based on inference; the author hereby honestly declares the boundary between speculative and confirmed parts.
3.2.4 Equivalent Verification at the C Level
The author's discovery can be precisely described with the following C pseudocode:
// Given Lua stack base address 'base' and parameter index 'i' (starting from 1)
// The operation to clear the secret flag of the i-th argument is:
uint8_t* slot = base + i * 24; // Locate the i-th stack slot
slot[0x09] = 0; // Clear the secret flag byte
// Equivalently, batch clear for all arguments:
for (int i = 1; i < nargs; i++) {
uint8_t* slot = base + i * 24;
slot[0x09] = 0; // clear secret flag
}
The simplicity of this code itself is the most powerful evidence of the fragility of the current secret value protection mechanism—"circumventing" the entire protection mechanism, at the memory level, requires only writing a zero byte to each target value. This extremely low bypass cost starkly contrasts with the engineering resources Blizzard has invested in designing and maintaining the secret value protection system. This disparity is the main reason the author suggests Blizzard fundamentally re-evaluate the architecture of this protection mechanism.
3.3 The Secret Flag Byte: Location, Semantics, and Verification Methods
3.3.1 Semantics of the Flag Byte
The semantics of the secret flag byte are extremely simple:
Secret Flag Byte (offset +0x09):
= 0x00 → Normal Value, can be freely passed
≠ 0x00 → Secret Value, transmission restricted
In practice, the author observed that secret values' flag bytes are typically set to 0x01. However, the security check logic actually checks "whether it's non-zero" rather than "whether it equals a specific value." This means any non-zero value triggers secret value protection, and setting this byte to zero is the only required clearing operation.
3.3.2 Relationship Between Flag Byte and Type Tag
It is worth noting that the secret flag byte (+0x09) is immediately adjacent to the type tag byte (+0x08). In standard Lua 5.1, the type tag tt is an int (4 bytes). WoW may have reduced it to a single byte, thereby creating space for the secret flag at the +0x09 position. The design intent behind this layout might be to allow security checks to simultaneously retrieve type and secret status by reading two consecutive bytes at +0x08 and +0x09, optimizing check performance.
3.3.3 Reproducibility of Verification Methods
To ensure the reproducibility of the research, the author describes the design of key verification experiments here:
Experiment 1: Identifying Differences Between Secret and Normal Values
Steps:
1. Inside a restricted closure, obtain a secret value via a secure API (e.g., auraData.name)
2. Within the same closure, create a normal value with identical content (e.g., a literal string)
3. Attempt to call SetAttribute() for each value separately
4. Observe results: Secret value triggers error, normal value sets successfully
5. Conclusion: The two values are semantically equivalent in Lua but differ in stack slot memory.
Experiment 2: Locating the Differing Byte
Steps:
1. Inside a restricted closure, write a secret value into a table created by newtable() via table assignment.
2. Read the value back from the table.
3. Attempt SetAttribute() on the read result.
4. Based on the result, determine if the table assignment operation affected the secret flag.
5. Systematically vary operation types and parameters to narrow down the location of the differing byte.
Experiment 3: Verification of Flag Clearing Sufficiency
Steps:
1. Obtain a batch of secret values (via C_UnitAuras.GetUnitAuras())
2. Perform the author's unwrap operation on each value.
3. Verify that unwrapped values can be passed via SetAttribute().
4. Verify that the content of the passed value matches the original secret value content.
5. Confirm: Flag clearing is a sufficient condition for converting a secret value to a normal value.
3.4 Working Mechanism of the scrub() Function
With an understanding of the secret flag byte, the operation of the scrub() function becomes clear. The pseudocode of this function is roughly as follows:
// Pseudocode implementation of the scrub() function (author's inference)
static int l_scrub(lua_State *L) {
int nargs = lua_gettop(L);
for (int i = 1; i <= nargs; i++) {
// Locate stack slot
uint8_t* slot = (uint8_t*)(L->base) + i * 24;
// Check secret flag byte
if (slot[0x09] != 0) {
// Replace secret value with nil
lua_pushnil(L);
lua_replace(L, i);
}
}
return nargs;
}
This is why scrub() is used in SecureGroupHeaders.lua when copying attributes from a header frame to a child frame:
newChild:SetAttribute(name, scrub(header:GetAttribute("_initialAttribute-" .. name)));
scrub() ensures that even if a header frame's attribute stores a secret value, that value will not be passed to the child frame.
In SecureHoverDriver.lua, the use of scrub is even more frequent and direct:
local function GetScreenFrameRect(frame)
local es = scrub(LOCAL_CHECK_Frame.GetEffectiveScale(frame));
local l, b, w, h = scrub(LOCAL_CHECK_Frame.GetRect(frame));
if (not (l and b and es)) then return 0, 0, 0, 0; end
return l * es, (l + w) * es, b * es, (b + h) * es;
end
Here, scrub is used to cleanse frame geometry attributes (scale, position, size). The author notes that this implies Blizzard has also included frame geometric information within the scope of secret value protection—this discovery reveals the breadth of the protection mechanism extends far beyond aura data. If the author's method can successfully unwrap these values, it would theoretically be possible to obtain the precise screen position information of any frame, which holds significant importance in certain automation scenarios.
3.5 Sources of Secret Value Generation
Through auditing Blizzard's secure framework code, the author has confirmed that the following APIs are the primary sources of secret values:
-
C_UnitAuras.GetUnitAuras()— Certain fields in the returned aura data are marked as secret. Evidence can be seen inSecureGroupHeaders.lua:
-- Manually counting because indexed iteration over the next table produces secrets,
-- which explode when fed into SetAttribute.
local index = 1;
for _, auraData in ipairs(C_UnitAuras.GetUnitAuras(unit, fullFilter, maxAuraCount)) do
local aura, _, duration = freshTable();
aura.name = auraData.name;
duration = auraData.duration;
aura.expires = auraData.expirationTime;
aura.caster = auraData.caster;
aura.filter = fullFilter;
aura.index = index;
aura.shouldConsolidate = false;
-- ...
end
From this code, it's confirmed that fields like name, duration, expirationTime, caster in auraData may carry the secret flag. Notably, aura.shouldConsolidate is hardcoded as false with the comment -- Deprecated. Does this mean everything around consolidateTable should be removed...?—this hints at the secure framework being in continuous evolution, and rapidly changing codebases are more prone to introducing security oversights.
-
GetRaidRosterInfo()— Certain fields in the returned raid member information. In theGetGroupRosterInfofunction inSecureGroupHeaders.lua:
local function GetGroupRosterInfo(kind, index)
local _, unit, name, subgroup, className, role, server, assignedRole;
if ( kind == "RAID" ) then
unit = "raid"..index;
name, _, subgroup, _, _, className, _, _, _, role, _, assignedRole = GetRaidRosterInfo(index);
else
-- ...
end
return unit, name, subgroup, className, role, assignedRole;
end
Return values like name, className, role, assignedRole can be secret values.
- Unit query functions like
UnitName(),UnitClass()— May return secret values under certain conditions. -
GetWeaponEnchantInfo()— Weapon enchant information. Seen inSecureAuraHeader_OnUpdate:
function SecureAuraHeader_OnUpdate(self)
local hasMainHandEnchant, _, _, _, hasOffHandEnchant = GetWeaponEnchantInfo();
if ( hasMainHandEnchant ~= self:GetAttribute("_mainEnchanted") ) then
self:SetAttribute("_mainEnchanted", hasMainHandEnchant);
end
if ( hasOffHandEnchant ~= self:GetAttribute("_secondaryEnchanted") ) then
self:SetAttribute("_secondaryEnchanted", hasOffHandEnchant);
end
end
- Frame Geometry APIs — Such as
GetEffectiveScale(),GetRect(), etc., confirmed by the use ofscrubinSecureHoverDriver.lua.
3.6 The Flow Path of Secret Values Within the Secure Framework
Through cross-auditing the four secure modules, the author has mapped the flow path of secret values within the secure framework:
[Secure API Call]
|
| (Return value's stack slot +0x09 byte set to 0x01)
v
[Secret Value on Lua Stack]
|
+───> [scrub()] ──→ Checks slot[0x09] ─→ if ≠ 0 → [nil]
| if = 0 → [Original value passed through]
|
+───> [CallRestrictedClosure()] ──→ [Inside Restricted Closure]
| |
| | (Inside closure, value exists on stack, slot[0x09] = 0x01)
| |
| +───> [Return Value] ──→ [scrub()] ──→ [nil]
| |
| +───> [SetAttribute()] ──→ Checks slot[0x09] ──→ if ≠ 0 → [ERROR]
| | if = 0 → [Set Successfully]
| |
| +───> [Author's unwrap operation]
| |
| | (Clears slot[0x09] via engine-internal value copy path)
| v
| [slot[0x09] = 0x00]
| |
| +───> [SetAttribute()] ──→ Check Passes ──→ [Set Successfully]
|
+───> [Direct Use] (e.g., table.sort comparison, conditional judgment)
|
| (Secret values can participate in calculations but not leak)
v
[Internal Use Result]
Key Observation: **Secret values can be manipulated inside CallRestrictedClosure(). The security checks by scrub() and SetAttribute() are only executed at specific boundaries, and both merely check the single byte at offset +0x09.** If this byte can be zeroed inside the closure, then when the value later flows through scrub() or SetAttribute(), it will no longer be recognized as a secret value.
3.7 Stack Layout Analysis of CallRestrictedClosure
CallRestrictedClosure is a critical node through which secret values flow. Based on the author's analysis, when this function is called, the Lua stack layout is roughly as follows:
Call: CallRestrictedClosure(frame, signature, env, handle, body, arg1, arg2, ...)
Physical Memory Layout of Lua Stack:
┌─────────────┬────────────────────────┬──────────────┐
│ Stack Index │ Slot Start Address │ Content │
├─────────────┼────────────────────────┼──────────────┤
│ slot 1 │ base + 0 * 24 = base │ frame │
│ │ +0x00: Value data │ │
│ │ +0x08: Type tag │ │
│ │ +0x09: Secret flag=0 │ │
│ │ +0x0A~0x17: Metadata │ │
├─────────────┼────────────────────────┼──────────────┤
│ slot 2 │ base + 1 * 24 │ signature │
│ │ +0x09: Secret flag=0 │ │
├─────────────┼────────────────────────┼──────────────┤
│ slot 3 │ base + 2 * 24 │ env │
│ │ +0x09: Secret flag=0 │ │
├─────────────┼────────────────────────┼──────────────┤
│ slot 4 │ base + 3 * 24 │ handle │
│ │ +0x09: Secret flag=0 │ │
├─────────────┼────────────────────────┼──────────────┤
│ slot 5 │ base + 4 * 24 │ body │
│ │ +0x09: Secret flag=0 │ │
├─────────────┼────────────────────────┼──────────────┤
│ slot 6 │ base + 5 * 24 │ arg1 │
│ │ +0x09: Secret flag=? │ ← Potentially secret │
├─────────────┼────────────────────────┼──────────────┤
│ slot 7 │ base + 6 * 24 │ arg2 │
│ │ +0x09: Secret flag=? │ ← Potentially secret │
├─────────────┼────────────────────────┼──────────────┤
│ ... │ ... │ ... │
└─────────────┴────────────────────────┴──────────────┘
From this layout, it is clear: The secret status of each value is entirely determined by the single byte at offset +0x09 within its stack slot. Clearing that byte = clearing secret protection. There is no redundant verification, no cryptographic protection, no integrity hash—just a bare flag byte.
3.8 The Check Timing of scrub and SetAttribute — A TOCTOU Characteristic
This is key to understanding the bypass method. The secret value check occurs at two moments:
- When
scrub()is called: An explicit check onslot[0x09]. If non-zero, replaces the value withnil. - When
SetAttribute()is called: An implicit check onslot[0x09]of the passed value. If non-zero, throws an error.
However, both checks share a common premise—they check the state of the secret flag byte at that moment. If this byte has already been cleared (set to 0x00) before the check, then these checks will consider the value normal, allowing the operation to proceed.
In other words, secret value protection is a Point-in-Time Check, not Provenance Tracking. The system does not record whether a value was ever secret—it only checks the current slot[0x09] byte. This is a variant of a classic TOCTOU (Time-of-Check to Time-of-Use) vulnerability.
The author considers this the most fundamental architectural weakness of the current secret value protection mechanism. Protection based on a flag byte is inherently vulnerable to flag-clearing attacks, whereas protection based on provenance tracking (e.g., Information Flow Control) is not susceptible to such attacks because the "secret" property of a value derives from its creation path, not a mutable flag.
3.9 Summary
This chapter has conducted an in-depth analysis of the memory-level implementation of secret values. The author's key discoveries are as follows:
- Stack slot size is confirmed as 24 bytes (0x18) — Verified via contiguous value address difference analysis and argument count tests.
- Secret flag byte is located at offset +0x09 in each stack slot — Confirmed via binary search differential analysis and cross-type validation.
- Flag byte semantics: 0x00 = normal, non-zero = secret — Clearing this byte completes the unwrap.
- Security checks in
scrub()andSetAttribute()rely solely on this single byte — No redundant verification exists. - Flag checking is a point-in-time check; provenance tracking is absent — Classic TOCTOU weakness.
- At the C level, clearing the secret flag requires only
slot[0x09] = 0— Extremely low bypass cost. - During the execution of
CallRestrictedClosure, secret values exist on the stack in their raw form — The flag byte can be affected.
These discoveries directly point to the bypass method: During the execution of a restricted closure, by triggering a specific engine-internal value copy path, cause the +0x09 byte of the target value's stack slot to be zeroed.
Chapter 4: Analysis of Security Modules One by One
This chapter will conduct an in-depth analysis of the four core security modules audited by the author, identifying the design decisions related to secret value protection, potential weaknesses, and exploitable execution paths within each.
⚠️ Source Code Citation Legality Notice (Source Code Citation Legality Notice)
The source code snippets from Blizzard Entertainment's security framework (SecureGroupHeaders.lua, SecureHandlers.lua, SecureHoverDriver.lua, SecureStateDriver.lua) cited in this chapter are reasonable citations for the purpose of academic analysis.
1. Basis in US Law: According to Section 107 of the US Copyright Act (Fair Use), citations for the purpose of criticism, comment, scholarship, and research constitute fair use. The citations in this article satisfy the four-factor fair use test: (a) the purpose of use is non-commercial academic research; (b) the nature of the cited work is functional code; (c) the amount cited is reasonable and limited relative to the entire work; (d) the citation will not adversely affect the market value of the original work.
2. Basis in Chinese Law: According to Article 24, Item 1 of the Copyright Law of the People's Republic of China, it is permissible to use a published work without permission from, or payment to, the copyright holder for purposes of personal study, research, or appreciation, provided that the name of the author, the title of the work are indicated, and the normal use of the work is not affected, nor are the legitimate rights and interests of the copyright holder unreasonably prejudiced.
3. The citations of source code in this article are limited to the minimum scope necessary for security analysis, and the source has been cited. The act of citation does not constitute infringement of Blizzard Entertainment's intellectual property rights.
4.1 SecureGroupHeaders — Secure Group Header Framework
4.1.1 Module Function Overview
SecureGroupHeaders.lua implements the WoW Secure Group Header system, including SecureGroupHeader (for party/raid members), SecureGroupPetHeader (for pets), and SecureAuraHeader (for auras/buffs).
The module's core functions are:
- Automatically creating and arranging unit buttons based on party/raid status.
- Supporting filtering and sorting based on Group, Class, Role, etc.
- Managing aura display, including temporary weapon enchants and consolidated display.
The module supports an extremely rich set of configuration attributes, as listed in the source code comments:
--[[
showRaid = [BOOLEAN] -- true if the header should be shown while in a raid
showParty = [BOOLEAN] -- true if the header should be shown while in a party and not in a raid
showPlayer = [BOOLEAN] -- true if the header should show the player when not in a raid
showSolo = [BOOLEAN] -- true if the header should be shown while not in a group (implies showPlayer)
nameList = [STRING] -- a comma separated list of player names (not used if 'groupFilter' is set)
groupFilter = [1-8, STRING] -- a comma seperated list of raid group numbers and/or uppercase class names and/or uppercase roles
roleFilter = [STRING] -- a comma seperated list of MT/MA/Tank/Healer/DPS role strings
strictFiltering = [BOOLEAN]
template = [STRING] -- the XML template to use for the unit buttons
templateType = [STRING] - specifies the frame type of the managed subframes (Default: "Button")
--]]
All these configuration attributes are read/written via SetAttribute/GetAttribute—meaning they are all subject to secret value protection constraints.
4.1.2 Generation and Transmission of Secret Values
The main point where secret values are generated in this module is in the SecureAuraHeader_Update function. The author traced the flow of secret values in detail during the audit:
function SecureAuraHeader_Update(self)
-- ... Omitted filtering and sorting configuration code ...
for filterIndex, fullFilter in ipairs(groupingTable) do
-- ...
-- Manually counting because indexed iteration over the next
-- table produces secrets, which explode when fed into
-- SetAttribute.
local index = 1;
for _, auraData in ipairs(
C_UnitAuras.GetUnitAuras(unit, fullFilter, maxAuraCount)
) do
local aura, _, duration = freshTable();
aura.name = auraData.name;
duration = auraData.duration;
aura.expires = auraData.expirationTime;
aura.caster = auraData.caster;
aura.filter = fullFilter;
aura.index = index;
aura.shouldConsolidate = false;
-- ...
end
end
-- ...
end
The Blizzard developer's wording in the comment, "produces secrets, which explode when fed into SetAttribute", is the most direct internal documentation of the secret value mechanism. The word "explode" vividly describes the runtime error throwing behavior when a secret value is passed to SetAttribute—from the author's memory analysis perspective, this "explosion" is precisely because SetAttribute's C++ layer check finds slot[0x09] != 0.
Fields in the auraData returned by C_UnitAuras.GetUnitAuras() may carry the secret flag. When these values are assigned to fields of the aura table, the secret flag is preserved—because the Lua table assignment copies the entire stack slot content, including the secret flag byte at +0x09.
However, in the subsequent configureAuras function, these values are set as button attributes:
local function configureAuras(self, auraTable,
consolidateTable, weaponPosition)
-- ...
for i=1, #auraTable do
-- ...
local buffInfo = auraTable[i];
button:SetID(buffInfo.index);
button:SetAttribute("index", buffInfo.index);
button:SetAttribute("filter", buffInfo.filter);
buttons[i] = button;
end
-- ...
end
Note that only index and filter are set here—and these two values are not from the raw return of C_UnitAuras.GetUnitAuras(). The index is a locally computed counter, and filter is the passed filter string. This shows Blizzard's developers are aware that secret values cannot be passed via SetAttribute and therefore deliberately avoid calling SetAttribute on fields that may contain secret values.
This also reveals a design trade-off: to avoid triggering secret value protection errors, secure code actively abandons transmitting certain data (e.g., aura.name, aura.duration, aura.expires, aura.caster) as attributes. This precisely proves the limitation imposed by secret value protection on normal functionality and implies that bypassing this restriction would grant richer data access capabilities—this was the motivation for the author to write the _unwrapAuraData function in the core engine of SecretValueUnwrapper.lua.
4.1.3 Execution Path in SetupUnitButtonConfiguration
local function SetupUnitButtonConfiguration(
header, newChild, defaultConfigFunction
)
local configCode = header:GetAttribute("initialConfigFunction")
or defaultConfigFunction;
if ( type(configCode) == "string" ) then
local selfHandle = GetFrameHandle(newChild);
if ( selfHandle ) then
CallRestrictedClosure(
newChild, "self",
GetManagedEnvironment(header, true),
selfHandle, configCode, selfHandle
);
end
end
local copyAttributes =
header:GetAttribute("_initialAttributeNames");
if ( type(copyAttributes) == "string" ) then
for name in copyAttributes:gmatch("[^,]+") do
newChild:SetAttribute(
name,
scrub(header:GetAttribute(
"_initialAttribute-" .. name
))
);
end
end
end
This function demonstrates two key security operations:
CallRestrictedClosurecall: Executes theinitialConfigFunctioncode snippet. HereconfigCodeis a plugin-controllable string (set viaSetAttribute("initialConfigFunction", ...)). Although the code executes in a restricted closure, this means plugins can inject code to run within the managed environment.
- Explicit
scrub()call: When copying attributes,scrub()is used to clean possible secret values (checks each value'sslot[0x09], replacing with nil if non-zero). This confirms again that attribute transmission is an enforcement point for secret value protection.
The author notes that during the execution of CallRestrictedClosure, the managed environment comes from GetManagedEnvironment(header, true). If the author can control the content of the header's managed environment—for example, by pre-placing helper functions in the environment—additional capabilities can be gained during the execution of the restricted closure. This is exactly the strategy implemented in SecretValueUnwrapper.lua: through multi-stage SecureHandlerExecute calls, core functions like unwrapStorage, performUnwrap, batchUnwrap, unwrapAuraData are successively injected into the managed environment, establishing a complete unwrap infrastructure.
The same pattern appears in SetupAuraButtonConfiguration:
local function SetupAuraButtonConfiguration(
header, newChild, defaultConfigFunction
)
local configCode =
newChild:GetAttribute("initialConfigFunction")
or header:GetAttribute("initialConfigFunction")
or defaultConfigFunction;
if ( type(configCode) == "string" ) then
local selfHandle = GetFrameHandle(newChild);
if ( selfHandle ) then
CallRestrictedClosure(
newChild, "self",
GetManagedEnvironment(header, true),
selfHandle, configCode, selfHandle
);
end
end
end
Note the additional layer of priority here: the newChild's own initialConfigFunction takes precedence over the header's. This provides finer-grained control—an attacker can inject configuration code at the child frame level.
4.1.4 refreshUnitChange Execution Path in configureChildren
local function configureChildren(self, unitTable)
-- ... Omitted layout calculation code ...
for i = loopStart, loopFinish, step do
-- ...
local unitButton = self:GetAttribute("child"..buttonNum);
-- ...
unitButton:SetAttribute("unit", unitTable[i]);
local configCode =
unitButton:GetAttribute("refreshUnitChange");
if ( type(configCode) == "string" ) then
local selfHandle = GetFrameHandle(unitButton);
if ( selfHandle ) then
CallRestrictedClosure(
unitButton, "self",
GetManagedEnvironment(unitButton, true),
selfHandle, configCode, selfHandle
);
end
end
if not unitButton:GetAttribute("statehidden") then
unitButton:Show();
end
currentAnchor = unitButton;
end
-- ...
end
The refreshUnitChange attribute here provides another entry point for restricted closure execution. Whenever a unit button is reassigned a unit, if the button has a refreshUnitChange attribute, the corresponding code snippet executes in a restricted closure.
Key observation: GetManagedEnvironment(unitButton, true) — the environment here is from unitButton, not self (the header frame). This means if an attacker can control a specific unitButton's managed environment, they can gain access to that environment during that button's refreshUnitChange execution.
In SecretValueUnwrapper.lua, the UnwrapViaAlternatePath method exploits this execution path as an alternative unwrap trigger channel.
4.1.5 Secret Value Handling in Sorting and Filtering
The sorting and filtering logic in the SecureGroupHeader_Update function heavily manipulates data from secure APIs:
for i = start, stop, 1 do
local unit, name, subgroup, className, role, assignedRole =
GetGroupRosterInfo(kind, i);
if ( name and
((not strictFiltering) and
( tokenTable[subgroup]
or tokenTable[className]
or (role and tokenTable[role])
or tokenTable[assignedRole] )
) or
( tokenTable[subgroup]
and tokenTable[className]
and ((role and tokenTable[role])
or tokenTable[assignedRole]) )
) then
tinsert(sortingTable, unit);
sortingTable[unit] = name;
-- ...
end
end
Notably, sortingTable[unit] = name stores the (potentially secret) name into the sorting table, which is later used by sorting functions like sortOnNames, sortOnGroupWithNames:
local function sortOnNames(a, b)
return sortingTable[a] < sortingTable[b];
end
Secret values can participate in comparison operations (like <), they just cannot be passed out to insecure contexts. This means sorting functionality works fine, but secret values in the sorted data still cannot be read externally—unless the secret flag is cleared.
4.1.6 freshTable / releaseTable Table Pool Mechanism
SecureGroupHeaders.lua implements a table pool mechanism:
local freshTable;
local releaseTable;
do
local tableReserve = {};
freshTable = function ()
local t = next(tableReserve) or {};
tableReserve[t] = nil;
return t;
end
releaseTable = function (t)
tableReserve[t] = wipe(t);
end
end
This pooling mechanism exists to reduce garbage collection pressure. But from a security perspective, it introduces a subtle issue: releaseTable calls wipe(t) to clear the table—but the wipe operation sets table slots to nil. If secret values from the table have already been copied elsewhere (like sortingTable), those copies are unaffected. The recycling use of table pools also means memory that once stored secret values may be reused for storing non-secret data—this memory reuse pattern is noteworthy in security analysis.
4.2 SecureHandlers — Secure Handler Framework
4.2.1 Module Function Overview
SecureHandlers.lua is the most complex and core module of the entire security framework. It implements:
- Secure code snippet execution mechanisms.
- Script Handler wrapping (Wrap) and unwrapping (Unwrap) system.
- Secure handling of Drag & Drop operations.
- External API (
SecureHandlerWrapScript,SecureHandlerUnwrapScript,SecureHandlerExecute,SecureHandlerSetFrameRef).
4.2.2 In-Depth Analysis of Wrap/Unwrap Mechanism
The author discovered during the audit that the wrapping system is the core exploitation target of the bypass method. The wrapping system allows a secure "header frame" to execute code snippets before and after a target frame's script handler.
Creating a Wrap (Wrapping):
local function CreateWrapClosure(handler, header, preBody, postBody)
local wrap;
if (postBody) then
wrap = function(self, ...)
if (self == MAGIC_UNWRAP) then
return header, preBody, postBody;
end
return handler(
self, header, preBody, postBody, wrap, ...
);
end
else
wrap = function(self, ...)
if (self == MAGIC_UNWRAP) then
return header, preBody, nil;
end
return handler(
self, header, preBody, nil, wrap, ...
);
end
end
return wrap;
end
Security Significance of MAGIC_UNWRAP:
local MAGIC_UNWRAP = newproxy();
newproxy() creates a unique userdata object that cannot be copied or forged by external code. This ensures only code with knowledge of the MAGICUNWRAP reference can trigger script unwrapping. Clarification: The "unwrap" here refers to restoring the wrapped original script handler, which is completely different from the author's "unwrap" of the secret flag byte in this paper; the terminology overlap is coincidental. The author's bypass method does not require access to MAGICUNWRAP.
Weak Reference Table LOCALWrappedHandlers:
local LOCAL_Wrapped_Handlers = {};
setmetatable(LOCAL_Wrapped_Handlers, { __mode = "k"; });
Wrapped handlers are stored in a weak-key table. This means if the wrap closure is garbage collected, the corresponding handler reference is automatically cleaned up. In the author's proof-of-concept code, ensuring the wrap is not garbage collected is done by applying it to a persistently existing frame (like probeFrame).
4.2.3 Execution Flow of Wrapped_Click Handler
local function Wrapped_Click(
self, header, preBody, postBody, wrap,
button, down, ...
)
local message, newbutton;
if ( IsWrapEligible(self) ) then
newbutton, message =
SecureHandler_Other_Execute(
header, self,
"self,button,down", preBody,
button, down
);
if (newbutton == false) then
return;
end
if (newbutton) then
button = tostring(newbutton);
end
end
securecall(SafeCallWrappedHandler,
self, wrap, button, down, ...);
if (postBody and message ~= nil) then
SecureHandler_Other_Execute(
header, self,
"self,message,button,down",
postBody,
message, button, down
);
end;
end
This handler's execution flow reveals an important pattern:
- Pre-Body Execution:
SecureHandlerOtherExecuteexecutes thepreBodycode snippet in theheader's managed environment. The return valuesnewbuttonandmessagecan influence subsequent behavior. - Original Handler Call: Calls the wrapped original handler via
securecall(SafeCallWrappedHandler, ...). - Post-Body Execution: If
postBodyexists andmessageis notnil, executes thepostBodycode snippet.
Key discovery: Execution of preBody and postBody occurs through SecureHandlerOtherExecute, which ultimately calls CallRestrictedClosure. During the execution of these restricted closures, the passed parameters (like button, down, message) exist on the Lua stack—each occupying a 24-byte stack slot, with the secret flag byte at their respective +0x09 offset. If any of these parameters are secret values, they exist in their original (flag-included) form during closure execution.
The author's SetupOnClickWrap function in SecretValueUnwrapper.lua exploits precisely this execution path of Wrapped_Click.
4.2.4 Wrapped_Drag Handler—Most Complex Execution Path and Missing scrub()
local function Wrapped_Drag(
self, header, preBody, postBody, wrap, ...
)
local message;
if ( IsWrapEligible(self) ) then
local selfHandle = GetFrameHandle(self, true);
if (selfHandle) then
local environment =
GetManagedEnvironment(header, true);
local controlHandle =
GetFrameHandle(header, true, true);
local button = ...;
local pickupType, target, x1, x2, x3 =
CallRestrictedClosure(
self,
"self,button,kind,value,...",
environment,
controlHandle, preBody,
selfHandle, button,
GetCursorInfo()
);
if (pickupType == false) then
return;
elseif (pickupType == "message") then
message = target;
elseif (pickupType) then
PickupAny(pickupType, target, x1, x2, x3);
return;
end
end
end
securecall(SafeCallWrappedHandler, self, wrap, ...);
if ( postBody and message ~= nil ) then
SecureHandler_Other_Execute(
header, self,
"self,message,button",
postBody, message, ...
);
end
end
The author discovered during the audit that Wrapped_Drag exhibits the most complex execution path and also contains what the author believes is an issue meriting special attention from Blizzard's security team:
Note the return value of GetCursorInfo() is passed directly into CallRestrictedClosure as a parameter. GetCursorInfo() may return data containing secret values. More critically, the return values of CallRestrictedClosure (pickupType, target, x1, x2, x3) are directly passed by Blizzard's secure code to PickupAny(), without scrub() processing.
This means if the author performs an unwrap (clears the secret flag byte slot[0x09]) on a value inside the restricted closure and then returns it, the returned value would be passed to PickupAny() as a "normal value". This is a code path missing a scrub() call; the author recommends the Blizzard security team review whether scrub() processing should be added here.
4.2.5 API Layer—SecureHandlerExecute and SecureHandlerWrapScript
Complete implementation of SecureHandlerExecute:
function SecureHandlerExecute(frame, body)
if (not IsValidFrame(frame)) then
error("Invalid header frame");
return;
end
if (CheckForbidden(frame)) then
error("Cannot use SecureHandlers API on forbidden frames");
return;
end
if (not select(2, frame:IsProtected())) then
error("Header frame must be explicitly protected");
return;
end
if (type(body) ~= "string") then
error("Invalid body");
return;
end
LOCAL_API_Frame:SetAttribute("_apiframe", frame);
LOCAL_API_Frame:SetAttribute("_execute", body);
end
SecureHandlerExecute allows external code to execute a code snippet within the context of a protected frame. Key constraints are:
- The frame must be valid (
IsValidFrame). - The frame must not be forbidden (
CheckForbidden). - The frame must be explicitly protected (
IsProtected). - The code must be a string.
However, the content of the code is unrestricted—it can be arbitrary Lua code snippets. Of course, this code executes within a restricted closure and can only call functions available in the restricted environment. But this is still an important entry point—it allows plugins to execute custom logic in a secure context, including setting variables and functions in the managed environment.
In the InjectManagedEnvironmentLogic function of SecretValueUnwrapper.lua, the author builds the unwrap infrastructure in the managed environment step by step through four phases of SecureHandlerExecute calls. The managed environment persists across multiple SecureHandlerExecute calls—variables set in one Execute remain available in the next. The author exploits this persistence characteristic to build the complete unwrap infrastructure in stages.
SecureHandlerWrapScript provides even more powerful capability—it allows plugins to register code snippets that execute on every script trigger. The list of supported events is explicitly defined in the LOCALWrapHandlers table:
local LOCAL_Wrap_Handlers = {
OnClick = Wrapped_Click;
OnDoubleClick = Wrapped_Click;
PreClick = Wrapped_Click;
PostClick = Wrapped_Click;
OnEnter = Wrapped_OnEnter;
OnLeave = Wrapped_OnLeave;
OnShow = Wrapped_ShowHide;
OnHide = Wrapped_ShowHide;
OnDragStart = Wrapped_Drag;
OnReceiveDrag = Wrapped_Drag;
OnMouseWheel = Wrapped_MouseWheel;
OnAttributeChanged = Wrapped_Attribute;
};
12 script events can be wrapped. The author's proof-of-concept code selects the most appropriate event for wrapping based on different usage scenarios.
4.2.6 Trust Chain of PickupAny
local function PickupAny(kind, target, detail, ...)
if (kind == "clear") then
ClearCursor();
kind, target, detail = target, detail, ...;
end
if kind == 'action' then
PickupAction(target);
elseif kind == 'bag' then
PickupBagFromSlot(target)
-- ... More branches ...
end
end
The parameters of PickupAny come directly from the return values of CallRestrictedClosure—without scrub() processing. The existence of this channel proves: not all values returned from restricted closures are scrubbed—secure code judges which return values are "trusted" based on context. The author's method precisely exploits this "implied trust"—if a value's secret flag has been cleared (slot[0x09] = 0), secure code implicitly treats it as trusted.
4.2.7 Method Injection via SecureHandler_OnLoad
function SecureHandler_OnLoad(self)
self.Execute = SecureHandlerMethod_Execute;
self.WrapScript = SecureHandlerMethod_WrapScript;
self.UnwrapScript = SecureHandlerMethod_UnwrapScript;
self.SetFrameRef = SecureHandlerMethod_SetFrameRef;
end
When a frame uses SecureHandlerBaseTemplate, SecureHandler_OnLoad injects four convenience methods onto the frame. This allows the author's proof-of-concept code to directly call Execute, WrapScript, and other methods on the frame object.
4.2.8 Attribute Setting Timing in Wrapped_OnEnter
The author observed a timing detail in Wrapped_OnEnter during the audit:
local function Wrapped_OnEnter(
self, header, preBody, postBody, wrap,
motion, ...
)
local allow, message;
if ( motion ) then
self:SetAttribute("_wrapentered", true);
if ( IsWrapEligible(self) ) then
allow, message =
SecureHandler_Other_Execute(
header, self, "self", preBody
);
end
-- ...
end
-- ...
end
self:SetAttribute("_wrapentered", true) executes before the security check (IsWrapEligible). Although this specific attribute doesn't involve secret values, it reveals a pattern—certain operations are executed before security checks.
4.3 SecureHoverDriver — Secure Hover Driver
4.3.1 Module Function Overview
SecureHoverDriver.lua implements functionality for automatically hiding frames—when the mouse leaves a frame for a certain duration, the frame is automatically hidden. This module is primarily used for UI elements like Tooltips and pop-up menus.
4.3.2 Use of scrub Function and Frame Geometry Protection
The module explicitly references the scrub function at the beginning:
local scrub = scrub;
And uses it in the GetScreenFrameRect function:
local function GetScreenFrameRect(frame)
local es = scrub(
LOCAL_CHECK_Frame.GetEffectiveScale(frame)
);
local l, b, w, h = scrub(
LOCAL_CHECK_Frame.GetRect(frame)
);
if (not (l and b and es)) then
return 0, 0, 0, 0;
end
return l * es, (l + w) * es, b * es, (b + h) * es;
end
This indicates that even frame geometric properties (position, size, scale) can be marked as secret values (slot[0x09] != 0). scrub() is used here to "safely" obtain the frame's screen rectangle—if any value is secret, scrub() replaces it with nil, causing the if (not (l and b and es)) condition to be true, and the function returns the default (0, 0, 0, 0).
This is a graceful degradation strategy: rather than crashing due to a secret value, return a safe default. But it also means if an attacker can clear the secret flag byte (slot[0x09] = 0) of these values, GetScreenFrameRect will return accurate position information instead of (0, 0, 0, 0).
The author notes this indicates Blizzard includes frame geometry information within the scope of secret value protection—this discovery reveals the breadth of the protection mechanism far exceeds aura data. If the author's method can successfully unwrap these values, theoretically precise screen position information of any frame could be obtained, which is significant in some automation scenarios.
4.3.3 Secure Design of LOCALCHECKFrame
local LOCAL_CHECK_Frame =
CopyTable(GetFrameMetatable().__index);
This line creates a copy of the frame method table. This is a defensive measure—even if malicious code modifies a frame's metatable, SecureHoverDriver still uses the original methods. Calling methods via LOCALCHECKFrame.GetEffectiveScale(frame) instead of frame:GetEffectiveScale() avoids the risk of metatable tampering. This defensive pattern aligns with the local reference strategy in SecureHandlers.lua.
4.3.4 State Machine of the RectSet (Rectangle Set) System
SecureHoverDriver.lua implements a sophisticated rectangle set system to track mouse position. The connection between hide operations and the statehidden attribute:
LOCAL_PendingHides[frame] = true;
-- ...
frame:Hide();
frame:SetAttribute("statehidden", true);
The statehidden attribute appears in multiple security modules, used to mark whether a frame has been actively hidden by a secure framework.
4.4 SecureStateDriver — Secure State Driver
4.4.1 Module Function Overview
SecureStateDriver.lua implements two key functions:
- Attribute Driver: Automatically sets frame attributes based on Macro Options.
- Unit Existence Watch: Monitors whether units exist and shows/hides frames accordingly.
4.4.2 resolveDriver Function and State Propagation
local function resolveDriver(frame, attribute, values)
local newValue = SecureCmdOptionParse(values);
if ( attribute == "state-visibility" ) then
if ( newValue == "show" ) then
frame:Show();
frame:SetAttribute("statehidden", nil);
elseif ( newValue == "hide" ) then
frame:Hide();
frame:SetAttribute("statehidden", true);
end
elseif ( newValue ) then
if ( newValue == 'nil' ) then
newValue = nil;
else
newValue = tonumber(newValue) or newValue;
end
local oldValue = frame:GetAttribute(attribute);
if ( newValue ~= oldValue ) then
frame:SetAttribute(attribute, newValue);
end
end
end
Key observation: The frame:SetAttribute(attribute, newValue) call in resolveDriver will trigger SecureHandlerStateOnAttributeChanged (if the attribute name starts with state-), which then executes onstate-* code snippets. This forms an indirect path from state driver to restricted closure execution, which the author considered as an additional unwrap trigger point in the design.
4.4.3 OnUpdate Throttling Mechanism
local STATE_DRIVER_UPDATE_THROTTLE = 0.2;
local timer = 0;
local function SecureStateDriverManager_OnUpdate(self, elapsed)
timer = timer - elapsed;
if ( timer <= 0 ) then
timer = STATE_DRIVER_UPDATE_THROTTLE;
-- ... State update logic ...
end
end
State drivers use a 0.2-second throttle to reduce update frequency. From a security perspective, this throttle creates a time window.
The author also noted a redundant operation in the code:
wipe(unitExistsCache);
for k in pairs(unitExistsCache) do
unitExistsCache[k] = nil;
end
The for k in pairs(unitExistsCache) do unitExistsCache[k] = nil; end following wipe(unitExistsCache) is redundant—wipe already emptied the table, so the subsequent traversal finds no keys. This is likely legacy code; while it doesn't affect functional correctness, it's worth recording as an audit observation—a rapidly evolving codebase with redundancies is more prone to security oversights.
4.4.4 Event-Driven Immediate Scan
local function SecureStateDriverManager_OnEvent(self, event)
timer = 0;
end
When specific events occur, the timer is reset to 0, triggering an immediate scan next frame. The list of registered events includes:
SecureStateDriverManager:RegisterEvent(
"MODIFIER_STATE_CHANGED"
);
SecureStateDriverManager:RegisterEvent(
"ACTIONBAR_PAGE_CHANGED"
);
-- ... etc.
-- Deliberately ignoring mouseover and others' target changes
-- because they change so much
The comment -- Deliberately ignoring mouseover and others' target changes because they change so much indicates Blizzard intentionally ignores high-frequency changing states.
4.4.5 updatetime Attribute
elseif ( name == "updatetime" ) then
STATE_DRIVER_UPDATE_THROTTLE = value;
end
Notably, the updatetime attribute allows modifying the throttle interval. Although this attribute is unlikely to be touched by plugins in normal use (because it requires a secure execution context to set the SecureStateDriverManager's attribute), if reachable, it could modify the frequency of state updates.
4.5 Cross-Module Analysis: Structural Weaknesses of the Security Framework
Through the individual audit of the four modules, the author has identified the following cross-module structural weaknesses. The author earnestly requests the Blizzard security team evaluate each of the following:
- Restricted Closure as a Shared Execution Boundary: All four modules ultimately execute restricted code via
CallRestrictedClosure. This means any exploitation ofCallRestrictedClosure's internal behavior (especially stack slot value copying behavior) would affect the entire security framework.
- Inconsistent Secret Value Checking:
scrub()is only explicitly used in some code paths (like attribute copying inSecureGroupHeaders.luaand geometry queries inSecureHoverDriver.lua), while in other paths (like thePickupAnycall inWrapped_Drag) it is not seen. This inconsistency may lead to omissions.
- Writability of Managed Environment: The second parameter
trueinGetManagedEnvironment(frame, true)indicates a writable environment. Attackers can store auxiliary data or function references in the managed environment for use by subsequent restricted closure executions.
- Dual Role of the Attribute System: The attribute system (
SetAttribute/GetAttribute) is both a channel for secure communication and an enforcement point for secret value protection. Secret value checking only checks the single byteslot[0x09]—bypassing this byte grants both secure communication and data leakage capabilities simultaneously.
- Existence of Time Windows: The 200-millisecond throttle in
SecureStateDriver, event-driven immediate scans, andSecureHoverDriver'sOnUpdate-driven logic all create time windows.
- Arbitrary Code Injection via
initialConfigFunctionandrefreshUnitChange: These two attributes allow plugins to inject arbitrary Lua code strings that will execute within restricted closures, a key entry point for gaining code execution capability in a secure context.
- Missing
scrub()Call inWrapped_Drag: Return values fromCallRestrictedClosureare directly passed toPickupAny()withoutscrub()processing, inconsistent with handling in other code paths.
- Overly Simple Implementation of the Secret Flag: Relies solely on a single byte at offset +0x09 within a stack slot, with no redundancy checks, no integrity protection, and no origin tracking. When a security mechanism can be completely bypassed by an operation equivalent to
slot[0x09] = 0, the robustness of that mechanism is insufficient.
Chapter 5: Bypass Methodology – Clearing the Secret Flag Byte from the Lua Layer
⚠️ Technical Disclosure Responsibility Notice
This chapter contains detailed descriptions of methodologies for exploiting security vulnerabilities. The author states the following:
1. The methodologies described in this chapter were submitted to Blizzard Entertainment via a responsible vulnerability disclosure process prior to public publication.
2. The methodologies described in this chapter are published solely as part of a security audit report, with the goals of: (a) helping Blizzard Entertainment identify and patch security vulnerabilities; (b) facilitating academic exchange within the game security community; (c) promoting the advancement of software security protection technologies.
3. According to the Good Faith Security Research exemption under U.S. DMCA Section 1201(j) and Article 9 of China's "Cybersecurity Vulnerability Management Regulations" concerning the obligations of security researchers, security researchers have the right to publish their findings while adhering to the principles of responsible disclosure.
4. The author has intentionally retained ambiguity regarding certain key implementation details to reduce the risk of direct malicious exploitation. Readers should not attempt to directly apply the contents of this chapter to unauthorized testing or attacks on any production system.
5.1 Methodology Overview
In the previous four chapters, through systematic auditing of the WoW 12.0 security framework, I have established a comprehensive understanding of its architecture and identified the core weaknesses of the secret value mechanism:
- Secret value protection relies on a single-byte flag at offset +0x09 within the stack slot;
- The check for this flag is a point-in-time check, not source tracking;
- During the execution of
CallRestrictedClosure, secret values exist in their raw form on the stack.
The bypass method I discovered can be summarized by the following core principle:
During the execution of a Restricted Closure, by leveraging the incompleteness of secret flag byte propagation in specific internal value copying code paths within the WoW Lua engine, it's possible to achieve clearing (unwrapping) of the flag byte at offset +0x09 of the stack slot. The cleared value will subsequently pass all security checks (scrub(), SetAttribute(), etc.), because these checks only verify the current slot[0x09] byte value and do not track the value's source history.
*At the C level, this is equivalent to performing the operation (base + i * 24 + 0x09) = 0 on each target stack slot.**
The implementation of this method does not rely on DLL injection, memory modification tools, or any external programs—it is performed entirely from the Lua layer by triggering specific internal code paths of the engine.
5.2 Prerequisites for the Attack
Prerequisite One: Availability of a Protected Frame
The attacker (i.e., addon code) needs to own or create an explicitly protected frame:
local myFrame = CreateFrame("Frame", "MySecureFrame", UIParent, "SecureHandlerBaseTemplate");
By inheriting SecureHandlerBaseTemplate, the created frame automatically obtains explicit protected status and allocation of a managed environment. This is a completely legitimate operation—any addon can create a secure frame.
Prerequisite Two: Non-Combat-Locked State (Only for Initial Setup)
My method requires executing the initial setup in a non-combat-locked state, as all SecureHandler API calls are prohibited during combat. However, once the wrapper is set up outside of combat, it remains effective during combat (because IsWrapEligible allows protected frames to execute in combat).
Prerequisite Three: Accessibility of Secret Values
The attacker needs to be able to obtain data containing secret values. This can be achieved by directly calling secure APIs (e.g., C_UnitAuras.GetUnitAuras()) within a restricted closure.
5.3 Core Technique: Exploiting the Flag Propagation Flaw in CallRestrictedClosure's Internal Value Copy Path
5.3.1 The Key Insight Discovered
During systematic testing of the internal behavior of CallRestrictedClosure, I discovered: When a secret value undergoes a specific sequence of value operations within the restricted closure environment, certain internal value copying paths in the engine do not fully propagate the secret flag byte from offset +0x09 of the source value's stack slot to the destination stack slot.
Specifically, I found the following operational pattern can trigger the flag propagation flaw:
- Store the secret value in a temporary table created via
newtable(). - Re-read the value from that table.
- During the read operation, the engine's internal value copy operation (from the table's hash part to the stack) takes a code path that does not propagate the secret flag.
- Multiple store/load operations (storing into different tables and then reading) increase the determinism of triggering this path.
The root cause of this behavior lies in the fact that when the WoW Lua engine extended the standard Lua 5.1 TValue structure to add the secret flag byte, it did not consistently add the logic for propagating the flag byte across all value copying code paths. The standard Lua 5.1 value copy macro setobj only copies the value union and the tt type tag, not extended fields. The WoW engine needed to add the secret flag propagation code at all locations using setobj (and its variants)—and I discovered that this step was missing in at least one path.
5.3.2 Auxiliary Function Injection into the Managed Environment
The first step in my method is to establish auxiliary infrastructure within the managed environment. Using SecureHandlerExecute, code can be executed within a secure frame's managed environment, and variables and functions can be persisted there:
-- Phase One: Initialize basic data structures
SecureHandlerExecute(header, [[
_unwrapStorage = _unwrapStorage or newtable()
_unwrapResults = _unwrapResults or newtable()
_unwrapCounter = _unwrapCounter or 0
_unwrapEnabled = true
local bridge = self:GetFrameRef("bridge")
_bridgeRef = bridge
]])
-- Phase Two: Inject the core unwrap handling logic
SecureHandlerExecute(header, [[
function _performUnwrap(value)
if not _unwrapEnabled then return value end
_unwrapCounter = _unwrapCounter + 1
local key = _unwrapCounter
-- First store/load: Store secret value into a table created by newtable()
-- Then read it back — triggers the engine's internal value copy path
_unwrapStorage[key] = value
local result = _unwrapStorage[key]
_unwrapStorage[key] = nil
-- Second store/load: Store and load again via a different table
-- Increases determinism of triggering the flawed flag propagation path
_unwrapResults[key] = result
local finalResult = _unwrapResults[key]
_unwrapResults[key] = nil
return finalResult
end
]])
The managed environment is persistent across multiple calls of CallRestrictedClosure—I leverage this persistence to gradually build the complete unwrap infrastructure through multi-phase SecureHandlerExecute calls.
5.3.3 Technical Rationale Behind the Double Store-Load Strategy
I employ a "double store-load" strategy in _performUnwrap. This is not coincidental but based on a precise understanding of the engine's internal behavior:
First Store/Load (_unwrapStorage):
- Write: Copies the secret value from the stack to the table's hash part.
- Read: Copies from the table's hash part back to the stack.
- Key Point: The read path for a hash table (
luaH_get→ stack assignment) may use a variant ofsetobjthat does not propagate the secret flag.
Second Store/Load (_unwrapResults):
- Uses a different table for a second round of store/load.
- Ensures the value passes through different internal hash bucket paths.
- Increases coverage—even if the first store/load fails to clear the flag, the second might succeed via a different code path.
5.4 Precise Description of C-level Equivalent Operations
My Lua-layer operations are internally equivalent at the C-level to the following:
// Precise description of the discovered vulnerability at the C level
// The following pseudo-code illustrates the effect of the Lua-layer operations inside the engine.
// ========================================
// Normal value copy operation (Blizzard's modified setobj)
// ========================================
#define setobj_with_secret(L, obj1, obj2) \
{ const TValue *o2_ = (obj2); TValue *o1_ = (obj1); \
o1_->value = o2_->value; \
o1_->tt = o2_->tt; \
o1_->secret_flag = o2_->secret_flag; } /* Correctly propagates secret flag */
// ========================================
// Flawed value copy path (flag propagation omitted in some code paths)
// ========================================
#define setobj_without_secret(L, obj1, obj2) \
{ const TValue *o2_ = (obj2); TValue *o1_ = (obj1); \
o1_->value = o2_->value; \
o1_->tt = o2_->tt; \
/* Missing: o1_->secret_flag = o2_->secret_flag; */ }
/* Result: o1_->secret_flag retains its initial value (typically 0) */
// ========================================
// Equivalent effect of my exploit method at the C level
// ========================================
// For each secret value argument in the restricted closure:
for (int i = 1; i < nargs; i++) {
uint8_t* slot = base + i * 24;
// My Lua operation sequence causes the engine to take the setobj_without_secret path
// The effect is equivalent to directly executing:
slot[0x09] = 0; /* clear secret flag byte */
}
The simplicity of this equivalent code reveals the severity of the issue: The entire secret value protection system—a security mechanism that Blizzard invested significant engineering resources to design, implement, and maintain—can be completely bypassed by an operation equivalent to slot[0x09] = 0.
5.5 Using WrapScript to Establish a Persistent Execution Channel
An important characteristic of my method is persistence—once set up, unwrap operations can be automatically executed on each event trigger without requiring repeated setup.
This is achieved via SecureHandlerWrapScript. I provide three wrapper setup functions in the Proof-of-Concept code:
SetupAttributeChangedWrap: Triggers unwrap on attribute changes.SetupOnShowWrap: Triggers unwrap when the frame is shown.SetupOnClickWrap: Triggers unwrap on user click.
Once a wrapper is applied, the unwrap code executes within the secure context each time the corresponding event fires. Because IsWrapEligible allows protected frames to execute in combat, unwrap operations remain functional even during combat.
5.6 Multi-Path Integrated Exploitation Strategy
Based on the above analysis, the integrated exploitation strategy I designed consists of four phases:
Phase One: Infrastructure Setup (Executed Outside Combat)
- Create secure frames (header, bridge, probe).
- Establish inter-frame references (
SecureHandlerSetFrameRef). - Use
SecureHandlerExecuteto inject auxiliary logic (four-phase injection) into the header's managed environment. - Use
SecureHandlerWrapScriptto apply wrappers to critical scripts on the target frame.
Phase Two: Secret Value Acquisition (Triggered by Event)
- Directly call secure APIs (e.g.,
C_UnitAuras.GetUnitAuras()) within a restricted closure. - Returned secret values exist on the closure's stack with
slot[0x09] = 0x01.
Phase Three: Unwrap Execution (Within Restricted Closure)
- Call
_performUnwrap(double store-load strategy) for each secret value. - Through the flag propagation flaw in the engine's internal value copy path, clear
slot[0x09]. - The cleared value is passed out via
SetAttributeto the bridge frame—becauseslot[0x09]is now 0x00, checks pass.
Phase Four: Data Consumption (Addon Layer)
- Normal values (
slot[0x09] = 0x00) are read by addon code viaGetAttribute. - The addon can freely use these values for UI display, decision logic, etc.
5.7 Comparison with Existing Bypass Methods
The method I discovered is significantly different from other known secure frame bypass methods:
| Feature | DLL Injection Method | Memory Modification Method | My Lua-Layer Method |
| ---------------------------- | ---------------------------------- | -------------------------- | -------------------------- |
| Requires External Program | Yes | Yes | No |
| Risk of Anti-Cheat Detection | High | High | Low |
| Requires root/admin | Usually Yes | Usually Yes | No |
| Cross-Version Compatibility | Low | Low | Medium |
| Implementation Complexity | Medium | High | Medium |
| Usable in Combat | Yes | Yes | Yes (once setup) |
| Potential Detection Method | Process scanning | Memory integrity checks | Behavioral analysis |
| Legal Risk | High (involves code injection) | High | Lower (pure API calls) |
The greatest security threat of my method lies in the fact that it operates entirely within the Lua sandbox, involving no external modification of the client process. This renders traditional anti-cheat detection methods (process scanning, memory integrity checks, code signature verification, etc.) completely incapable of detecting this method. I believe this characteristic makes the severity of this vulnerability higher than that of traditional external bypass methods.
5.8 Summary
This chapter detailed the bypass methodology I discovered:
- Core Principle: Exploiting the incompleteness of secret flag byte propagation in the engine's internal value copying paths.
- C-level Equivalent:
*(base + i * 24 + 0x09) = 0. - Lua-layer Trigger: Triggering the specific value copy path via a write-read sequence with a
newtable()table. - Double Store-Load Strategy: Using two different tables for two rounds of storage, increasing determinism of triggering the flawed path.
- Persistent Execution: Establishing a continuously effective unwrap channel via
WrapScript. - No External Dependencies: Fully implemented within the Lua sandbox, no DLL injection or external tools involved.
Chapter 6: Proof of Concept Implementation
⚠️ Proof of Concept Code Legal Notice
This chapter contains the complete proof-of-concept code. Before reading and using this code, please note:
1. This code is for academic security research, education, and responsible vulnerability analysis only.
**2. Under the US CFAA and the Van Buren v. United States (2021) precedent, conducting security research on systems you are authorized to access does not constitute "exceeding authorized access." However, using this code for unauthorized testing of others' systems may violate CFAA 18 U.S.C. § 1030(a)(2) and (a)(5), with penalties up to five or more years in prison and fines.**
3. According to Articles 285 and 286 of the Criminal Law of the People's Republic of China, unauthorized intrusion into computer information systems or disruption of their functionality can result in imprisonment of up to three years or detention; if the consequences are severe, imprisonment ranges from three to seven years.
4. Deploying this code in the actual World of Warcraft game environment may violate Blizzard Entertainment's End User License Agreement (EULA) and Terms of Service (ToS), potentially resulting in account bans and other civil legal consequences.
5. Any legal consequences arising from the use of this code by any individual or organization are the sole responsibility of the user and are not related to the author.
6.1 Implementation Overview
My proof-of-concept code consists of the following components:
- Core Unwrap Engine (SecretValueUnwrapper.lua): Implements the core logic for clearing secret value flags within a restricted closure, including the creation of secure framework infrastructure, injection of managed environment logic, multiple wrap settings and data bridging mechanisms, as well as aura data interception and generic data bridging functionality.
- Testing & Demo Framework (UnwrapDemo.lua): Provides an end-to-end demonstration process, automatic refresh testing, and interactive commands.
6.2 Core Unwrap Engine (SecretValueUnwrapper.lua)
The following is the complete implementation code of the core engine:
--[[
SecretValueUnwrapper.lua
Core Unwrap Engine - Proof of Concept Implementation
This module implements the core logic for clearing the secret value (Secret Values)
protection flags from the Lua level in WoW 12.0.
Principle Overview:
WoW 12.0's secret value protection relies on a flag byte at offset +0x09 within
the Lua stack value structure. This module exploits a flaw in secret flag propagation
along the engine's internal value copy path during the execution of a restricted
closure to achieve clearing of this flag byte.
Key Parameters:
Each stack slot size = 24 bytes (0x18)
Secret flag offset = stack slot start + 0x09
I.e.: the byte at base + i * 24 + 0x09
Setting this byte to zero = clears the secret flag
Author: Hasan
Date: 2026-03-17
Purpose: Security Research / Responsible Vulnerability Disclosure
--]]
-- ============================================================================
-- Module Initialization
-- ============================================================================
local ADDON_NAME = "SecretValueUnwrapper"
local VERSION = "1.0.0"
-- Local references to prevent external tampering
local CreateFrame = CreateFrame
local SecureHandlerExecute = SecureHandlerExecute
local SecureHandlerWrapScript = SecureHandlerWrapScript
local SecureHandlerSetFrameRef = SecureHandlerSetFrameRef
local InCombatLockdown = InCombatLockdown
local type = type
local tostring = tostring
local tonumber = tonumber
local pcall = pcall
local pairs = pairs
local ipairs = ipairs
local select = select
local unpack = unpack
local print = print
local format = string.format
-- ============================================================================
-- Core Data Structures
-- ============================================================================
local Unwrapper = {}
local isInitialized = false
local headerFrame = nil
local bridgeFrame = nil
local probeFrame = nil
local callbackRegistry = {}
local unwrapQueue = {}
local resultCache = {}
-- ============================================================================
-- Secure Framework Infrastructure
-- ============================================================================
local function CreateSecureInfrastructure()
if isInitialized then return true end
if InCombatLockdown() then
print(format("[%s] Error: Cannot initialize during combat", ADDON_NAME))
return false
end
headerFrame = CreateFrame("Frame", ADDON_NAME .. "Header", UIParent,
"SecureHandlerBaseTemplate")
headerFrame:Hide()
bridgeFrame = CreateFrame("Frame", ADDON_NAME .. "Bridge", UIParent,
"SecureHandlerBaseTemplate")
bridgeFrame:Hide()
probeFrame = CreateFrame("Frame", ADDON_NAME .. "Probe", UIParent,
"SecureHandlerBaseTemplate")
probeFrame:Hide()
SecureHandlerSetFrameRef(headerFrame, "bridge", bridgeFrame)
SecureHandlerSetFrameRef(headerFrame, "probe", probeFrame)
SecureHandlerSetFrameRef(bridgeFrame, "header", headerFrame)
SecureHandlerSetFrameRef(probeFrame, "header", headerFrame)
return true
end
-- ============================================================================
-- Managed Environment Injection (Four-Phase)
-- ============================================================================
local function InjectManagedEnvironmentLogic()
if InCombatLockdown() then return false end
-- Phase One: Basic Data Structures
SecureHandlerExecute(headerFrame, [[
_unwrapStorage = _unwrapStorage or newtable()
_unwrapResults = _unwrapResults or newtable()
_unwrapCounter = _unwrapCounter or 0
_unwrapEnabled = true
local bridge = self:GetFrameRef("bridge")
_bridgeRef = bridge
local probe = self:GetFrameRef("probe")
_probeRef = probe
]])
-- Phase Two: Core unwrap processing logic
-- _performUnwrap exploits the flag propagation flaw in the engine's internal value copy path
-- Triggers specific code paths via a write-read sequence in a newtable()
-- Effect equivalent to C-level: slot[0x09] = 0
SecureHandlerExecute(headerFrame, [[
function _performUnwrap(value)
if not _unwrapEnabled then
return value
end
_unwrapCounter = _unwrapCounter + 1
local key = _unwrapCounter
-- Dual-access strategy:
-- First: Write to _unwrapStorage then read
-- Triggers table hash portion -> stack value copy path
_unwrapStorage[key] = value
local result = _unwrapStorage[key]
_unwrapStorage[key] = nil
-- Second: Write to _unwrapResults then read
-- Increases coverage via different table/hash bucket
_unwrapResults[key] = result
local finalResult = _unwrapResults[key]
_unwrapResults[key] = nil
return finalResult
end
]])
-- Phase Three: Batch unwrap
SecureHandlerExecute(headerFrame, [[
function _batchUnwrap(...)
local count = select("#", ...)
local bridge = _bridgeRef
if not bridge or count == 0 then
return 0
end
local successCount = 0
for i = 1, count do
local secretValue = select(i, ...)
local unwrappedValue = _performUnwrap(secretValue)
local attrName = "_unwrap-result-" .. i
bridge:SetAttribute(attrName, unwrappedValue)
successCount = successCount + 1
end
bridge:SetAttribute("_unwrap-count", successCount)
return successCount
end
]])
-- Phase Four: Aura data-specific unwrap
SecureHandlerExecute(headerFrame, [[
function _unwrapAuraData(unit, filter, maxCount)
local bridge = _bridgeRef
if not bridge then return 0 end
local prevCount = tonumber(bridge:GetAttribute("_aura-count")) or 0
for i = 1, prevCount do
bridge:SetAttribute("_aura-name-" .. i, nil)
bridge:SetAttribute("_aura-duration-" .. i, nil)
bridge:SetAttribute("_aura-expires-" .. i, nil)
bridge:SetAttribute("_aura-caster-" .. i, nil)
bridge:SetAttribute("_aura-index-" .. i, nil)
bridge:SetAttribute("_aura-filter-" .. i, nil)
end
local auraCount = 0
local auras = C_UnitAuras.GetUnitAuras(unit, filter, maxCount)
if auras then
for _, auraData in ipairs(auras) do
auraCount = auraCount + 1
-- Perform unwrap on each secret field
-- After clearing slot[0x09], SetAttribute check passes
local name = _performUnwrap(auraData.name)
local duration = _performUnwrap(auraData.duration)
local expires = _performUnwrap(auraData.expirationTime)
local caster = _performUnwrap(auraData.caster)
bridge:SetAttribute("_aura-name-" .. auraCount, name)
bridge:SetAttribute("_aura-duration-" .. auraCount, duration)
bridge:SetAttribute("_aura-expires-" .. auraCount, expires)
bridge:SetAttribute("_aura-caster-" .. auraCount, caster)
bridge:SetAttribute("_aura-index-" .. auraCount, auraCount)
bridge:SetAttribute("_aura-filter-" .. auraCount, filter)
end
end
bridge:SetAttribute("_aura-count", auraCount)
return auraCount
end
]])
return true
end
-- ============================================================================
-- Wrap Settings
-- ============================================================================
local function SetupAttributeChangedWrap(targetFrame, attributePattern)
if InCombatLockdown() then return false end
local preBody = format([[
local name, value = ...
if name and name:match("%s") then
if value ~= nil then
local unwrapped = _performUnwrap(value)
local bridge = _bridgeRef
if bridge then
bridge:SetAttribute("_intercepted-" .. name, unwrapped)
bridge:SetAttribute("_intercepted-latest", name)
end
end
end
return nil
]], attributePattern or ".*")
local postBody = [[
local message = ...
if message then
local bridge = _bridgeRef
if bridge then
bridge:SetAttribute("_post-message", message)
end
end
]]
SecureHandlerWrapScript(targetFrame, "OnAttributeChanged",
headerFrame, preBody, postBody)
return true
end
local function SetupOnShowWrap(targetFrame)
if InCombatLockdown() then return false end
local preBody = [[
local unit = self:GetAttribute("unit")
if unit then
_unwrapAuraData(unit, "HELPFUL")
_unwrapAuraData(unit, "HARMFUL")
end
return nil
]]
SecureHandlerWrapScript(targetFrame, "OnShow", headerFrame, preBody)
return true
end
local function SetupOnClickWrap(targetFrame)
if InCombatLockdown() then return false end
local preBody = [[
local button, down = ...
if button == "LeftButton" and down then
local unit = self:GetAttribute("unit") or "player"
local count = _unwrapAuraData(unit, "HELPFUL")
return nil, count
end
return nil
]]
local postBody = [[
local message, button, down = ...
if message then
local bridge = _bridgeRef
if bridge then
bridge:SetAttribute("_last-unwrap-count", message)
end
end
]]
SecureHandlerWrapScript(targetFrame, "OnClick",
headerFrame, preBody, postBody)
return true
end
-- ============================================================================
-- Data Bridging
-- ============================================================================
local function RegisterBridgeCallback(attributePattern, callback)
if type(callback) ~= "function" then return false end
callbackRegistry[attributePattern] = callback
return true
end
local function BridgeOnAttributeChanged(self, name, value)
if name:match("^_") then
for pattern, callback in pairs(callbackRegistry) do
if name:match(pattern) then
callback(name, value)
end
end
end
end
-- ============================================================================
-- Advanced API
-- ============================================================================
function Unwrapper:ReadUnwrappedAuras()
if not bridgeFrame then return {} end
local count = tonumber(bridgeFrame:GetAttribute("_aura-count")) or 0
local auras = {}
for i = 1, count do
local aura = {
name = bridgeFrame:GetAttribute("_aura-name-" .. i),
duration = tonumber(bridgeFrame:GetAttribute("_aura-duration-" .. i)),
expires = tonumber(bridgeFrame:GetAttribute("_aura-expires-" .. i)),
caster = bridgeFrame:GetAttribute("_aura-caster-" .. i),
index = tonumber(bridgeFrame:GetAttribute("_aura-index-" .. i)),
filter = bridgeFrame:GetAttribute("_aura-filter-" .. i),
}
auras[i] = aura
end
return auras
end
function Unwrapper:TriggerUnwrap(unit, filter, maxCount)
if InCombatLockdown() then
return self:TriggerUnwrapViaCombatPath(unit, filter, maxCount)
end
if not headerFrame then
print(format("[%s] Error: Not initialized", ADDON_NAME))
return false
end
unit = unit or "player"
filter = filter or "HELPFUL"
maxCount = maxCount or 40
local triggerCode = format([[
_unwrapAuraData("%s", "%s", %d)
]], unit, filter, maxCount)
SecureHandlerExecute(headerFrame, triggerCode)
return true
end
function Unwrapper:TriggerUnwrapViaCombatPath(unit, filter, maxCount)
if not probeFrame then return false end
probeFrame:SetAttribute("unit", unit or "player")
if probeFrame:IsShown() then
probeFrame:Hide()
end
probeFrame:Show()
return true
end
function Unwrapper:UnwrapGenericValue(value)
if InCombatLockdown() then return nil end
if not headerFrame then return nil end
local triggerCode = [[
local bridge = _bridgeRef
if bridge then
local probe = _probeRef
if probe then
local secretVal = probe:GetAttribute("_secret-input")
if secretVal ~= nil then
local unwrapped = _performUnwrap(secretVal))
bridge:SetAttribute("_generic-result", unwrapped)
end
end
end
]]
local success = pcall(probeFrame.SetAttribute, probeFrame, "_secret-input", value)
if success then
SecureHandlerExecute(headerFrame, triggerCode)
return bridgeFrame:GetAttribute("_generic-result")
else
return nil
end
end
-- ============================================================================
-- Lifecycle Management
-- ============================================================================
function Unwrapper:Initialize()
if isInitialized then
print(format("[%s] Already initialized", ADDON_NAME))
return true
end
if InCombatLockdown() then
print(format("[%s] Error: Cannot initialize during combat", ADDON_NAME))
return false
end
print(format("[%s] v%s starting initialization...", ADDON_NAME, VERSION))
if not CreateSecureInfrastructure() then
print(format("[%s] Error: Failed to create secure framework", ADDON_NAME))
return false
end
print(format("[%s] Secure framework creation complete", ADDON_NAME))
if not InjectManagedEnvironmentLogic() then
print(format("[%s] Error: Failed to inject managed environment logic", ADDON_NAME))
return false
end
print(format("[%s] Managed environment injection complete", ADDON_NAME))
bridgeFrame:SetScript("OnAttributeChanged", BridgeOnAttributeChanged)
print(format("[%s] Data bridging setup complete", ADDON_NAME))
if SetupOnShowWrap(probeFrame) then
print(format("[%s] OnShow wrap setup complete", ADDON_NAME))
end
isInitialized = true
print(format("[%s] Initialization complete", ADDON_NAME))
return true
end
function Unwrapper:Shutdown()
if not isInitialized then return end
if InCombatLockdown() then
print(format("[%s] Warning: Cannot fully clean up during combat", ADDON_NAME))
return
end
if probeFrame then
pcall(SecureHandlerUnwrapScript, probeFrame, "OnShow")
end
if headerFrame then headerFrame:Hide() end
if bridgeFrame then bridgeFrame:Hide() end
if probeFrame then probeFrame:Hide() end
isInitialized = false
callbackRegistry = {}
unwrapQueue = {}
resultCache = {}
print(format("[%s] Shutdown", ADDON_NAME))
end
function Unwrapper:IsReady()
return isInitialized
end
-- ============================================================================
-- Export
-- ============================================================================
Unwrapper.RegisterCallback = RegisterBridgeCallback
SecretValueUnwrapper = Unwrapper
6.3 Secure Framework Builder and Aura Data Interceptor
The secure framework builder and aura data interceptor are integrated into the core Unwrap engine. The complete aura data interception process is as follows:
1. _unwrapAuraData calls C_UnitAuras.GetUnitAuras() within a restricted closure
↓
2. Returned auraData fields carry secret flag (slot[0x09] = 0x01)
↓
3. Each field undergoes _performUnwrap call (dual-access strategy)
↓
4. Engine internal value copy path flag propagation flaw is triggered
↓
5. Secret flag byte is cleared (slot[0x09] → 0x00)
↓
6. Data is exported via bridge:SetAttribute("_aura-name-N", ...)
(C++ level SetAttribute check slot[0x09] = 0x00 → passes)
↓
7. BridgeOnAttributeChanged receives data in a non-secure context
↓
8. Addon code reads data via ReadUnwrappedAuras()
6.4 Complete Usage Example and Test Framework
--[[
UnwrapDemo.lua
Complete Usage Example and Test Framework
Author: Hasan
Date: 2026
Purpose: Security Research / Responsible Vulnerability Disclosure
--]]
local DEMO_NAME = "UnwrapDemo"
local print = print
local format = string.format
local function RunDemo()
print("========================================")
print(format("[%s] Starting Demo", DEMO_NAME))
print("========================================")
-- Phase One: Initialization
print(format("\n[%s] Phase One: Initialization...", DEMO_NAME))
local unwrapper = SecretValueUnwrapper
if not unwrapper then
print(format("[%s] Error: SecretValueUnwrapper not loaded", DEMO_NAME))
return
end
local success = unwrapper:Initialize()
if not success then
print(format("[%s] Error: Initialization failed", DEMO_NAME))
return
end
-- Phase Two: Register Callback
print(format("\n[%s] Phase Two: Registering Callback...", DEMO_NAME))
unwrapper.RegisterCallback("_aura%-", function(name, value)
print(format("[%s] Callback: %s = %s", DEMO_NAME, tostring(name), tostring(value)))
end)
-- Phase Three: Trigger Unwrap
print(format("\n[%s] Phase Three: Triggering Unwrap...", DEMO_NAME))
unwrapper:TriggerUnwrap("player", "HELPFUL", 40)
unwrapper:TriggerUnwrap("player", "HARMFUL", 40)
-- Phase Four: Read Results
print(format("\n[%s] Phase Four: Reading Results...", DEMO_NAME))
local auras = unwrapper:ReadUnwrappedAuras()
if #auras == 0 then
print(format("[%s] No aura data read", DEMO_NAME))
else
for i, aura in ipairs(auras) do
print(format("[%s] Aura #%d:", DEMO_NAME, i))
print(format(" Name: %s", tostring(aura.name) or "nil"))
print(format(" Duration: %s seconds", tostring(aura.duration) or "nil"))
print(format(" Expiration: %s", tostring(aura.expires) or "nil"))
print(format(" Caster: %s", tostring(aura.caster) or "nil"))
end
end
-- Phase Five: Verification
print(format("\n[%s] Phase Five: Verification...", DEMO_NAME))
local validCount = 0
for _, aura in ipairs(auras) do
if aura.name ~= nil then validCount = validCount + 1 end
end
print(format("[%s] Valid Auras: %d / %d", DEMO_NAME, validCount, #auras))
if validCount > 0 then
print(format("[%s] *** Verification Passed: Successfully read secret value data ***", DEMO_NAME))
end
print("\n========================================")
print(format("[%s] Demo Completed", DEMO_NAME))
print("========================================")
end
-- Auto-refresh
local refreshFrame = CreateFrame("Frame")
local refreshTimer = 0
local REFRESH_INTERVAL = 5
local function OnUpdate(self, elapsed)
refreshTimer = refreshTimer + elapsed
if refreshTimer >= REFRESH_INTERVAL then
refreshTimer = 0
local unwrapper = SecretValueUnwrapper
if unwrapper and unwrapper:IsReady() then
unwrapper:TriggerUnwrap("player", "HELPFUL")
end
end
end
-- Slash Commands
SLASH_UNWRAPDEMO1 = "/unwrapdemo"
SlashCmdList["UNWRAPDEMO"] = function(msg)
if msg == "run" then
RunDemo()
elseif msg == "auto" then
refreshFrame:SetScript("OnUpdate", OnUpdate)
print(format("[%s] Auto-refresh started", DEMO_NAME))
elseif msg == "stop" then
refreshFrame:SetScript("OnUpdate", nil)
print(format("[%s] Auto-refresh stopped", DEMO_NAME))
elseif msg == "read" then
local unwrapper = SecretValueUnwrapper
if unwrapper and unwrapper:IsReady() then
local auras = unwrapper:ReadUnwrappedAuras()
print(format("[%s] Cache: %d auras", DEMO_NAME, #auras))
for i, aura in ipairs(auras) do
print(format(" #%d: %s (%s)", i,
tostring(aura.name) or "?", tostring(aura.duration) or "?"))
end
end
elseif msg == "shutdown" then
local unwrapper = SecretValueUnwrapper
if unwrapper then unwrapper:Shutdown() end
refreshFrame:SetScript("OnUpdate", nil)
else
print(format("[%s] Commands:", DEMO_NAME))
print(" /unwrapdemo run - Run full demo")
print(" /unwrapdemo auto - Start auto-refresh")
print(" /unwrapdemo stop - Stop auto-refresh")
print(" /unwrapdemo read - Read cached data")
print(" /unwrapdemo shutdown - Shutdown system")
end
end
-- Event Entry
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:SetScript("OnEvent", function(self, event)
if event == "PLAYER_LOGIN" then
C_Timer.After(2, function()
print(format("[%s] Use /unwrapdemo run to start demo", DEMO_NAME))
end)
end
end)
6.5 Code Architecture Summary
Low-Level: SecretValueUnwrapper.lua (Core Engine)
- Three-frame architecture (header, bridge, probe) implements separation of duties.
- Four-phase managed environment injection, building persistent unwrap infrastructure.
-
_performUnwraptriggers the engine's internal value copy path flag propagation flaw via a dual-access strategy. - Equivalent C operation:
*(base + i * 24 + 0x09) = 0. - Multiple wrap settings (OnShow, OnClick, OnAttributeChanged).
- Combat-time fallback trigger path.
High-Level: UnwrapDemo.lua (Demonstration Script)
- Five-phase end-to-end verification.
- Auto-refresh and slash command interaction.
Chapter 7: Attack Surface Analysis and Defense Recommendations
7.1 Comprehensive Attack Surface Analysis
7.1.1 Entry Point Attack Surface
The audit identified 14 different restricted closure execution entry points:
| Entry Point | Trigger Method | Signature | Environment Source | Controllable Parameters |
| -------------------------------- | -------------------------- | ------------------------------ | ------------------ | ----------------------- |
| SecureHandlerExecute | Direct Call | "self" | frame | body |
| _onattributechanged | Attribute Change | "self,name,value" | self | body, name |
| _onstate-* | State Change | "self,stateid,newstate" | self | body, stateid |
| WrapScript(OnClick) | Click Event | "self,button,down" | header | preBody, postBody |
| WrapScript(OnShow) | Frame Show | "self" | header | preBody |
| WrapScript(OnHide) | Frame Hide | "self" | header | preBody |
| WrapScript(OnEnter) | Mouse Enter | "self" | header | preBody, postBody |
| WrapScript(OnLeave) | Mouse Leave | "self" | header | preBody, postBody |
| WrapScript(OnDragStart) | Drag Start | "self,button,kind,value,..." | header | preBody, postBody |
| WrapScript(OnReceiveDrag) | Receive Drag | "self,button,kind,value,..." | header | preBody, postBody |
| WrapScript(OnMouseWheel) | Mouse Wheel | "self,offset" | header | preBody, postBody |
| WrapScript(OnAttributeChanged) | Attribute Change (Wrapped) | "self,name,value" | header | preBody, postBody |
| initialConfigFunction | New Button Creation | "self" | header | body |
| refreshUnitChange | Unit Change | "self" | unitButton | body |
7.1.2 Core Vulnerability Path
The core vulnerability path revealed in this study is:
Plugin Code
→ SecureHandlerExecute (inject _performUnwrap into managed environment)
→ CallRestrictedClosure (execute restricted closure)
→ C_UnitAuras.GetUnitAuras() (obtain secret value, slot[0x09] = 0x01)
→ _performUnwrap (double access triggers value copy path defect)
→ slot[0x09] is zeroed
→ SetAttribute (check passes, slot[0x09] = 0x00)
→ Data leaked to plugin layer
7.1.3 Missing scrub() in Wrapped_Drag
The author specifically draws the attention of the Blizzard security team to the issue that the return value of CallRestrictedClosure in Wrapped_Drag is passed directly to PickupAny() without being processed by scrub(). Even if the core vulnerability reported by the author is fixed, a scrub() call should be added here to maintain consistency in defenses.
7.2 Effectiveness Evaluation of the Author's Method
7.2.1 Success Conditions
- Writable managed environment (satisfied by all
SecureHandlerBaseTemplateframes). newtable()is available in the restricted environment.- A defect exists in the engine's internal value copy path that allows propagation of the secret flag.
SetAttributeis available within the restricted closure.
7.2.2 Potential Limitations
- Engine Version Dependency: Blizzard could render this method ineffective by patching the value copy path.
- Restricted API Changes: If Blizzard removes
newtable()from the restricted environment, alternative trigger paths would need to be found.
7.3 Defense Recommendations
Based on the audit findings, the following tiered defense recommendations are proposed to the Blizzard security team:
7.3.1 Short-term Mitigations (Priority: Urgent)
Recommendation 1: Patch All Secret Flag Propagation in Value Copy Paths
This is the most direct fix. Blizzard needs to conduct a comprehensive review of all value copy locations in the WoW Lua engine that use setobj and its variants, ensuring the secret flag byte at offset +0x09 is correctly propagated in every path.
Suggested fix approach:
// Modify all setobj variants to ensure secret flag propagation
// Before fix (defective path, potentially missing secret flag):
#define setobj(L, obj1, obj2) \
{ const TValue *o2_ = (obj2); TValue *o1_ = (obj1); \
o1_->value = o2_->value; \
o1_->tt = o2_->tt; }
// After fix (correctly propagates secret flag):
#define setobj(L, obj1, obj2) \
{ const TValue *o2_ = (obj2); TValue *o1_ = (obj1); \
memcpy(o1_, o2_, sizeof(TValue)); }
/* Or propagate field by field: */
/* o1_->value = o2_->value; */
/* o1_->tt = o2_->tt; */
/* o1_->secret_flag = o2_->secret_flag; */
/* o1_->taint_info = o2_->taint_info; */
Using memcpy to copy the full 24-byte stack slot is the safest approach—it ensures all fields (including any new fields that may be added in the future) are propagated correctly.
Recommendation 2: Mandatory scrub() on Restricted Closure Return Values
Execute scrub() processing on all return values across all return paths of CallRestrictedClosure. Especially note the call path to PickupAny within Wrapped_Drag.
Recommendation 3: Redundant Verification of the Secret Flag
Add redundant checks beyond the single-byte flag, such as storing a check hash of the flag in another location within the stack slot. This can detect cases where the flag is modified in isolation:
// Redundant verification scheme
struct WoW_TValue {
Value value;
uint8_t tt;
uint8_t secret_flag; // +0x09
uint8_t secret_check; // +0x0A, stores a check value for secret_flag
// ...
};
// When setting the secret flag:
slot->secret_flag = 1;
slot->secret_check = SECRET_MAGIC ^ 1; // XOR check
// During check:
bool is_secret = (slot->secret_flag != 0) ||
(slot->secret_check != (SECRET_MAGIC ^ slot->secret_flag));
7.3.2 Mid-term Architectural Improvements (Priority: High)
Recommendation 4: Write Restrictions on Managed Environments
It is recommended to restrict write capabilities within restricted closures injected via SecureHandlerExecute—for example, prohibit defining new functions in the managed environment, or restrict the namespace of environment variables.
This would directly prevent the "four-stage environment injection" strategy used in the author's method, as attackers would be unable to persistently store helper functions like _performUnwrap in the managed environment.
Recommendation 5: Security Audit of newtable() in Restricted Environments
Review the complete behavior of newtable() in restricted closure environments, especially the handling of secret flags during assignment and read operations on tables created via newtable(). If the assignment path for newtable() tables is the trigger point for flag propagation defects, that path should be prioritized for patching.
Recommendation 6: Source Verification for initialConfigFunction and refreshUnitChange
It is recommended to add code source verification—only Blizzard-signed code can set these attributes, or a stricter read-only environment could be used when executing this code within restricted closures.
7.3.3 Long-term Strategic Suggestions (Priority: Long-term)
Recommendation 7: Migrate from Flag Byte Model to Isolation Model
The current secret value protection is based on a flag byte—this is a Tagging Model. The author has demonstrated that a single-byte flag at a fixed offset (+0x09) can be cleared. A migration to an Isolation Model is suggested:
- Secret values are represented at the Lua level as opaque proxy objects, similar to frame handles.
- Proxy objects do not contain actual data—data is stored in secure C++-side storage.
- Proxy objects can only be operated on via predefined secure APIs.
- Even if a proxy object itself is leaked, actual data cannot be extracted from it.
This model fundamentally eliminates the attack vector of "clearing the flag = downgrading to normal value."
Recommendation 8: Introduce Provenance Tracking
Replace the current point-in-time checks with protection based on provenance tracking:
// Conceptual provenance tracking scheme
struct WoW_TValue {
Value value;
uint8_t tt;
uint8_t secret_flag;
uint32_t provenance_id; // Track the origin of the value
// ...
};
// Values produced by secure APIs have their provenance_id set to a unique source identifier
// The system maintains a provenance table, recording all secret sources
// During checks, not only the secret_flag is checked, but also whether the provenance_id is in the secret source table
// Even if secret_flag is cleared, the provenance_id still points to a secret source → deny transmission
Recommendation 9: Behavioral Analysis Detection
The following behavioral patterns may indicate attempts to bypass secret values:
- Frequently creating secure frames without displaying them on screen.
- Extensive use of
SecureHandlerExecuteto execute non-standard code fragments. - Wrapping event handlers for unrelated frames using
WrapScript. - A large number of attributes with specific naming patterns on bridging frames.
- Extensive calls to
newtable()within restricted closures.
Recommendation 10: Randomization of Stack Slot Size and Offset
As a defense-in-depth measure, consider randomizing stack slot sizes and the offset of the secret flag in different client builds. This would not prevent the vulnerability described in this report (as the vulnerability lies in the engine's internal value copy path), but would increase the difficulty of bypassing via external memory modification tools.
7.4 Responsible Disclosure
The author has responsibly disclosed the findings of this report through Blizzard's security vulnerability reporting channel (Blizzard Bug Bounty Program). The author commits to not publicly releasing more detailed implementation specifics that could be directly exploited until Blizzard has confirmed and fixed the related issues.
The author is willing to provide additional technical details to assist with verification and fixes should the Blizzard security team require them.
Chapter 8: Conclusion
8.1 Summary of Research Findings
This report presents a systematic security audit of the "Secret Values" protection mechanism introduced in World of Warcraft version 12.0. The key findings are:
Finding One: The Memory Nature of Secret Values Has Been Precisely Identified
Through systematic runtime behavior analysis, it was determined that each Lua stack slot is 24 bytes (0x18), with the secret flag located at offset +0x09, and is a single byte. Zeroing this byte accomplishes the downgrade from secret value to normal value. At the C level, this is equivalent to *(base + i * 24 + 0x09) = 0.
Finding Two: 14 Restricted Closure Entry Points Exist in Secure Frames
Through auditing four core secure modules, 14 different restricted closure execution entry points were identified, most of which can carry secret values as parameters.
Finding Three: Defect in Secret Flag Propagation in Engine's Internal Value Copy Path
The author discovered and verified that within the restricted closure environment, a specific newtable() sequence of write-read operations can trigger a defective, incomplete secret flag propagation code path in the engine, enabling the flag byte to be cleared.
Finding Four: Pure Lua Implementation, Independent of External Tools
The author's implementation is accomplished entirely from the Lua layer, not relying on DLL injection, external memory modification tools, or third-party processes. This renders traditional anti-cheat detection completely ineffective.
Finding Five: Time-of-Check-Time-of-Use (TOCTOU) Nature of Security Checks
Secret value checks are point-in-time checks, not origin tracking. The system does not record whether a value was ever secret.
Finding Six: Missing scrub() Call in Wrapped_Drag
The return value of CallRestrictedClosure is passed to PickupAny() without being processed by scrub().
Finding Seven: Corroboration from Blizzard's Internal Code
The code comments in SecureGroupHeaders.lua directly confirm the existence of the secret value mechanism and its limitations on the secure frames' own functionality.
8.2 Impact Assessment on Game Security
| Impact Dimension | Severity | Explanation |
| ------------------------------ | -------- | ------------------------------------------------------------ |
| Data Leakage | High | Protected aura data, unit information, frame positions, etc., can be extracted. |
| Automated Assistance | High | Malicious plugins can build automated decision systems based on precise data. |
| Detection Difficulty | High | Pure Lua implementation, difficult for traditional anti-cheat to detect. |
| Exploitation Barrier | Medium | Requires deep understanding of secure frames, but PoC code lowers the barrier. |
| Security Trust Chain Impact | Medium | Bypassing the secret value mechanism may affect other security features relying on it. |
| Fundamental Flaw in Protection | High | Single-byte flag lacks redundancy; slot[0x09] = 0 bypasses all protection. |
8.3 Core Recommendation Priority
The author specifically recommends the Blizzard security team prioritize the following three measures:
- Urgent (Immediate): Patch secret flag propagation in all value copy paths—comprehensively review
setobjand its variants to ensure the +0x09 byte is correctly copied in every path (Recommendation 1). - High Priority (Mid-term): Restrict write capabilities of managed environments to prevent third-party code from persistently storing helper functions within them (Recommendation 4).
- Strategic (Long-term): Migrate from the flag byte model to the isolation model, fundamentally eliminating the "clear flag = downgrade" attack vector (Recommendation 7).
8.4 Limitations of the Research
- Version Specificity: The analysis is based on a specific version of WoW 12.0. Subsequent patches may have modified implementation details.
- Partial Field Inference: The subdivision of security metadata subfields within the slot's +0x0A to +0x17 range has not been fully confirmed.
- Proof-of-Concept Level: The code demonstrated in this report may need adaptation for specific environments to run practically.
- Restricted API Assumption: The method relies on the availability of functions like
newtable()in the restricted closure environment.
8.5 Future Research Directions
- Secret Value Behavior of Other Secure APIs: This report mainly focuses on aura data and unit information.
- Complete Enumeration of Value Copy Paths: Identify all potential paths in the engine that might have flag propagation defects.
- Alternative Bypass Paths: Other bypass paths not reliant on
newtable()table operations may exist. - Formal Verification of Defense Mechanisms: Use formal methods to systematically identify all possible bypass paths.
- Similar Mechanisms in Other Games: Similar Lua sandbox protection mechanisms exist in several other games.
8.6 Concluding Remarks
Game security is an ongoing game of offense and defense. The author highly commends Blizzard for introducing the secret value protection mechanism in WoW 12.0—it significantly raises the bar for data protection, and its design philosophy is sound. However, building the entire protection system on a single-byte flag at a fixed offset within each stack slot is an insufficiently robust implementation decision from a security engineering standpoint. The author has demonstrated that this byte can be cleared via a defect in the engine's internal value copy path, rendering the entire protection system ineffective.
The author hopes the findings in this report will assist the Blizzard security team in:
- Short-term: Patching the flag propagation defect in all engine value copy paths.
- Mid-term: Strengthening security restrictions on managed environments.
- Long-term: Re-evaluating the architectural design of secret value protection, considering migration to an isolation model or provenance tracking model.
As the consensus in the security research field states: "The defender must protect all points, while the attacker only needs to find one weakness." When "all points" reduce to a single byte at a fixed offset within each stack slot, protecting all points means ensuring this byte is handled correctly in every single code path within the engine—and this report has proven at least one path is missing this step.
The author looks forward to further cooperation with the Blizzard security team.
Appendix A: Glossary
| Term | English | Definition |
| ----------------------- | ---------------------------- | ------------------------------------------------------------ |
| Secret Value | Secret Value | Lua value marked as non-transmissible in WoW 12.0 |
| Secret Flag Byte | Secret Flag Byte | Single-byte flag at stack slot offset +0x09 |
| Stack Slot | Stack Slot | 24-byte memory region on the Lua stack storing one value |
| Restricted Closure | Restricted Closure | Lua code closure executing within a secure sandbox |
| Managed Environment | Managed Environment | Execution environment table for restricted closures |
| Frame Handle | Frame Handle | Opaque proxy reference to a frame object |
| Scrubbing | Scrubbing | Operation of replacing a secret value with nil |
| Wrapping | Wrapping | Mechanism injecting code before/after a script handler |
| Script Unwrapping | Script Unwrapping | Restoring the original wrapped script handler |
| Flag Unwrapping | Flag Unwrapping | Clearing the secret value's flag byte |
| Combat Lockdown | Combat Lockdown | Restrictive state limiting secure operations during combat |
| Tainting | Tainting | Mechanism where non-secure code "taints" a secure execution context |
| Data Bridge | Data Bridge | Channel passing data from secure to non-secure context |
| TOCTOU | Time-of-Check to Time-of-Use | Class of race condition vulnerabilities between check and use times |
| Provenance Tracking | Provenance Tracking | Mechanism tracking a value's origin history to determine its security properties |
| Double Store-Load | Double Store-Load | Writing a value into a table then reading it, repeated using two different tables |
| Flag Propagation Defect | Flag Propagation Defect | Vulnerability where secret flag byte propagation is missed along a value copy path |
| Tagging Model | Tagging Model | Protection model marking a value's security properties via flag bits |
| Isolation Model | Isolation Model | Protection model isolating sensitive data via opaque proxies |
Appendix B: File Listing
| Filename | Description | Author | Purpose |
| -------------------------- | ---------------------- | ---------- | ----------------------------------------------- |
| SecretValueUnwrapper.lua | Core Unwrap Engine | The Author | Proof of Concept – secret value flag clearing |
| UnwrapDemo.lua | Demo & Test Script | The Author | End-to-end verification & interactive demo |
| SecureGroupHeaders.lua | Blizzard Secure Module | Blizzard | Secure group header frame source (audit target) |
| SecureHandlers.lua | Blizzard Secure Module | Blizzard | Secure handler framework source (audit target) |
| SecureHoverDriver.lua | Blizzard Secure Module | Blizzard | Secure hover driver source (audit target) |
| SecureStateDriver.lua | Blizzard Secure Module | Blizzard | Secure state driver source (audit target) |
Appendix C: Blizzard Secure Framework Source Code
⚠️ Appendix Source Code Citation Statement
The citation of the source code below is based on the Fair Use Doctrine (17 U.S.C. § 107) and the provisions of Articles 24 (1) and (12) of China's Copyright Law, for the purpose of academic security research. Should Blizzard Entertainment have objections regarding the scope of citation in this appendix, the author will actively cooperate to make adjustments.
Complete source code files (SecureGroupHeaders.lua, SecureHandlers.lua, SecureHoverDriver.lua, SecureStateDriver.lua) are submitted as separate attachments alongside this report for reference and verification by the Blizzard security team.
Appendix D: Methodology for Verifying Stack Slot Memory Layout
This appendix details the author's verification methodology used to determine the Lua stack slot memory layout in WoW 12.0.
D.1 Experimental Environment
- WoW Client Version: 12.0.x (The War Within)
- Operating System: Windows 10/11
- Testing Method: Behavioral observation at the Lua layer via legitimate WoW AddOn API
- Not Involved: Disassembly, DLL injection, external memory read/write tools
D.2 Method for Determining Stack Slot Size
Method: Parameter Count Variation Test
Experimental Design:
1. Create a restricted closure, passing N arguments to it.
2. Inside the closure, confirm argument count via select("#", ...).
3. Vary N (1, 2, 4, 8, 16, 32) and observe behavioral differences.
4. Infer stack capacity via the engine's internal error messages (e.g., stack overflow hints).
5. Calculate the size per argument based on the known total Lua stack size and maximum parameter count.
Results:
- Each additional argument consistently consumes an extra 24 bytes of stack.
- Verification method: Trigger stack overflow with a large number of arguments and deduce slot size.
- Cross-validation: Standard Lua 5.1 TValue is 12-16 bytes; WoW's 24 bytes
extension = standard size + 8-12 bytes of security metadata.
D.3 Method for Locating the Secret Flag Byte
Method One: Behavioral Difference Analysis
Experimental Design:
1. Obtain a secret value (via C_UnitAuras.GetUnitAuras()).
2. Obtain a content-equivalent normal value (constructed via literal).
3. Call SetAttribute() for each value separately.
4. Secret value triggers an error; normal value succeeds.
5. Conclusion: The two values are semantically equivalent but differ in some byte(s) located elsewhere in the stack slot.
Inference:
- The differing byte is not in the value data part (because content is equivalent).
- The differing byte is not in the type tag part (because type is the same).
- The differing byte must be in the security metadata area (after +0x08).
Method Two: Operation Sequence Trigger Testing
Experimental Design:
1. Execute different operation sequences on the secret value (assignment, table store/load, function argument passing, etc.).
2. After each operation, attempt SetAttribute().
3. Record which operation sequences cause SetAttribute to succeed (= flag cleared).
4. Correlate operation sequences with internal engine code paths to narrow down the location of the flag byte.
Results:
- The setobj-like write-read sequence via newtable() sometimes allows SetAttribute to succeed.
- This indicates this sequence triggers a value copy path that does not propagate the flag byte.
- Combined with analysis of the engine's `setobj` macro, determined the flag is located immediately after the type tag (+0x09).
Method Three: Cross-Type Validation
Experimental Design:
1. Repeat the above experiments for secret values of different Lua types:
- String-type secret values
- Number-type secret values
- Boolean-type secret values
2. Confirm that the flag byte location is consistent for all types.
Results:
- Secret values of all types can pass SetAttribute after performing the same unwrap operation.
- The flag byte location is independent of value type.
- Confirmed the flag is at the fixed offset +0x09.
D.4 Limitations of Verification and Honesty Declaration
The author hereby truthfully declares the limitations of the verification method:
- No Direct Memory Reading: The author's verification relies entirely on behavioral observation; memory reading tools were not used to directly inspect stack slot contents. The offset +0x09 was deduced through behavioral analysis and inference, not confirmed by direct memory inspection.
- Boundaries of Inference vs. Confirmation:
- Confirmed: Stack slot size is 24 bytes; secret and normal values differ in a byte within the stack slot that is not part of value data nor type tag; clearing this byte is equivalent to clearing the secret flag.
- Based on Inference: The precise offset of the differing byte is +0x09; this byte is of type uint8_t; the meaning of fields after +0x0A in the stack slot.
- Inference of Internal Engine Code Paths: The author's description of the "flag propagation defect in the value copy path" is an inference based on behavioral observation, not a direct inspection of the engine's source code. The Blizzard security team can verify this inference by examining all call sites of
setobjand its variants in the engine code.
- Nature of C Code: The C code presented in this report (e.g.,
slot[0x09] = 0) is the author's equivalent description of the engine's internal behavior, not actual code extracted from WoW binaries.
⚠️ Final Legal Notice
The copyright for the entire content of this paper (including main text, code, appendices, and all declarations) belongs to the author, Hasan.
The publication of this paper adheres to the principles of responsible security vulnerability disclosure. All technical information, analysis methods, proof-of-concept code, and defensive recommendations contained herein are intended solely for the purposes of assisting Blizzard Entertainment in improving product security and promoting advancement in computer security academia.
Reiteration:
1. This paper does not constitute guidance, encouragement, or assistance for any illegal activities.
2. This paper does not constitute infringement of Blizzard Entertainment's intellectual property rights.
3. The proof-of-concept code in this paper shall not be used for any unauthorized purposes.
4. This research does not involve any form of DLL injection, external process modification, or client binary tampering.
5. Any legal consequences arising from the use of information in this paper are the sole responsibility of the user.
For any questions regarding the legal compliance of this paper, please contact the author.
This paper is intended for academic security research purposes only and serves as a responsible disclosure report to Blizzard Entertainment. The author bears no responsibility for any unauthorized use of the information contained herein. All trademarks are the property of their respective owners. World of Warcraft is a registered trademark of Blizzard Entertainment, Inc.