2026 unknown 하계 세미나

시큐어 코딩

해피가스트 2026. 7. 8. 01:02

안녕하세요!

오늘은 시큐어 코딩에 대해서 알아볼게요

현업에서 시큐어 코딩은 정말 중요하다고 하는데요?

지금 같은 AI 시대에는 AI가 코딩을 해주긴 하지만

실제로 배포할려면 시큐어 코딩이 꼭 필요하다고 합니다!!

그럼 시큐어 코딩에 대해서 알아보도록 하죠 후후 

(2026-07-07에 진행한 UNKNOWN 세미나)


임베디드 자동차 코딩 표준

시큐어 코딩

개발 흐름

요구 사항 → 설계 → 구현 → 분석 → 검증

👉 요구사항부터 구현까지 보안 질문을 앞당겨야 수정 비용이 줄어듦

취약점은 대부분 ‘경계와 수명’에서 시작

1. 입력 경계

  • 길이, 타입, 허용 문자, 쿼리 파라미터를 검증

2. 메모리 수명

  • 할당, 소유권, 해베, use-after-free를 관리

3. 권한 경계

  • 누가 어떤 파일, API, 진단 기능을 호출할 수 있는지 확인

4. 시간 예측성

  • 자동차 코드는 평균 속도보다 최악 실행시간 예측이 더 중요

취약점 실습 지도

실습 주제 공격 감각 시큐어코딩 질문 방어/검증
스택 overflow 반환 주소/인접 메모리 손상 버퍼 경계가 보이는지 길이 검사, ASan
힙 overflow 동적 할당 영역 오염 소유권과 크기가 맞는지 RAII, allocator 검증
format string 메모리 읽기/쓰기 외부 입력을 format으로 쓰는지 고정 format
canary 우회 방어값 변조 탐지 방어 기법을 과신하지 않는지 다층 방어

스택과 힙

Stack

함수 호출마다 생기는 임시 공간
지역 변수, 반환 주소, 저장된 레지스터와 가까움
고정 기링 버퍼를 넘기면 제어 흐름 손상으로 이어질 수 있음
Heap

동적으로 할당하고 해제하는 공간 
수명관리가 어렵고 use-after-free, double free, heap overflow가 생길 수 있음 
임베디드에서는 아예 금지하거나 제한하기도 함 
높은 주소 

Stack (아래로 자람)
수명: 함수 반환까지 

빈 공간

Heap (위로 자람)
수명: free까지 

.data / .bss - 전역/정적

.text - 코드(읽기 전용)

낮은 주소 

취약 코드

스택 버퍼 오버플로우

void handle_name(const char *input) {
	char name[16];
	// 위험: input 길이를 확인하지 않는다
	strcpy(name, input);
	printf("hello %s\n", name);
}

// 수정 방향
// - 길이 제한
// - 명시적 종료 문자
// - 입력 크기 검증
// - 테스트 케이스 추가
  • 버퍼 크기보다 긴 입력이 들어오면 인접 메모리를 덮는다
  • 경계 검사 없는 문자열 복사는 금지 또는 강하게 제한
  • ASan으로 오류를 관찰한 뒤 안전한 복사로 수정

버퍼 오버플로우

크가를 넘은 입력은 남는 데이터가 아니라 옆 메모리를 덮는 쓰기다.

  • 원인
    • 길이 검사를 하지 않고 strcpy, gets, sprintf처럼 복사
  • 영향
    • 인접 변수, 저장된 프레임 포인터, 반환주소가 오염될 수 있음
  • 방어
    • 입력 길이 제한, 안전 API, 컴파일러 보호, 테스트를 함께 씀
  • 오버플로우 탐지를 위한 방어 기법은 존재하지만, 이를 우회하는 새로운 공격도 개발되는 중

버퍼 오버플로우 공격 예시

  • 버퍼 넘침
    • 입력이 buffer[10] 크기를 넘겨 위쪽 메모리를 덮어씀 (AAAA=0x41)
  • ret 변조
    • 저장된 ebp를 지나 반환 주소까지 공격자 값으로 덮임
  • 실행 탈취
    • 함수가 리턴하는 순간 ret이 가리키는 셸코드로 점프함

