포스트

포너블을 위한 ARM 바이너리 디버깅 및 ARM 아키텍처 공부

ARM 분석 환경 설정부터 공부한 내용 정리

포너블을 위한 ARM 바이너리 디버깅 및 ARM 아키텍처 공부

올해 첫 글이 되겠다
드림핵 ARM 기초 로드맵에 ARM 두 문제가 있는 걸로 알고 있는데 가장 쉬운 문제 하나를 전에 푼 적이 있었다

그때 나름대로 찾아보면서 레지스터랑 호출 규약 좀 공부했었는데 기억이 1도 안 난다
어차피 언젠가 한 번은 arm 문제를 마주할 것 같아서 이왕 직접 써서 정리해놓으려 한다

아마 이런 내용들이 드림핵 강의에 있겠지만 나는 돈없는 학생이기에..
목차는 나오니까 목차대로 찾아서 정리해보자
머리가 없으면 몸이 고생하지만 돈이 없으면 머리도 몸도 고생한다


QEMU 환경 설정

우선 본인은 맥북을 가장 많이 사용하는 관계로 arm64 아키텍처를 사용 중이다
그렇다고 워게임에서 주어지는 arm32 바이너리를 바로 실행할 수 있는 건 아니라서 amd64 우분투 VM에서 arm 분석 환경을 구축할 생각이다

처음엔 도커로 하려고 지피티한테 Dockerfile 써달라 했는데 QEMU라는게 더 편해보여서 이걸로 진행한다

  • Host: Apple Silicon M1
  • VM: UTM
  • Image: Linux Ubuntu 24.03.1 LTS (Server, amd64)

맥에서 Desktop 이미지(GUI)를 쓰면 사용을 못 할 정도로 렉이 심해서 서버 이미지(CLI)를 쓰는 것이 훨씬 낫다

본인의 경우 포너블 공부를 하기 위한 VM이기 때문에 CLI로만 작업하는게 오히려 깔끔하고 편하다.. 이것저것 설정 만지고 오류 풀면서 명령어 공부는 덤


1
2
3
4
5
6
sudo apt update
sudo apt install -y \
  qemu-user qemu-user-static \
  gdb-multiarch \
  binutils-arm-linux-gnueabihf \
  libc6-armhf-cross libstdc++6-armhf-cross

필요한 패키지들이다


실행

1
qemu-arm-static -L /usr/arm-linux-gnueabihf ./arm_training-v2

패키지 설치하고 qemu-arm-static 명령어를 사용해서 amd64 환경에서도 arm 바이너리를 실행할 수 있다

-L 옵션으로는 로더가 위치한 경로를 준다 (아마 기본이 /usr/arm-linux-gnueabihf인 듯?? 아님 말고)


Image

이렇게 /lib/ld-linux.so.3 파일이 없어서 실행이 안 되는 경우가 있다


Image

이런 오류가 뜨는 이유는 아마 바이너리 문제 자체가 ld-linux.so.3를 로더로 사용하도록 만들어져서 그런가보다


Image

실제로 경로로 이동해보면 비슷한 네임이 있긴 하다
이거 이름을 바꾸던가 파일을 복제해서 ld-linux.so.3로 설정해두면 된다
(만약 없다면 libc6-armhf-cross, libstdc++6-armhf-cross 패키지가 잘 설치됐는지 확인해보기를 추천한다)

본인은 혹시 armhf 로더를 쓰는 바이너리가 있을 수 있으니 남겨놓긴 했다
근데 사실 있어봐야 patchelf 쓰거나 로더만 만들면 되는 거라 상관 없긴 할 것 같다


디버깅

Image

먼저 qemu-arm-static으로 바이너리를 실행하는데 여기서 -g 인자로 안 쓰는 포트 번호를 입력한다
(사진에서는 arm 명령어를 사용했는데 alias라서 사실은 qemu-arm-static -L {/lib/ld-linux.so.3 위치} 이다)

다른 터미널 세션에서 gdb-multiarch를 실행하고 target remote :{port}로 연결하면 디버깅을 진행할 수 있다


ARM

Advanced RISC Machine
RISC: Reduced Instruction Set Computer

