[pwnable.kr][Toddler] leg 문제풀이
leg
#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; }
key1(), key2(), key3() 함수를 각 실행해서 리턴값을 모두 더한 값이 사용자가 입력한 값과 같으면 문제가 풀리게 된다.
그리고 어셈 코드를 보니 ARM어셈 코드이다.
ARM 사용 이유
- 저전력
- RISC
- Fast Interrupt
- 많은 임베디드 장비에서 활용
ARM Mode : User Mode, Supervisor Mode 등...
ARM Register
- R0 : 주소 리턴값을 보관
- R1 ~ R3 : R0과 함께 argument 1~4 까지 담당함
- R7 : Stack Base ptr로 쓰임
- R14 : LR(Linked Register)
- R13 : SP (Stack Pointer)
- R15 : PC (eip)
- R0~R7 : 공유되는 레지스터
leg.asm파일을 살펴보자.
(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.
key값을 비교하는 부분만 살펴 보자.
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
key1함수가 실행되고 리턴값이 r0에 저장된다. 그 값이 r4로 mov되고, key2함수의 리턴값이 r3에 mov된 후 r3와 r4를 더한 값을 r4에 넣는다.
key3의 리턴값을 r3에 넣고, r4와 r3fmf 더한 값을 r2에 넣는다.
그리고 r2와 사용자가 입력한 값과 비교한다.
그럼 이제 각 key함수들이 어떠한 값들을 리턴하는지 알아보자.
(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.
리턴되는 r0레지스터를 중심적으로 살펴보면 r0에는 r3가 들어가고, r3에는 pc가 들어간다.
즉, r0에는 pc의 값이 들어간다.
pc는 실행되는 명령어 주소를 가리킨다. 따라서 0x8ce0이 들어 갈 것 같지만, ARM은 Pipe Line개념을 생각해야 한다.
ARM Pipeline
ARM core는 메모리에 있는 코드를 읽어오고, 해석하고, 실행한다.
즉, Fetch - Decode - Execute라고 한다.
여기서 각 단계별로 CPU 클럭을 1사이클 소모한다고 가정해보자.
2개의 어셈블리 명령어가 수행된다고 할 때, 총 6사이클이 필요하게 된다(사진 상단 파란색).
이때 fetch의 입장으로 볼 때, 처음 fetch가 된 후로 나머지 2사이클동안 쉬게 된다.
ARM은 이렇게 쉬는 것을 방지하게 위해 Pipeline을 도입하였다(사진 하단 연두색).
fetch가 일어나고 다음 명령어를 다음 사이클에서 미리 fetch하여 4사이클만에 실행 완료되어 성능향상하였다.
Pipeline은 기본적으로 3단계가 있으며 세부적으로 5단계, 8단계, 13단계로 나눌 수 있다.
문제로 넘어와서.
pc의 값은 명령어 실행주소가 들어있다고 했는데 pc가 속한 어셈블리 명령어(0x8cdc)가 fetch되고, execute는 2사이클이 동작된 후인 0x8ce4부분에서 실행된다.
따라서 pc값에는 0x8ce4에 값이 들어가게된다. 따라서 key1의 리턴값은 0x8ce4가 된다.
다음 key2함수를 보자.
(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.
key2도 r0을 중심으로 살펴보면, r0에는 r3가 들어간다.
r3에는 pc값이 들어가고 4를 더한 값이 들어간다.
pc값은 Pipeline이 적용되어 0x8d08값이 들어가고 4를 더하면 0x8d0c값이 된다.
따라서 key2의 리턴값은 0x8d0c이다.
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)
key3함수도 r0에 r3값이 들어가고 r3에는 lr이 들어간다.
lr은 함수의 복귀 주소를 가지고 있다. intel에서 sfp값이라고 생각하면 될것 같다.
main함수에서 key3함수가 호출한 주소는 0x8d7c이고 복귀 할 주소는 다음 명령어주소인 0x8d80이 된다.
따라서 key3의 리턴값은 0x8d80이 된다.
각 함수의 리턴값을 모아보면,
- key1 : 0x8ce4
- key2 : 0x8d0c
- key3 : 0x8d80
이를 모두 더한 10진수값은 108400이 된다.
flag : My daddy has a lot of ARMv5te muscle!
잘못 된 개념을 서술하였거나, 잘못 풀이된 내용이 있으면 댓글 달아주시면 감사합니다 :) 태클 댓글이나 메일(513.eunice@gmail.com) 환영입니다 !! 😊☺️👍 |