This is a write-up for the pwning task ARVM from Codegate2022 Preliminaries.
This problem is basically an ARM32 emulator built on ARM32. In this program, we are allowed to run about 4000 bytes of shellcode. However, it wouldn't even be a challenge if that was all. The program analyzes the shellcode and emulates its behavior to verify it against some criteria.
To analyze the program, the program state is managed with the following structure.
struct state {
_DWORD *code;
char *stack;
char *data;
_DWORD **registers;
}
The function which I named emulate()
performs this action. It follows the flow of the code execution of the given shellcode, incrementing and jumping the program counter as needed.
int emulate@10bb0()
{
for ( inst = -1; sc->registers[PC] < (unsigned int)(sc->code + 4096); inst = cinst )
{
if ( (char *)sc->registers[PC] < sc->code )
break;
cinst = *sc->registers[PC];
sc->registers[PC] += 4;
if ( !inst )
break;
if ( inst != -1 && !cond_eval(inst) ) // all instructions must matter
invalid_instruction(inst);
type = inst_type(inst);
if ( type <= 4 )
{
switch ( type )
/* ... */
}
As the ARM32 architecture supports conditional execution for most instructions, the function evaluates current instruction's execution condition against the current CPSR. Here, if any of the reachable instructions are not executed, it considers the shellcode malicious and rejects its execution.
If that's not the case, the function checks the type of the instruction and undergoes further evaluation of the instruction. There are five types of instructions that this code supports, and they are arithmetic (also known as Data Processing in ARM ), multiplication, branching, software interrupt, and finally load/store instructions. It evaluates each instruction types and updates the program state according to the instruction. For example, arithmetic instructions calculates the results, updating the GPRs. It also updates the CPSR if the s flag is set on the instruction. In most cases, updates to the CPSR is made through this function.
int update_cpsr@12d4c(int a1)
{
int v1; // ST04_4
v1 = a1;
set_zf(a1);
set_nf(v1);
return 0;
}
It sets or clears the zero flag and negative flag on the CPSR according to the calculation result provided.
int check_b_bl@11f28(int inst)
{
int offset; // r0
offset = get_b_offset(inst);
if ( sc->registers[PC] + 4 * (4 * (offset >> 8) / 4) >= (unsigned int)(sc->code + 0x4000) )
return -1;
sc->registers[PC] += 4 * (4 * (offset >> 8) / 4);
return 0;
}
There are many other codes to support emulation of many other instructions of the ARM32 architecture. However, after throughly analyzing the code they were not useful for the exploit.
If the emulator were able to mimic the behavior of the real ARM32 core it was running on, breaking through would've been a harsh time. Thankfully, there is a mistake which causes a gap between this emulation and real execution.
int update_cpsr@12d4c(int a1)
{
int v1; // ST04_4
v1 = a1;
set_zf(a1);
set_nf(v1);
return 0;
}
The function update_cpsr()
calls the functions I named set_zf()
and set_nf()
to actually update the according flags in CPSR. However these two internal functions does not work as it would.
void set_zf@12c4c(int a1)
{
if ( a1 )
sc->registers[CPSR] |= 0x40000000u;
else
sc->registers[CPSR] &= ~0x40000000u;
}
void set_nf@12ccc(int a1)
{
if ( a1 )
sc->registers[CPSR] |= 0x80000000;
else
sc->registers[CPSR] &= 0x70000000u;
}
As you can see, both the zero flag and the negative flag is set if the evaluation result is not zero, causing a gap between emulation and real execution.
There are many different ways to exploit this flaw, but the branching instructions provide the easiest way of doing it. If an instruction clears the zero flag on emulation and sets it on real execution, a following conditional branch will allow us to make a block of instructions which is only reachable on real machine. Such instruction is very easy to make. I just used EORs r0, r0, r0
to set the zero flag. For the branch, I used bne
because equal/non equal conditional execution suffixes are straightforward. Then, just inserting any ARM32 shellcode between bne
and its branch destination will allow any shellcode to be executed. Please refer to the attached code for the exploit code.