Home 정적 분석의 기초
Post
Cancel

정적 분석의 기초

소스 코드를 정적 분석의 기초에 대해서 알아봅시다.

정적 분석이란?

  • 정적분석은 코드를 실행하지 않고 애플리케이션 코드의 잠재적 오류를 분석할 수 있는 프로세스
  • 다양한 검사, 검증을 수행하고 코드의 문제점을 파악하는데 사용할 수 있음
  • Github에서는 자체 Symantec 엔진인 CodeQL을 통해 코드스캐닝에 정적분석을 적용함

아마 대부분의 회사에서 DevSecOps를 하고 있다고 하면 대부분 CI/CD 체계에 SAST 도구를 이용하여 정적 분석을 수행하고 있을겁니다. Sonaqube, Securityprizm, Sparrow, CodeQL, Semgrep등 다양한 SAST 솔루션이 있으며 어떻게 취약점을 탐지하는지 궁금하여 글을 쓰게 되었습니다.

CodeQL zero to hero 해당 글을 참고로 작성했습니다.

Source & Sink

기본적으로 소스코드에서 취약점이 발생한다고 하면 신뢰할 수 없는 입력값과 그걸 트리거 하는 위험한 함수가 존재합니다.

신뢰할 수 없는 사용자의 입력 부분을 소스(Source)라 하며, 이를 실행하는 위험한 함수를 싱크(Sink)라고 하며, 소스에서 싱크로 의 데이터 흐름(Dataflow)이 존재해야 합니다.

SourceSink

Find Source & Sink

소스와 싱크를 찾는 방법은 다양하며, 소스->싱크로 이어지는 정방향, 싱크->소스로 역방향으로 확인하는 방법이 있습니다.

일반적인 생각으로는 아래 처럼 패턴 매칭의 두 가지의 경우를 생각해 볼 수 있습니다.

  1. grep와 같은 함수를 이용해서 GET 요청과 같은 소스를 검색
    -> 패턴 검색은 오탐을 많이 발생시켜, 분석에 실제로 필요하지 않은 부분이 많이 생김(주석?)
  2. 한 번에 한가지 유형의 소스만 검색하고 검토하는 방법
    -> GET 요청을 검색하면 실제 취약점으로 이어지지 않는 수십개의 결과가 나올 가능성이 높음

소스가 최종적으로 싱크에 도달하는 경우는 수천, 수만가지의 경로가 존재하기 떄문에, 이를 수동으로 검색하는 불가능하며 반대의 경우도 마찬가지입니다.

초기 정적 분석 도구

정적 분석 도구가 수행하는 많은 단계가 컴파일러의 단계와 유사합니다.
컴파일러와 인터프리터는 타입 검사를 통해 일종의 정적 분석 작업을 수행하고 있습니다.
타입 검사는 객체의 타입이 주어진 컨택스트에서 예상되는 타입과 일치하는지, 올바른 타입의 객체에 적용되었는지 확인하는 작업입니다.

컴파일러에서 사용되고 있는 기술들을 활용하여 보안을 위한 정적분석에 적용 시킬 수 있습니다.

앞서 grep을 사용하는 경우 주석, 함수 이름등 불필요한 정보가 결과에 포함되고 이는 어휘 분석을 이용하여 해결 했습니다.

어휘 분석은 소스코드를 읽어 문자열에서 토큰으로 변환하는데 코드의 의미와 관련없는 문자는 모두 무시하고, 토큰은 쉼표와 같은 문자, 문자열이나 정수와 같은 리터럴, 그리고 파이썬의 예약어와 같은 단어들로 구성됩니다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# SQLi 예시
1. from django.db import connection
2.
3. def show_user(request, username):
4.    with connection.cursor() as cursor:
5.        cursor.execute("SELECT * FROM users WHERE username = '%s'" % username)

