[Pwnable 풀이] 11. leg
이번문제는 arm 어셈블리 언어에 대한 문제이다 해당 링크의 파일을 확인해보자
#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;
}
leg.c의 내용
(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 0x8da8 <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)
leg.asm의 내용
이번 문제는 우리에게 익숙한 인텔 어셈블리어가 아닌 arm 어셈블리어에 관한 문제로 해당 문제를 푸는 동안 아래 블로그의 어셈블리 명령어 설명을 참조했다. 더 공부하고 싶으신 분들은 아래 링크 참조.
https://johyungen.tistory.com/506
ARM 기초 (레지스터 종류, 명령어 등, LDR, STR)
레지스터 종류 R0 ~ R12 : 범용 레지스터 (다목적 레지스터), R11(스택 프레임 포인터) R0 : 함수 리턴 값 저장 (EAX 같은 느낌) R0 ~ R3 : 함수 호출 인자 전달 R13 ~ R15 : 특수 레지스터 R13(SP) : 스택 포인터
johyungen.tistory.com
LDR과 STR 명령어 이해한다고 눈알 빠지는줄 알았지만 결론적으로 문제를 해결하는데에는 큰 이해는 필요하지 않았다.먼저 코드를 살펴보면 key1, key2, key3 함수의 리턴 값을 모두 더해 입력값과 비교해서 flag를 출력해준다. 세가지 함수 모두 어셈블리어로 적혀져 있기에 어셈블리어로 함수를 뜯어보자.
먼저 메인 함수에서 세가지 함수를 호출하는 명령어 부분이다.
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
모든 함수에서 리턴값은 r0에 저장되는 것으로 보이기 때문에 세개 함수 모두 r0에 저장되는 값을 눈여겨 보면 될 것이다.
<key1>
0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>: mov r0, r3
아주 간단하다, r3에 pc 값이 저장되는데 pc는 program counter 값으로 arm 아키텍처의 pc 레지스터에는 fetch 단계에 있는 명령어의 주소가 저장되어 있다.
보통 프로세서의 명령어 처리는 fetch->decode->execute 과정으로 파이프라이닝되어 수행되어서, 해당 key1 <+8> 라인이 실행될 때 fetch 단계에 있는 명령어는 key1 <+8>의 다음 다음 명령어인 key1 <+16>이다. 따라서 r3 레지스터에 저장되는 값은 0x8ce4 값이다.
<key2>
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
<+24>, <+26>, <+28> 라인은 무시해도 좋다. 여기도 r3와 관련된 것만 확인하자면 <+20>에서 pc값이 저장되고 <+22>에서 4가 더해지기 때문에 0x8d0c 값은 쉽게 구할 수 있다. 하지만 <+12>와 <+16>에서 조금 헤맸기 때문에 문제 해결에 직접적인 관련은 없지만 잠깐 설명하고 넘어가겠다.
<+16> 라인 이후로 명령어의 크기가 작아졌음을 확인할 수 있다, 라인 한줄마다 메모리 주소가 4씩 증가하다 2씩 증가하고 있기 때문이다. 이는 bx 명령어의 용도 때문인데 bx 명령어는 값으로 주어진 레지스터의 주소로 이동하는 역할을 한다. 그런데 이때 레지스터에 주어진 값이 홀수이면 arm 아키텍쳐의 저전력 모드인 thumb 모드로 전환을 하고 짝수이면 arm 모드로 전환을 하기 때문이다.
ARM과 Thumb 모드란?
원래 ARM은 32bit RISC machine으로 word 단위가 32bit, 한번에 32비트씩 처리할 수 있지만, 16bit를 word 단위로 하는 Thumb 모드로 전환을 통해 code 압축 효과와 성능 개선효과가 있고, 구 시대의 16bit word 단위로 작성된 언어 또한 작동 시킬수 있다는 장점이 있다.
때문에 <+20> 라인에서 원래 arm 모드에서 PC였다면 +8을 더한 값이었을 텐데 thumb 모드로 전환했기 때문에 다음다음 명령어의 주소가 +4를 더한 값이다.
<key3>
0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>: mov r0, r3
여기선 r3에 lr의 주소를 저장하는데 lr은 함수가 돌아가서 실행하는 명령어의 주소이기 때문에 값은 찾아보면 0x8d80 임을 알 수 있다.
때문에 세가지 값을 더한 값을 정수로 변환한 108400을 입력값으로 주면 flag를 얻을 수 있다.