👉 카나리(canary) : ret 앞에 경보값을 두어 덮어쓰기를 먼저 탐지

방어 기법

  • 막는 것
    • 전형적인 스택 오버플로우로 반환주소가 바뀌는 시도를 탐지함
  • 못 막는 것
    • 논리 버그, 힙 오염, 정보 유출, canary 값 유출
  • 오버플로우 쓰기 방향
    • 버퍼 → canary → ret

👉 스택은 높은 주소에서 낮은 주소로 자라고, 오버플로우 쓰기는 그 반대 방향으로 진행됨

스택 카나리

  • 반환 주소 앞에 무작위 canary 값을 두고, 함수 종료 때 변조 여부를 확인
  • 단순한 연속 덮어쓰기형 스택 버퍼 오버플로우를 탐지
  • 정보 누출로 canary를 알거나, 다른 취약점과 조합하면 우회 가능성이 생김
void bad(char *input) {
	char name[16];
	strcpy(name, input); // 길이 검사가 없다
}
void better(char *input) {
	char name[16];
	snprintf(name, sizeof(name), "%s", input);
	name[sizeof(name)-1] = '\0';
}

  • 복사 대상 버퍼의 크기를 기준으로 제한해야 함
  • 안전 API도 반환값과 잘림 여부를 확인해야 함
  • 자동차 코드 - 입력 길이는 메시지 정의와 상태 조건에 묶어 검증

힙 수명 오류

char *make_token(void) {
	char *p = malloc(32);
	if (p == NULL) return NULL;
	strcpy(p, "session");
	free(p);
	return p; // 위험: 해제한 포인터 반환
}
// 수정 방향
// - 소유권 규칙 명확화
// - free 후 NULL 대입
// - 반환/해제 책임 문서화
  • 해제된 메모리를 다시 쓰면 동작이 예측 불가능
  • 동적 메모리를 금지하거나 초기화 단계로 제한하는 조직도 많음
  • 소유권과 해제 책임을 명확히 하고, free 이후 포인터 사용 여부를 확인

널 포인터 역참조

int read_sensor(struct sensor *s) {
	// 위험: s가 NULL일 수 있다
	return s->value;
}
int safe_read_sensor(struct sensor *s, int *out) {
	if (s == NULL || out == NULL) {
	return -1;
}
	*out = s->value;
	return 0;
}

// 수정 방향
// - 입력 계약 명시
// - NULL 확인
// - 오류 경로 테스트
  • 크래시가 곧 서비스 거부나 안전 기능 중단으로 이어질 수 있음
  • 포인터 유효성, 입력 계약, 오류 처리를 명확히 한다
  • 정상 입력보다 실패 경로 테스트가 더 중요할 때가 있음

정수 오버플로우와 크기 계산

uint16_t count = get_count();
uint16_t size = count * sizeof(struct item); // 문제: 16
비트 절단 가능

struct item *buf = malloc(size);
read_items(buf, count);

// 위험:
// count가 크면 size가 16비트로 절단되어 작아질 수 있다.
// 수정 방향:
// - 더 넓은 타입으로 계산
// - 곱셈 전 상한 검사
// - malloc 결과와 길이 검증
  • 작게 할당하고 많이 쓰면 힙 오버플로우로 이어짐
  • 자동차 코드 - 센서값, 길이 필드, 네트워크 입력은 모두 신뢰하면 안됨
  • count 상한을 먼저 확인하고, 크기 계산은 size_t로 수행한 뒤 실제 할당 크기와 읽기 길이를 검증함

안전한 C 코드의 기본 습관

  • 복사/파싱/계산 전에 길이와 상한을 확인함
  • 할당/해제 책임과 포인터 수명을 함수 계약에 씀
  • NULL, 실패 반환, 부분 초기화 상태를 테스트함
  • strcpy, sprintf 같은 함수는 금지하거나 래퍼로 통제

힙과 수명

  • 한 버퍼를 넘으면 옆 객체의 길이, 포인터, 상태값을 바꿀 수 있음
  • 해제 후 사용, 이중 해제, 누수는 동적 메모리의 대표 위험
  • 소유권을 RAII 객체에 묶고 직접 new/delete를 줄임
  • 실행 중 할당은 지연 편차와 단편화를 만들 수 있음

