본문 바로가기

Software

Secure Cordng C - Expression

EXP-00 연산자 우선순위를 나타내는데 괄호를 사용하라


괄호를 적절하게 사용하면 우선순위 때문에 발생하는 실수를 피할 수 있으며, 

방어적으로 에어를 줄일 수 있고 코드 가독성도 높아진다.


1. 부적절한 코드

  • 연산자 우선순위를 잘못 이해하여 코드가 의도대로 실행되지 않는다.
#include <stdio.h>

int is_even(int x){
return x & i == 0 ? 1 : 0;
}

int main(){
int n = 2;
if(is_even(n))
printf("even\n");
else
printf("odd\n");

return 0;
}


  • 해결 방법 - 표현식이 의도대로 평가되도록 괄호를 사용한다.
#include <stdio.h>

int is_even(int x){
return (x & i) == 0 ? 1 : 0;
}

int main(){
int n = 2;
if(is_even(n))
printf("even\n");
else
printf("odd\n");

return 0;
}



2. 부적절한 코드

  • 연산자 우선순위를 잘못 이해하여 코드가 의도대로 실행되지 않는다.
#include <stdio.h>

void incr(int* p){
*p++;
}

int main(){
int cnt = 0;

incr(&cnt);
printf("cnt = %d\n", cnt);

return 0;
}


  • 해결 방법 - 표현식이 의도대로 평가되도록 괄호를 사용한다.
#include <stdio.h>

void incr(int* p){
(*p)++;
}

int main(){
int cnt = 0;

incr(&cnt);
printf("cnt = %d\n", cnt);

return 0;
}


EXP-01 논리 연산자 AND와 OR의 단축 평가 방식을 알고 있어라


논리 연산자 AND와 OR은 단축 평가를 수행한다.

즉, 첫 번째 피연산자로 평가가 완료되면 두 번째 피연산자는 평가하지 않는다.


연산자 

첫 번째 피 연산자 

두 번째 피 연산자 

AND(&&)

거짓

평가 안함 

OR(||) 

참 

평가 안함 



1. 부적절한 코드

  • 다음 코드는 배열에서 0이 몇 개인지를 세는 코드이다. 그러나 AND 연산자의 첫 번째 피 연산자가 거짓이므로
    두 번째 피 연산자는 평가되지 않는다.
#include <stdio.h>
#define SIZE_MAX    (5)

int main(){
int arr[SIZE_MAX] = { 0,1,2,3,0 };

int i = 0;
int cnt = 0;

while((arr[i] == 0) && (++i < SIZE_MAX))
++cnt;
printf("%d\n", cnt);

return 0;
}


  • 해결 방법 - 단축 평가가 이루어지지 않도록 한다.
#include <stdio.h>
#define SIZE_MAX    (5)

int main(){
int arr[SIZE_MAX] = { 0,1,2,3,0 };

int i = 0;
int cnt = 0;

for(i = 0; i < SIZE_MAX; i++){
if(arr[i] == 0)
++cnt;
}
printf("%d\n", cnt);

return 0;
}


EXP-02 구조체의 크기가 구조체 멤버들 크기의 합이라고 가정하지 마라


구조체의 크기는 항상 멤머들 크기의 총합과 일치하지 않는다. C99 표준에 의하면

"구조체 객체에는 명명되지 않은 패딩(padding)이 들어갈 수 있으며, 앞쪽에는 위치하지 않는다"고 명시되어 있다.

이는 CPU가 메모리에 빠르게 접근할 수 있도록 하기 위함이다.

구조체의 패딩이 어떻게 포함될지는 컴파일러의 정의에 따른다.


1. 부적절한 코드

  • 다음은 구조체의 크기가 멤버들의 총합과 같다고 가정하고 있다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct _string{
char arr[10];
int len;
}string;

string* make_str(cont char* str){
string* p = malloc(14);
strcpy(p->arr, str);
p->len = strlen(str);

return p;
}

int main(){
string* hello = make_str("hello");

string buf;
memcpy(&buf, hello, sizeof(string));

free(hello);

return 0;
}


  • 해결 방법 - sizeof 연산자를 사용한다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct _string{
char arr[10];
int len;
}string;

