티스토리 뷰

SAST 결과를 개발팀에 내려보내면 가장 자주 돌아오는 소명이 이겁니다.
"해당 변수는 char 포인터이며, 공통함수를 이용해 포인터의 메모리를 동적으로 할당하는 부분입니다.
이러한 사유로 메모리 버퍼보다 크게 사용할 수 없습니다."
읽어보면 그럴듯합니다. 고정 배열이 아니라 필요한 만큼 잡아 쓰는 구조니까 넘칠 일이 없다는 얘기죠. 그런데 이 답변을 그대로 받아 오탐 처리했다가는, 다음 점검 때 같은 항목이 그대로 다시 올라옵니다. 이 글에서는 왜 "동적 할당"이 오탐 근거가 될 수 없는지, 트레이스의 어느 줄을 봐야 하는지, 그리고 어떤 숫자를 받아와야 판정이 끝나는지를 정리했습니다.
본문의 코드와 함수명은 설명을 위해 일반화한 예시입니다.
특정 제품이나 소스의 실제 코드가 아닙니다.
🎯 어떤 진단인가
도구에 따라 이름이 조금씩 다르지만, 국내 SAST 제품에서는 보통 이렇게 표기됩니다. [잘못된 조건으로 인한 동적 할당 버퍼 오버플로우] 이름을 끝까지 읽는 게 중요합니다. "동적 할당 버퍼 오버플로우"가 아니라 "잘못된 조건으로 인한" 동적 할당 버퍼 오버플로우입니다. 즉 이 진단은 이렇게 말하고 있는 겁니다.
| 진단이 전제하는 것 | 진단이 지적하는 것 |
|---|---|
| 이 코드는 동적 할당을 쓴다 | 그 할당 크기를 정한 조건이 실제 접근 범위를 못 덮는다 |
동적 할당이라는 사실은 반박이 아니라 전제입니다. 그래서 "동적 할당이니 안전하다"는 소명은 논점 자체가 어긋나 있습니다.
관련 약점 분류
| 항목 | 값 |
|---|---|
| 1차 원인 | CWE-131 (Incorrect Calculation of Buffer Size) |
| 발현 형태 | CWE-193 (Off-by-one Error) |
| 최종 영향 | CWE-787 (Out-of-bounds Write), 힙이면 CWE-122 |
| 코딩 규칙 | SEI CERT C — STR31-C (문자 데이터 + 널 종단 공간 확보) |
| 언어 | C / C++ |
🧩 트레이스에서 봐야 할 줄은 따로 있다
소명서를 받으면 개발자가 보고 답한 줄과, 도구가 실제로 지목한 줄이 다른 경우가 많습니다. 아래는 전형적인 트레이스 구조입니다.
| 순서 | 트레이스 표시 | 의미 |
|---|---|---|
| 1 | void * 형식 메모리를 할당합니다 |
공통함수 내부에서 새 버퍼 확보 |
| 2 | str_set_length 함수를 호출합니다 |
호출부 → 공통함수 진입 |
| 3 | void * 형식 메모리를 해제합니다 |
기존 버퍼 해제 (재할당 패턴) |
| 4 | str_rtrim 함수를 호출합니다 |
후속 공통함수 진입 |
| 5 | pszStr[sizeData] 식이 버퍼의 끝을 넘어서는 메모리를 참조합니다. 버퍼 크기는 17이나, 인덱스는 17입니다 |
실제 지적 지점(sink) |
1~4번은 경로일 뿐이고, 결함은 5번 한 줄에 있습니다. 개발자는 대개 1~2번만 보고 "동적 할당이라 안전하다"고 답하는데, 도구는 이미 버퍼 17바이트 / 접근 인덱스 17이라고 숫자까지 찍어놨습니다. 유효 인덱스는 0 ~ 16입니다. 17을 건드렸으니 정확히 1바이트 초과입니다.
🔍 1바이트는 어디서 사라졌나
실무에서 이 패턴은 거의 항상 같은 모양으로 나옵니다. 고정 길이 필드를 sizeof로 넘기고, 공통함수가 그 값을 그대로 할당 크기로 쓰는 구조입니다.
/* 호출부 */
p = str_set_length(p, src, sizeof(src)); /* sizeof(src) = 17 */
p = str_rtrim(p);
/* 공통함수 내부 (문제가 되는 형태) */
char *str_set_length(char *pszStr, const char *pSrc, int sizeData)
{
if (pszStr != NULL) free(pszStr);
pszStr = (char *)malloc(sizeData); /* ← 17바이트만 확보 */
memcpy(pszStr, pSrc, sizeData);
return pszStr;
}
char *str_rtrim(char *pszStr)
{
/* ... 뒤쪽 공백 제거 ... */
pszStr[sizeData] = '\0'; /* ← 18번째 칸에 쓴다 */
return pszStr;
}
sizeof(src)는 데이터가 들어갈 길이입니다. 널 종단 문자 자리는 여기에 포함돼 있지 않습니다. 그런데 할당은 그 값 그대로 하고, 종단 처리는 [sizeData] 위치에 합니다. 필요한 건 sizeData + 1바이트인데 sizeData바이트만 잡은 겁니다.

