arm에 관한 문제를 풀어봤습니다.
Daddy told me I should study arm.
But I prefer to study my leg!
Download : http://pwnable.kr/bin/leg.c
Download : http://pwnable.kr/bin/leg.asm
ssh leg@pwnable.kr -p2222 (pw:guest)
이 문제는 arm 어셈블리언어를 알아야 풀 수 있습니다.
주어진 두 링크를 들어가보면 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;
}
C 코드를 보면 key1, key2, key3 함수의 반환값을 더한 값과 input값이 같아야하는데,
key1()를 살펴보니 mov r3, pc 즉 r3라는 레지스터에 pc(program counter)의 값을 넣는 것을 알 수 있습니다.
근데 이 C코드만 보고서는 pc에 해당하는 값이 무엇인지 알 수 없겠죠?
따라서 어셈블리 코드 분석이 필수적으로 요구됩니다.
문제를 풀기 위해 arm 어셈블리어를 알아봅시다.
[레지스터]
- R0 ~ R15 (16개)
- R0 ~ R12 : 범용 레지스터, 인자값 및 임시 계산 저장소 등
- R13(SP) : Stack Pointer // x86에서 ESP와 비슷한 역할을 수행
- R14(LR) : Link Register, 함수 호출 전 LR에 리턴 주소를 저장하고 점프함
- PC : x86에서 EIP와 동일한 역할 수행, 다음 실행할 코드의 주소를 저장
여기서 조금 중요하다고 생각되는 부분이 있는데요.
바로 PC와 LR입니다.
먼저 PC는 앞서 설명했듯 Program Counter의 약자인데요.
다음 실행할 주소를 담고 있는 레지스터입니다.
따라서 x86에서 EIP와 아주 동일하다고 생각할 수 있는데,
역할은 다음 실행할 주소를 담고 있는 것으로 동일하지만, 그 "다음 실행할 주소"가 EIP와는 다르게 다음 다음을 의미한다는 것입니다.
따라서 key1()의 어셈코드를 살펴보면
(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 // -> pc : 0x00008ce4
0x00008ce0 <+12>: mov r0, r3
0x00008ce4 <+16>: sub sp, r11, #0
0x00008ce8 <+20>: pop {r11} ; (ldr r11, [sp], #4)
0x00008cec <+24>: bx lr // -> lr : 0x00008d6c
End of assembler dump.
key1 + 8 부분에 pc가 가지고 있는 주소는 0x00008ce4가 됩니다.
이렇듯 pc는 우리가 알고있는 EIP와 동일한 역할을 수행하지만 담기는 주소값이 다른것 을 알 수 있습니다.
다음으로 설명할 부분은 LR입니다.
LR은 Link Register로 함수 호출 전 LR에 주소를 저장하고 점프하는 레지스터입니다.
쉽게 풀어서 설명하면 LR은 함수 호출 전에 다시 되돌아가서 실행할 다음 주소를 담고 있다고 생각하면 됩니다.
따라서 main()를 살펴보면 LR은 0x00008d6c임을 알 수 있습니다.
(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> // -> lr : 0x00008d6c
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.
이렇게 pc와 lr을 알아봤습니다.
문제풀이에 있어 핵심이 되는 내용이기 때문에 먼저 알아봤고, 이제 차례대로 슥삭! 풀이해봅시다.
main()부터 분석해보겠습니다.
(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.
여기서 볼 수 있듯 함수의 return 값은 r0에 들어있는 것 같네요.
별 내용은 없고, r0의 값을 다른 레지스터에 넣고 마지막에 모두 더한 후 input값과 비교하는 것 같습니다.
(input : [r11, #-16])
따라서 각 함수에서 r0에 무엇이 들어가는지 보면 되겠습니다.
다음으로 key1()를 분석해보겠습니다.
(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.
key1()에서 pc값을 r3에 넣고, 이 값을 다시 r0에 넣음으로서 r0의 값은 pc값이 되었네요.
pc의 값은 0x00008ce4 이므로 key1()의 return 값은 0x00008ce4 가 됩니다.
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.
key2()는 +32줄에 r3의 값이 r0에 들어가는데,
r3에는 pc + 4의 값이 들어가기 때문에 0x00008d08 + 4 =>0x8d0c
마지막으로 key3()
(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.
key3() 역시 r3의 값이 들어가는데, r3의 값은 lr이 들어갑니다.
따라서 return 값은 0x00008d80
각 함수의 return 값을 모두 더해서 input값으로 넣어주면 됩니다.
이상으로 leg write up을 마칩니다.