ARM을 공부하라고 한다.
먼저 leg.c와 leg.asm 파일을 다운받았다.
소스코드 분석
leg.c
#include <stdio.h>
#include <fcntl.h>
int key1(){
asm("mov r3, pc\n");
}
int key2(){
asm(
"push {r6}\n"
"add r6, pc, $1\n"
"bx r6\n"
".code 16\n"
"mov r3, pc\n"
"add r3, $0x4\n"
"push {r3}\n"
"pop {pc}\n"
".code 32\n"
"pop {r6}\n"
);
}
int key3(){
asm("mov r3, lr\n");
}
int main(){
int key=0;
printf("Daddy has very strong arm! : ");
scanf("%d", &key);
if( (key1()+key2()+key3()) == key ){
printf("Congratz!\n");
int fd = open("flag", O_RDONLY);
char buf[100];
int r = read(fd, buf, 100);
write(0, buf, r);
}
else{
printf("I have strong leg :P\n");
}
return 0;
}
main()
1. scanf로 key 값을 입력 받는다
2. key1() key2() key3() 값들이 사용자가 입력한 key 값과 일치하면 flag를 반환한다.
key1()
pc 값을 r3 레지스터에 저장한다.
key2()
1. r6 값을 스택에 저장한다.
2. r6 레지스터에 (pc값 + 1) 값을 저장한다.
3. bx(Branch and Exchange)의 약자로 r6에 저장된 주소로 분기한다.
사실 이 부분까지는 별 의미 없는것 같고
이 부분이 의미 있는 것 같다.
pc의 값을 r3에 저장하고, r3에 4만큼 더한 후, 스택에 r3를 push 한다.
key3()
1. r3에 lr 값을 저장한다.
lr이 뭘까? lr은 Link Register이다. 서브루틴 호출 시 호출한 주소를 저장하는데 사용된다. 서브루틴 호출 시 PC가 LR에 저장되고, 실행이 끝나면 LR에 저장된 주소가 PC로 복원된다.
leg.asm
asm 파일을 줬다. 해당 파일로 pc 레지스터 및 lr 값들을 유추할 수 있을 것이다. 분석해보자.
(gdb) disass main
Dump of assembler code for function main:
0x00008d3c <+0>: push {r4, r11, lr}
0x00008d40 <+4>: add r11, sp, #8
0x00008d44 <+8>: sub sp, sp, #12
0x00008d48 <+12>: mov r3, #0
0x00008d4c <+16>: str r3, [r11, #-16]
0x00008d50 <+20>: ldr r0, [pc, #104] ; 0x8dc0 <main+132>
0x00008d54 <+24>: bl 0xfb6c <printf>
0x00008d58 <+28>: sub r3, r11, #16
0x00008d5c <+32>: ldr r0, [pc, #96] ; 0x8dc4 <main+136>
0x00008d60 <+36>: mov r1, r3
0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf>
0x00008d68 <+44>: bl 0x8cd4 <key1>
0x00008d6c <+48>: mov r4, r0
0x00008d70 <+52>: bl 0x8cf0 <key2>
0x00008d74 <+56>: mov r3, r0
0x00008d78 <+60>: add r4, r4, r3
0x00008d7c <+64>: bl 0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0
0x00008d84 <+72>: add r2, r4, r3
0x00008d88 <+76>: ldr r3, [r11, #-16]
0x00008d8c <+80>: cmp r2, r3
0x00008d90 <+84>: bne 0x8dja8 <main+108>
0x00008d94 <+88>: ldr r0, [pc, #44] ; 0x8dc8 <main+140>
0x00008d98 <+92>: bl 0x1050c <puts>
0x00008d9c <+96>: ldr r0, [pc, #40] ; 0x8dcc <main+144>
0x00008da0 <+100>: bl 0xf89c <system>
0x00008da4 <+104>: b 0x8db0 <main+116>
0x00008da8 <+108>: ldr r0, [pc, #32] ; 0x8dd0 <main+148>
0x00008dac <+112>: bl 0x1050c <puts>
0x00008db0 <+116>: mov r3, #0
0x00008db4 <+120>: mov r0, r3
0x00008db8 <+124>: sub sp, r11, #8
0x00008dbc <+128>: pop {r4, r11, pc}
0x00008dc0 <+132>: andeq r10, r6, r12, lsl #9
0x00008dc4 <+136>: andeq r10, r6, r12, lsr #9
0x00008dc8 <+140>: ; <UNDEFINED> instruction: 0x0006a4b0
0x00008dcc <+144>: ; <UNDEFINED> instruction: 0x0006a4bc
0x00008dd0 <+148>: andeq r10, r6, r4, asr #9
End of assembler dump.
(gdb) disass key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.
(gdb) disass key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
(gdb) disass key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
(gdb)
main.asm
0x00008d64 <+40>: bl 0xfbd8 <__isoc99_scanf>
0x00008d68 <+44>: bl 0x8cd4 <key1>
0x00008d6c <+48>: mov r4, r0
0x00008d70 <+52>: bl 0x8cf0 <key2>
0x00008d74 <+56>: mov r3, r0
0x00008d78 <+60>: add r4, r4, r3
0x00008d7c <+64>: bl 0x8d20 <key3>
0x00008d80 <+68>: mov r3, r0
0x00008d84 <+72>: add r2, r4, r3
0x00008d88 <+76>: ldr r3, [r11, #-16]
0x00008d8c <+80>: cmp r2, r3
bl 은 Branch with Link의 약자로 서브루틴을 호출할때 사용된다.
1. 현재의 PC 값을 LR에 저장한다.
2. 지정된 주소로 분기한다.
결국 서브 루틴에서 저장한 값들을 r4, r3에 저장하고, 이 값들을 전부 더해서 r2에 저장한다. 즉 r2가 key1+key2+key3 값이다.
풀이 방법
각각의 레지스터 값을 계산해보자
key1
Dump of assembler code for function key1:
0x00008cd4 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cd8 <+4>: add r11, sp, #0
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr
End of assembler dump.
pc 값은 다음 명령어를 저장한다. 그렇다면 바로 밑에 줄인 0x8ce0 이라고 생각된다.(하지만 아니였다)
따라서 0x8ce0.
key2
Dump of assembler code for function key2:
0x00008cf0 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008cf4 <+4>: add r11, sp, #0
0x00008cf8 <+8>: push {r6} ; (str r6, [sp, #-4]!)
0x00008cfc <+12>: add r6, pc, #1
0x00008d00 <+16>: bx r6
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
0x00008d0a <+26>: pop {pc}
0x00008d0c <+28>: pop {r6} ; (ldr r6, [sp], #4)
0x00008d10 <+32>: mov r0, r3
0x00008d14 <+36>: sub sp, r11, #0
0x00008d18 <+40>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d1c <+44>: bx lr
End of assembler dump.
r3에 pc값을 저장하고, 그 값 + 4를 한 값이 key2의 값이다.
0x8d06+4 = 0x8d0a.
key3
Dump of assembler code for function key3:
0x00008d20 <+0>: push {r11} ; (str r11, [sp, #-4]!)
0x00008d24 <+4>: add r11, sp, #0
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
0x00008d30 <+16>: sub sp, r11, #0
0x00008d34 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008d38 <+24>: bx lr
End of assembler dump.
r3에 lr 값이 들어감으로 lr 값은
0x8d80 으로 예상된다.
따라서 0x8ce0 + 0x8d0a + 0x8d80 이면
정수로 108,394 이다.
어라... 뭐가 잘못된건지 안된다.
이유는 PC 레지스터 값이 저장될 때 바로 다음 명령어의 위치가 저장되는 것이 아니다.
파이프라이닝
파이프라이닝이란 무엇일까?
CPU의 성능을 향상시키기 위해 명령어를 병령 처리해서 동시에 실행하는 기법이다.
CPU의 명령어가 처리되는 과정을 인출(fetch)와 실행(execution) 등 다양하게 나눌 수 있지만, 예를들어 인출, 해석, 실행, 저장으로 나눠보자.
1. 명령어 인출(Instruction Fetch) : 명령어를 기억장치로 부터 인출
2. 명령어 해석(Instruction Decode) : 디코더를 사용하여 명령어 해석
3. 명령어 실행(Execute Instruction) : 해석된 명령어에 따라 데이터 연산을 수행
4. 결과 저장(Write Back) : 명령어대로 처리된 데이터를 메모리에 기록
(출처 https://www.robotstory.co.kr/raspberry/?vid=144)
파이프라이닝 미적용
파이프라이닝이 적용되지 않았을 때
파이프라이닝을 모른다면 3가지 명령어에 대해 다음과 같이 실행이 된다고 생각을 할 수 있다. 하지만 이렇게 실행되면 인출을 하고있지 않은 t2, t3, t4 시간에 인출을 안하고 버리는 시간이 생기게 된다.
따라서 하나의 명령어를 수행중이더라도 인출기능이 끝나면 다음 명령어의 인출을 하면 병렬적으로 빠르게 처리할 수 있다.
파이프라이닝 적용 시
출처 : https://www.robotstory.co.kr/raspberry/?vid=144
따라서 파이프라이닝을 적용하면 위에처럼 t6의 시간만에 명령어를 3개 수행할 수 있다.
이러한 파이프라이닝 기법은 모든 아키텍쳐에 동일하게 적용되는게 아니다. 각 아키텍쳐마다 각자의 방식대로 파이프라이닝을 개발하고 운용하여 빠른 명령어 실행을 하고 있다.
ARM 파이프라이닝
문제의 경우 ARM 아키텍쳐를 사용하기 때문에 ARM 아키택쳐의 파이프라이닝에 대해 봐보자.
ARM 장치는 컴파일러 복잡성을 강조하는 RISC 때문에 파이프라이닝이 필요하다.
3단계로 보면
1) Fetch
2) Deode
3) Execute
로 이루어진다.
각 단계에 대한 설명으론
1. Fetch는 메모리에서 명령어를 로드한다.
2. 디코드는 실행할 명령을 식별한다.
3. Execute는 명령어를 처리하고 결과를 레지스터에 작성한다.
이러한 단계들을 겹쳐서 실행 속도를 빠르게 한다.
ARM 파이프라인 특성
1) ARM 파이프라인은 execute 단계를 완전히 통과할 때까지 명령어를 처리하지 않는다.
2) execute 단계에서는 PC(Program Counter)가 항상 명령어 주소 + 8 바이트를 가리킨다.
3) 프로세서가 Thumb 상태인 경우 PC는 항상 명령어 주소 + 4 바이트를 가리킨다.
더보기
Thumb 상태란?
ARM 아키텍처의 하위 세트 중 하나로, ARM 명령어 세트보다 더 작은 코드 크기와 더 효율적인 코드 실행을 위해 설계되었다.
Thumb 명령어는 16비트로 구성되어 있고, ARM 명령어보다 더 작은 공간을 차지하며 실행 시간을 줄일 수 있다.
Thumb 상태에선 명령어의 주소가 4바이트씩 증가하므로, PC(Program Counter)는 명령어 주소 + 4 바이트를 가리킨다.
이는 ARM 상태에서 명령어 주소 + 8 바이트와는 달리, Thumb 상태에서의 PC 증가량을 나타낸다.
4) 분기 명령어를 실행하거나 PC를 직접 수정하여 분기할 때 ARM 코어는 파이프라인을 플러시한다.
5) execute 단계에 있는 명령어는 인터럽트가 발생했더라도 execute가 완료된다.
오류수정
위 ARM 파이프라인 특성에서 알 수 있듯이 execute 단계에서는 PC가 명령어 주소 + 8 바이트를 가리킨다고 되어있다.
key1
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
따라서 위 명령어에서 pc는 0x8ce4를 가르키고 있는 것이다!! (두둥) 맞는가 모르겄다.
그렇게 값을 다시 구해보면
key1
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
key1) 0x8ce4
key2
0x00008d04 <+20>: mov r3, pc
0x00008d06 <+22>: adds r3, #4
0x00008d08 <+24>: push {r3}
key2) 0x8d08 + 4
0x8ce4 + 0x8d0c + 0x8d80 = 0x1a770 이다.
이는 정수로 108,400이다.
exploit
성공이다.
ARM과 컴퓨터 구조에 대해 공부를 해야 풀 수 있는 문제인 것 같다.
만약 라업을 봤다면 다시한번 pipeline과 어셈블리어에 대해 공부해보자.