* 본 포스팅은 개인의 학업 목적으로 작성된 것이며, 본문의 내용이나 해석은 받아들이는 사람의 입장에 따라 다를 수 있습니다. 특히, 해외 논문을 번역하여 옮기는 과정에는 본인 나름의 표현을 취사선택하므로 그 결과물은 Official이 아닙니다. 가능한 원문의 표현을 직접 확인하시기 바랍니다. 

* 원문의 의도가 왜곡되었다고 느껴지는 부분이 있거나, 문의사항이 있다면 cpuu@kaist.ac.kr로 언제든지 제보 바랍니다.


저자인 Thompson Ken은 AT&T Bell 연구소에서 데니스 리치와 함께 Unix를 개발한 인물이며, 2006년부터 구글에서 근무하고 있다. 본 논문의 원제는 "Reflections on trusting trust"이며, 켄 톰슨이 튜링상을 수상하여(Turing Award) 1984년 ACM에서 기념연설에서 발표한 글이다. 대부분 인터넷을 찾아보면 'Trusting trust에 관한 고찰' 정도로 번역이 되어있다. 직역하자면 '신뢰를 신뢰하는 것'이라는 표현이 한국어로는 잘 와닿지 않기 때문에 그대로 표기한 것 같다. 나름대로 제목을 붙여보자면 '소프트웨어 신뢰성에 대한 재고찰'정도가 되겠다.(개인 의견입니다.) 1984년의 저작물이므로 아마도(?) 저작권 등의 보호에 문제가 없을듯 하여 아래에 첨부한다. 사실 해당 논문은 구글 학술검색 등에서 제목만으로도 쉽게 구할 수 있다.

논문 링크 : https://www.ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf


이 논문에서 다루는 내용은 현대에서는 보통 백도어(backdoor)라고 지칭한다. 그리고 다양한 백도어 중에서도 톰슨의 제안은 컴파일러 백도어(Compiler Backdoors)로 분류된다. 이 개념은 톰슨이 최초로 제안한 것은 아니다. 논문의 가장 마지막 부분에 저자의 첨언을 보면, 그가 공군(Air Force)의 어떤 문서를 읽고나서 해당 내용을 작성했다고 하는데 안타깝게도 당시에는 구글검색 따위가 없었으므로.. 찾기가 어려웠는지.. 그는 참고문헌을 Unknown Air Force Document 라고 기술하였다. 이후 그 문서의 정확한 출처를 알아내었는데 이는 미공군의 "Multics Security Evaluation: Vulnerability Analysis(1974)"을 참조한 것이다. 이를 알게된 편집자(Karger, Paul A.; Schell, Roger R.)들이 톰슨에게 제보를하고, 톰슨은 이후 논문에서 출처를 수정한다. 어쨌든 당시의 미공군의 Vulnerablity Analysis 논문은 당시의 프로그래밍 언어로 사용되던 PL/I의 컴파일러에 악의적인 코드를 섞어서 일종의 함정(Trap Door)를 삽입하는 내용을 담고있었다. 켄 톰슨은 여기에 착안하여서, C언어 컴파일러에도 동일한 공격을 취할 수 있다는 것을 증명하였다.


그의 주장에 따르면 신뢰(Trust)는 사실 상대적인 개념이며, 모든 코드가 100% 완전하게 믿을만하다고 확신하려면 처음 코딩부터 시작해서 그것이 컴파일되고 빌드되는 모든 순간을 매번 철저하게 감시하고 검사해야만 비로소 '안전한 소프트웨어'라고 볼 수 있다는 것이다. 그동안 사람들은 소프트웨어를 작성할 때 사람이 알아듣는 (일명 고급언어) 수준에서 코드를 검사하는데에 그쳤었다. 그런데 코드가 컴파일되어 기계어로 번역되고, 실행가능한(Excutable) 파일로 생성되는 과정은 굳이 관찰하지 않았던 것이다. 즉, 컴파일러는 암묵적으로 모두가 동의하는 '신뢰할 만한 소프트웨어'라는 가정에 근본적으로 문제가 있다는 점을 지적한 것이다.


본 논문은 3단계로 구성되어있으며 점진적으로 내용을 확장해간다.


Stage 1 – Self-reproducing programs

여기에서는 quines 이라는 것을 소개한다. 이는 프로그래밍을 공부하는 학생들이 재미삼아서 시도하는 것으로써, 프로그램이 자기 자신을 정확히 복제하여 재생산하도록 하는 코드를 말한다. 위키백과에서는 'self-replicating, self-reproducing, self-copying' 등의 용어와 동일하다고 언급하고있다. 이 프로그램은 별도의 입력(input)이 요구되지 않으며, 자기 자신의 소스코드를 복사하여 결과물로 출력(output)하는 기능을 수행한다. 톰슨은 아와 같은 C언어 코드를 제시하였다. 추가적으로 참고할만한 C언어 버전의 Quines 모음이다 : https://www.nyx.net/~gthompso/self_c.txt

아래의 사진은 Java 버전의 Quine 프로그램 예제이다. 문법에 알맞은 Keyword와 심지어 주석까지, 원본 소스코드와 완벽히 똑같은 결과물을 표출하고 있다.

이러한 코드는 다양한 프로그래밍 언어로 쉽게 구현이 가능하다는 점을 먼저 강조한 후, 다음 스텝으로 넘어간다.



Stage 2 - Learned behaviour

