Layer7 리버싱 4차시

Layer7 리버싱 4차시 과제 정리

GDB Handray 실습 정리

실습 예제 코드 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int divide(int a, int b) {

  if (b == 0) return -1;

  return a / b;
}

int main() {

  int x = 12;
  int y = 0;
  int z = divide(x, y);

  printf("Result: %d\n", z);

  return 0;
}

풀이 정리

gdb를 실행하고 b divide if b == 0 명령어를 통해 divide 함수에서 나누는 값이 0일 때 중단하도록 설정하였다.


실행을 해보니 divide 함수의 나누는 값인 두 번째 인자인 b0이므로 중단된 것을 확인할 수 있다.


print 명령어로 인자인 ab의 값을 확인할 수 있다.


info args 명령어로 divide 함수의 인자 값을 확인할 수 있고, step 명령어로 함수를 빠져나와 info locals 명령어로 divide 함수의 반환값이 저장된 z 변수를 비롯한 지역 변수들을 확인할 수 있다.


backtrace 명령어로 스택에 쌓인 호출된 함수를 확인할 수 있다.


display a 명령어로 매 실행마다 변수의 값을 출력하도록 설정할 수 있다.


실습 예제 코드 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <stdio.h>

void print_message(const char *msg) {
  printf("%s\n", msg);
}

void call_wrapper() {
  print_message("tracing into function");
}

int main() {
  call_wrapper();
  return 0;
}

풀이 정리

gdb를 실행하고 b print_message 명령어로 중단점을 설정하고 실행하였다.


disas print_message 명령어로 print_message 함수의 어셈블리어 코드를 확인해볼 수 있다.


1
2
3
4
   0x0000555555555149 <+0>:     endbr64                         ; ROT 공격 방지
   0x000055555555514d <+4>:     push   rbp                      ; 베이스 포인터 저장
   0x000055555555514e <+5>:     mov    rbp,rsp                  ; 스택 프레임 설정
   0x0000555555555151 <+8>:     sub    rsp,0x10                 ; 스택 16byte 할당

함수의 프롤로그다. 베이스 포인터를 저장하고, 새 스택 프레임을 설정해주는 것을 확인할 수 있다. 16byte의 공간을 할당하는 이유는 인자인 문자열 포인터를 저장하기 위해 8byte를 필요로 하는데, 스택 포인터를 16byte의 배수인 주소에 위치하여 정렬해야 하기 때문이다.


1
2
3
4
5
   0x0000555555555155 <+12>:    mov    QWORD PTR [rbp-0x8],rdi  ; 인자 스택에 저장 
   0x0000555555555159 <+16>:    mov    rax,QWORD PTR [rbp-0x8]  ; 인자 rax에 복사 
   0x000055555555515d <+20>:    mov    rdi,rax                  ; 인자 rdi에     
   0x0000555555555160 <+23>:    call   0x555555555050 <puts@plt>; put 함수 호출
   0x0000555555555165 <+28>:    nop                             ; 패딩

인자를 스택에 저장한 뒤, rax 레지스터에 복사하고 다시 rdi 레지스터에 복사한다. 그리고 rdi 레지스터를 인자로 puts 함수를 호출한다. 사실상 위 세 줄은 필요가 없는데, 컴파일 최적화가 되지 않아서 그렇다. -O2-O3 옵션을 사용하면 최적화하여 컴파일할 수 있다. 마지막 패딩은 위에서 설명한 스택 정렬 때문이다.


1
2
   0x0000555555555166 <+29>:    leave                           ; 스택 프레임 정리
   0x0000555555555167 <+30>:    ret                             ; 반환

함수의 에필로그다. 함수의 스택 프레임을 정리한 뒤 반환한다.


x/i $rip 명령어나 layout 명령어를 통해 어셈블리어를 실행하며 확인해볼 수 있다.




직접 예제 만들고 실습


예제 코드

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

void print(char* str, int n) {
    for (int i = 0; i < n; i++) {
        printf("%s\n", str);
    }
}

int main() {

    int n;

    scanf("%d", &n);

    if (n > 0) print("과제하기귀찮다", n);
    else printf("출력 횟수 음수 불가능");

    return 0;
}

실습 정리

b main 명령어로 main 함수에 중단점을 설정하고 실행하였다.


