소프트웨어 개발보안 가이드 분석(2021) : 경쟁조건: 검사시점과 사용시점(TOCTOU)

"경쟁조건: 검사시점과 사용시점(TOCTOU) 이란, 동시 또는 거의 동시 수행을 지원하는 병렬 시스템이나 하나 이상의 프로세스가 동작되는 환경에서 시간 및 상태를 부적절하게 관리하여 발생할 수 있는 보안약점

소프트웨어 개발보안가이드(2021), 한국인터넷진흥원

 

취약점 항목에 대한 이해

이 보안약점은 특히 공유 자원을 다루는 애플리케이션에서 발생하기 쉽습니다. 예를 들어, 파일 시스템, 데이터베이스 레코드, 메모리와 같은 공유 자원에 동시에 접근하려고 할 때 이 문제가 발생할 수 있습니다.

하나의 자원에 대해 여러 스레드가 동시에 접근하게 될 경우 교착상태 또는 자원의 비정상 처리가 발생할 수 있습니다.

이 취약점은 시스템의 무결성을 해치고, 민감한 정보의 노출, 데이터 손상 또는 손실과 같은 보안 문제를 초래할 수 있습니다. 그렇기 때문에 공유자원(예: 파일)을 여러 프로세스가 접근하여 사용할 경우, 동기화 구문을 사용하여 한 번에 하나의 프로세스만 접근 가능하도록(synchronized, mutex 등) 하는 한편, 성능에 미치는 영향을 최소화하기 위해 임계코드 주변만 동기화 구문을 사용할 필요가 생기게 됩니다.

공격 메커니즘

사실 해당 취약점은 "경쟁조건"이라는 이름보다는 "Race Condition"이라는 이름으로 더 알려져 있습니다. 이 취약점은 동시에 여러 프로세스 또는 스레드가 공유 자원에 접근 할 때 발생하는 문제로, 메모리에 접근하는 과정에서 보안사고가 발생할 수 있다는 공통점 때문에 한 때는 "Heartbleed(CVE-2014-0160)"취약점의 원인으로 연관지어 알려지기도 하였습니다. 하지만 Heartbleed는 메모리의 안전하지 않은 읽기 작업으로 인해 발생하는 취약점이고, Race Condition은 동시에 여러 연산이 공유 자원에 접근하려 할 때 발생하는 동기화 문제로 발생하기 때문에 엄연히 따지면 두 개의 취약점은 서로 직접적인 연관이 없는 것으로 보아야 합니다.

Race Condition은 CWE-363으로 자원 동기화에 대한 취약점 항목으로 등재되어 있습니다.

Race Condition을 이용한 공격은 공유 자원에 대한 동시 접근을 통해 발생합니다. 공격자는 애플리케이션의 정상적인 처리 순서를 방해하여 민감한 정보에 접근하거나 시스템을 조작할 수 있습니다. 예를 들어, 공격자가 시스템에 동시에 두 개의 요청을 보내어 하나의 요청이 자원을 사용하는 도중 다른 요청이 무결성 검사를 우회할 수 있다면, 이는 심각한 보안 위험을 초래합니다.

 

소프트웨어 개발보안가이드의 예제 분석

SW개발보안가이드의 취약한 예시 코드는 파일 작업을 처리하는 두 개의 자바 스레드를 구현합니다. 하나의 스레드는 "Test_367.txt"라는 파일이 존재할 경우 읽기 작업을 수행하고, 다른 스레드는 같은 파일이 존재할 경우 삭제합니다. 이러한 작업은 FileMgmtThread 인스턴스 생성 시 설정된 "manageType"에 의해 결정됩니다. 메인 클래스 CWE367은 두 스레드를 동시에 시작하여, 읽기 작업을 시도하는 동안 삭제 작업이 이루어질 수 있는 잠재적인 경쟁 조건을 야기합니다.

안전한 코드 예시는 'FileMgmtThread'라는 스레드 클래스를 정의하고 있으며, 'READ'와 'DELETE' 두 가지 작업 유형을 처리합니다. 'SYNC'라는 정적 문자열을 동기화 블록의 락 객체로 사용하여, 파일 'Test_367.txt'을 읽거나 삭제하는 동작이 서로 충돌하지 않도록 동기화합니다. 이렇게 함으로써, 두 스레드가 동시에 같은 파일에 접근할 때 발생할 수 있는 경쟁 조건을 방지합니다.

 

Synchronized 를 활용한 동기화 처리 코드 예제

Java에서 동기화 처리는 멀티 스레드 프로그램에서 데이터의 일관성과 무결성을 보장하는 필수적인 기법 중 하나입니다. 특히, synchronized 키워드는 이러한 동기화 작업을 수행하는 데 가장 기본적이면서도 중요한 역할을 합니다.

Java에서 synchronized 키워드는 메소드 전체나 특정 코드 블록에 대한 접근을 단일 스레드로 제한하여 동기화를 구현합니다. synchronized 키워드가 적용된 영역은 한 번에 하나의 스레드만이 실행할 수 있으며, 다른 스레드는 현재 실행 중인 스레드가 작업을 완료하고 영역을 떠날 때까지 대기해야 합니다.

* synchronized 메소드

클래스의 메소드 전체를 동기화할 때 사용합니다. 이 경우, 메소드가 포함된 객체의 락(lock)을 획득해야만 메소드를 실행할 수 있습니다.

public synchronized void synchronizedMethod() {
    // 메소드 전체 코드
}

 

* synchronized 블록

특정 객체에 대한 블록을 동기화할 때 사용합니다. 이 방식은 동기화가 필요한 특정 부분에만 락을 적용하기 때문에 성능 면에서 더 유리할 수 있습니다.

public void method() {
    synchronized(this) {
        // 동기화가 필요한 코드 블록
    }
}

 

* synchronized 블록을 활용한 예시

다음 예시에서는 두 개의 스레드가 서로 다른 객체의 메소드를 호출하지만, 각 메소드 내부에서 synchronized 블록을 통해 특정 객체의 락을 활용하여 동기화를 구현합니다. 이를 통해 동시에 실행되는 스레드 간의 데이터 무결성을 보장합니다.

class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized(lock) {
            count++;
        }
    }

    public void decrement() {
        synchronized(lock) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SyncExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 1000; i++) {
                counter.decrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

이 예시에서 Counter 클래스는 increment()decrement() 메소드를 통해 카운터 값을 증가시키거나 감소시킵니다. 공유 리소스(여기서는 count)에 대한 동시 접근을 관리하고 스레드가 동시에 실행될 때 발생할 수 있는 잘못된 계산을 방지하기 위한 동기화 사용 방법을 보여줍니다.

  • 이전 소프트웨어 개발보안 가이드 분석(2021) : 반복된 인증시도 제한 기능 부재
  • 다음 소프트웨어 개발보안 가이드 분석(2021) : 종료되지 않는 반복문 또는 재귀함수