이러한 프로그램의 조금 더 확장된 예시가 바로 컴파일러이며, 여기에서는 C - Compiler를 예로 든다. C 컴파일러는 "닭이 먼저냐? 달걀이 먼저냐?"라는 말이 나올만큼, 신기하게도 그 자체가 C 언어로 제작되어 있다. 그렇다면 만약 "Hello world\n"이라는 문자열을 출력한다면 어떤 일이 벌어질까? 여기에서 중요한 것은 "\n"이라는 글자이다. 프로그래밍을 해본 사람은 알겠지만 이것은 줄바꿈(newline)을 지시하는 키워드이다. 따라서 H부터 시작해서 d까지는 단순히 배열내의 문자를 출려하는 일만 수행했다면, \n이라는 글자를 처리할 때에는 기존과 다른 동작을 수행해야 한다. 이를 위해서는 컴파일러가 escape character 대한 사전지식(knowledge)을 습득하고 있어야 한다. 그리하여 다음과 같이 백슬러쉬를 만나면 별도의 처리 루틴을 수행하도록 하는 것이다.

그렇다면 이번에는, \v 라는 새로운 글자(예를들어 vertical tab)를 처리하는 루틴에 대해 생각해보자. 위의 소스코드를 약간 수정한다면 다음과 같을 것이다.

하지만 안타깝게도 이 코드는 정상적으로 작동하지 않는다. 컴파일러가 \v에 대한 정보를 가지고있지 않기 때문이다. 따라서 해당 소스코드는 정상적인 C언어 문법을 준수했다고 볼 수 없다. 그럼에도 불구하고 이를 정상 작동시키기 위해서는 컴파일러를 수정하면 된다는 발칙한 상상을 해보자. 만약 컴파일러를 고칠 수 있다면, 이 코드도 마치 정상인 것처럼 속일 수 있지 않을까?


이를 위해서 아스키 코드표의 값을 이용해보자. ASCII상으로 숫자 11은 Vertical Tab을 나타낸다. 이를 활용해 다음과 같이 코드를 작성해본다.

이렇게 수행한다면, C언어 컴파일러가 해당 내용을 인지할 수 있게 된다. return(11)이 Vertical Tab 값을 반환하기 때문이다. 그리고 이 방법을 사용하여 얻은 바이너리 결과물을 새로운 공식 C언어 컴파일러로 대체한다면(Stage 1에서 언급한 self-referencing을 기억하라!), 위에서는 작동하지 않았던 return('\v') 역시 정상적으로 실행될 것이다. 이는 마치 프로그램이 몰랐던 부분을 스스로 '학습(Learning)'한듯한 효과를 보여준다.

This is a deep concept. It is as close to a “learning” program as I have seen. You simply tell it once, then you can this use self-referencing definition.



Stage 3 - Double blind

이제는 위에서 언급한 기법을 본격 '악의적으로'사용할 수 있다는 가능성을 제기한다. 예를들어 다음이 정상적인 컴파일러의 행위라 가정한다.

이때 특정한 패턴이 발견될 때마다 의도적으로 소스코드를 mis-compile하도록 컴파일러를 약간 (불법)개조할 수 있다.

이것이 의도되지 않은 실수라면 그저 "소프트웨어 결함(bug)"라고 부르지만, 만약 이것이 고도로 의도된 행동이라면 일종의 "트로이의 목마(Trojan horse)"라고 볼 수 있다. 이러한 상황이 유닉스 운영체제의 로그인(login) 기능에서 발생했다고 생각해보라. 인가되지 않은 유저가 권한이 없이 시스템에 접근할 수 있게된다. 이는 매우 치명적인 보안위협이다. 이러한 코드는 발견하기가 매우 어려울 것이다. 


뿐만 아니라 이 공격의 마지막 단계가 남아있다.

위의 코드는 트로이 목마가 하나 더 추가된 상황을 보여주고 있다. 이것은 C 컴파일러를 노리는 킬러이다. 공격자는 Stage1에서 본 것처럼 자기 자신을 똑같이 복제하는 방법으로 컴파일러 안에 트로이 목마를 주입하고자 한다. 이때 Stage2에서 언급하였던 'Learning phase'를 이용한다. 이를 정상적인 C 컴파일러를 사용하여 바이너리로 만든다.(이 바이너리에는 트로이목마를 유발하는 버그가 들어있다.) 이렇게 생성된 바이너리를 정상 C 컴파일러와 대체하고, 조금 전에 만들었던 소스코드에서 버그를 유발하는 부분을 삭제한다. 이후 (정상이 된) 소스코드를 (바꿔치기한) 컴파일러로 다시 컴파일한다. 이렇게되면 해당 컴파일러를 사용할 때마다 소스코드에는 전혀 없던 트로이목마가 삽입된 채로 바이너리가 생성된다. 프로그래머는 아마 아무것도 눈치재지 못한다. 그가 확인하는 소스코드 그 어디에도 버그를 유발하는 지점을 발견할 수 없기 때문이다.



결론 : 이 이야기가 주는 교훈

여기에서 주로 언급한 것은 C - Compiler이지만, 단순히 컴파일러에만 국한되는 내용이 아니다. 어셈블러나 로더, 마이크로 컴파일러 등등에서도 동일한 속임수를 발휘할 수 있다. 프로그래밍 수준이 점점 낮아질수록(기계어에 가까워질수록) 이 버그를 발견하는 것은 거의 불가능에 가까워진다. 따라서 본인이 처음부터 끝까지 모든 (기계어) 코드를 직접 작성하고 관리한 것이 아니라면, 결코 그 결과물을 100% 신뢰할 수 없다는 것이다. 


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