# 1번 라인 어휘 분석
TokenInfo(type=63 (ENCODING), string='utf-8', start=(0, 0), end=(0, 0), line='')
TokenInfo(type=62 (NL), string='\n', start=(1, 0), end=(1, 1), line='\n')
TokenInfo(type=1 (NAME), string='from', start=(2, 0), end=(2, 4), line='from django.db import connection\n')
TokenInfo(type=1 (NAME), string='django', start=(2, 5), end=(2, 11), line='from django.db import connection\n')
TokenInfo(type=54 (OP), string='.', start=(2, 11), end=(2, 12), line='from django.db import connection\n')
TokenInfo(type=1 (NAME), string='db', start=(2, 12), end=(2, 14), line='from django.db import connection\n')
TokenInfo(type=1 (NAME), string='import', start=(2, 15), end=(2, 21), line='from django.db import connection\n')
TokenInfo(type=1 (NAME), string='connection', start=(2, 22), end=(2, 32), line='from django.db import connection\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(2, 32), end=(2, 33), line='from django.db import connection\n')

초창기 정적 도구 분석의 중요한 기능으로 위험한 싱크에 대한 정보를 담고있는 지식베이스가 있었으며, 토큰에서 싱크 이름을 찾아 일치 시키며, 일치하는 경우 위험한 싱크에 대한 정보와 사용이 위험한 이유를 표시했습니다.

대표적인 유명한 도구

다만 해당 어휘 분석으로는 위험한 싱크를 보고하는데 신뢰할 수 없는 데이터와 함께 사용되었는지, 취약점을 트리거할 수 있는지는 확인 하지 못하기 때문에 여전히 오탐이 많았습니다.

이를 해결하는 방법은 소스와 싱크사이에 연결(Dataflow)이 있는지 확인 하는 방법이 있습니다.

구문 패턴 매칭(Syntactic pattern matching) & 추상 구문 트리(AST) 및 제어 흐름 그래프(Control Flow Graph)

초기 정적 분석 도구는 컴파일러 이론의 어휘 분석을 활용했고, 이후 취약점 탐지의 정확도를 높이기 위해 컴파일러 이론에서 사용되는 다양한 기법을 활용했습니다.

구문 분석이나 추상 구문 트리(AST)와 같은 컴파일러의 더 많은 기술들을 도입하게 되었습니다. 다양한 정적 분석 방법이 존재하지만, 여기서는 가장 널리 사용되는 방법 중 하나인 데이터 흐름 분석과 오염 분석을 살펴보겠습니다.

일반적인 접근 방식 중 하나는 코드를 파싱 트리 로 만들고 추상 구문 트리(AST)를 구축하는 것 입니다. 추상 구문 트리는 소스 코드를 트리 형태로 표현한 것이며, AST를 사용하면 각 노드가 특정 타입을 가지는 구조화된 표현으로 코드의 의미(Semantic)을 고려할 수 있습니다.

예시 SQL Injection 파이썬 코드의 AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Module(
    body=[
        ImportFrom(
            lineno=2,
            col_offset=0,
            end_lineno=2,
            end_col_offset=32,
            module='django.db',
            names=[alias(lineno=2, col_offset=22, end_lineno=2, end_col_offset=32, name='connection', asname=None)],
            level=0,
        ),
        FunctionDef(
            lineno=5,
            col_offset=0,
            end_lineno=7,
            end_col_offset=78,
            name='show_user',
            args=arguments(
                posonlyargs=[],
                args=[
                    arg(lineno=5, col_offset=14, end_lineno=5, end_col_offset=21, arg='request', annotation=None, type_comment=None),
                    arg(lineno=5, col_offset=23, end_lineno=5, end_col_offset=31, arg='username', annotation=None, type_comment=None),
                ],
                vararg=None,
                kwonlyargs=[],
                kw_defaults=[],
                kwarg=None,
                defaults=[],
            )
            # output cut for readability
            )])

파이썬 모듈의 출력과 추상 트리를 순회하는 과정을 간소화한 그래프로 바꾼 것 AST

소스코드를 AST로 표현하면 예시로 django.db 라이브러리의 execute 메소드 호출을 나타내는 모든 노드를 쿼리할 수 있습니다.

이렇게 되면 위의 예시 코드에서 django.db의 execute 메서드 호출만 반환되고, 다른 관심 없는 유형의 데이터는 포함되지 않습니다. 다음은 문자열 리터럴을 인자로 받지 않는 execute 메서드 호출을 통해 단순 문자열을 사용하는 호출은 결과에서 제외할 수 있습니다.

즉 잠재적으로 위험한 데이터를 사용하지 않으며, 이미 안전한다고 판단되는 코드들을 사전에 걸러 낼 수 있습니다. 이를 이용하여 오탐을 크게 줄일 수 있습니다.

제어 흐름 그래프 (Control Flow Graph)

분석의 정확도를 더욱 높이기 위해 제어 흐름 그래프라는 또 다른 소스 코드 표현을 사용할 수 있습니다.

제어흐름그래프는 프로그램의 모든 가능한 실행 경로에서 제어 흐름(AST 노드들이 실행되는 순서)를 표현합니다.
각 노드는 프로그램 내의 기본 명령문을 나타내며, 기본 명령문에는 할당(Assignments)와 조건문(conditions)등이 포함됩니다.

각 노드에서 나가는 Edge는 해당 명령문 이후에 실행될 수 있는 가능한 다음 명령문(Successor)을 의미합니다.

CFG를 통해 프로그램 전체에서 코드가 어떻게 흐르는지를 추적할 수 있으며, 이를 기반으로 추가적인 분석을 수행할 수 있습니다.

예시 -5~7 코드에서 관리자가 아닌 인증 사용자에게 SQLi가 발생함

1
2
3
4
5
6
7
8
9
1. username = request.GET.get("username")
2. if request.user.is_superuser:
3.    sql = "SELECT * FROM users"
4.    cursor.execute(sql)
5. elif request.user.is_authenticated:
6.    sql = f"SELECT * FROM users WHERE username={username}"
7.    cursor.execute(sql)
8. else:
9.    print("404")

CFG

다이어그램의 각 블록에는 해당하는 소스 코드 라인 번호가 할당되어 있습니다.
AST를 사용하면 3번 줄과 7번 줄에 있는 2개의 메서드 호출을 추출할 수 있지만, 그 데이터가 실제로 소스에서 싱크로 흐르는지 여부는 아직 알 수 없다.

Data flow analysis and taint tracking

소스 코드의 제어 흐름 그래프를 활용하여 특정 소스와 싱크 사이의 연결, 즉 데이터 흐름이 있는지 확인하는 기법을 데이터 흐름 분석 이라고 합니다.

어느 정도는 데이터 흐름 분석을 통해 취약점을 찾지만, 문제점은 값이 변하지 않는 데이터만 추적 한다는 것이며, 제어 흐름 그래프도 마찬가지 입니다.

이 문제를 해결 하기 위해 오염 추적(Taint tracking)을 사용할 수 있습니다.

오염 추적은 특정 입력(소스)를 오염됨(안전하지 않거나, 사용자가 제어하는 것을 의미)으로 표시하면 이를 통해 정적 분석 도구는 안전하지 않은 입력이 애플리케이션의 특정 지점까지 전파되는지 확인할 수 있습니다.

Taint Tracking

그 외 다른 분석 방법

단일 정적 할당(SSA)으로 모든 변수에 정확히 한 번씩만 할당되도록 제어흐름그래프를 수정하는 방식 -> SSA 방식은 다양한 데이터 흐름 분석의 정확도를 상당히 향상 시킬 수 있다.

Conclusion

취약점도 결국은 내부적으로 트리거가 일어나야 된다는 점이고, 이를 찾는 것이 참 어려운거 같습니다. 허허;;;
토스의 LLM을 이용한 서비스 취약점 분석 자동화 문서를 보다가 감명받아서
GPT-5.4랑 같이 비슷하게 취약점을 찾아내도록 만들고는 있는데, 정적분석으로 취약점을 찾는 원리가 뭔지 궁금해서 글을 쓰게 됬네요.

제가 만드는건 미탐도 많고 어떻게 누락없이 잘 찾아낼지, 로컬 LLM은 성능이 괜찮은지 등등 고려할 부분이 많아 상당히 어려운 영역인거 같습니다. ㅠㅠ XintCode 같은 SaaS 형태로 솔루션도 굉장히 좋아 보이던데 하루 빨리 법제도에서 해결되어 내부에서 사용 가능하도록 도입할 수 있었으면 좋겠습니다.
그래야 현업과 저의 일도 많이 줄고, 제 자리도 없어지고?!?!?