string* make_str(cont char* str){
string* p = malloc(sizeof(string));
strcpy(p->arr, str);
p->len = strlen(str);

return p;
}

int main(){
string* hello = make_str("hello");

string buf;
memcpy(&buf, hello, sizeof(string));

free(hello);

return 0;
}


EXP-03 const를 캐스트로 없애지 마라


const로 선언된 객체를 비 상수 객체로 변환하여 사용할 경우, 의도하지 않는 결과가 나타날 수 있다.


1. 부적절한 코드

  • strlen 함수 내부에서 인자로 전달된 문자열의 값을 변경할 수 있다.
#include <stdio.h>

int str_len(const char* s){
char* p = s;
while(*p)
++p;

return p - s;
}

int main(){
char str[] = "hello";
printf("%d\n", str_len(str));

return 0;
}


  • 해결 방법 - 비 상수 객체의 포인터로 캐스팅하지 않는다.
#include <stdio.h>

int str_len(const char* s){
const char* p = s;
while(*p)
++p;

return p - s;
}

int main(){
char str[] = "hello";
printf("%d\n", str_len(str));

return 0;
}


EXP-04 포인터 연산이 정확하게 수행되고 있는지 보장하라


포인터 연산을 수행할 때 포인터에 더해지는 값은 자동적으로 포인터가 가리키는 객체의 타입으로 조정된다.

때문에 포인터 연산이 어떻게 동작하는지 이해하고 있지 않는다면 심각한 에러나 버퍼 오버플로를 초래한다.


어떤 포인터 ptr과 임의의 정수 n이 있을 때, 포인터 연산은 내부적으로 다음과 같이 동작한다.

  • ptr + n == ptr + (sizeof(*ptr) * n)

1. 부적절한 코드
  • 다음 코드는 버퍼 오버플로가 발생한다.
#include <stdio.h>
#define BUF_SIZE    (10)

int main(){
int buf[BUF_SIZE];
int i;

int* cur = buf;
while(cur < (buf + sizeof(buf)))
*cur++ = 0;

for(i = 0; i < BUF_SZIE; i++){
printf("buf[%d] = %d\n", i, buf[i]);
}

return 0;
}


  • 해결 방법 1 - 포인터 연산 시, 정수의 크기만큼 계산될 수 있도록 char* 타입으로 캐스팅 한다.
#include <stdio.h>
#define BUF_SIZE    (10)

int main(){
int buf[BUF_SIZE];
int i;

int* cur = buf;
while(cur < ((char*)buf + sizeof(buf)))
*cur++ = 0;

for(i = 0; i < BUF_SZIE; i++){
printf("buf[%d] = %d\n", i, buf[i]);
}

return 0;
}


  • 해결 방법 2 - 첨자 연산자를 사용한다.
#include <stdio.h>
#define BUF_SIZE    (10)

int main(){
int buf[BUF_SIZE];
int i;

for(i = 0; i < BUF_SZIE; i++)
buf[i] = 0;

for(i = 0; i < BUF_SZIE; i++){
printf("buf[%d] = %d\n", i, buf[i]);
}

return 0;
}


EXP-05 타입이나 변수의 크기를 결정할 때는 sizeof를 사용하라


애플리케이션에서 타입 크기를 하드 코딩하지 않는 것이 좋다.

대부분 타입의 크기가 컴파일러마다 다르고 동일한 컴파일러 내에서도 버전에 따라 다를 수 있다.


1. 부적절한 코드

  • 다음의 코드는 포인터와 정수를 4바이트로 가정하고 있다.
    시스템에 따라 정수의 크기는 달라질 수 있으므로 의도하지 않는 결과를 초래할 수 있다.
#include <stdio.h>
#include <stdlib.h>
#define ARR_SIZE    (10)

int main(){
int** pArr = (int**)malloc(4 * ARR_SIZE);
int i;

for(i = 0; i < ARR_SIZE; i++){
pArr[i] = (int*)malloc(4 * ARR_SIZE);
}


//...

for(i = 0; i < ARR_SIZE; i++){
free(pArr[i]);
}
free(pArr);

return 0;
}


  • 해결 방법 - sizeof 연산자를 사용한다.