ARM32(AArch32), ARM64(AArch64) 이렇게 있는데 요즘에는 ARM64를 많이 사용하는 추세이지만 일단 지금까지 드림핵에서는 ARM32가 나왔으니 별도의 언급이 없으면 ARM32를 기준으로 정리한다


프로세서

평범하게 알고 있는 프로세서와 기본적인 구조는 같다

ARM에 있는 큰 차이점은 Barrel Shifter와 Register Bank이다

Barrel Shifter는 산술 연산에서 사용되는 Shift 연산 가속기라고 할 수 있다
Barrel Shifter는 이게 지금 컴구 공부도 아니고 사실 익스플로잇하는데 크게 도움이 될 지식은 아닌 것 같긴 한데 3학년 되면 들어야 되니까 그냥 써봤다

Register Bank는 32비트 크기의 범용 레지스터 31개와 상태 레지스터 6개가 묶여 있는 Register의 집합이라고 할 수 있다


아키텍처

가장 큰 특징은 Thumb 명령어 세트이다
기본 32비트 길이의 명령어보다 짧은 16비트 길이의 명령어 세트이다

Thumb의 확장형으로 32비트 길이 명령어가 추가된 명령어 세트도 있다
이는 기존 16비트 Thumb 명령어와 혼합하여 사용할 수 있다



RISC

RISC 구조는 CPU 명령어의 수를 최소화하고 단순화하는 고속 고효율 컴퓨터 설계 방식이다

  • 32비트 길이의 고정된 명령어 길이를 사용한다
    • ARM32: 기본 32비트 / Thumb 명령어는 16(32)비트
    • ARM64: 기본 32비트
  • load(ldr), store(str) 방식 사용
    • 메모리에 직접 데이터를 쓰거나 가져올 수 없다
    • 메모리 값을 레지스터에 load하고, 레지스터 값을 메모리에 store해야 한다

amd64와 동일하게 리틀 엔디안 방식을 사용한다
(사실 arm은 리틀 엔디안, 빅 엔디안 모두 사용할 수 있지만 기본적으로 리틀 엔디안 방식을 사용한다)


동작 모드 (Modes of Operation)

User, FIQ(Fast IRQ), IRQ(Interrupt Request), SVC(Supervisor), Abort, Undefiend, System

ARM에는 7개의 동작 모드가 있다
프로세서가 어떤 일을, 어떤 권한으로 처리 중인지 나타내는 동작 상태이다

모드는 인터럽트나 에러 또는 개발자에 의해 전환될 수 있다
Exception에 의해 모드가 변경된다

(근데 대충 듣기로는 문제 풀 때는 User Mode만 생각하면 된다고 하더라)


괄호 안에 CPSR.M 비트 포함

  • User (USR, 10000)
    • User Task나 Application이 실행될 때 사용되는 모드
    • 유일한 비특권 모드
    • 메모리, I/O 장치와 같은 시스템 리소스에 대해 제한을 두어 사용자의 실수를 방지
    • 소프트웨어 인터럽트를 발생시켜 다른 모드(SVC)로 이동
  • Fast IRQ (FIQ, 10001)
    • 2개의 인터럽트 소스 중 빠르게 인터럽트를 처리할 수 있도록 구성된 모드
    • 빠른 처리를 위해 Exception Vector에서 최하단에 존재
    • 빠른 처리를 위해 별도의 레지스터 소유
    • (Exception) Fast Interrupt 발생하면 전환됨
  • Interrupt Request (IRQ, 10010)
    • (Exception) 하드웨어적인 Interrupt 발생 시 전환됨
  • Supervisor (SVC, 10011)
    • 대부분의 시스템 리소스를 자유롭게 관리할 수 있는 모드
    • 커널이나 디바이스 드라이버를 처리(System Call)할 때 동작
    • (Exception) Power on이나 Reset 신호 입력 시 또는 SWI가 발생하면 SVC 모드로 전환됨
  • Abort (ABT, 10111)
    • 메모리에서 명령을 읽거나 데이터를 읽고 쓰는 과정에서 오류가 발생하면 Abort Mode로 전환하여 오류 처리
    • (Exception) 접근할 수 없는 주소에 접근하거나 명령어 fetch에 실패하면 전환된다
      • MMU나 MPU를 사용하는 경우 Access Protection이 걸린 주소로 접근할 때 Exception이 발생한다
  • Undefined (UND, 11011)
    • (Exception )명령어를 읽어 실행하고자 하나 읽은 명령이 디코더에 정의되어 있지 않으면 전환된다
  • System (SYS, 11111)
    • User 모드와 동일한 레지스터를 동일한 용도로 사용한다
    • 특권을 가진다



