소프트웨어 개발보안 가이드 분석(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) : 종료되지 않는 반복문 또는 재귀함수