Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unicorn 2 regression: ARM32 mode transition from USR32 to SVC32 in emulated code is allowed (privilege escalation) #1520

Closed
gerph opened this issue Dec 31, 2021 · 5 comments

Comments

@gerph
Copy link
Contributor

gerph commented Dec 31, 2021

(Firstly, thank you for the prior fixes for mode transitions - my OS is getting further through execution now)

Unicorn 2 is allowing the the transition from USR32 to SVC32 using emulated code.
This differs from bug #1500 which was that the operation was disallowed using direct access to the CPSR. This bug is where code is being executed in USR32 mode is able to gain privilege by the use of an MSR instruction.

The actual problem that I'm hitting in my OS is that I'm in SVC32 and Unicorn isn't allowing the transition to USR32, and investigation showed that the IS_USER() function believes we're in user mode, but the CPSR is currently 0x60000013 - ie SVC32. The two problems are, I believe related, and the USR32 -> SVC32 privilege escalation is the easier to demonstrate.

I think it is the change that is being made in the direct code which isn't updating the state properly.

The consequence of this problem is that whilst my operating system runs some code, depending on the operations performed for mode transitions the system may disallow a mode change in the emulated code which should have been allowed, or vice-versa.

Source environment

I built unicorn on the dev branch at sha cddc9cf.
This is after the fix for #1500 (which relates to corruption inability to change mode in code).
This has been tested on OSX system on Intel hardware (10.14.6)

In order to test this behaviour I have added additional debug which prints the state of registers within functions which were relevant. This code can be found here: https://github.com/gerph/unicorn/tree/cjf-debug-privileged-mode-problems and is shown as a change here: gerph@bda7f29

Failure mode

The failures appears to be that emulated code is able to escalate its privilege to priviledged mode because the state set in the CPSR and the state described by IS_USER() are inconsistent. I have been unable to tie down exactly where they're inconsistent, but I believe it's some interaction between the mode that is manipulated by the emulated code and the mode that is manipulated by the direct access in the management code.

What are we testing?

We want to set up the state of the system, and observe that we can freely change moves.

