외부 입력이 시스템 명령어 실행 인수로 적절한 처리 없이 사용되면 위험하다.
일반적으로 명령어 줄 인수나 스트림 입력 등 외부 입력을 사용하여 시스템 명령어를 생성하는 프로그램이 많이 있다.
하지만 이러한 경우 외부 입력 문자열은 신뢰할 수 없기 때문에 적절한 처리를 해주지 않으면, 
공격자가 원하는 명령어 실행이 가능하게 된다.
웹 페이지에 악의적인 스크립트를 포함시켜 사용자 측에서 실행되게 유도할 수 있다.

 

아래 그림과 같이 검증되지 않은 외부 입력이 동적 웹페이지 생성에 사용될 경우, 전송된 동적 웹페이지를 열람하는 접속자의 권한으로 부적절한 스크립트가 수행되어 정보유출 등의 공격을 유발할 수 있다.

 

외부 입력값을 검증하지 않고 시스템 자원에 대한 식별자로 사용하는 경우, 공격자는 입력값 조작을 통해
시스템이 보호하는 자원에 임의로 접근하거나 수정할 수 있다.

 

📌 안전한 코딩 기법

  • 외부의 입력을 자원( 파일, 소켓의 포트 등 ) 식별자로 사용하는 경우, 적절한 검증을 거치도록 하거나 사전에 정의된 적합한 리스트에서 선택되도록 작성한다. 외부의 입력이 파일명인 경우에는 경로 순회를 수행할 수 있는 문자를 제거한다.
데이터베이스와 연동된 웹 어플리케이션에서 입력된 데이터에 대한 유효성 검증을 하지 않을 경우,
공격자가 입력 폼 및 URL 입력란에 SQL 문을 삽입하여 DB로부터 정보를 열람하거나 조작할 수 있는 보안약점을 말한다.

 

취약한 웹 어플리케이션에서는 사용자로부터 입력된 값을 필터링 과정없이 넘겨받아 동적 쿼리를 생성한다.

이는 개발자가 의도하지 않은 쿼리가 생성되어 정보유출에 악용될 수 있다.

 

📌 안전한 코딩기법

  • 외부 입력이나 외부 변수로부터 받은 값이 직접 SQL 함수의 인자로 전달되거나, 문자열 복사를 통하여 전달되는 것은 위험하다. 그러므로 인자화된 질의문을 사용해야 한다.
  • 외부 입력값을 그대로 사용해야 하는 환경이라면, 입력받은 값을 필터링을 통해 처리한 후 사용해야 한다. 필터링은 SQL문에서 사용하는 단어 사용 금지, 특수문자 사용금지, 길이 제한의 기준을 적용한다.

 

📌 예제

#include <stdlib.h>
#include <sql.h> 
void Sql_process(SQLHSTMT sqlh) 
{ 
    char *query = getenv("query_string"); 
    SQLExecDirect(sqlh, query, SQL_NTS);
}

 

위 코드를 살펴보자.

외부 입력이 SQL 질의어에 어떠한 처리도 없이 삽입되었기 때문에, name' OR 'a'='a 와 같은 문자열을 입력으로 주면

WHERE 절이 항상 참이 된다.

 

#include <sql.h> 
void Sql_process(SQLHSTMT sqlh) 
{ 
    char *query_items = "SELECT * FROM items"; 
    SQLExecDirect(sqlh, query_items, SQL_NTS);
}

 

위 코드는 인자화된 질의를 사용해 질의 구조의 변경을 막을 수 있다.

시큐어 코딩은 소프트웨어 개발 과정에서 보안 취약점을 최소화하여 악의적인 공격으로부터 시스템을 보호하기 위해
안전한 코드를 작성하는 방법을 말한다.

 

📌 시큐어 코딩의 주요 원칙

  • 입력 검증 : 모든 '입력'을 신뢰하지 말고 검증해야한다.
  • 출력 인코딩 : 출력 데이터를 인코딩하여 악의적인 코드 실행을 방지해야한다.
  • 최소 권한 원칙 : 시스템 내에서 각 사용자나 프로세스가 최소한의 권한만을 가지고 작업할 수 있도록 설정해야한다.
  • 암호화 : 중요한 데이터는 전송 중에나 저장 중에 암호화하여 유출을 방지해야한다.
  • 에러 처리 및 로깅 : 시스템 오류 메시지나 스택 트레이스를 외부에 노출하지 않도록 하고, 필요한 에러 로그는 안전하게 기록하여 디버깅과 추적이 가능하게 해야한다.
쉘코드는 보통 악성 코드에서 사용되는 용어로, 컴퓨터 시스템의 취약점을 이용해 공격자가 원격으로 코드를 실행하거나
특정 동작을 강제로 실행할 수 있도록 하는 작은 코드를 말한다.

 

📌 쉘 코드 작동 원리

쉘코드는 보통 시스템의 메모리 공간에 삽입되어, 시스템 취약점을 악용하여 악성 코드를 실행하게 만든다.

 

  • 윈도우 탐색기처럼 다른 프로그램을 실행 할 수 있는 코드조각을 말함
  • 핵심은 다른 프로그램을 실행 시키는 것
  • 특정 프로세스가 스스로 새 프로세스를 생성할 경우 권한이 그대로 복제된다.

 

  • 한 프로세스는 최소 1개 이상의 쓰레드를 갖는다.
  • 쓰레드는 개별화된 흐름( 문맥 )과 전용 스택을 갖는 실행의 단위다.
  • 모든 쓰레드는 자신이 속한 프로세스의 가상 메모리 공간을 공유한다.

 

 