포맷스트림

void bad(char *user) {
	printf(user); // user가 포맷 명령이 된다
}
void better(char *user) {
	printf("%s", user); // 포맷은 상수, 값은 인자
}

// 위험 입력 예: %x %x %x 또는 %n
  • %x, %s5는 스택/메모리 내용을 노출할 수 있음
  • %n은 출력 길이를 메모리에 쓸 수 있어 위험
  • 포맷 문자열은 상수로 고정하고 사용자 입력은 값으로만 전달

MISRA

  • goto 남발, switch의 break 빠짐처럼 코드가 어디로 실행될지 헷갈리는 구조를 피한다
  • signed/unsigned 섞기, 큰 값을 작은 타입에 넣기처럼 예상 못 한 타입 변환을 막음
  • 무분별한 포인터 산술과 잘못된 수명 접근을 줄임
  • 런타임 할당은 단편화와 수명 오류 때문에 제한되는 경우가 많음

MISRA C에서 자주 만나는 룰 5가지

요지
Rule 1.3 정의되지 않은 동작에 기대는 코드를 쓰지 않음
Rule 10.3 더 좁은 타입으로 값을 대입하지 않음
Rule 17.7 void가 아닌 반환값은 반드시 사용함
Rule 18.1 포인터 산술은 같은 배열 범위 안에서만 함
Rule 21.3 malloc/free 계열 동적 할당을 쓰지 않음

Rule 10.3 코드

// 위반: Rule 10.3
// uint32_t 값을 uint8_t에 암시적으로 대입하는 축소 변환이다.
// len이 255보다 크면 상위 비트가 잘려 dlc가 의도와 다른 값이 될 수 있다.

uint32_t len = get_frame_len();
uint8_t dlc = len; // 256 이상이면 잘린다
process(frame, dlc);

// 준수
if (len > 8u) {
	return ERR_BAD_LEN; // 범위를 먼저 좁힌다
}
uint8_t dlc_ok = (uint8_t)len; // 검사 뒤 명시적 변환
process(frame, dlc_ok);
  • 검사 없이 넓은 값이 좁은 변수로 들어가며 상위 비트가 사라짐
  • 대입 전에 범위를 좁히고, 변환은 명시적으로 드러냄
  • 취약 코드 4의 크기 계산 절단과 같은 구조

Rule 17.7 - 18.1 코드

// 위반: Rule 17.7 - 반환값 무시
// can_read() 성공 여부를 확인하지 않으면,
// "메시지를 읽지 못한 경우"에도 msg를 정상 수신 메시지처럼 처리할 수 있다.

can_read(&msg); // 실패해도 msg를 쓴다
use(&msg);

// 위반: Rule 18.1 - 배열 밖 포인터 산술
// offset 검증이 없으므로 rx_buf + offset이 rx_buf 배열 밖을 가리킬 수있다.
// 배열 밖 포인터를 만들거나 역참조하면 정의되지 않은 동작이 발생할 수 있다.

uint8_t *p = rx_buf + offset; // offset 검증 없음

// 준수: 반환값과 배열 인덱스를 먼저 검증한 뒤에만 msg 사용 및 포인터 산술 수행

if (can_read(&msg) != CAN_OK) return;
if (offset >= RX_LEN) return;
uint8_t *q = rx_buf + offset; // 같은 배열 범위 안
  • 오류 코드를 버리면 실패 경로가 사라지고, 실패가 정상값처럼 흘러감
  • 배열 시작 + 외부 입력 오프셋은 검증 전까지 경계 밖이라고 가정함

Rule 21.3 코드

// 위반: Rule 21.3 - 동적 메모리 할당 사용
// 메시지를 처리할 때마다 malloc/free를 수행한다.malloc은 실패할 수 있고, 걸리는 시간도 일정하지 않으며,반복 사용하면 힙 단편화로 나중에 할당이실패할 수 있다.

void on_message(size_t n) {
	uint8_t *buf = malloc(n); // 시간 예측 불가 + 단편화
	parse(buf, n);
	free(buf);
}