Sequence of tests

  1. Set up SVC32 mode
    • CPSR = 0x40000013 (SVC32)
    • sp_svc = 0x12345678
    • Dump the registers (they look fine)
  2. Set up USR32 mode
    • CPSR = 0x40000010 (USR32)
    • sp_usr = 0x0010000
    • Dump the registers (they look fine)
  3. Execute some code:
    E3C6601F : BIC     r6, r6, #&1F
    E3866013 : ORR     r6, r6, #&13            ; switch to SVC32 (should be ineffective from USR32)
    E121F006 : MSR     cpsr_c, r6
    E1A00000 : MOV     r0,r0
    EF000011 : SWI     OS_Exit
  1. Observe the final state
    • Dump the registers (the mode has changed - we've escalated to SVC32 from USR32)

What is the problem?

The emulated code should not be able to escalate its privilege.
This only seems to happen if the mode has been changed in the controlling python code to USR32 - I believe this state change is not being reflected properly in the variables. If the mode change to USR32 happens within emulated code with an MSR, everything is fine.

Test code

Test code which exhibits this problem:

#!/usr/bin/env python
# Sample code for ARM of Unicorn. Nguyen Anh Quynh <aquynh@gmail.com>
# Python sample ported by Loi Anh Tuan <loianhtuan@gmail.com>

import sys
import struct

from unicorn import (
        uc_version,
        UcError,
        Uc,
        UC_ARCH_ARM, UC_MODE_ARM,
        UC_ERR_EXCEPTION,
        UC_VERSION_MAJOR, UC_VERSION_MINOR, UC_VERSION_EXTRA)
from unicorn.arm_const import (UC_ARM_REG_R0,
        UC_ARM_REG_R1,
        UC_ARM_REG_R2,
        UC_ARM_REG_R3,
        UC_ARM_REG_R4,
        UC_ARM_REG_R5,
        UC_ARM_REG_R6,
        UC_ARM_REG_R7,
        UC_ARM_REG_R8,
        UC_ARM_REG_R9,
        UC_ARM_REG_R10,
        UC_ARM_REG_R11,
        UC_ARM_REG_R12,
        UC_ARM_REG_R13,
        UC_ARM_REG_R14,
        UC_ARM_REG_R15,
        UC_ARM_REG_SP,
        UC_ARM_REG_LR,
        UC_ARM_REG_PC,
        UC_ARM_REG_CPSR,
        UC_ARM_REG_SPSR)

reg_map = [
        UC_ARM_REG_R0,
        UC_ARM_REG_R1,
        UC_ARM_REG_R2,
        UC_ARM_REG_R3,
        UC_ARM_REG_R4,
        UC_ARM_REG_R5,
        UC_ARM_REG_R6,
        UC_ARM_REG_R7,
        UC_ARM_REG_R8,
        UC_ARM_REG_R9,
        UC_ARM_REG_R10,
        UC_ARM_REG_R11,
        UC_ARM_REG_R12,
        UC_ARM_REG_SP,
        UC_ARM_REG_LR,
        UC_ARM_REG_PC,
    ]
arm_names = [
        'r0', 'r1', 'r2', 'r3',
        'r4', 'r5', 'r6', 'r7',
        'r8', 'r9', 'r10', 'r11',
        'r12', 'sp', 'lr', 'pc'
    ]


ADDRESS = 0x1000
ARM_INSTS = """
E3C6601F : BIC     r6, r6, #&1F
E3866013 : ORR     r6, r6, #&13            ; switch to SVC32 (should be ineffective from USR32)
E121F006 : MSR     cpsr_c, r6
E1A00000 : MOV     r0,r0
EF000011 : SWI     OS_Exit
"""
ARM_WORDS = []
for line in ARM_INSTS.splitlines():
    if ' ' not in line or line.startswith('#'):
        continue
    word = line.split(' ')[0]
    value = int(word, 16)
    ARM_WORDS.append(struct.pack('<L', value))
ARM_CODE = b''.join(ARM_WORDS)

# Keep a copy of the code we're running (handy for manual debug and running in the full Pyromaniac OS)
with open('code', 'wb') as fh:
    fh.write(ARM_CODE)


def dump_registers(uc):
    print("Registers: ")
    for rn in range(0, 16):
        value = uc.reg_read(reg_map[rn])
        sys.stdout.write("  %3s : 0x%08x" % (arm_names[rn], value))
        if rn % 4 == 3:
            sys.stdout.write("\n")
    print("  CPSR = 0x{:08x}".format(uc.reg_read(UC_ARM_REG_CPSR)))
    print("  SPSR = 0x{:08x}".format(uc.reg_read(UC_ARM_REG_SPSR)))


def read_word(uc, address):
    data = uc.mem_read(address, 4)
    word = data[0] | (data[1]<<8) | (data[2]<<16) | (data[3]<<24)
    return word


# Test ARM
def test_arm():
    print("Testing under Unicorn : {!r}".format(uc_version()))
    print("Header version: {!r}".format((UC_VERSION_MAJOR, UC_VERSION_MINOR, UC_VERSION_EXTRA)))

    print("Changing ARM modes")
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)

        # initialize machine registers in different modes
        mu.reg_write(UC_ARM_REG_CPSR, 0x40000013)   # Current mode = SVC32 mode
        mu.reg_write(UC_ARM_REG_SPSR, 0x40000013)   # Saved mode = SVC32 mode
        mu.reg_write(UC_ARM_REG_R13, 0x12345678)    # SVC stack value
        mu.reg_write(UC_ARM_REG_R14, 0x00102220)    # SVC link value
        print("--- Should be in SVC32, with R13 = 0x12345678, R14 = 0x00102220")
        dump_registers(mu)

        mu.reg_write(UC_ARM_REG_CPSR, 0x40000010)   # Current mode = USR32 mode
        mu.reg_write(UC_ARM_REG_R13, 0x0010000)     # USR stack value
        mu.reg_write(UC_ARM_REG_R14, 0x0001234)     # USR link value
        print("--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234")
        dump_registers(mu)

        # Now the actual test, we're in USR mode and we are going to execute some code
        mu.mem_map(ADDRESS, 4096)                   # 4K at the execution address
        mu.mem_write(ADDRESS, ARM_CODE)

        print("--- Executing code at 0x{:08x}".format(ADDRESS))
        mu.reg_write(UC_ARM_REG_R15, ADDRESS)
        while True:
            pc = mu.reg_read(UC_ARM_REG_R15)
            print("Run code at 0x{:08x}".format(pc))
            dump_registers(mu)
            try:
                mu.emu_start(pc, ADDRESS + len(ARM_CODE))
            except UcError as e:
                if e.errno != UC_ERR_EXCEPTION:
                    raise
                # This is an exception call, so we want to process the SWI call.
                pc = mu.reg_read(UC_ARM_REG_R15) - 4
                word = read_word(mu, pc)
                swi_number = word & 0xFFFFFF
                print("--- Reached SWI at 0x{:08x} (0x{:x})".format(pc, swi_number))
                if swi_number == 0x11:
                    # OS_Exit - stop looping
                    break

                else:
                    # Unrecognised SWI call
                    raise

        print("--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234")
        dump_registers(mu)

    except UcError as e:
        print("ERROR: %s" % e)
        raise


if __name__ == '__main__':
    test_arm()

Failing output (Unicorn 2)

This produces the following output on Unicorn 2 (failing output) - using my branch which adds additional information about the state of some of the internal state:

