Description
(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
- Set up SVC32 mode
- CPSR = 0x40000013 (SVC32)
- sp_svc = 0x12345678
- Dump the registers (they look fine)
- Set up USR32 mode
- CPSR = 0x40000010 (USR32)
- sp_usr = 0x0010000
- Dump the registers (they look fine)
- 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
- 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.