#include <stdio.h>
#include <stdlib.h>
#define ARR_SIZE    (10)

int main(){
int** pArr = (int**)malloc(sizeof(int*) * ARR_SIZE);
int i;

for(i = 0; i < ARR_SIZE; i++){
pArr[i] = (int*)malloc(sizeof(int*) * ARR_SIZE);
}


//...

for(i = 0; i < ARR_SIZE; i++){
free(pArr[i]);
}
free(pArr);

return 0;
}


EXP-06 함수에 의해 반환되는 값을 무시하지 마라


일반적으로 함수는 반환 시 유용한 값을 전달하는데, 대게 이 값은 함수가 작업을 성공적으로 수행했는지 혹은 에러가 발생했는지를 확인하는데 사용한다.


1. 부적절한 코드

  • 다음 코드는 정수를 입력 받기 위해 scanf 함수를 사용하지만 에러가 발생하는지를 체크하지 않는다.
#include <stdio.h>

int main(){
int num;

printf("input number: ");
scanf("%d", &num);
printf("-> %d\n", num);

return 0;
}


  • 해결 방법 - 입력 에러가 발생했는지 체크한다.
#include <stdio.h>

int main(){
int num;

printf("input number: ");
int ret = scanf("%d", &num);

if(ret == 0 || ret == EOF)
printf("input error\n");
else
printf("-> %d\n", num);

return 0;
}


EXP-07 시퀀스 포인트들 간의 평가 순서에 의존하지 마라


표현식 평가(계산)이 완료되는 시점을 시퀀스 포인트(sequence pointer)라고 한다.

그리고 시퀀스 포인트 사이에서 객체는 표현식의 평가로 한 번만 수정할 수 있다.

가장 대표적인 시퀀스 포인터는 세미콜론이다.

  • i = ++i + 1;
  • arr[i++] = i;
  • func(n++, n);
1. 부적절한 코드
  • 함수 인자 평가 순서는 정의되어 있지 않다. 따라서 다음의 코드는 미정의 동작이다.
#include <stdio.h>

void func(int a, int b){
//...
}

int main(){
int i = 2;
func(i++, i);

return 0;
}


  • 해결 방법 1 - func 함수에 전달되는 인자를 동일한 값으로 설정하는 것이라면, 다음과 같이 해결한다.
#include <stdio.h>

void func(int a, int b){
//...
}

int main(){
int i = 2;
++i;
func(i, i);

return 0;
}


  • 해결 방법 2 - 두 번째 인자가 첫 번째보다 1만큼 크게 하려는 것이라면 다음과 같이 해결한다.
#include <stdio.h>

void func(int a, int b){
//...
}

int main(){
int i = 2;
int j = i++;

func(j, i);

return 0;
}


EXP-08 어썰션의 부수 효과를 피하라


assert와 함께 사용되는 표현식은 부수 효과를 가지면 안된다.

이는 assert 함수가 매크로이기 때문이고, 

매크로 함수 내에서 값의 할당, 증가, 감소, 메모리 변수의 접근, 함수 호출 등은 미정의 동작이다.


1. 부적절한 코드

  • assert 함수에서 값을 증가시키고 있다.
#include <stdio.h>
#include <assert.h>

int process(int i){
assert(i++ > 0);
printf("%d\n", i);
}

int main(){
int num;

printf("input size: ");

int ret = scanf("%d", &num);

if(ret != 1){
printf("scanf error\n");
}else{
process(num);
}

return 0;
}


  • 해결 방법 - assert 함수에서 증가 연산자를 제거한다.
#include <stdio.h>
#include <assert.h>

int process(int i){
assert(i > 0);
++i;
printf("%d\n", i);
}

int main(){
int num;

printf("input size: ");

int ret = scanf("%d", &num);

if(ret != 1){
printf("scanf error\n");
}else{
process(num);
}

return 0;
}


EXP-09 초기화되지 않는 메모리를 참조하지 마라


함수 내에 선언된 자동 변수(auto variable)는 초기화하지 않으면 쓰레기(garbage) 값으로 설정된다.

C99 표준에서는 자동 변수에 대해 다음과 같이 정의하고 있다.

  • 객체가 자동 변수 저장 공간에 있는 경우, 초기화되지 않았다면 변수의 값은 정의되어 있지 않다.