// 준수: 초기화 단계에서 고정 버퍼를 미리 확보
// 런타임에는 malloc/free를 쓰지 않고,
// 설계된 최대 크기(MSG_MAX)를 넘는 메시지는 거부한다.
static uint8_t msg_buf[MSG_MAX];

void on_message2(size_t n) {
	if (n > MSG_MAX) {
		return;
	}
	parse(msg_buf, n);
}
  • 할당 시간이 입력에 따라 흔들리고, 실패와 단편화가 런타임에 나타남
  • 고정 배열, 정적 풀, 초기화 단계 할당으로 상한을 설계로 옮김

CERT

  • CERT C는 취약점 언어에 가까움
  • Memory
    • 버퍼 경계, 해제 후 사용, 이중 해제, NULL 역참조
  • Integer
    • 오버플로우, 부호 변환, 크기 계산 오류
  • Input/Output
    • 형식 문자열, 파일 경로, 외부 입력 검증
  • Concurrency
    • 데이터 레이스, lock 순서, signal/interrupt 안전성

취약 코드에 그대로 붙는 CERT C 룰

요지 대응 취약 코드 
STR31-C 문자열은 널 종료까지 들어갈 공간을 보장함 스택 버퍼 오버플로우
MEM30-C 해제된 메모리에 접근하지 않음 힙 수명 오류
EXP34-C 널 포인터를 역참조하지 않음 널 포인터 역참조
INT30-C unsigned 연산이 래핑되지 않게 함 정수 오버플로우
INT31-C 정수 변환에서 값이 손실되지 않게 함 크기 계산 절단
FIO30-C 외부 입력을 포맷 문자열로 쓰지 않음 포맷스트링

STR31-C · FIO30-C 코드

// 위반: STR31-C - 공간 보장 없는 복사
// input이 15글자보다 길면 name[16]을 넘어서 써서 버퍼 오버플로우가 발생한다.

char name[16];
strcpy(name, input);

// 위반: FIO30-C - 입력이 포맷이 된다
// user_msg 안에 %x, %s, %n 같은 형식 지정자가 있으면 printf가 명령처럼 해석한다.
printf(user_msg);

// 준수
// snprintf는 name 크기 안에서만 문자열을 만들고,
// printf는 포맷을 "%s"로 고정해서 user_msg를 단순 문자열로 출력한다.
snprintf(name, sizeof(name), "%s", input);
printf("%s", user_msg);
  • 널 종료까지 들어갈 공간을 코드가 보장함
  • 외부 입력은 값으로만 전달함
  • 포맷 문자열 자리에 두지 않음

INT30-C · INT31-C 코드

uint32_t a = get_a(), b = get_b();

// INT30-C: 연산 자체가 래핑
uint32_t total = a + b; // 2^32를 넘으면 되돌아온다

// INT31-C: 변환에서 절단
uint16_t size = total; // 상위 16비트 소실

// 준수
if (a > UINT32_MAX - b) return ERR_OVERFLOW;
uint32_t total_ok = a + b;
if (total_ok > UINT16_MAX) return ERR_RANGE;
uint16_t size_ok = (uint16_t)total_ok;
  • INT30-C : 덧셈/곱셈 결과가 타입 폭을 넘는 연산 문제
  • INT31-C : 값이 좁은 타입으로 이동하는 변환 문제

매핑

취약 패턴 위험 연결 표준 수정 방향
strcpy 버퍼 경계 초과 위험 함수 제한/경계 검사 길이 제한 복사
free 후 사용 메모리 손상 동적 메모리 수명 관리 소유권 규칙
NULL 역참조 크래시/DoS 입력 계약/오류 처리 검사와 실패 경로
정수 오버플로우 작은 할당 후 큰 쓰기 타입 변환/범위 검사 상한 검사

AUTOSAR

규칙

영역 주요 내용 위험 줄이기 
Language Rules new/delete, casting, array indexing 위험한 언어 기능 남용
Type Safety 명시적 변환, auto 제한 숨은 형변환 버그
Expressions 평가 순서, 오버플로우 정수/논리 오류
Memory Safety RAII, 스마트 포인터 누수와 수명 오류
Exception Safety noexcept, checked/unchecked 예외 처리 실패

