적절한 한국어 제목을 붙이기가 참 어려웠다. "당신의 눈에 보이는 코드가 그대로 실행될 거라고 착각하지 말라!"로 번역한 원문은 "WYSINWYX: What you see is not what you execute."이다. 이것을 이해하기 위해서는 약간의 배경지식이 필요한데, 혹시 소싯적에 정보처리기능사나 워드프로세서 필기시험을 공부한 사람이라면 들어보았을 '위지위그'에서 차용한 제목이다. WYSIWYG은  'What You See Is What You Get'으로, 문서 편집 과정에서 화면에 포맷된 낱말, 문장이 출력물과 동일하게 나오는 방식을 말한다. 간단히 말해서 눈에 보이는 그대로 결과물을 얻는다는 뜻이다. 그렇다면 본 논문의 WYSINWYX은 위지위그에서 표현을 차용한 것임이 분명하다. 달라진 단어는 'Not'과 'Excute'이다. 눈에 보이는 것과 다르게 실행된다는 것은 무슨 의미일까. 


저자인 Gogul Balakrishnan은 동일한 주제를 2005년부터 2010년까지 계속해서 우려먹고 있다. 그렇다고해서 똑같은 내용인 것은 아니고, 최초에 소프트웨어관련 학회에서 발표한 것을 시작으로 해당 연구를 통해 University of Wisconsin–Madison에서 박사학위를 수여하였으며 학위논문에 가장 방대하게 설명되어 있다. 그리고 이를 증보 개정하여 ACM 저널에도 게재하였다. 관련 링크는 아래와 같다.

2005년 Working Conference on Verified Software: Theories, Tools, and Experiments

2007년 UNIVERSITY OF WISCONSIN–MADISON Ph.D Thesis

2010년 ACM Transactions on Programming Languages and Systems (TOPLAS) Journal


*아래의 설명은 논문 전체를 번역한 것이 아니라, 블로그 저자의 개인 주관으로 내용을 선별하여 강조한 것이므로 원문의 의도와 완전히 일치하지는 않을 수 있으므로 필요하다면 반드시 상기 링크를 통해 원문을 확인할 것.


Abstract :

컴퓨터가 프로그램을 실행할 때 사실은 소스코드 자체를 사용하는 것이 아니라, 그 소스코드로 부터 생성된 기계어 코드(Machine-level code)가 이용되는 것이다. 본 논문에서는 일명 WYSINWYX 현상에 대해 다루며, 이는 프로그래머의 원래 의도가 실제 CPU 처리장치에서 실행될 때 어떻게 달라질 수 있는지에 관한 것이다. 나아가 소스코드에서는 발견할 수 없었던 다양한 취약점이나 버그들이 유발될 수 있음을 암시한다. 


1. Introduction :

프로그래밍 언어론, 소프트웨어공학, 컴퓨팅보안 등의 분야의 최근 연구동향을 살펴보면, 소프트웨어의 버그나 결함을 찾아내기 위해 다양한 코드 분석 도구들을 활용하고 있다. 아래는 그와 관련된 논문들이다.

  • Havelund, Klaus, and Thomas Pressburger. "Model checking java programs using java pathfinder." International Journal on Software Tools for Technology Transfer (STTT) 2.4 (2000): 366-381.
  • Wagner, David, et al. "A First Step Towards Automated Detection of Buffer Overrun Vulnerabilities." NDSS. 2000.
  • Engler, Dawson, et al. "Checking system rules using system-specific, programmer-written compiler extensions." Proceedings of the 4th conference on Symposium on Operating System Design & Implementation-Volume 4. USENIX Association, 2000.
  • Corbett, James C., et al. "Bandera: Extracting finite-state models from Java source code." Software Engineering, 2000. Proceedings of the 2000 International Conference on. IEEE, 2000.
  • Bush, William R., Jonathan D. Pincus, and David J. Sielaff. "A static analyzer for finding dynamic programming errors." Software-Practice and Experience 30.7 (2000): 775-802.
  • Clarke, Edmund M., and Robert P. Kurshan. "Computer-aided verification." IEEE Spectrum 33.6 (1996): 61-67.
  • Chen, Hao, and David Wagner. "MOPS: an infrastructure for examining security properties of software." Proceedings of the 9th ACM conference on Computer and communications security. ACM, 2002.
  • Henzinger, Thomas A., et al. "Lazy abstraction." ACM SIGPLAN Notices 37.1 (2002): 58-70.
  • Das, Manuvir, Sorin Lerner, and Mark Seigle. "ESP: Path-sensitive program verification in polynomial time." ACM Sigplan Notices. Vol. 37. No. 5. ACM, 2002.