따라서 초기화되지 않은 변수를 사용하면 프로그램은 제대로 동작하지 않을 수 있다.


1.부적절한 코드
  • sum_to 함수 내에서 지역 변수 sum의 값을 제대로 초기화하지 않아 프로그램은 의도하지 않게 실행된다.
#include <stdio.h>

int sum_to(int num){
int sum, i;
for(i = 1; i <= num; i++)
sum += i;

return sum;
}

int main(){
int input;

printf("input number: ");
scanf("%d", &input);

printf("sum 1 to %d: %d\n", input, sum_to(input));

return 0;
}

  • 해결 방법 - 지역 변수 sum을 적절한 값으로 초기화해준다.

#include <stdio.h>


int sum_to(int num){
int sum, i = 0;
for(i = 1; i <= num; i++)
sum += i;

return sum;
}

int main(){
int input;

printf("input number: ");
scanf("%d", &input);

printf("sum 1 to %d: %d\n", input, sum_to(input));

return 0;
}


EXP-10 널 포인터가 역참조 되지 않음을 보장하라


널 포인터를 역참조 하면 프로그램은 알 수 없는 상태가 되며 보통은 종료된다.

때문에 널 포인터는 역참조 하지 않는 것이 좋다.


1. 부적절한 코드

  • malloc 함수 호출 후, 리턴 값을 조사하지 않고 있다. 여기서 만약 널 포인터가 반환될 경우, 프로그램은 비정상 종료된다.

#include <stdio.h>

#include <stdlib.h>


int main(){

int* pArr = malloc(-1);

int i;


for(i = 0; i < 10; i++)

pArr[i] = 0;


free(pArr);


return 0;

}


  • 해결 방법 - malloc 함수의 반환 값을 조사한다.
#include <stdio.h>

#include <stdlib.h>


int main(){

int* pArr = malloc(-1);

if(pArr == NULL){

printf("malloc error\n");

exit(-1);

}


int i;


for(i = 0; i < 10; i++)

pArr[i] = 0;


free(pArr);


return 0;

}



EXP-11 함수의 반환 값을 인접한 다음 시퀀스 포인터에서 접근하거나 수정하지 마라


C99에서는 다음과 같이 언급하고 있다.

  • 함수 호출 결과 값을 다음 시퀀스 포인트에서 수정하려고 한다면 정의되지 않은 결과를 얻게 한다.

C 함수는 배열을 반환할 수 없으나 배열을 가진 구조체나 공용체는 반환할 수 있다.

만약 함수 호출 후 얻은 반환 값에 배열이 있다면 그 배열은 표현식 내에서 접근되거나 수정되면 안된다.



1. 부적절한 코드

  • 다음의 코드는 함수 호출 후 얻은 구조체로 부터 배열을 얻어오려 하고 있다.
  • 함수 반환 값의 수명은 다음 시퀀스 포인트 전에 끝나게 되므로 함수에 의해 반환된 구조체는 더 이상
    유효하지 않거나 다른 값으로 변경될 가능성이 크다.
임시 객체 : 이름이 없음.(다음 시퀀스에서 파괴)
지역 객체 : 이름이 있다.(함수가 종료될 때 파괴)

#include <stdio.h>
#include <string.h>

typedef struct{
char buf[32];
}String;

String make_str(const char* s){
String str;
strcpy(str.buf, s);

return str;
}

int main(){
printf("%s\n", make_str("hello").buf);

return 0;
}


  • 해결 방법 - 반환되는 구조체를 저장 후 사용한다.
#include <stdio.h>
#include <string.h>

typedef struct{
char buf[32];
}String;

String make_str(const char* s){
String str;
strcpy(str.buf, s);

return str;
}

int main(){
String str = make_str("hello");

printf("%s\n", str.buf);

return 0;
}


반응형

'Software' 카테고리의 다른 글

Yocto Project 소개  (0) 2021.08.18
Error message 모음  (0) 2019.02.08
Secure Cordng C - Declaration  (0) 2018.07.24
Secure Cordng C - Preprocessor  (0) 2018.07.23
Secure Cordng C - Coding Style  (0) 2018.07.20