A18-5-1 · A18-5-2 코드

// 위반: A18-5-1 — malloc/free 사용 금지
int* p = (int*)std::malloc(sizeof(int));
// 생성자 미호출, 캐스팅 필요
std::free(p);
// free 사용 → 위반

// 준수: A18-5-2 — 스마트 포인터·컨테이너로 자동 관리
auto up = std::make_unique<int>(42);
// 범위 벗어나면 자동 해제
std::vector<int> v{1, 2, 3};
// 크기·수명 자동 관리

A18-5-3 코드

// 위반: A18-5-3 — new/delete 형태 불일치 → UB
int* a = new int[10];
delete a; // new[] 인데 delete → UB

// 준수: 짝을 맞춘다
int* b = new int[10];
delete[] b; // new[] → delete[]

타입·포인터 룰

요지 5강 연결
A5-2-2 C 스타일 캐스트 대신 명시적 캐스트만 사용 숨은 변환 → 정수 절단 예방
A5-5-1 나눗셈·나머지 연산의 우변은 0이 아니어야 함 크래시(DoS) 예방
A9-3-1 private 데이터의 비-const 참조/포인터를 반환하지 않음 권한 경계 유지
A27-0-2 C 문자열은 널 종료까지의 공간을 항상 보장 STR31-C의 C++판

A5-2-2 · A5-6-1 코드

// 위반: A5-2-2 — C 스타일 캐스트는 의도가 안 보인다
int i = (int)d;

// 준수: 의도를 드러내는 C++ 캐스트
int j = static_cast<int>(d);

// A5-6-1 — 정수 나눗셈 우변이 0이면 UB
int r = a / b; // 위반: b==0 가능
if (b != 0) { r = a / b; } // 준수: 사전 검사
  • A5-2-2 : static_cast는 변환의 종류를 컴파일러와 리뷰어에게 드러냄
  • A5-6-1 : 나눗셈 전 0 검사. 센서 개수처럼 외부에서 오는 분모가 위험

A9-3-1 · A27-0-2 코드

// 위반: A9-3-1 — 내부 데이터의 비-const 참조 반환
std::string& getName() { return name_; }
// 외부 코드가 name_을 직접 수정 가능

// 준수: const 참조 반환(또는 setter로만 변경)
const std::string& getName() const { return name_; }
// 외부 코드가 읽기만 가능, 수정은 불가

// 위반: A27-0-2 — C 문자열(char[]) 사용
char s[6] = "HELLO";
// 길이·'\0' 수동 관리

// 준수: std::string 사용
std::string s2 = "HELLO";
// 길이·'\0' 자동 관리
  • A9-3-1 : 비-const 포인터를 내주면 캡슐화와 권한 경계가 무너짐
  • A27-0-2 : char[] 대신 std::string으로 길이·'x0000' 를 자동 관리

오류·예외 룰

요지 5강 연결
M0-3-2 오류 정보를 주는 함수는 호출 즉시 결과를 검사함 실패 경로 설계·테스트
A15-5-1 소멸자·swap·move는 예외를 던지지 않음 해제 경로의 안전 보장
A15-0-4 복구 불가능한 오류에만 unchecked 예외를 씀 예외 정책 구분
A15-0-5 복구 가능한 오류는 checked 예외로 다룸 호출자의 복구 책임 명시

M0-3-2 코드

// 위반: M0-3-2 – 실패 여부 확인하지 않고 바로 사용
std::optional<Frame> f = rx.read();
process(f->id); // 비어 있으면 UB

// 준수: 값이 있는지 먼저 확인
std::optional<Frame> g = rx.read();
if (!g) {
	log_warn("no frame");
	return Error::NoFrame; // 실패 경로가 코드에 존재
}
process(g->id);
  • rx.read()가 실패할 수 있는데, 값이 있는지 확인하지 않고 바로 사용했음
  • 사용하기 전에 값이 있는지 확인
  • 실패 경로를 코드에 명확히 남김

A15-5-1 코드