User 모드는 Normal(표준 사용자) 모드, 그 외의 6개 모드는 Privileged(특권) 모드로 분류할 수 있다
(User 모드만 유일한 비특권 모드)

Privileged 모드까리는 서로 상호 전환이 가능하고 Normal 모드로도 전환이 가능하지만, Normal 모드에서 Privileged 모드로 전환은 불가능하다

User/System 모드 이외의 나머지 모드들을 Exception 모드로 분류할 수도 있다


User 모드는 OS 상에 올라가는 애플리케이션이 사용한다
OS는 기본적으로 System 모드이다

만약 OS나 애플리케이션이 시스템 리소스를 사용할 때에는 SVC 모드로 전환 후 사용한다
이때 SVC 모드로 전환되어 시스템 리소스를 사용할 권한을 얻는 것을 OS에서는 System Call이라 말한다

OS에서는 애플리케이션에 필요한 메모리 공간과 OS 실행에 따른 메모리 공간을 분리시켜서 관리한다
애플리케이션에서 접근 권한이 없는 시스템 리소스에 접근하는 것을 허용하기 위해서 OS는 프로세서가 제공하는 모드 전환을 System Call 함수로 정의하여 개발자에게 제공한다



User와 System 모드는 같은 스택을 사용하기에 거의 차이가 없는 것 같지만 User 모드에는 시스템 리소스를 사용하는 데 제한이 있다

SVC 모드는 Reset 신호를 입력했을 때 접근하는 모드로 시스템 리소스를 자유롭게 관리할 수 있고 별도의 스택을 사용한다

ARM의 기본 모드는 SVC 모드이다
부팅 시 거의 모든 권한을 사용해야 하기 때문에 부팅 또는 리셋 시 SVC 모드가 필요하다


Exception

Exception이 발생하면 프로세서는 Exception의 종류에 해당하는 모드에 진입하고 해당 Exception에 대응되는 주소를 PC에 저장하고 Exception에 대한 처리를 진행한다
Interrupt 또한 Exception의 일종이다

Exception

사실 이 글의 내용을 많이 참고했기 때문에 차라리 이 글을 보는 게 낫다
본인은 개인적인 공부 목적으로 정리할 겸 작성하는 것이기에..


Image

Exception이 발생하면 해당 Exception에 해당하는 주소의 프로그램을 실행하는데 그 주소를 Exception Vector라고 한다
그 Exception Vector들을 모아놓은 테이블이 Exception Vector Table이라고 한다

User 모드에서 Privileged 모드로 전환하는 Exception은 있지만 그 역은 없다
이 말은 즉슨, User 모드로 한 번 가면 Exception이 있어야만 Privileged 모드로 전환할 수 있다는 말이다


Exception에도 우선 순위가 있어서 여러 exception이 발생해도 순서대로 처리한다

Reset -> Data Abort -> FIQ -> IRQ -> Prefetch Abort -> Undefiend Instruction -> SWI




레지스터

레지스터의 용도에 대한 약속을 APCS(ARM Procedure Call Standard)라고 한다


범용 레지스터

R0, R1, … , R12 이렇게 13개의 레지스터가 범용 레지스터로 사용된다
여기서 R11은 SFP(Stack Frame Pointer)로 사용된다

  • R0 : 함수의 리턴 값이 저장된다
  • R0, R1, R3 : 함수를 호출할 때 전달할 인자가 저장된다
  • R11 : SFP, Stack Frame Pointer, 스택 프레임의 주소가 저장된다
  • R12 : IP, Intra-Procedure-call scratch register


특수 레지스터

