안녕하세요!
오늘은 시큐어 코딩에 대해서 알아볼게요
현업에서 시큐어 코딩은 정말 중요하다고 하는데요?
지금 같은 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 번호와 위험도 표시
- 빠르지만 문맥 판단은 약함
실습 순서
- 실습-0 설치
- 최초 1회만 실행
bash setup.sh
설치: cppcheck, clang-tidy,
flawfinder, gcc - 실습-1 정적분석
- 코드를 실행하지 않고 검사
./run_static.sh
안에서 4개 도구를 차례로 실행 - 실습-2 동적분석
- bugs.c는 실제 오류를 터뜨림
./run_dynamic.sh
마지막에 fixed.c가 sanitizer를 통과하는지 확인
정적 분석 원리
- 어휘 분석
- 구문 분석
- 데이터 흐름
- 제어 흐름 + 추상 해석
실습 흐름
$ 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으로 하나씩 실제로 터뜨림
- cat vuln.c
취약점을 눈으로 먼저 찾아본다 - bash run_static.sh
도구가 찾은 결함과 내 눈을 대조 - bash run_dynamic.sh
실제 실행해 Sanitizer로 확인 - 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 |