여기서 중요한 건 조건부가 아니라는 점입니다. 특정 입력값일 때만 터지는 게 아니라, 이 경로를 타면 매번 1바이트를 넘어갑니다.
⚠️ 개발자 답변, 항목별 검토
처음 인용한 소명을 그대로 뜯어보면 이렇게 됩니다.
| 개발자 주장 | 검토 의견 | 판정 |
|---|---|---|
| 해당 변수는 char 포인터다 | 사실 확인일 뿐 안전성 근거가 아님. 오히려 포인터라서 컴파일러·런타임 경계 검사가 없음 | 근거 부족 |
| 공통함수로 동적 할당한다 | 진단 자체가 "동적 할당 크기 계산 오류"를 대상으로 함. 반박이 아니라 전제 | 논점 이탈 |
| 그래서 버퍼보다 크게 쓸 수 없다 | 공통함수 내부의 할당식과 접근식을 제시해야 성립. 도구는 이미 반례(17 vs 17)를 제시함 | 성립 안 함 |
요약하면, 구조를 설명했을 뿐 숫자를 제시하지 않은 소명입니다. 정적 분석 결과에 대한 소명은 구조 설명이 아니라 수치 대조로 끝나야 합니다.
🚨 "1바이트인데 그게 문제가 되나요"
됩니다. 그리고 하필 제일 나쁜 종류의 1바이트입니다.
| 관점 | 내용 |
|---|---|
| 즉시 증상 | 거의 없음. 테스트에서 안 잡힘 |
| 실제 침범 대상 | 힙에서는 바로 뒤 청크의 헤더(크기·플래그 필드) |
| 터지는 시점 | 훼손 시점이 아니라 다음 free() / malloc() 시점 |
| 대표 공격 기법 | Off-by-one에 널 바이트가 들어가는 경우가 특히 위험 (청크 경계 조작) |
| 운영 관점 | 원인 모를 간헐적 코어 덤프로 나타나 재현·추적이 매우 어려움 |
즉 "지금까지 문제없이 돌았다"는 건 안전하다는 근거가 아니라, 아직 안 터졌다는 뜻입니다. 장애 조사 때 가장 시간을 많이 잡아먹는 유형이기도 합니다.
📋 오탐 판정은 숫자 세 개로 끝난다
길게 주고받을 필요 없습니다. 개발자에게 받아야 할 건 설명이 아니라 아래 세 가지입니다.
| # | 받아야 할 것 | 확인 포인트 |
|---|---|---|
| 1 | 할당 함수의 크기 계산식 | malloc(len)인가 malloc(len + 1)인가 |
| 2 | 접근 지점의 최대 인덱스 | 상한이 len - 1인가 len인가 |
| 3 | 넘긴 크기값의 의미 | sizeof 값이 널 종단을 포함한 크기인가, 데이터 전용 길이인가 |
그리고 판정식은 하나입니다.
확보한 바이트 수 ≥ 최대 접근 인덱스 + 1
이 부등식이 성립하면 오탐, 안 되면 결함입니다. 여기에 "안전한 공통함수를 씁니다" 같은 문장이 끼어들 자리는 없습니다.
추가로 같이 확인할 것
| 확인 항목 | |
|---|---|
| 공통함수가 포인터 재사용 시 이전 버퍼를 제대로 해제·재할당하는가 | ☐ |
호출 전 포인터가 NULL로 초기화되어 있는가 (미초기화 포인터 free 방지) |
☐ |
malloc 실패(NULL) 반환을 확인하는가 |
☐ |
넘기는 인자가 배열이 아니라 포인터인데 sizeof를 쓰고 있지는 않은가 |
☐ |
| 같은 공통함수를 쓰는 다른 호출부에도 동일 패턴이 있는가 | ☐ |
길이 계산에 strlen 결과를 쓸 때 +1을 빠뜨리지 않았는가 |
☐ |
마지막 항목이 핵심입니다. 이 유형은 한 곳만의 문제가 아닙니다. 공통함수에서 1바이트가 빠져 있다면 그 함수를 호출하는 모든 지점이 같은 상태입니다.
🛠️ 조치 방향
세 가지 선택지가 있고, 권고는 명확합니다.
| 방안 | 내용 | 평가 |
|---|---|---|
| ① 할당부 수정 | 공통함수에서 요청크기 + 1 할당 후 마지막 바이트를 널로 초기화 |
권고 — 한 곳 수정으로 동일 유형 전수 해소 |
| ② 접근부 수정 | 후속 함수의 인덱스 상한을 len - 1로 보정 |
호출부마다 크기값 의미가 다르면 부작용 발생 |
| ③ 호출부 수정 | 호출할 때마다 sizeof(...) + 1을 넘김 |
임시방편. 누락 가능성이 높아 비권고 |