이러한 논문들의 접근법을 살펴보면, 프로그램이 잘못된 행위로 빠져들 수 있을지에 대한 예측을 하기 위해 정적(Static) 분석을 수행한다. 그러나 이들은 High-level(고급언어, 인간이 이해할 수 있는)의 프로그램 소스코드를 분석하는 것에 초점을 두고 있으며 이는 굉장한 헛점이다. 프로그래머의 의도가 소스코드에 반영되어 있겠지만, 실제로 컴퓨터의 프로세서(CPU)가 프로그램을 작동시킬 때에는 그 소스코드가 글자 그대로 실행되는 것이 아니라, 컴퓨터가 이해할 수 있는 저수준(low-level)의 기계언어 코드로 변경된 것을 사용하기 때문이다. 아래의 코드를 예로 들어보자. 이는 로그인을 수행하는 프로그램의 일부이다.

소스코드에서는 사용자의 비밀번호를 임시적으로 저장하는데 평문으로 되어 있다. 동적으로 할당된 버퍼 저장공간에는 password 변수를 포인팅하고 있다. 비밀번호는 민감한 정보이기 때문에 프로그래머는 이를 적절히 보안처리하기 위하여 password를 0으로 채워넣은 후 free()함수를 통해 heap 메모리로부터 삭제하도록 지시하였다. 이쯤이면 프로그래머 입장에서는 나름대로 시큐어코딩을 수행한 것이 아니냐며 반문할 수 있다. 그러나 불행히도 실제로 컴파일러는 해당 코드를 그대로 실행하지 않는다. 컴파일러는 작업 효율을 향상시키기 위한 나름의 전략을 가지고 있다. 때문에 불필요한 코드는 굳이 실행하지 않고 건너뛰는 등의 임의판단을 하게 된다. 만약 memset의 결괏값이 향후에 사용될 일이 없다고 판단된다면 굳이 memset을 수행하지 않고 건너뛴다. 그렇게되면 heap에는 여전히 비밀번호 정보가 남아있게 되고, 메모리 분석을 통해 해당 값을 복원할 수 있다. 이는 단지 이론적(hypothetical)인 가정이 아니며, 실제로 2002년에 윈도우 운영체제에서 관련한 취약점이 발견되어 패치되었다. (참고 : Howard, Michael. "Some bad news and some good news. October 2002. MSDN, Microsoft Corp.") 이러한 취약점을 언급한다면 프로그래머는 정말 억울할 것이다. 소스코드에는 전혀 이상한 점이 없지 않은가? 그런데 컴파일러의 최적화 과정에서 생성된 기계어 코드에서 발생할 위험을 도대체 무슨 수로 점검한다는 말인가?


위의 예시는 함수(Procedure) 호출에서 발생한 것을 언급한 것이지만, 뿐만 아니라 더욱 다양한 상황에서도 적용될 수 있다. 메모리, 레지스터, 실제 CPU 레벨에서 처리되는 작업 순서, 성능 최적화, 컴파일러 자체의 버그 들을 종합적으로 고려한다면 각 플랫폼마다 무수히 많은 경우의 수를 가지게 되고, 이는 잠재적으로 다양한 프로그램의 취약점을 야기할 수 있다. 또한, 라이브러리(DLL) 참조 과정에서 발생하는 취약점 등등의 세부항목들은 소스코드 자체만으로는 절대 확인할 수 없는 부분이다.