layout asm 명령어로 한 줄씩 분석해보았다. (분석하는 모든 과정을 캡쳐하긴 어려워 설명이 중요한 부분만 사진을 삽입하였다.)


1
2
3
4
5
6
   0x5555555550c0 <main>:       endbr64
   0x5555555550c4 <main+4>:     sub    rsp,0x18
   0x5555555550c8 <main+8>:     lea    rdi,[rip+0xf35]        # 0x555555556004
   0x5555555550cf <main+15>:    mov    rax,QWORD PTR fs:0x28
   0x5555555550d8 <main+24>:    mov    QWORD PTR [rsp+0x8],rax
   0x5555555550dd <main+29>:    xor    eax,eax

스택 공간을 할당하고 스택 카나리를 설정한다. 카나리는 BOF 공격으로 스택이 변조되었는지 확인하기 위한 값이다.

세 번째 줄은 scanf의 첫 번째 인자인 서식 지정자 %d 문자열을 복사하는 코드이다.


1
2
3
4
5
6
7
   0x5555555550df <main+31>:    lea    rsi,[rsp+0x4]
   0x5555555550e4 <main+36>:    call   0x5555555550b0 <__isoc99_scanf@plt>
   0x5555555550e9 <main+41>:    mov    esi,DWORD PTR [rsp+0x4]
   0x5555555550ed <main+45>:    test   esi,esi
   0x5555555550ef <main+47>:    jle    0x555555555114 <main+84>
   0x5555555550f1 <main+49>:    lea    rdi,[rip+0xf0f]        # 0x555555556007
   0x5555555550f8 <main+56>:    call   0x555555555220 <print>

[rsp+0x4]에 숫자를 입력받고, 이 값이 0보다 작으면 <main+84>로 분기하고 아니면 [rip+0xf0f]값(“과제하기귀찮다”)을 인자로 print 함수를 호출한다.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
   0x555555555220 <print>:      endbr64
   0x555555555224 <print+4>:    test   esi,esi
   0x555555555226 <print+6>:    jle    0x555555555250 <print+48>
   0x555555555228 <print+8>:    push   r12
   0x55555555522a <print+10>:   mov    r12,rdi
   0x55555555522d <print+13>:   push   rbp
   0x55555555522e <print+14>:   mov    ebp,esi
   0x555555555230 <print+16>:   push   rbx
   0x555555555231 <print+17>:   xor    ebx,ebx
   0x555555555233 <print+19>:   nop    DWORD PTR [rax+rax*1+0x0]    ; 정렬용 패딩
   0x555555555238 <print+24>:   mov    rdi,r12
   0x55555555523b <print+27>:   add    ebx,0x1
   0x55555555523e <print+30>:   call   0x555555555080 <puts@plt>
   0x555555555243 <print+35>:   cmp    ebp,ebx
   0x555555555245 <print+37>:   jne    0x555555555238 <print+24>
   0x555555555247 <print+39>:   pop    rbx
   0x555555555248 <print+40>:   pop    rbp
   0x555555555249 <print+41>:   pop    r12
   0x55555555524b <print+43>:   ret
   0x55555555524c <print+44>:   nop    DWORD PTR [rax+0x0]          ; 정렬용 패딩
   0x555555555250 <print+48>:   ret

입력받은 n이 저장된 esi 레지스터의 값이 0이 될 때까지 puts 함수를 호출하고 esi 레지스터 값을 ebp 레지스터에 저장하여 ebx 레지스터를 카운터로 사용해 두 레지스터의 값이 같을 때까지 반복하여 호출한다.


info args 명령어로 인자를 확인해보면 잘 불러와진 것을 확인할 수 있다.


info stackinfo frame 또한 정상적으로 나오는 것을 확인할 수 있다.


1
2
3
4
5
6
   0x5555555550fd <main+61>:    mov    rax,QWORD PTR [rsp+0x8]
   0x555555555102 <main+66>:    sub    rax,QWORD PTR fs:0x28
   0x55555555510b <main+75>:    jne    0x555555555129 <main+105>
   0x55555555510d <main+77>:    xor    eax,eax
   0x55555555510f <main+79>:    add    rsp,0x18
   0x555555555113 <main+83>:    ret

print 함수가 반환되면, 스택 카나리를 검사하고 eax 레지스터를 0으로 초기화하여 종료 코드 0으로 반환한다.