[TryHackMe] Race Conditions - 1

코준·2025년 6월 11일

TryHackMe

목록 보기
17/32

Race Condition 이란

시스템에서 발생하는 이벤트의 타이밍이 프로그램의 동작과 결과에 영향을 미치는 상황을 말한다.
공학에서 말하는 정의는 이러하고,

CS에서는 OS내의 공유 리소스를 둘 이상의 스레드 혹은 프로세스가 읽기, 쓰기를 하면서 서로 경쟁하는 상황이라고 할 수 있다.

여러 스레드에 의해 변수가 동시에 접근되거나 수정될 때 발생하기도 하는데, Mutual Exclusion이나 Deadlock같은 매커니즘 혹은 동기화의 문제로 공격자는 이를 악용할 수 있을 것이다.

이번 주제에서는 꽤나 시스템적인 내용이 포함되긴 하지만 기존의 골조를 유지하며 학습하도록 한다.

Programs

프로그램은 어떤 작업을 위해 모인 일련의 명령어 집합이라고 정의할 수 있다.
어떤 작업을 위해서는 프로그램을 실행해야하고 실행하지 않으면 그저 명령어의 집합일 뿐이다.

카페의 메뉴 레시피는 존재하지만, 주문이 들어오지 않으면 제조되지 않는 것처럼말이다.

Processes

프로그램이 카페의 메뉴 레시피라면 프로세스는 레시피를 따라서 만드는 과정에 있다.

실행 중인 프로그램이라는 점에서 작업(job)이라는 동적인 실체를 의미한다.
실제로 일부 문헌에서는 프로세스가 job이라는 용어로 대체되곤 한다.

포트를 열거나 서버를 실행시킨다면 하나의 프로세스가 생성되고 수신을 대기(listening)하거나 실제로 대기(waiting)한다.

HTTP 리퀘스트를 받으면 CPU의 스케줄링에 따라서 실행 차례를 기다리면서 준비(Ready)상태가 된다. 자기 차례가 되고 실행(Running)상태가 되면 주어진 작업을 수행하고 다시 대기(Waiting)상태로 돌아간다.

이 과정에서 하나의 프로세스는 한 번에 하나씩 처리된다. 서버의 입장에서 보면 애플리케이션은 클라이언트에게 순차적으로 서비스를 제공하고 있다.

Threads

스레드는 가장 가벼운 실행 단위이다. 우리는 카페에 가면 바쁜 알바를 볼 수 있다. 그럴 때 에스프레소 머신이 하나인데 에스프레소 주문이 계속 들어온다면 어쩔 수 없이 하나의 에스프레소만 순서대로 처리할 수 있다.

하지만 머신이 둘, 셋, ...이라면 문제없이 다음 주문을 받을 수 있다.

이처럼 스레드는 프로세스와 다양한 메모리 부분을 공유한다.
대부분 같은 프로세스를 반복하는 경우가 많은데, 수 천, 수 만의 사용자에게 서비스를 제공하는 웹 서버라면 동일한 서비스(혹은 개인화된)를 제공하는데 방법이 존재할 것이다.

  • 직렬(Serial) : 하나의 프로세스를 통해 한 명씩 순차적으로 서비스하고 다음 사용자는 대기
  • 병렬(Parallel) : 하나의 프로세스에서 각 사용자에게 서비스하기 위해 스레드를 생성하고 최대 스레드 개수만큼 서비스, 그 이후의 사용자는 대기

몇 개의 스레드가 가용한지는 시스템과 설정마다 다르지만 이는 하나의 프로세스가 여러 작업을 실행할 수 있도록 도와준다.

Race Condition

레스토랑에서 예약되지 않은 하나의 테이블에 두 게스트가 서로 다른 호스트에게 예약을 시도할 때, 호스트는 예약되지 않은 상태인 테이블이 비어있다고 판단하고 두 게스트 모두가 예약한다고 가정하면, 누구에게 테이블을 넘겨야할까?

