[stack] 함수의 에필로그(epilogue)

두비니

·

2020. 7. 30. 01:10

 


함수의 에필로그(epilogue)에 대하여


 

 

오늘은 함수의 에필로그라는걸 배울 것입니다. 보통 함수의 프롤로그와 에필로그를 묶어서 설명하는데, 모든 함수의 처음(프롤로그)과 끝(에필로그)에 공통적으로 들어가기 때문에 프롤로그와 에필로그라고 합니다. 오늘은 에필로그만 보겠습니다. 에필로그의 각 단계를 설명하고, 그림으로 다시한번 이해해 보도록 하겠습니다.

 


Function Epilog


leave
ret

 

간단하죠? 에필로그는 leave와 ret로 구성되어있습니다. 그러나 각 단계를 더 나눠서 볼게요.

 


Internal of "Leave"


move esp, ebp
pop ebp

 

우선 Leave입니다. ebp를 esp로 복사하여 지역변수를 정리하고, stack의 가장 위에 있는 값을 ebp에 pop합니다. 근데 여기서 "가장 위에 있는 값"은 항상 sfp입니다. 이건 나중에 그림으로 한번 더 설명하겠습니다.

 


Internal of "Ret"


pop eip
jmp eip

 

다음은 Ret입니다. stack에서 sfp도 pop되었으니 stack에 남아있는 값은 ret값밖에 없겠죠? ret의 값이 eip에 들어가게 되고, 그 eip로 jmp하게 됩니다.

 

 

이제 이걸 예시 코드에 맞춰 다시한번 진행해봅시다. 먼저 정상적으로 진행되는 과정을 그려보겠습니다.

 

     1  /*
     2          The Lord of the BOF : The Fellowship of the BOF
     3          - darkknight
     4          - FPO
     5  */

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

     8  void problem_child(char *src)
     9  {
    10          char buffer[40];
    11          strncpy(buffer, src, 41);
    12          printf("%s\n", buffer);
    13  }

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

    20          problem_child(argv[1]);
    21  }

 

코드는 LOB의 golem문제 참조했습니다. 상황은 problem_child의 함수 에필로그의 상황을 보겠습니다.

main함수의 함수 에필로그도 problem_child와 동일하지만, 서브함수를 보는게 더 도움이 될 것 같아 problem_child의 상황으로 봅니다. 참고로 파란색 stack은 problem_child에서 쌓이는 스택입니다.

 

mov는 뒤에 있는 값을 앞에 있는 값으로 복사시키는 어셈블리어니깐 ebp의 값이 esp로 복사되면서 ebp와 esp모두 sfp를 가리키게 됩니다. 그래서 이걸 "지역변수를 정리한다"라고는 하는데 여기서 buffer의 값이 날라간다거나 그러진 않습니다. 오해하지 마시길!

 

pop ebp를 하면 stack에서 가장 위에있는 값이 ebp안으로 들어가기 때문에 sfp의 값이 ebp로 들어가게 되고, 원래는 main함수에서의 ebp값이 들어가있었으니 main함수에서의 ebp값으로 돌아가게 됩니다. 그리고 pop했으니 esp는 ret의 값을 가리키고 있겠죠?

 

+) 혹시나 stack의 가장 위에는 buffer가 있으니 이 값이 pop되어야하는게 아니냐고 할 수도 있지만, stack의 가장 최상단은 esp가 정합니다. 따라서 stack의 가장 상단의 값은 sfp입니다.

 

 

다음은 ret의 과정인 pop eip입니다. 상황을 보니 ret의 값이 eip로 pop되겠죠? 지금 ret에 0x0804827f라는 값이 들어가있는데, 그냥 제가 임의적으로 넣어놓은 값입니다. 중요한것은 problem_child()라는 함수를 실행한 뒤에 실행하기로 한 instruction이 들어가 있다는 것입니다.

 

+) 막상 이렇게 써보고나니깐 무슨말인지 잘 모를것같아서 직접 어셈코드도 첨부합니다.

(gdb) disas main
Dump of assembler code for function 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
0x80484a5 <main+57>:    nop
---Type <return> to continue, or q <return> to quit---
0x80484a6 <main+58>:    nop
0x80484a7 <main+59>:    nop
0x80484a8 <main+60>:    nop
0x80484a9 <main+61>:    nop
0x80484aa <main+62>:    nop
0x80484ab <main+63>:    nop
0x80484ac <main+64>:    nop
0x80484ad <main+65>:    nop
0x80484ae <main+66>:    nop
0x80484af <main+67>:    nop
End of assembler dump.

 

다음은 main함수의 어셈코드입니다.

보면 main+45에서 problem_child를 call하고 main+50, 0x0804849e에서 다음 instruction인 add $0x4, %esp를 진행하죠? 즉 위에서 예시로 든 ret안에 있는 값은 0x0804849e인겁니다. 이렇게 설명하면 더 헷갈리려나...?

 

 

아무튼 이렇게 진행한 후 main함수가 진행되겠죠.

main함수도 끝나면 같은 방법으로 함수 에필로그가 진행되고, 정상적으로 종료가 될 수 있는 값들이 main함수의 sfp와 ret에 들어가있겠죠?

 

 

이해를 하는데 좀 도움이 되었으면 좋겠고, 함수의 에필로그를 이용하는 공격기법이 많기때문에 step-by-step으로 완벽하게 이해하고 넘어가시길 바랍니다. 그냥 넘어가시면 분명히 나중에 발목잡힙니다. (경험담)

사족이 길었네요. 감사합니다!

 

끝!