// 위반: 소멸자에서 예외가 밖으로 나갈 수 있음
~Logger() {
flush(); // 로그를 비우는 중 실패하면 예외를 던질 수 있음
} // 소멸자 밖으로 예외가 나가면 terminate 위험

// 준수: 소멸자 안에서 실패를 처리함
~Logger() noexcept {
	try {
		flush(); // 실패할 수 있는 작업
	} catch (...) {
		// 예외를 밖으로 내보내지 않음
		// 필요하면 에러 카운터 증가, 내부 로그 기록 등만 수행
	}
}
  • 로그를 비우다 실패하면 소멸자 밖으로 예외가 나갈 수 있음
  • try-catch로 실패를 잡고, 소멸자 밖으로 던지지 않음

Language independent — Unnecessary constructs

// 위반: M0-1-1 — 도달 불가능한 코드
int f(int x) {
	return x;
	x++; // return 뒤 → 절대 실행 안 됨 (dead code)
}
// 준수: 죽은 코드를 제거
int g(int x) {
	return x;
}
  • return 뒤 문장·불가능한 case는 절대 실행되지 않음
  • 죽은 코드를 제거해 제어 흐름을 예측 가능하게

Language independent — Storage

// 위반: M0-2-1 — 겹치는 메모리에 복사 → UB
char buf[16] = "HELLO WORLD";
std::memcpy(buf + 2, buf, 8); // 원본과 대상이 겹침 → UB

// 준수: 겹칠 땐 memmove, 아니면 별개 객체끼리
std::memmove(buf + 2, buf, 8); // 겹침 안전 처리
char a[8], b[8];
std::memcpy(a, b, 8); // 서로 겹치지 않음 → 안전
  • 원본과 대상이 겹치면 memcpy는 UB가 됨
  • 겹치는 객체 복사(memcpy 포함)는 UB

Data Integrity & Encapsulation

// 위반: explicit 누락 → int가 Id로 자동 변환
class Id { public: Id(int n); };
void use(Id id);
use(42); // 42 → Id 암묵 변환 (의도치 않음)

// 준수: A12-1-4 — 단일 인자 생성자는 explicit
class Id { public: explicit Id(int n); };
use(42); // 컴파일 오류
use(Id(42)); // 변환을 명시해야 통과
  • explicit 없으면 int→객체 암묵 변환이 일어남
  • 의도치 않은 객체 생성·가독성 저하 방지

Exception Safety & Control Flow

// A15-0-4 — 복구 불가 오류는 Unchecked 예외
// 메모리 부족·범위 초과처럼 잡아도 대응 불가
throw std::out_of_range("index"); // logic_error 계열

// A15-0-5 — 복구 가능 오류는 Checked 예외
// 통신 실패·타임아웃처럼 재시도로 대응 가능
throw std::runtime_error("timeout"); // runtime_error 계열

  • Unchecked - 복구 불가(메모리 부족·논리 오류) → logic_error·bad_alloc.
  • Checked - 복구 가능(통신·타임아웃·I/O) → runtime_error·system_error.

실습

취약 코드 시연 + 표준 매핑

실습 흐름
1. bad.c 실행 또는 테스트
2. sanitizer로 오류 관찰
3. 어떤 입력이 경계를 깨는지 설명
4. MISRA / CERT 관점의 위반 유형 매핑
5. fixed.c로 수정
6. 테스트와 정적분석 재실행

제출물
- 취약점 설명
- 표준 위반 매핑
- 수정 diff
- 재발 방지 테스트

실습 도구

cppcheck

  • 배열 경계, 널 포인터
  • 미초기화, 누수, UAF
  • MISRA 애드온으로 규칙 번호 연결

clang-tidy

  • C++ bugprone 패턴
  • CERT/AUTOSAR 계열 체크
  • readability·modernize로 리뷰 개선

flawfinder

  • strcpy, sprintf 같은 위험 함수
  • CWE 번호와 위험도 표시
  • 빠르지만 문맥 판단은 약함