/* 조치 후 */
char *str_set_length(char *pszStr, const char *pSrc, int sizeData)
{
char *pNew = (char *)malloc((size_t)sizeData + 1); /* 널 종단 자리 확보 */
if (pNew == NULL) return NULL; /* 실패 처리 */
memcpy(pNew, pSrc, (size_t)sizeData);
pNew[sizeData] = '\0'; /* 명시적 종단 */
if (pszStr != NULL) free(pszStr);
return pNew;
}
free를 뒤로 미룬 이유가 하나 더 있습니다. 먼저 해제해버리면 malloc 실패 시 호출부가 이미 해제된 포인터를 들고 있게 됩니다. 조치하는 김에 같이 정리해두는 편이 낫습니다.
💬 자주 나오는 질문
Q. 도구가 크기를 17이라고 특정했는데, 이것도 틀릴 수 있지 않나요?
가능은 합니다. 다만 그 주장을 하려면 도구가 17로 본 근거를 뒤집을 실제 할당식을 제시해야 합니다. 대부분은 상수나 sizeof로 크기가 확정되는 경로라 도구가 정확히 추적합니다. 반대로 매크로나 조건 분기로 크기가 갈리는 코드라면, 그 분기를 보여주는 것만으로 소명이 끝납니다.
Q. 공통함수라 수정하면 영향 범위가 너무 큽니다
영향 범위가 크다는 건 결함 범위도 크다는 뜻입니다. 1바이트 늘리는 방향의 변경은 기존 동작을 깨지 않습니다. 회귀 시험 부담 때문에 미뤄야 한다면, 위험 수용이 아니라 조치 계획(일정 포함)으로 관리하는 게 맞습니다.
Q. 사내 위험 수용으로 종결해도 되나요?
내부 기준에 따라 가능은 하지만, 이 항목은 권하지 않습니다. 수정 비용이 한 줄 수준인 데 비해, 재현 안 되는 장애로 돌아왔을 때의 비용이 비대칭적으로 큽니다. "고치기 어려워서 수용"이 아니라 "고치기 쉬운데 수용"이 되면 소명서가 심사에서 그대로 문제가 됩니다.
Q. 컴파일러 옵션이나 보호 기법으로 막을 수 있나요?
완화일 뿐 제거가 아닙니다. 힙 보호 기법은 훼손을 탐지해 프로세스를 죽이는 쪽으로 동작하므로, 결과적으로 가용성 문제로 바뀔 뿐입니다. 근본 조치는 할당식 수정입니다.
Q. 같은 진단이 다른 파일에서도 계속 올라옵니다
거의 확실하게 같은 공통함수를 타고 있습니다. 파일 단위가 아니라 함수 단위로 묶어서 한 건으로 관리하고, 조치도 한 번에 하세요. 도구 쪽에는 수정 완료 후 재스캔으로 해소를 확인하고, 안전해진 공통함수는 필요 시 분석 룰에 반영해 반복 탐지를 줄일 수 있습니다.
📮 회신 요청 문안 예시
소명이 구조 설명으로만 왔을 때, 아래 정도면 대부분 다음 회신이 구체화됩니다.
[재검토 요청]
회신 주신 "동적 할당 구조이므로 버퍼보다 크게 사용할 수 없다"는 설명은
본 진단의 반박 근거로 보기 어렵습니다. 해당 진단은 동적 할당 자체가 아니라
할당 크기를 결정한 조건이 실제 접근 범위를 포괄하지 못하는 경우를 대상으로 하며,
분석 결과에는 버퍼 크기 17바이트, 접근 인덱스 17로 수치가 특정되어 있습니다.
유효 인덱스 범위는 0~16이므로 1바이트 경계 초과에 해당합니다.
오탐 판정을 위해 아래 세 가지를 소스 기준으로 제시해 주시기 바랍니다.
1. 메모리를 확보하는 공통함수의 할당 크기 계산식
(널 종단 문자를 위한 1바이트가 포함되는지)
2. 해당 버퍼에 접근하는 후속 공통함수의 최대 인덱스 상한
3. 호출부에서 넘기는 크기값이 널 종단을 포함한 크기인지,
데이터 전용 길이인지
위 자료를 근거로 "확보한 바이트 수 ≥ 최대 접근 인덱스 + 1"이 항상 성립함이
확인되면 오탐으로 종결하겠습니다. 성립하지 않을 경우, 공통함수의 할당식을
수정하는 방향으로 조치를 요청드립니다. 해당 공통함수를 사용하는 다른 호출부에도
동일한 조치가 필요한지 함께 검토 부탁드립니다.
회신 기한: YYYY-MM-DD
🎉 정리
- 진단명을 끝까지 읽는다. "잘못된 조건으로 인한" 동적 할당 버퍼 오버플로우다. 동적 할당은 전제이지 반박이 아니다.
- 트레이스의 마지막 줄을 본다. 앞의 할당·해제 표시는 경로이고, 결함은 경계를 넘는 그 한 줄에 있다.
- 도구가 숫자를 줬으면 숫자로 답받는다. 버퍼 17 / 인덱스 17처럼 수치가 나왔다면 구조 설명은 소명이 되지 않는다.
sizeof는 널 종단을 포함하지 않는다. 고정 길이 필드를 그대로 할당 크기로 쓰면 1바이트가 항상 모자란다.- 1바이트는 가장 나쁜 종류다. 즉시 안 터지고, 다음 힙 연산 때 엉뚱한 곳에서 터진다.
- 공통함수에서 고친다. 호출부 개별 수정은 누락되고, 접근부 보정은 다른 호출부를 깨뜨릴 수 있다.
- 판정식은 하나다. 확보한 바이트 수 ≥ 최대 접근 인덱스 + 1.
정적 분석 소명 검토가 어려운 이유는 도구를 몰라서가 아니라, 설명으로 온 답변을 숫자로 바꿔 받는 과정이 빠져서입니다. 이 유형은 특히 그렇습니다. 질문 세 개만 정확히 던지면 길게 갈 일이 없습니다.
🔗 참고 자료
- CWE-131: Incorrect Calculation of Buffer Size — 할당 크기 계산 오류의 정의와 완화 방안
- CWE-193: Off-by-one Error — 경계를 1만큼 벗어나는 계산 오류
- CWE-787: Out-of-bounds Write — 경계 밖 쓰기의 영향
- CWE-122: Heap-based Buffer Overflow — 힙 영역에서 발생할 때의 특성
- SEI CERT C Coding Standard — STR31-C, MEM35-C (문자열 저장 공간과 객체 할당 크기 확보 규칙)
- Total
- Today
- Yesterday
- 보안뉴스
- 전자금융기반시설취약점분석평가
- IACS
- sast
- E27
- DevSecOps
- E26
- 사이버보안
- 해킹예방
- 금취분평
- 취약점
- 정보보안
- 개발자보안
- 공급망공격
- 보안상식
- 악성코드
- 선박사이버보안
- IEC62443
- 전자금융기반시설
- 해킹주의
- 개인정보보호
- 스마트폰보안
- 개인정보유출
- cve
- 랜섬웨어
- 시큐어코딩
- 2단계인증
- 보안꿀팁
- 샤이니헌터스
- AI보안
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
