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 파일 내 있는 실행 파일 rao
를 pwndbg
로 실행한다.
$ 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바이트를 리턴 주소에 덮어쓰면 될 것이다.