프로그래밍 언어는 점차 다양해지고 있는데, 각 언어마다 별도의 시큐어 코딩 가이드를 제시하는 것이 얼마나 번거로운 일인가? 언어가 바뀔때마다 그에 알맞은 소스코드 진단 도구를 새로 개발해야 하는 현 상황이 안타깝다. 또한, 원본 소스코드 자체가 아예 주어지지 않은 상황이라면 어떡할 것인가? 


이에 본 논문에서는 소스코드 분석 방식이 아닌, 실행파일(Executable 또는 바이너리) 자체를 분석하는 것에 초점을 맞추고자 한다. 2절에서는 Executable 자체를 분석하는 것이 소스코드 분석에 비해 더욱 정확하다는 것을 예시를 통해 살펴보도록 하고, 3절에서는 Executable을 분석하는 방법론에 대해 논하고자 한다. 


2. 바이너리 분석 방식의 장점

1절에서 살펴보았듯이, 성능 최적화를 위한 컴파일러의 지나치게 열성적인 노력이 오히려 프로그래머의 의도와는 다른 결과를 낳을 수 있다. H.-J. Boehm. 역시 "Threads cannot be implemented as a library(2005)." 논문에서 C/C++ 언어에서 사용되는 쓰레딩 라이브러리로 인해 컴파일러에서 오류가 발생하거나 예기치 않은 문제를 유발할 수 있음을 지적하였다.


대부분의 프로그래밍 언어는 그 설계 구조상 특정한 행위(behavior)를 파악할 때 문맥상의 의미(semantics)를 기준으로 판단하기 때문에 런타임(run-time)이 아닌이상 어떤 동작을 할지 확신할 수가 없다. 때문에 소스코드 기반으로 이러한 행위들을 예측하려면 가능한 모든 경우를 고려해야 하며 이것은 무리한 선택이다. 그러나 바이너리 그 자체를 분석할 때에는 컴파일러가 선택한 오직 한가지의 특정 작동만을 점검하면 된다. 아래의 예시는 산술 포인터와 그에 대한 간접 호출을 보여주고 있다.

소스코드 레벨로 분석하는 방식으로는 위의 코드가 어떻게 될지 도무지 예측할 수가 없다. 전달되는 값에 따라 너무 다양한 경우가 가능하기 때문이다. 통상적으로 함수 포인터에 대한 산술연산은 정의되지 않은 행동(undefined behavior)을 불러온다. 우선, 소스코드 레벨에서 분석해보면 다음과 같은 두가지 중 하나로 진행될 것 같다.(단, ANSI-C 로 가정한다.)

  • (a) inderect function은 어떤 함수든 호출할 수 있다.
  • (b) 산술 연산을 무시해버리고, 간접함수 호출이 f1을 호출

그러나 Excutables 분석에서는 f2가 invoke된다는 것이 밝혀졌고, 그것이 구체적으로 어떤 조건에서 결정되는지 확인할 수 있다.( 참고 : Reps, Thomas, Gogul Balakrishnan, and Junghee Lim. "Intermediate-representation recovery from low-level code.") 이러한 방식을 악용하여 특정주소로 함수 호출을 유도하는 방식은 소프트웨어 버그(bug)로 볼 수 있으며, 교활하고 의도적인 보안 취약점이 될 수도 있다.


세번째 예제는 아래 그림으로 설명한다. 아래의 C언어 코드는 초기화되지 않은 변수가 사용되었다. 컴파일러는 보통 경고(Warning)을 띄우긴 하지만, 어쨌든 컴파일 자체는 성공한다. 단순히 소스코드만 살펴보면, local 변수는 어쨌든 정수형으로 되어있으며 callee의 실행결과 1 또는 2의 결괏값을 반환할 것으로 보인다. 

그림의 오른쪽 부분을 보면 각각의 C언어 코드가 어셈블리어로 번역된 결과가 있다. Microsoft 컴파일러(cl)은 Standard에서 다소 변형된 명령어를 사용하고 있다. sub esp, 4 명령어는 변수 local에 할당을 수행하는 부분인데, 이것이 임의의 레지스터(여기서는 ecx)에 대한 push 작업으로 대체되어 있음을 알 수 있다. 