실습 순서

  1. 실습-0 설치
  2. 최초 1회만 실행
    bash setup.sh
    설치: cppcheck, clang-tidy,
    flawfinder, gcc
  3. 실습-1 정적분석
  4. 코드를 실행하지 않고 검사
    ./run_static.sh
    안에서 4개 도구를 차례로 실행
  5. 실습-2 동적분석
  6. bugs.c는 실제 오류를 터뜨림
    ./run_dynamic.sh
    마지막에 fixed.c가 sanitizer를 통과하는지 확인

정적 분석 원리

  1. 어휘 분석
  2. 구문 분석
  3. 데이터 흐름
  4. 제어 흐름 + 추상 해석

실습 흐름

$ bash run_static.sh

[cppcheck] 5건
	vuln.c:20 Buffer accessed out of bounds [① 버퍼]
	vuln.c:32 Use after free (deallocuse) [② UAF]
	vuln.c:44 Null pointer dereference [③ 널]
	vuln.c:59 Division by zero [④ 정수]
	
[clang-tidy] 8건
	... format-security CWE-134 [⑤ 포맷]
[flawfinder] 2건
	strcpy CWE-120 / printf CWE-134

도구 비교

취약 코드  cppcheck clang-tidy flawfinder
버퍼 오버플로우 O O O
힙 UAF O O X
널 포인터 O O X
정수/0나누기 O O X
포맷스트링 O O

 

도구 vuln.c fixed.c
cppcheck 5건 0건
clang-tidy 8건 0건
flawfinder 2건 0건

동적 분석 원리

1. 계측

  • 컴파일 시 검사 코드 삽입

2. ASan · shadow memory

  • 모든 메모리 접근이 "허용된 바이트인가" 실시간 검사 → 오버플로우·UAF 즉시 탐지

3. UBSan

  • UB 발생 지점에 체크 삽입 (정수 오버플로·0나누기·널·형변환)

4. 실제 값·실제 경로로 터지는 순간을 포착

$ bash run_dynamic.sh

[1] 스택 버퍼 오버플로우
	AddressSanitizer: stack-buffer-overflow
	WRITE of size 17 — 'id'(8칸) 초과 bugs.c:22
[2] 힙 use-after-free
	AddressSanitizer: heap-use-after-free bugs.c:31
[3] 널 포인터 역참조
	runtime error: load of null pointer bugs.c:38
[4] 부호있는 정수 오버플로우
	runtime error: signed integer overflow bugs.c:44
	2147483647 + 1 cannot be represented
[5] 0으로 나누기
	runtime error: division by zero bugs.c:51

실습 안내

vuln.c - 취약본

  • 취약점 5종(버퍼, UAF, 널, 정수, 포맷)이 한 파일에

fixed.c - 보완판

  • 표준대로 고친 버전 (재검사하면 결함 0)

bugs.c - 동적 메모

  • ASan/UBSan으로 하나씩 실제로 터뜨림
  1. cat vuln.c
    취약점을 눈으로 먼저 찾아본다
  2. bash run_static.sh
    도구가 찾은 결함과 내 눈을 대조
  3. bash run_dynamic.sh
    실제 실행해 Sanitizer로 확인
  4. fixed.c 확인
    재검사하면 결함 0 = 수정의 증거

예제 코드 ↔ 보완판

취약점 vuln.c (취약본) fixed.c (보완판)

버퍼 오버플로우 strcpy(id, input); snprintf(id, sizeof id, "%s", input);
힙 use-after-free free(token); token[0]='X'; free(*t); *t = NULL;
널 포인터 return *sensor; if (sensor==NULL) return -1;
정수 오버플로우 uint16_t total = a + b; if ((uint32_t)a+b > UINT16_MAX)
포맷 스트링 printf(user_msg); printf("%s", user_msg);

실습도 있어서 나중에 실습을 통해 

시큐어코딩에 대해 더 잘 알아보도록 하겠습니다!

 

다음에 계속.....

'2026 unknown 하계 세미나' 카테고리의 다른 글

물리계층 보안 (PHY-Security) 입문  (0) 2026.07.13
보안 적합성 검사  (0) 2026.07.09
자동차 보안  (0) 2026.07.08
PQC II  (0) 2026.07.08
격자 기반 암호SIS · LWE부터 Kyber와 ML-KEM  (0) 2026.07.08