이것이 경쟁 조건이다.
둘 혹은 그 이상의 호스트가 예약을 받고 전화와 테이블 상태 업데이트 작업 모두 시간이 엇갈렸기 때문이다.
하나의 스레드가 어떤 작업을 수행하는 동안 그 작업이 실제로 일어나기 전에 다른 스레드가 작업의 값을 변경하거나 접근할 수 있다.

간단하게 계좌와 인출에 관해서 예를 들어보자.

#1

  • 은행 계좌에는 100만원이 존재
  • 두 스레드가 동시에 계좌에서 돈을 인출하려고 시도
  • 스레드 1은 잔액 100만원을 확인하고 40만원을 인출
  • 스레드 1의 잔액 업데이트 이전에 스레드 2가 잔액을 확인 (100만원)후 30만원을 인출

이 시나리오에서는 우선순위가 정해지지 않지만 스레드 1이 먼저 잔액을 업데이트한다고 가정해보자.

스레드 1은 잔액을 60만원으로 설정하고 업데이트하여 인출을 마쳤다. 그 후에 스레드 2의 잔액 업데이트가 마쳐졌다면 계좌의 잔액은 30만원이 아니라 70만원이 된다.
업데이트는 스레드 1이 먼저 마쳤지만 그 전에 스레드 2가 확인한 잔액은 100만원이기 때문이다.

결과적으로 사용자는 2번 인출을 시도해 70만원을 인출했지만 잔액은 스레드 2의 업데이트 기준으로 70만원이 남아있는 것이다.

#2

  • 은행 계좌에 70만원 존재
  • 두 스레드가 동시에 계좌에서 돈을 인출하려고 시도
  • 스레드 1은 잔액 70만원을 확인하고 50만원을 인출
  • 스레드 1의 잔액 업데이트 이전에 스레드 2가 잔액을 확인 (70만원)후 50만원을 인출

이 시나리오는 분명 스레드 2가 시도한 인출은 거부되어야 하지만, 스레드 2가 확인한 잔액은 70만원이었기 때문에 문제없이 인출은 성공할 수 있다.

#1과 #2는 검사 시점부터 사용 시점까지의 취약점(Time-of-Check to Time-of-Use, TOCTOU)을 보이고 있다.

두 스레드는 같은 변수에 접근하고 값을 변경하려고 하고 있다.
스레드에게 CPU 할당 시간이 부여될 때마다 자신의 작업을 완료하기 위해서 명령어를 실행한다.
두 스레드는 모두 현금을 인출하기 위해서 Race하고 있었다. 단일 호스트에서 발생하는 매우 간단한 예시로 들 수 있는 충분한 시나리오이다.

일반적으로 경쟁 조건은 공유 리소스의 사용에서 발생한다. 여러 스레드가 동시에 공유 리소스에 접근하고 변경할 때 발생하는데, 데이터베이스 레코드, 메모리의 데이터 구조같은 경우가 있다.

원인

  • 병렬 실행 (Parallel Execution) : 웹 서버에서 동시 사용자의 상호작용을 처리하기 위해서 리퀘스트를 병렬로 실행할 수 있다. 동기화되지 않은 상태로 공유 리소스나 애플리케이션 상태에 접근해 수정하는 경우 경쟁 조건 혹은 예상치 못한 프로그램 동작으로 이어질 수 있다.

  • 데이터베이스 작업 (Database Operation) : read-modify-write의 한 시퀀스에서 동시 데이터베이스 작업은 경쟁 조건의 원인이 될 수 있다. 두 사용자가 같은 레코드를 업데이트하기를 동시에 시도하면 데이터의 충돌이 발생할 수 있다.

  • 타사 라이브러리 및 서비스 (Third-Party Libraries and Services) : 웹 애플리케이션에서 종종 타사 라이브러리, API 등과 통합해 서비스하는데, 외부 구성 요소가 동기화 처리가 적절히 이뤄지지 않은 경우 애플리케이션 상호작용이나 리퀘스트를 보낼 때 경쟁 조건이 발생할 수 있다.

profile
Hi !

0개의 댓글