R13, R14, R15는 특수 레지스터로 사용된다

  • R13 : SP, Stack Pointer, 스택의 가장 위를 가리키는 포인터가 저장된다
  • R14 : LR, Link Register, 서브루틴(jump) 후에 돌아갈 리턴 주소가 저장된다
    • 함수를 호출하기 전에 저장된다
  • R15 : PC, Program Counter, 현재 fetch 중인 명령어를 fetch해온 주소로 현재 실행 중인 명령어의 다음 주소가 아닌 다다음 주소가 저장된다


CPSR (Current Program Status Register)

Image

현재 모드의 상태를 저장하는 레지스터이다
사실상 이 레지스터의 값으로 모드가 결정된다


SPSR (Saved Program Status Register)

CPSR 값을 그대로 복사한 값을 저장하는 특수한 레지스터이다

SPSR에 현재 CPSR을 복사하여 저장해놓고 모드를 변경하는데 다시 원래 모드로 돌아올 때 SPSR에 저장해두었던 값을 사용하여 CPSR을 복구할 수 있다


Condition Flags

MSB에서부터 4개 비트는 N, Z, C, V로 Condition Flags를 저장하는 플래그 필드로 사용된다

  • N(Negative) : 연산 결과가 음수인 경우 1로 설정
  • Z(Zero) : 연산 결과가 0인 경우 1로 설정
  • C(Carry) : 연산 결과에 캐리가 발생한 경우 1로 설정
  • V(oVerflow) : 연산 결과에서 오버플로우가 발생한 경우 1로 설정

ARM에서는 명령어를 fetch한다고 해서 그냥 실행하지 않고 Condition Flag를 확인해 다음의 명령어를 실행한다


T

Thumb 모드를 나타내는 필드인데 Thumb 모드일 경우 1로 설정된다


M

LSB에서부터 5개 비트는 모드를 나타내는 정보를 저장하는 데 사용된다
CPSR.M으로 표기하는데 각 모드별 비트는 [Modes of Operation]에 같이 써놓았다


Mask bits

LSB 기준 7번째 비트에서부터 9번째 비트까지 영역을 마스크 비트라고 하는데 Exception과 Interrrupt를 비활성화할 때 사용된다

상단 이미지에서 볼 수 있듯 순서대로 F, I, A 비트이다

  • F(FIQ) : FIQ 비활성화
  • I(IRQ) : IRQ 비활성화
  • A(Abort) : Asynchronoous Abort 비활성화

각각의 비트를 1로 설정하면 비활성화된다


모드별 레지스터

각 동작 모드마다 사용되는 레지스터 세트가 다른데 그렇다고 해서 전부 각자 다른 레지스터를 쓰는 게 아니라 공통으로 사용하는 레지스터가 있고 일부 레지스터들만 따로 사용하는 것이다

일단 결론부터 이야기하면 총 37개의 레지스터를 가진다


Image

글로 설명하려 했는데 그림이 가장 직관적이고 이해하기 쉬워서 그냥 이미지 첨부한다

우선 앞서 이야기했듯 User 모드와 System 모드는 완전히 같은 레지스터를 사용한다
(User/System : R0, R1, … , R14, R15(PC), CPSR => 17개)

FIQ, SVC, ABT, IRQ, UND 모드(Exception 모드)에서는 R0, R1, … , R7 8개의 레지스터와 R15, CPSR 레지스터를 공유한다
다른 R8, R9, … , R14 7개의 레지스터는 User/System 모드와 다른 레지스터를 사용한다
여기서 Exception 모드끼리는 또 R8, R9, … , R12 5개의 레지스터를 공유한다
(Exception : R8, R9, … , R12 => 5개)

Exception 모드에서 그 외 R13, R14, SPSR 레지스터는 각자 다른 레지스터를 사용한다
(Exception : R13, R14, SPSR => 3 * 5 = 15개)

FIQ에 레지스터가 별도로 존재하는 이유
FIQ는 Fast Interrupt Request라는 이름에 맞게 빠른 처리가 필요하기 때문이다

Banked Register
그림에서 삼각형으로 표시된 FIQ의 R8, R9, … , R12 레지스터, 그리고 Exception 모드의 R13, R14, SPSR 레지스터들을 Banked Register라고 한다