컴파일러 최적화로 인한 예기치 않은 동작의 예
컴파일러 최적화로 인한 예기치 않은 동작의 예

즉, Excutable 분석을 통해 살펴보니 컴파일러 최적화로 인해 local 변수가 5로 초기화되어버렸고, 때문에 Main함수의 v는 callee()의 실행 결과로 항상 1만을 획득할 것이다. 이는 소스코드 레벨의 분석에서는 도무지 납득할 수 없는 상황인 것이다.


마지막으로 한가지 더 예시를 들자면, 함수를 호출할 때 프로시저가 예상한 매개변수의 갯수보다 적게 전달되는 경우 역시 프로그램이 정의되지 않은 이상행동을 보일 수 있다. 대부분의 컴파일러는 프로그래머의 편의를 위해 이런 안전하지 않은 코드를 통과시키도록 설계되어 있다. 그리고 이는 결국 주어진 로컬 변수들중 적당한 몇개를 나머지 파라미터로 사용하도록 전달한다. 심지어 이 때 Call by Reference 방식으로 전달되기 때문에 함수 수행에 의해 원본 값이 변경되어버릴 소지가 있으며 이는 프로그래머의 의도와 다른 결과를 낳을 수 있다. 


지금까지 언급한 상황들은 모두 Source 코드 분석에서 간과되는 부분이다. 때로는 과하게, 때로는 약하게 추정해야 하므로 근거가 명확하지 않으며 결국 분석가의 직감에 의존해야 하는 상황인 것이다.

그러나, Executable 분석을 통한다면 위에서 언급한 단점들을 극복할 수 있다. 


3. Executable 분석 방법론

1~2절에서 살펴보았듯이, 소스코드를 분석하는 것보다 바이너리 파일 자체를 분석할 때 많은 이점이 있다. 실행파일에는 실제로 실행될 명령어(instruction)이 포함되어 있고, 이는 해당 프로그램이 실행될 때 발현되는 행위에 대한 굉장히 정확한 정보이다. 실행파일을 분석함으로써 다양한 이점을 가질 수 있다고 했는데, 그렇다면 이를 수행해주는 시스템을 개발하는 것이 어떨까? 만약 그런 체계를 만든다면 최소한 다음과 같은 사항들이 요구될 것이다.

  • 해당 프로그램 뿐만 아니라 링크된 라이브러리 들까지 모두 살펴볼 수 있다.
  • 실행 파일이 컴파일 후 수정되었다면, 분석시 이러한 내역을 파악할 수 있다.
  • 소스코드가 필수 불가결한 요소는 아니다.
  • 만약 소스코드가 두개 이상의 언어의 조합으로 작성되었더라도, 바이너리는 한가지 언어(*어셈블리)만 확인하면 된다.
  • 소스코드 상에서 inline-어셈블리 구문으로 직접 삽입된 명령어도 별도로 표시가 되며, 굳이 따로 고민할 필요가 없다.

또한 바이너리를 분석할 때에는 해당 플랫폼에 대한 구체적인 정보(메모리 레이아웃, 레지스터 사용방식, 명령어 실행 순서, 최적화, 컴파일러 버그 등)가 함께 고려되어야 한다. 이러한 점들을 고려하여 저자는 바이너리를 분석할 수 있는 플랫폼을 개발하였다.


* 4절에서는 저자가 개발한 80x86 머신의 실행파일들을 분석하는 CodeSurfer 플랫폼를 설명함으로써, 소스코드 없이 실행파일을 분석하는 것이 가능함을 보인다. 해당 내용은 별도의 논문으로 발행된 것을 요약한 것으로 보이며 아래 내용을 참고하면 된다. 4절에 관한 자세한 설명은 생략한다. 

  • Balakrishnan, Gogul, et al. "CodeSurfer/x86—A platform for analyzing x86 executables." International Conference on Compiler Construction. Springer Berlin Heidelberg, 2005.


CPUU님의 창작활동을 응원하고 싶으세요?