구글에 레이스 컨디션 이라고 검색하니 공격기법만 난무해서, 간단한 예제와 함께 포스팅을 시작하려 합니다.


우선 레이스 컨디션이란 단어에 대한 뜻은 위키 백과에 나와 있는 부분을 가져왔습니다.



전산학에서 경쟁 상태란 공유 자원에 대해 여러 개의 프로세스가 동시에 접근을 시도하는 상태를 말한다. 동시에 접근할 때 자료의 일관성을 해치는 결과가 나타날 수 있다. 이를 방지하기 위해서는 프로세스 협력 기법이 필요하다. 

-http://ko.wikipedia.org/wiki/경쟁_상태


사실 프로세스가 아니더라도 경쟁상태에 있을수 있는것은 수많은 것들이 있습니다. 그것은 스레드가 될수도 있고, 다른 어떤 자원이 될수도 있겠죠!


사실 이러한 레이스 컨디션을 이야기 하기전에 조금 다른 이야기 일수 있는 fork, system, execl, thread 등의 프로세스 내부에서 다른 프로세스나 스레드를 생성하는 것들에 대해서 이야기 해봅시다.


복잡한 코딩없이 시스템 함수를 실행시키기 편한 system의 예제부터 보도록 하죠.


system.c

#include <stdio.h>

#include <unistd.h> int main() { printf("I'm Parent!\n"); system( "./call_exec"); printf("is over!\n"); }


call_exec.c

#include <stdio.h>
int main()
{
        int i;
        printf("I'm child!\n");
        for(i =0; i <100; i++)
                printf("now.... %d\n",i);
        while(1)
        {}
}



system.c와 call_exec를 컴파일 하고 돌려봅시다.

물론, call_exec의 무한반복루프 때문에 프로그램이 종료되지 않은채 살아 있을겁니다.

컨트롤+c 키를 이용해서 무한반복문을 빠져나와 보죠.


맨 마지막의 is over! 도 출력이 정상적으로 되는걸 볼 수 있습니다. 예상했던 결과대로 프로그램이 흘러가는군요.

system은 내부적으로 결과값을 리턴하기 전까지 부모 프로세스가 죽지 않았다는걸 알수 있습니다.

이제는 일반적인 pwnable 환경에서 자주 쓰이는 execl 등의 함수를 알아보도록 하죠.


예제코드는 아래와 같습니다.


exec.c

#include <stdio.h>

#include <unistd.h> int main() { printf("I'm Parent!\n"); execl( "./call_exec", "./call_exec", NULL); printf("is over!\n"); }

 


두번째 인자값은 argv[0]로 들어가는 인자값이라고 합니다. argv[0]는 자신이 실행된 경로가 적힌 arg로,

int main(int argc, char** argv)를 사용하셔서 확인하실수 있습니다.

이제 이것을 컴파일하고 실행시켜보죠.


예상과는 다르게 execl이 프로그램을 실행시키든 말든 is over!라는 문자열을 출력해주지 않는군요.

왠지 바이너리 자체가 다른걸로 교체된것 같은 느낌을 강하게 받습니다. 그럼 fork는 어떨까요?

fork.c

#include <stdio.h> #include <stdlib.h> int global_secret_number; int main() { int main_secret_number = 1; global_secret_number = 3; int pid; pid = fork(); if(pid>0) { main_secret_number++; global_secret_number++; printf("I'm Parent!\n main: %d global: %d\n",main_secret_number,global_secret_number); printf("PID %d\n",pid); } else { printf("I'm child!\n main: %d global: %d\n",main_secret_number,global_secret_number); printf("PID: %d\n",pid); } }


소스가 이전것들과 비해 좀 복잡한 느낌이 있습니다. fork는 부모의 프로세스를 그대로 '복사' 하여 한번 더 실행한것과 같은 효과를 가집니다.

부모와의 차이를 두기위해 pid로 자식/부모를 구분하며, pid가 0이면 자식, 0 이상이면 부모으로 판별하는 방법을 주로 사용합니다.

여기서 복사 하였다는 표현을 사용했는데, 아래 결과를 보면 어느정도 짐작할 수 있습니다.



전역변수로 사용한 global의 값을 자식프로세스에서 +1을 더했지만, 부모프로세스에서는 변화가 없는걸 보실수 있습니다.

이것은 fork를 하면서 모든 메모리 영역을 똑같이 복사해서 자식만의 메모리를 가졌기 때문입니다.

(똑같은 행동을 하는 프로세스를 하나 더 실행했다고 생각하시면 이해가 편하실겁니다.)


그럼 이쯤에서 레이스 컨디션의 이야기로 넘어가 보도록 하죠.

우선 아래와 같은 상황을 가정해봅시다.


1. 두 프로세스가 공통된 파일에 접근한다.

2. 하나의 프로세스에 있는 두개의 쓰레드가 공통된 파일에 접근한다

3. 부모 프로세스가 값을 생성하고, 그 값을 자식 프로세스가 가져와 사용하려한다.


