[pwnable.kr] leg

2018. 6. 8. 17:06


이번에 이 leg라는 문제를 풀어보겠다. 저번에 한번 이 문제를 풀려했었는데, 지금까지는 Intel 기반 어셈만 봐오다가 ARM기반 어셈블리코드를 보니

뭘해야할지 몰라서 포기했었다 ㅡㅡ;


그런데 이번에 ARM공부를 할 기회가 생겼고, 얼마전에 논리회로에서 SAP-1을 구현하고나니

왠지 이번에는 풀 수 있을 것 같아서 풀어보게되었다. 확실히 SAP-1이나 ARM공부를 조금 하고 나니까 이해가 가더라 ㅇㅇ



문제를 보면 먼저 ARM을 공부하란다. 다행히 이번에는 저번과 다르게 공부한 상태이므로 바로 들어가보도록하자!

leg.asm과 leg.c를 주는데, leg.asm을 보면 ARM어셈블리 코드를 던져주고, leg.c에서 어떻게 플래그를 획득할 수 있는지 나온다.


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
34
35
36
37
38
#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;
}
cs


Daddy has very strong arm! 이라는 문구가 나오고 key1()+key2()+key3()의 값을 맞추면 플래그를 읽어와 출력해 줄 것이다.


key1()을 보자. 우리는 리턴값만 알면되므로 r0에 뭐가 저장되는지만 추적하면 된다.


1
2
3
4
5
6
7
8
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
cs


여기서는 r3을 pc에 넣고 다시 r0에 넣게 된다. 여기서 pc란 program counter로 intel에서 eip와 대응한다고 보면 된다. 그러므로 pc에는 다음에 실행한 명령어의 주소가 저장되어있을 것이고, 이게 r3에 옮겨지게 된다.

하지만 여기서 ARM에서는 명령을 실행할 때, fetch -> decode -> execute 순으로 실행되기 때문에 pc값을 실제로 읽으면 다음명령어가 아니라 +8한 값이 나온다.


fetch -> decode -> execute

1. instruction을 메모리로 가져온다. (Fetch)

2. 가져온 Instruction을 Dedode하고 Register 값을 확인 (Decode)

3. Decoding된 Instruction을 실행한다. (Execution)


아래는 AZERIA-Labs에서 퍼온 내용이다.


During execution, PC stores the address of the current instruction plus 8 (two ARM instructions) in ARM state, and the current instruction plus 4 (two Thumb instructions) in Thumb(v1) state. This is different from x86 where PC always points to the next instruction to be executed.


…right? Wrong. Look at the address in R0. While we expected R0 to contain the previously read PC value (0x8054) it instead holds the value which is two instructions ahead of the PC we previously read (0x805c). From this example you can see that when we directly read PC it follows the definition that PC points to the next instruction; but when debugging, PC points two instructions ahead of the current PC value (0x8054 + 8 = 0x805C). This is because older ARM processors always fetched two instructions ahead of the currently executed instructions. The reason ARM retains this definition is to ensure compatibility with earlier processors.

출처 : https://azeria-labs.com/arm-data-types-and-registers-part-2/


ARM에서는 실행 상태일 때 PC에는 현재 수행하는 명령어+8의 주소가 저장된다.

이는 이전 ARM 프로세서가 항상 현재 실행 된 명령어보다 두 개의 명령어를 먼저 가져 오기 때문인데,

ARM은 cpu의 효율을 위해 fetch, decode, execute를 동시에 수행하고, 그래서 명령어를 execute할 때 그 다음 명령어는 decode되고 또 다음 명령어는 fetch된다.

여기서 pc는 CPU가 현재 실행하고 있는 명령어(instruction)의 주소를 가리키고 있고, pc는 fetch에서 증가하고 pc는 다음 명령어 의 주소를 가리키게 된다. 즉, 명령어를 execute를 하는 시점에서 다음 명령어의 fetch는 끝난 상황이며 pc는 execute하는 명령어에서 두 번째 명령어의 주소값을 가리키는 것이다.

참고로 ARM이 이 방식을 유지하는 이유는 이전 프로세서와의 호환성을 보장하기 위해서라고 한다. 


그래서 이 문제에서 r0에 들어가는 값은 r3에 pc값을 옮기는 명령어 주소+8의 값이 들어가게 된다.

즉, 0x00008cdc+4 = 0x00008ce4 이다.


이제 key2()를 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(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.
cs


자... 여기서도 r0에 r3를 저장하고 끝난다. r3에는 역시 pc값을 저장하고 이 명령어주소+8의 값이므로 0x00008d08 이다.

그런데 여기에 +4의 값을 해주기때문에 r0에 저장되는 값은 0x00008d0c 이다.



이제 key3()만 보면 된다. key3()를 보자.


1
2
3
4
5
6
7
8
9
10
(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.
cs


이번에는 pc가 아니라 lr을 r3에 저장하고 r0에 저장하게 된다.

lr은 함수가 호출하기전에 함수복귀주소를 저장하는 레지스터로 인텔로 따지면 스택에 있는 ret와 같다고 보면 된다.

그러므로 이 값은 0x00008d80 이다.


1
2
3
   0x00008d78 <+60>:    add    r4, r4, r3
   0x00008d7c <+64>:    bl    0x8d20 <key3>
   0x00008d80 <+68>:    mov    r3, r0
cs


이제 key1() + key2() + key3()의 값을 계산하면 된다. python으로 계산하였다.

0x00008ce4 +  0x00008d08 + 4 + 0x00008d80



108400이란 값이 나왔다. 이제 leg를 실행해서 이 값을 넣어주자



성공~ 플래그를 획득했다.




다른 사람들 롸업보다가 참고할만한 글이 있어서 가져옴..

위의 fetch -> decode -> execute에 대해 자세히 알고싶다면 참고하면 좋을 듯하다.

http://recipes.egloos.com/4982170

'Wargame > Pwnable.kr' 카테고리의 다른 글

[pwnable.kr] memcpy  (0) 2018.07.27
[pwnable.kr] asm  (0) 2018.07.27
[pwnable.kr] cmd2  (0) 2018.06.08
[pwnable.kr] uaf - 8 pt  (0) 2018.05.31
[pwnable.kr] passcode - 10 pt  (0) 2018.05.28

+ Recent posts