SD19. 무부호 정수를 부호 정수로 타입 변환 오류 — signed로 받아 검증 후 변환으로 치환
Properties1
tags
—
14 min read
SD19. 무부호 정수를 부호 정수로 타입 변환 오류 — signed로 받아 검증 후 변환으로 치환
원본 항목: Part 1-4. 정수·타입변환 계열 — 19. 무부호 정수를 부호 정수로 타입 변환 오류CWE-196repo 폴더: sd19_unsignedtosigned/ (빌드·실행은 §3-1)
목표 시간: 1회차 15분 / 2회차 9분 / 3회차 6분
Part 1-4의 마지막 드릴.
-1(에러 표시)이 unsigned 로 뒤집혀 memcpy 크기로 흘러가는, 가장 고전적인 “에러코드 → 크기값 오염” 패턴이다.
이 드릴의 문서 구성 (SD01~SD58 공통)
① 요구사항서 — 어떤 코드가 뚫려 있고, 무엇을 만드나.
② pseudo code 템플릿 — Bad 와 Good 의 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 공격은 막히고 정상은 통과해야 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 같은 치환 규칙을 다른 소재로 다시 치는 것이다.
0. 이 드릴로 체화할 것
- unsigned size;- size = (unsigned)get_chunk_size(initialized, chunk_size); /* -1 -> 4294967295 */- memcpy(dst, src, size);+ int size = get_chunk_size(initialized, chunk_size); /* signed로 받는다 */+ if (size < 0) return -1; /* 에러 여부 먼저 확인 */+ memcpy(dst, src, (size_t)size);
크기·인덱스는 “부호 있는 채로 검증 → 검증 후 unsigned로 변환” 순서를 지키고, 비교 양쪽의 타입을 통일한다.
회차별 진행
회차
변형
보는 것
순서
1회차
A 초기화 여부에 따른 청크 복사
① 요구사항서 ② pseudo code 전체
1. Bad 를 그대로 쳐서 취약점을 관찰한다. 2. Good pseudo 를 한 줄씩 코드로 바꾼다. 3. ③ 테스트를 실행한다.
2회차
B 파일 읽기 크기로 변형
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. Bad 를 치고 취약점을 확인한다. 2. 번호만 보고 Good 을 쓴다. 3. ③ 테스트를 실행한다.
3회차
C 비교 연산으로 변형
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 Bad 부터 Good 까지 재현한다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-A. 변형 A — 초기화 여부에 따른 청크 복사
“초기화됨” 상태일 때만 지정된 크기만큼 src 를 dst 로 복사한다.
초기화가 안 됐으면 크기 조회 함수는 에러(-1)를 반환한다.
에러 상태에서는 복사를 수행하면 안 된다.
함수
int get_chunk_size(int initialized, int chunk_size);void chunk_copy_bad(void *dst, const void *src, int initialized, int chunk_size);int chunk_copy_good(void *dst, size_t dst_size, const void *src, int initialized, int chunk_size);
공격 입력표
initialized
Bad 결과
이유
1 (정상, 정상 크기)
정상 복사
에러 없음
0 (미초기화)
memcpy가 약 4GB 복사를 시도하며 크래시
get_chunk_size() 가 반환한 -1 을 unsigned 로 받아 4294967295 가 된다
1-B. 변형 B — 파일 읽기 크기로 변형
read() 가 실패 시 반환하는 -1 을 size_t 로 받아 버퍼 크기로 쓰는 흔한 실무 패턴으로 재현
1-C. 변형 C — 비교 연산으로 변형
unsigned int userLevel 과 int SUPER_USER_LEVEL 을 비교할 때 비교 양쪽의 타입을 통일하지 않으면 어떤 값에서 비교 결과가 뒤집히는지 직접 찾아본다
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 소스 파일에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. chunk_copy_good — Level 1 (1회차, 전부 본다)
먼저 Bad 를 그대로 쳐서 §3 의 test_bad_is_vulnerable 이 통과하는지(= 실제로 뚫리는지) 확인한다.
그다음 Good 을 쓴다.
void chunk_copy_bad(void *dst, const void *src, int initialized, int chunk_size){ /* 1. unsigned size = (unsigned)get_chunk_size(initialized, chunk_size) * ← -1 이 4294967295 로 바뀐다 * 2. memcpy(dst, src, size) */}int chunk_copy_good(void *dst, size_t dst_size, const void *src, int initialized, int chunk_size){ /* [ ] 1. signed 로 받는다 * int size = get_chunk_size(initialized, chunk_size) */ /* [ ] 2. 에러 먼저 확인 * size < 0 → return -1 */ /* [ ] 3. 목적지 크기 검사 * (size_t)size > dst_size → return -1 */ /* [ ] 4. memcpy(dst, src, (size_t)size) */ /* [ ] 5. return 0 */}
2-B. chunk_copy_good — Level 2 (2회차, 번호와 제목만 본다)
int chunk_copy_good(void *dst, size_t dst_size, const void *src, int initialized, int chunk_size){ /* [ ] 1. signed 로 받는다 */ /* [ ] 2. 에러 먼저 확인 */ /* [ ] 3. 목적지 크기 검사 */ /* [ ] 4. memcpy(dst, src, (size_t)size) */ /* [ ] 5. return 0 */}
2-C. chunk_copy_good — Level 3 (3회차, 백지)
int chunk_copy_good(void *dst, size_t dst_size, const void *src,{ /* 아무것도 안 본다. * §1 요구사항서와 §0 의 치환 규칙만 보고 친다. * * 막히면 아래만 찾아본다. * - `get_chunk_size` 는 바꾸지 않는다 (받는 쪽 타입이 문제) * - 검증은 signed 로, 변환은 검증 뒤에 */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
공격 입력표(§1)와 테스트가 답이다.
unsigned size = get_chunk_size(...) 로 받은 뒤 if (size == (unsigned)-1) 로 검사
동작은 하지만 “부호 있는 채로 먼저 검증”이라는 원칙에서 벗어난 임기응변이다. 애초에 int 로 받으면 이런 매직 넘버 비교 자체가 필요 없다
함수 리턴 타입은 고쳤지만 호출부에서 여전히 unsigned 변수에 대입
선언부만 바꾸고 호출부의 타입을 안 맞추면 암묵적 변환이 도로 일어난다. 호출부까지 한 세트로 확인한다
dst_size 검사 없이 size < 0 만 확인
크기가 양수여도 대상 버퍼보다 크면 여전히 오버플로우다. 이 드릴은 두 가지(부호·용량)를 모두 요구한다
5. 모범답안 (변형 A)
다 치고 나서 열 것 — chunk_copy.h / chunk_copy.c
헤더 (chunk_copy.h)
#ifndef CHUNK_COPY_H#define CHUNK_COPY_H#include <stddef.h>int get_chunk_size(int initialized, int chunk_size);void chunk_copy_bad(void *dst, const void *src, int initialized, int chunk_size);int chunk_copy_good(void *dst, size_t dst_size, const void *src, int initialized, int chunk_size);#endif
구현 (chunk_copy.c)
#include <string.h>#include "chunk_copy.h"int get_chunk_size(int initialized, int chunk_size){ if (!initialized) return -1; return chunk_size;}void chunk_copy_bad(void *dst, const void *src, int initialized, int chunk_size){ unsigned size; size = (unsigned)get_chunk_size(initialized, chunk_size); memcpy(dst, src, size);}int chunk_copy_good(void *dst, size_t dst_size, const void *src, int initialized, int chunk_size){ int size = get_chunk_size(initialized, chunk_size); if (size < 0) return -1; if ((size_t)size > dst_size) return -1; memcpy(dst, src, (size_t)size); return 0;}
눈여겨볼 점: get_chunk_size 자체는 바뀌지 않는다 — 에러를 -1 로 표현하는 함수 자체는 정상적인 관용구다.
문제는 그 반환값을 받는 쪽의 타입이었다.
SD17~19를 관통하는 교훈: 오류는 값을 만드는 곳이 아니라 그 값을 다음 계산에 넘기는 지점에서 막는다.