Testing under Unicorn : (2, 0, 33554437L)
Header version: (2, 0, 5)
Changing ARM modes
QEMULog: CPU_LOG_INT: cpsr_write: 40000013, mask ffffffff
--- Should be in SVC32, with R13 = 0x12345678, R14 = 0x00102220
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x12345678   lr : 0x00102220   pc : 0x00000000
  CPSR = 0x40000013
  SPSR = 0x40000013
QEMULog: CPU_LOG_INT: cpsr_write: 40000010, mask ffffffff
QEMULog: CPU_LOG_INT: AArch32 mode switch from svc to usr PC 0x0
--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x00010000   lr : 0x00001234   pc : 0x00000000
  CPSR = 0x40000010
  SPSR = 0x00000000
--- Executing code at 0x00001000
Run code at 0x00001000
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x00010000   lr : 0x00001234   pc : 0x00001000
  CPSR = 0x40000010
  SPSR = 0x00000000
QEMULog: CPU_LOG_INT: arm_tr_init_disas_context: dc->user = 0, dc->mmu_idx = 18, dc->current_el = 1
QEMULog: CPU_LOG_INT: msr_mask: flags 00000001
QEMULog: CPU_LOG_INT:           valid mask = ff1fffff (features=1ca71ef979)
QEMULog: CPU_LOG_INT:          = mask 000000df
QEMULog: CPU_LOG_INT: trans_MSR_reg: mask 000000df
QEMULog: CPU_LOG_INT: gen_set_psr: mask 000000df
QEMULog: CPU_LOG_INT: gen_set_cpsr: mask 000000df
QEMULog: CPU_LOG_INT: cpsr_write: 00000013, mask 000000df
QEMULog: CPU_LOG_INT: arm_tr_init_disas_context: dc->user = 0, dc->mmu_idx = 18, dc->current_el = 1
--- Reached SWI at 0x00001010 (0x11)
--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000013   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x12345678   lr : 0x00102220   pc : 0x00001014
  CPSR = 0x40000013
  SPSR = 0x40000013

The final register dump should have sp = 0x00010000, and CPSR = 0x40000010.

At the point of execution (--- Executing code at 0x00001000) the register dump shows that that CPSR contains 0x40000010 (USR32 mode). However, the debug from within the function arm_tr_init_disas_context shows that the internal state still thinks that we're in a privileged mode - current_el != 0 and therefore dc->user is false.

It is this state which then allows the subsequent changes of the state to happen. In msr_mask there is a check IS_USER() which checks the state of dc->user and finding that we're in a privileged mode allows the mode change to escalate.

Successful output (Unicorn 1)

Does not include the additional debug from the internal state:

Testing under Unicorn : (1, 0, 256L)
Header version: (1, 0, 2)
Changing ARM modes
--- Should be in SVC32, with R13 = 0x12345678, R14 = 0x00102220
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x12345678   lr : 0x00102220   pc : 0x00000000
  CPSR = 0x40000013
  SPSR = 0x40000013
--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x00010000   lr : 0x00001234   pc : 0x00000000
  CPSR = 0x40000010
  SPSR = 0x00000000
--- Executing code at 0x00001000
Run code at 0x00001000
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000000   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x00010000   lr : 0x00001234   pc : 0x00001000
  CPSR = 0x40000010
  SPSR = 0x00000000
--- Reached SWI at 0x00001010 (0x11)
--- Should be in USR32, with R13 = 0x00010000, R14 = 0x0001234
Registers: 
   r0 : 0x00000000   r1 : 0x00000000   r2 : 0x00000000   r3 : 0x00000000
   r4 : 0x00000000   r5 : 0x00000000   r6 : 0x00000013   r7 : 0x00000000
   r8 : 0x00000000   r9 : 0x00000000  r10 : 0x00000000  r11 : 0x00000000
  r12 : 0x00000000   sp : 0x00010000   lr : 0x00001234   pc : 0x00001014
  CPSR = 0x40000010
  SPSR = 0x00000000

Observe that the CPSR at the end is correctly set to 0x40000010 - the privilege escalation has not happened.

@wtdcode
Copy link
Member

wtdcode commented Dec 31, 2021

Thanks for your report. Would have a look asap.

@wtdcode wtdcode added bug and removed bug labels Jan 4, 2022
wtdcode added a commit that referenced this issue Jan 4, 2022
@wtdcode
Copy link
Member

wtdcode commented Jan 4, 2022

It is introduced because of 4f73d75. Fixed in 47097b5

@wtdcode wtdcode closed this as completed Jan 4, 2022
@gerph
Copy link
Contributor Author

gerph commented Jan 4, 2022

This now seems to have broken the CPSR register when mode changes happen - I'll raise a new ticket for this.

@wtdcode
Copy link
Member

wtdcode commented Jan 4, 2022 via email

@gerph
Copy link
Contributor Author

gerph commented Jan 4, 2022

No worries. Hopefully with each example and fix we get closer to getting the right combination.

I'm really glad that I've got a pretty reliable OS to be able to test this against, and tests for many of the low level operation (although not the instructions) so that I can find problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants