pwndbg 로 버퍼와 리턴 주소 거리 계산하는 방법

pwndbg 로 버퍼와 리턴 주소 거리 계산하는 방법

CTF 중 pwnable 분야의 문제를 풀다보면 버퍼 오버 플로우를 이용해 리턴 주소를 덮어써야하는 경우가 종종 있다. 이때 사용자 입력이 저장되는 버퍼와 덮어 써야할 리턴 주소를 저장하는 스택 주소 간 거리를 계산할 필요가 있다.

이 글에서는 pwnable 문제에서 자주 사용되는 pwndbg 툴을 이용해 버퍼와 리턴 주소 사이의 거리를 계산하는 방법을 설명해보겠다.

예제 문제 설명

DreamHack 에서 제공하는 System Hacking 로드맵의 Stack Buffer Overflow 강의의 실습 중 하나인 Return Address Overwrite 문제를 예제로 사용하겠다.

예제에 첨부된 문제를 다운로드하면 zip 파일 내부에 rao.c 파일이 존재하는 것을 확인할 수 있다.

// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie

#include <stdio.h>
#include <unistd.h>

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};

  execve(cmd, args, NULL);
}

int main() {
  char buf[0x28];

  init();

  printf("Input: ");
  scanf("%s", buf);

  return 0;
}

문제 풀이를 하는 글이 아니니 자세한 설명은 생략하겠다. 요점은 buf 버퍼에 충분히 큰 데이터를 입력하여 버퍼 오버플로우를 발생시켜 리턴 주소를 get_shell() 함수로 덮어쓰는 것이다. 이를 위해 pwndbg 로 buf 와 리턴 주소 간 거리를 계산해보겠다.

pwndbg 를 이용한 거리 계산

당연하지만 pwndbg 를 이용하는 방법을 다루고 있으니 pwndbg 를 미리 설치해야 한다. 문제에서 다운로드한 zip 파일 내 있는 실행 파일 raopwndbg 로 실행한다.

$ gdb ./rao

pwndbg 실행 후 start 명령어로 main() 함수의 시작 지점까지 이동한다.

pwndbg> start

main() 함수에는 여러 명령어가 있으나, 우린 main() 함수가 리턴하는 순간에 리턴 주소를 저장하고 있는 rsp 레지스터의 상태가 궁금하니, main() 함수의 ret 명령어가 존재하는 0x400729 주소까지 이동해야 한다.

► 0x4006ec <main+4>     sub    rsp, 0x30     RSP => 0x7fffffffdcf0 (0x7fffffffdd20 - 0x30)
   0x4006f0 <main+8>     mov    eax, 0        EAX => 0
   0x4006f5 <main+13>    call   init                        <init>
 
   0x4006fa <main+18>    lea    rdi, [rip + 0xbb]     RDI => 0x4007bc ◂— outsb dx, byte ptr [rsi] /* 'Input: ' */
   0x400701 <main+25>    mov    eax, 0                EAX => 0
   0x400706 <main+30>    call   printf@plt                  <printf@plt>
 
   0x40070b <main+35>    lea    rax, [rbp - 0x30]
   0x40070f <main+39>    mov    rsi, rax
   0x400712 <main+42>    lea    rdi, [rip + 0xab]     RDI => 0x4007c4 ◂— and eax, 0x1000073 /* '%s' */
   0x400719 <main+49>    mov    eax, 0                EAX => 0
   0x40071e <main+54>    call   __isoc99_scanf@plt          <__isoc99_scanf@plt>

하지만 ret 명령어까지 이동하기 전에 우선 cyclic 명령어를 실행해 cyclic 패턴 문자열을 생성하자. 이 문자열은 일정한 규칙을 가지며 순환하는 문자열로, 추후 cyclic -l 명령어에 패턴의 서브 스트링 값을 전달하면 서브 스트링이 전체 패턴으로부터 얼마나 멀리 떨어져 있는지 쉽게 계산할 수 있다. 이 예제에서는 버퍼와 리턴 주소 간 거리가 300바이트보다 적을거라 예상해 cyclic 300 명령어를 실행해 300바이트 크기의 패턴을 생성했다.

pwndbg> cyclic 300
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa

b *0x400729 명령어로 ret 명령어에 브레이크포인트를 걸어놓은 후, continue 명령어로 run 하면 버퍼에 사용자 입력을 전달하는 Input: 문자열이 출력된다. 이때 앞서 생성한 cyclic 패턴을 입력하자.

pwndbg> c
Continuing.
Input: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa

ret 명령어가 있는 0x400729 주소까지 도착하면 현재 rsp 가 가리키는 스택 상태가 아래와 같은 것을 확인할 수 있다. 또는 telescope $rsp 명령어로 $rsp 상태를 확인할 수 있다. 앞서 입력한 cyclic 패턴이 리턴 주소의 값을 침범하여 덮어쓴 것을 확인할 수 있다.

─────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdd28 ◂— 0x6161616161616168 ('haaaaaaa')
01:0008│     0x7fffffffdd30 ◂— 0x6161616161616169 ('iaaaaaaa')
02:0010│     0x7fffffffdd38 ◂— 0x616161616161616a ('jaaaaaaa')
03:0018│     0x7fffffffdd40 ◂— 0x616161616161616b ('kaaaaaaa')
04:0020│     0x7fffffffdd48 ◂— 0x616161616161616c ('laaaaaaa')
05:0028│     0x7fffffffdd50 ◂— 0x616161616161616d ('maaaaaaa')
06:0030│     0x7fffffffdd58 ◂— 'naaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa'
07:0038│     0x7fffffffdd60 ◂— 'oaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa'

현재 rsp 에 저장된 값은 'haaaaaaa' 이다. 이 문자열은 앞서 입력한 cyclic 패턴의 일부이므로, 이 문자열이 cyclic 패턴의 시작 지점으로부터 얼마나 떨어졌는지 알 수 있다면 그 값이 바로 버퍼와 리턴 주소 사이의 거리와 같을 것이다.

직접 거리를 계산할 필요 없이 cyclic -l {리턴 주소에 저장된 패턴} 명령어로 거리를 확인할 수 있다. haaaaaaa 문자열은 cyclic 패턴의 시작 지점으로 부터 56바이트 떨어졌으며, 이는 버퍼와 리턴 주소 간 거리와 같다.

pwndbg> cyclic -l haaaaaaa
Finding cyclic pattern of 8 bytes: b'haaaaaaa' (hex: 0x6861616161616161)
Found at offset 56

두 주소 사이의 거리를 구했으니, 실제 문제 풀이에서는 buf 버퍼에 56바이트만큼의 패딩 데이터를 추가한 후, get_shell() 주소 8바이트를 리턴 주소에 덮어쓰면 될 것이다.