📌 함수 포인터 활용

고속 처리

 

int main(void)
{	
	int instructions[1024] = { 0 };
	int pc = 0;

	while (instructions[pc])
	{
		switch (instructions[pc])
		{
		case 1: //add
			break;
		case 2: //sub
			break;
		case 3: //mul
			break;
		case 4: //div
			break;
		default:
			break;
		}

		pc++;
	}

	return 0;
}

 

위 코드는 instructions 배열 안에 있는 값에 따라 switch - case로 분기를 해 함수를 실행하는 구조문이다.

 

instructions 배열 안에 값이 1일 경우, add를 호출해 빠르게 처리할 수 있지만

만약 항목이 더 많아져서 배열안의 값이 1000일 경우 ( 물론 지금은 default로 처리 되지만 )

1000까지 비교해가면서 함수를 실행해야하기 때문에 속도가 느려진다.

 

이때 함수포인터 배열을 활용해 구조를 바꾸면 보다 빠르게 함수를 실행할 수 있어진다.

 

#include<stdio.h>

typedef struct MyParams {
	int param1;
	int param2;
	int param3;
} MYPARAMS;

int (*g_op_list[5])(MYPARAMS*); // 함수 포인터 배열

int add(MYPARAMS* pParam)
{
	return pParam->param1 + pParam->param2 + pParam->param3;
}

int sub(MYPARAMS* pParam)
{
	return pParam->param1 - pParam->param2 - pParam->param3;
}

int mul(MYPARAMS* pParam)
{
	return pParam->param1 * pParam->param2 * pParam->param3;
}

int div(MYPARAMS* pParam)
{
	return pParam->param1 / pParam->param2 / pParam->param3;
}

void init_op_list(void)
{
	g_op_list[1] = add;
	g_op_list[2] = sub;
	g_op_list[3] = mul;
	g_op_list[4] = div;
}

int main(void)
{
	init_op_list();
	int instructions[1024] = { 0 };
	int pc = 0;

	MYPARAMS param = { 0 };

	while (instructions[pc])
	{
		g_op_list[instructions[pc]](&param);

		pc++;
	}

	return 0;
}

 

위 처럼 switch - case 구문을 삭제하고 g_op_list 함수 포인터 배열을 활용해 직접적으로 해당 함수를 빠르게 호출해 줄 수 있다.

함수 포인터는 함수를 가리키는 포인터로, 특정 함수의 주소를 저장하고 이를 통해 해당 함수를 호출할 수 있다.

 

  • 함수의 이름은 그 자체가 주소다.
  • 함수호출 연산자의 피연산자는 함수 포인터와 같은 형식이여야 한다.
  • 실행 코드가 저장된 메모리를 가리키는 포인터다.

 

이름이 주소인 경우는 배열 또한 마찬가지인데, 

배열은 프로세스에서 데이터 영역에 저장된다. 데이터 영역은 읽고 쓰기가 가능하며, 실행은 불가능하다.

 

반면 실행 코드가 담겨 있는 실행 코드 영역에서는 읽기와 실행이 가능하며 쓰기는 불가능하다.

만약 실행 코드 영역이 쓰기가 가능하면 해당 실행 코드가 위조 혹은 변조가 되기 때문

 

운영체제는 실행코드의 영역과 데이터의 영역을 강력하게 구별한다.

 


 

📌 함수 포인터 형식

반환형 (*이름)(매개변수, ... )

 

int add(int a, int b) {
	return a + b;
}

 

위와 같은 함수가 있을때, add 함수를 함수포인터에 담으려면 다음과 같이 선언하고 add 함수를 담으면 된다.

 

int (*pfAdd)(int, int) = add;

 


#include<stdio.h>

int add(int a, int b) {
	return a + b;
}

int main() {
	int result = 0;
	result = add(1, 2);

	printf("Result: %d\n", result);

	int (*pfAdd)(int, int) = add;
	result = pfAdd(3, 4);
	printf("Result: %d\n", result);

	return 0;
}

 

위와 같이 코드를 구성하고 실행한 후 어셈블리어를 확인해보자.

 

Release에서 살펴보면, 함수 포인터 역시 함수를 호출하는 변수이기 때문에 최적화가 될 경우 함수 포인터를 콜해도 어셈블리에서는 call 하지 않는 것을 확인 할 수 있다.

 

 

반면 Debug에서는 pfAdd에 add 함수의 주소를 담고, pfAdd를 call하는 것을 확인 할 수 있다.

 

 

 

 

inline함수는 C++에서 사용되는 키워드로, 함수 호출 오버헤드를 줄이기 위해 함수의 기계어 코드가 호출된 위치에 직접 삽입되도록 컴파일러에게 요청하는 역할을 한다.

 

  • 함수 호출에 필요한 스택 프레임 생성, 스택 증/감 매개변수 복사 등 부가적 코드 수행에 따른 오베헤드 발생을 줄여 속도를 향상시킨다.
  • 불필요한 오버헤드 제거를 위해 코드 상 존재하는 Callee의 코드를 Caller에 포함시켜 실제로는 호출하지 않도록 해 최적화 한다.
  • 다만 inline 키워드를 사용했다고 해서 반드시 되는 것은 아니고, 컴파일러가 결정한다.

 

+ Recent posts