이러한 상황에서 발생할수 있는 문제가 있을까요?

우선 간단한 thread를 통한 예제를 살펴봅시다.


thread.c

#include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <time.h> #define bool char #define STDOUT 1 bool islock; bool outlock; bool inlock; int money; void* input(void* arg) { char buf[50] = {0,}; int i; //for busy waiting while(islock && inlock) {} outlock = 0; for(i =0; i<100; i++) { int len; len = sprintf(buf,"INPUT MONEY : %d\n",money); money += 100000; printf("%s",buf); } } void* output(void *arg) { char buf[50] = {0,}; int i; //for busy waiting while(islock && outlock) {} inlock =0; for( i =0; i <100; i++) { int len; len = sprintf(buf,"OUTPUT MONEY : %d\n",money); money -=100000; write(STDOUT,buf,len); } } int main() { money = 100000; pthread_t thread[2]; islock= inlock= outlock= 1; //Do input! printf("THREAD MAKE!\n"); pthread_create(&thread[0],NULL,&input,NULL); pthread_create(&thread[1],NULL,&output,NULL); sleep(1); islock = 0; sleep(1); }



우선 input과 output의 함수의 속도 차이를 주기 위해서 하나는 저수준의 입출력을, 하나는 단순 printf문을 사용해서 프로그램을 설계해보았습니다. 물론, 저수준의 입출력이 보통의 printf보다 약간은 더 빠르겠죠?

그리고 예상되는 결과를 생각하시고 컴파일후 실행해 보시길 바랍니다.

제가 실행해 보았을때는 아래와 같이 2군데 에서 이상한 점을 찾을 수 있었습니다.




쓰레드(프로세스)를 만들고 나서 OUTPUT MONEY에 돈이 100000 증가한 점,

그리고 OUTPUT MONEY 이후에 INPUT MONEY의 금액이 갑자기 급증한 점.

과연 관계가 있을까요?


여기서 예상가능한 상황은 다음과 같습니다.


1. OUTPUT MONEY 가 동작한 이후, INPUT MONEY가 동작하면서 전역변수 money의 값이 변경되었다.

2. 하나의 전역변수 money에 대해서 두개의 쓰레드(프로세스)는 각자 다른값을 가지고 자신이 취해야 하는 행동을 했다.

3. 그 결과, 100000이 유지되어야 할 전역변수 money의 값이 서로 다르게 책정되었고, 원하지 않는 결과를 가져왔다.


그림으로 나타내면 이렇게 되겠죠!




이게 레이스 컨디션입니다.

OUTPUT 함수가 실행될 동안 어떠한 함수나 프로세스, 쓰레드는 전역변수 money에 접근해서는 안되며,

INPUT 함수가 실행될 동안 어떠한 함수나 프로세스, 쓰레드는 전역변수 money에 접근해서는 안된다 라는 원칙을 정해주지 않았기 때문에 

서로가 가지고 있는 값이 다르게 된 것이죠.


이 예시를 좀더 와닿을수 있는 예제 코드를 준비해 보았습니다.


bank.c


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h> // for logging!

#define ACCOUNT "ACCOUNT"
#define LOG "LOG"
#define DEPOSIT 1
#define DRAW 2
#define ASK 3

//is not important in this example. it just write log.
void WriteHistory(unsigned int menu, unsigned int amount, unsigned int money)
{
	FILE* log = fopen(LOG,"a");
	char action[10] = {0,};
	char buf[100] = {0,};
	time_t current;
	time(&current);

	fwrite("--------------------------------\n",1,33,log);
	switch(menu)
	{
		case DEPOSIT:
			sprintf(buf,"MONEY :: %d (DEPOSIT :: %d) \n TIME :: %s\n\n",money,amount,ctime(&current));
			break;
		case DRAW:
			sprintf(buf,"MONEY :: %d (DRAW :: %d) \n TIME :: %s\n\n",money,amount,ctime(&current));
			break;
		case ASK:
			sprintf(buf,"ACCOUNT ASK \n TIME :: %s\n",ctime(&current));
			break;
	}
	fwrite(buf,1,strlen(buf),log);
	fclose(log); //LOG EXIT
}

void clear()
{
	system("clear");
}
void printmenu()
{
	clear();
	printf("+--------------------------------+\n");
      	printf("|     WELCOME TO SAKUYA ATM      |\n");
        printf("+--------------------------------+\n");
	printf("|[*]HERE IS YOUR MENU            |\n");
        printf("+--------------------------------+\n");
        printf("|1. Deposit                      |\n");
        printf("|2. Draw                         |\n");
        printf("|3. Ask my account               |\n");
        printf("+--------------------------------+\n");
        printf(">> ");

}

FILE* openaccount(char* mode)
{
	FILE* ret;
	while(1)
	{
		ret = fopen(ACCOUNT,mode);
		if(ret>0)
			return ret;
	}	
}

