What You Should Know About Mitigation Bypass

What You Should Know About Mitigation Bypass

June 28, 2019 | Mina Hao

Mitigation measures are implemented with many implicit assumptions. They can work only when these assumptions prove to be true and would be bypassed if these assumptions were broken.

During the R&D process, we should try to minimize assumptions and ensure that necessary assumptions are ceteris paribus (Latin phrase, meaning “other things held constant”), thereby making mitigations work to the best effect.

Mitigations

Mitigation is an important part in Microsoft’s in-depth defense system against software vulnerabilities, aimed to prevent vulnerability exploitation by breaking key exploitation techniques.

Currently, Windows 10 provides the following mitigations:

  • SEHOP/SafeSEH
  • Heap randomization and metadata protection
  • Address space layout randomization (ASLR)
  • Data Execution Prevention (DEP)
  • Control Flow Guard (CFG)
  • Arbitrary Code Guard (ACG)
  • Code Integrity Guard (CIG)

Mitigation Bypass

Mitigation bypass is a process of fighting against and breaking mitigation measures in an environment where mitigations are enabled for the ultimate end of arbitrary code execution.

To bypass mitigations, you should, for starters, know how a mitigation measure works.

As for the essence of security, an expert once said, “Security is a conditional statement.”

Simply put, a mitigation can be abstracted into the following conditional statement:

1

2

3

4

5

if (is_allowed_by_mitigation_policy()) {

do_sensitive_action();

} else {

fail_fast();

}

This conditional statement contains many assumptions, which must stand to make the mitigation work.

If one of the assumptions cannot stay ceteris paribus, an attacker can break it by spoofing the system, thus bypassing the mitigation.

Data Execution Prevention (DEP)

DEP can be expressed with the following conditional statement:

1

2

3

4

5

if (PTE(address).NX == 0) {

execute(address);

} else {

fail_fast();

}

Here, assumptions include:

  • The code is trustworthy.
  • The W^X principle is strictly followed.

Is the code always trustworthy? Not really.

CPUs built on the x86 architecture use complex instruction set computing (CISC). As instruction words vary in length, different instructions can be obtained for the same data decoded from different locations. As a result, code can be re-decoded into other instructions, which, after being combined, can achieve arbitrary code execution. This is the underlying mechanism of the return-oriented programming (ROP) technique.

Can the W^X principle be strictly followed? Hardly so.

Because this not only means avoiding using PAGE_EXECUTE_READWRITE memory, but also requires maintaining W^X throughout the lifecycle of memory. However, applications with the just-in-time (JIT) feature, such as browsers, tend to violate such assumptions and therefore are often exploited to bypass DEP.

Control Flow Guard (CFG)

The conditional statement of CFG is:

1

2

3

4

5

if (CFG_Bitmap[address] == 1) {

call(address);

} else {

fail_fast();

}

Here, assumptions include:

  • The address in the CFG Bitmap is trustworthy.
  • The pointer used by CFG is trustworthy.

Is the address in the CFG Bitmap always trustworthy? Not really.

In fact, as CFG is just a coarse-grained implementation of CFI, most addresses in the CFG Bitmap should not be directly called. Abuse of such addresses will allow CFG bypass, leading to arbitrary code execution.

Is the pointer used by CFG always trustworthy?

The CFG implementation uses two key pointers: __guard_check_icall_fptr and __guard_dispatch_icall_fptr, which are protected just through read-only memory.

However, current Windows systems’ read-only memory is not really read-only. It is no difficult job to tamper with read-only memory via a pure-data attack. Once these pointers are tampered with, CFG will become useless.

Arbitrary Code Guard (ACG)

The conditional statement of ACG is:

1

2

3

4

5

if (W^X(address, flNewProtect)) {

change_protection(address, flNewProtect);

} else {

fail_fast();

}

Here, the assumption is:

  • The local dynamic link library (DLL) is trustworthy.

Is it really so? The answer is “No”.

It is possible to bypass ACG by leveraging the cache feature of browsers and dispatching arbitrary DLL files to a local disk drive.

Code Integrity Guard (CIG)

The conditional statement of CIG is:

1

2

3

4

5

if (is_signed_by_microsoft(file)) {

create_section(file);

} else {

fail_fast();

}

Here, the assumption is:

  • Microsoft-signed DLLs are trustworthy.

Is it always the case? Not really.

DLLs from an earlier version may behave differently in a later system. An example of this is ntdll.dll. Because of changes in system call IDs, the NtQueryDefaultUILanguage function in version 6.3.9600.17936 changes to NtContinue in version 10.0.15063.0. The two are, in nature, the same, but the difference is that the former can be indirectly called. Therefore, an attacker can load ntdll.dll of version 6.3.9600.17936 and then call the NtQueryDefaultUILanguage function, which is equivalent to calling of the NtContinue function, thus achieving arbitrary code execution.

Takeaway

Mitigations are implemented with many implicit assumptions. They can work only when these assumptions prove to be true and would be bypassed if these assumptions were broken.

During the R&D process, we should try to minimize assumptions and ensure that necessary assumptions are ceteris paribus, thereby making mitigations work to the best effect.