중요한 건 모든 모드는 CPSR, R15(PC), R0~R7 레지스터를 공유한다는 것이다
그리고 User와 System은 같은 레지스터 세트를 사용한다는 것

사실 컴구 관점에서 중요한 내용이지 않을까 싶은데 간단한 워게임 익스플로잇 관점에서는 User 모드만 보기에 어떤 레지스터가 있는지만 알면 될 것 같다

Thumb 모드에서 사용되는 레지스터
Thumb 모드와 ARM 모드에서 사용되는 레지스터의 범위에도 차이가 있다

ARM 모드는 R0, R1, … ,R12 레지스터를 모두 사용하지만, Thumb 모드의 경우 R0, R1, … , R7 레지스터를 사용한다
다만 R13(SP), R14(LR)는 모두 사용한다

위 그림에서 진하게 색칠된 레지스터가 Thumb 모드에서 사용되지 않는 레지스터이다



ARM 어셈블리 명령어

ARM은 ARM Instruction Set와 Thumb Instruction Set 두 종류의 명령어 집합이 있다
ARM Instruction Set의 명령어는 모두 32비트, Thumb Instruction Set의 명령어는 모두 16비트의 길이를 가진다

이렇듯 명령어 집합이 두 개이기에 명령어를 실행하는 프로세서의 상태도 두 개이다
ARM Instruction Set의 명령어를 실행하는 프로세서의 상태를 ARM State, Thumb Instruction Set의 명령어를 실행하는 상태를 Thumb State라고 한다


Instruction의 Operand는 레지스터 또는 데이터 자체가 될 수 있다

아래 명령어에 대한 예시에서 레지스터는 Rd, Rn, Rm, … 등으로 표기한다
결과가 저장되는 operand를 destination operand라 하여 Rd로 표기한다
다른 operand는 source operand로, 레지스터인 경우 Rn 또는 Rm 등으로 표기하지만 레지스터 또는 데이터 자체(상수)인 경우 Op2, Op3 등으로 표기한다

label은 메모리 상 주소를 나타내지만 대개 의미를 갖는 하나의 명칭으로 쓰인다



데이터 처리

산술 연산
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; Addition (with Carry)
add Rd, Rn, Op2   ; Rd에 Rn + Op2 저장
adc Rd, Rn, Op2   ; Rd에 Rn + Op2 + carry 저장

; Subtraction (with Carry)
sub Rd, Rn, Op2   ; Rd에 Rn - Op2 저장
sbc Rd, Rn, Op2   ; Rd에 Rn - Op2 - !carry 저장

; Reverse Subtraction (with Carry)
rsb Rd, Rn, Op2   ; Rd에 Op2 - Rn 저장
rsc Rd, Rn, Op2   ; Rd에 Op2 - Rn - !carry 저장

; Multiple
mul Rd, Rn, Rm    ; Rd에 Rn * Rm 저장

; Division (Unsigned/Signed)
udiv Rd, Rn, Rm   ; Rd에 Rn / Rm 저장
sdiv Rd, Rn, Rm   ; Rd에 Rn / Rm 저장
논리 연산
1
2
3
4
5
; Logical AND, OR, XOR, Bit Clear
and Rd, Rn, Op2  ; Rd에 Rn & Op2 저장  
orr Rd, Rn, Op2  ; Rd에 Rn | Op2 저장
eor Rd, Rn, Op2  ; Rd에 Rn ^ Op2 저장
bic Rd, Rn, Op2  ; Rd에 Rn & !Op2 저장
비교 연산
1
2
3
4
5
6
7
; Compare (Negative)
cmp Rn, Op2  ; Status Flag에 Rn - Op2 반영
cmn Rn, Op2  ; Status Flag에 Rn - (-Op2) 반영

; Test Bits/Equivalence
tst Rn, Op2  ; Status Flag에 Rn & Op2 반영
teq Rn, Op2  ; Status Flag에 Rn | Op2 반영
레지스터
1
2
3
; Move (Negative)
mov Rd, Op2  ; Rd에 op2 저장
mvn Rd, Op2  ; Rd에 ~op2 저장