void deposit()
{
	//OPEN MY ACCOUNT
	FILE* Raccount = openaccount("r");
	unsigned int money;
	char buf[10] = {0,};
	unsigned int input;
	fscanf(Raccount,"%d",&money);
	fclose(Raccount);	//this file no more use for read mode. aren't you?

	//DO Deposit
	clear();
	printf("How much depoist in your account?\n>> ");
	scanf("%d",&input);
	
	money += input;

	sprintf(buf,"%d",money);
	
	FILE* account = openaccount("w");
	fwrite(buf,1,strlen(buf),account);
	fclose(account);
	
	//logging
	WriteHistory(DEPOSIT,input,money);	
}

void draw()
{
	//OPEN MY ACCOUNT
	FILE* Raccount = openaccount("r");
	unsigned int money;
	char buf[10] = {0,};
	unsigned int output;
	fscanf(Raccount,"%d",&money);
	fclose(Raccount);	//this file no more use for read mode. aren't you?

	//DO Draw
	while(1)
	{
		clear();
		printf("Your account has %d won!\n",money);
		printf("How much draw in your account?\n>> ");
		scanf("%d",&output);
		if(money < output)
			continue;
		else
		{
			money -= output;
			break;
		}
	}
	
	sprintf(buf,"%d",money);	//for fwrite!

	//OPNE MY ACCOUNT to write my money
	FILE* account = openaccount("w");
	fwrite(buf,1,strlen(buf),account);
	fclose(account);

	//logging
	WriteHistory(DRAW,output,money);
}


void ask()
{
	WriteHistory(ASK,0,0);
	clear();
	system("cat ./LOG");
	printf("For security reason, we will show this log 5 second.\n");
	sleep(5);
}

int main()
{
	int sel;
	while(1)
	{
		printmenu();
		scanf("%d",&sel);
		switch(sel)
		{
			case DEPOSIT:
				deposit();
				break;
			case DRAW:
				draw();
				break;
			case ASK:
				ask();
				break;
		}
	}	
}


소스가 좀 길군요. 실행화면을 가져와 볼께요.


간단한 ATM 기계네요!

입금, 출금을 할수 있고 내 계좌에 대해 조회도 할 수 있습니다.

우선 간단히 입금과 출금 기능을 살펴 볼게요.

아,참 이 프로그램은 관리자만 ACCOUNT에 접근해야 하므로 setuid를 걸어놓았습니다. 아무래도 그게 더 현실적이니까요 :D



우선 입금 기능을 살펴보도록 하죠.

원하는 액수만큼 돈을 넣으시면 됩니다.

간단하죠?



출금기능에선 내가 가진 잔고를 보여주고 그 잔고 이상을 출금하려고 하면 출금이 안됩니다.



그리고 계좌조회 기능에서는 내가 어떤행동을 언제 했는지 친절히 보여주는 프로그램입니다. 전체적으로 간단한 기능을 가진 프로그램들이죠.


여기서 레이스컨디션이 일어날수 있을까요?


우선 이 프로그램을 2개 켜보도록 합시다.그리고 돈을 출금해보도록 하죠.

그런데, 정말 우연히도,

제 계좌에 누군가가 돈을 송금했습니다. 그것도 무려 1000만원이나 되는 금액을요!

제가 계좌에서 돈을 뺴 가기도 전에 말이죠!

그림으로 보면 다음과 같은 상황일겁니다


돈이 필요해서 20만원을 모두 빼려는데,


누군가 저에게 천만원을 입금해 줬고!


그 금액이 그대로 들어가서 돈이 총 1020만원이 되었지만


제 계좌에는 0원이 남아있군요!


순서대로 정리를 하면, 



1번 상황에서 제가 가지고 있는 돈은 200000입니다. 

그리고 2번 상황이 오죠 (입금). 여기서도 물론 제가 가지고 있는 돈은 200000입니다.

3번처럼 입금이 완료되고, 원래 흐름이었던 출금으로 돌아왔을때 기억하고 있는 금액은 200000이죠. 입금이 더해진 금액이 아닙니다.


그리고 출금을 완료하고 기록을 하게되면, 금액은 0원이 되겠죠.


이것 또한 제 계좌에 대해서 한번에 2개이상의 프로세스가 접근했기 때문에 발생하는 것입니다.

제가 출금을 하고있을때 다른분이 제 계좌에 접근하지 못했다면, 좀더 정확히는 제 계좌의 돈이 동기화가 됬다면 이 문제는 발생하지 않았겠죠.


사실 이러한 문제를 일으키는 코드들이 도처에 널려있을지도 모릅니다.



------------------------------------------------

int -> unsigned int :: thx to chofly 

설명을 위한 그림추가

fork 관련 내용 수정 :: thx to bongbong

'System' 카테고리의 다른 글

How 2 heap - first_fit.c  (0) 2017.05.15
LLVM -3  (2) 2016.08.24
LLVM - 2  (0) 2016.08.17
LLVM - 1  (0) 2016.08.12
사기를 위한 프로그램 분석  (0) 2014.12.26
Posted by IzayoiSakuya
,