산술 연산의 add, adc, sub, sbc, rsb, rsc / 논리 연산의 and, orr, eor, bic 명령어의 뒤에 s를 붙이면 CPSR의 플래그 값을 설정할 수 있다
반면 비교 연산의 cmp, cmn, teq, tst는 s를 붙이지 않아도 CPSR의 플래그 값이 설정된다


분기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
; Unconditional Branch
b label     ; label로 무조건 분기
b .         ; 무한 루프 (제자리로 분기)

; Conditional Branch
bcs label   ; C Flag = 1 (Rn이 Op2보다 크거나 같으면 분기)
bhs label   ; C Flag = 1 (Rn이 Op2보다 크거나 같으면 분기)

bcc label   ; C Flag = 0 (Rn이 Op2보다 작으면 분기)
blo label   ; C Flag = 0 (Rn이 Op2보다 작으면 분기)

beq label   ; Z Flag = 1 (Rn이 Op2와 같으면 분기)
bne label   ; Z Flag = 0 (Rn이 Op2와 다르면 분기)

bls label   ; C Flag = 0 || Z Flag = 1 (Rn이 Op2보다 작거나 같으면 분기)
bhi label   ; C Flag = 1 || Z Flag = 0 (Rn이 Op2보다 크면 분기)

bmi label   ; N Flag = 1 (Rd가 음수면 분기)
bpl label   ; N Flag = 0 (Rd가 0 또는 양수면 분기)

bvs label   ; V Flag = 1 (Rd에서 오버플로우가 발생하면 분기)
bvc label   ; V Flag = 0 (Rd에서 오버플로우가 발생하지 않으면 분기)

; Conditional Branch (signed)
bge label   ; N Flag = V Flag (Rn이 Op2보다 크거나 같으면 분기)
blt label   ; N Flag != V Flag (Rn이 Op2보다 작으면 분기)

bgt label   ; Z Flag = 0 || N Flag = V Flag (Rn이 Op2보다 크면 분기)
ble label   ; Z Flag = 1 || N Flag != V Flag (Rn이 Op2보다 작거나 같으면 분기)

; Subroutine
bl label    ; Link 후 label이 있는 주소로 분기  
bx lr       ; LR에 저장된 주소로 복귀

조건 분기에서는 명령어 실행 전 반드시 플래그에 영향을 주는 명령어들이 실행되어야 한다
결과 저장 없이 비교 및 테스트만 필요한 경우에는 cmp, tst, teq 명령어가 사용된다

bl 명령어가 실행되면 다음 명령어의 주소(복귀할 주소)를 LR(R14)에 저장하고, 서브루틴(label)의 주소를 PC(R15)에 저장한다
bx lr 명령어가 실행되면 LR에 저장된 값을 PC에 저장하여 복귀한다

bxblx 명령어를 사용하여 State를 상호 전환 가능하다
사용할 Instruction Set를 전환할 때에는 프로세서의 상태 또한 전환해주어야 한다



메모리 접근

1
2
3
4
5
6
7
8
; Load
ldr Rd [Rx]      ; Rx를 참조해 Rd에 로드
ldr Rd, [Rx, #0x00]  ; Rx를 참조해 Rd에 로드 후 Rx에 #0x00을 더함
ldr Rd, [Rx, $000]  ; Rx를 참조해 Rd에 로드 후 Rx에 #0x00을 더함

; Store
str `register1`, [`register2`]          ; register1의 값을 register2를 참조해 저장
str `register1`, [`register2`], `#num`  ; register1의 값을 register2를 참조해 저장 후 register2에 num을 더함



ARM 함수 구조

AAPCS

인자 전달 리턴값 callee-saved caller-saved

함수 프롤로그/에필로그

리턴은 pop {…, pc} or BX LR LR이나 PC 덮을 수 있는지

Stack Frame

지역 변수 저장된 레지스터 saved LR alignment

ARM Stack Buffer Overflow (Dreamhack #860)

ARM Return Oriented Programming (Dreamhack #861)

Dreamhack #862


References
이 게시글은 저작권자의 CC BY 4.0 라이센스를 따릅니다.