본문으로 바로가기

0x00 리버스 엔지니어링이란?

category Education/리버싱 2024. 7. 7. 23:04

들어가며

군생활 동안 제작했던 "사지방 해제기"에는 사실 특별한 기법이 사용되지 않았다. Windows API에서 제공하는 프로세스 일시중지, 죽이기 기능을 이용해 MaestroWeb Agent의 특정 프로세스를 일시중지/죽이거나 맘아이 솔루션에서 맘아이 프로세스가 죽었을 때 다시 살리는 프로세스를 일시중지하고 (죽이는 것은 권한 부족 문제로 수행할 수 없었으나, 프로세스 일시중지는 가능했다) 맘아이에서 실질적으로 사이트를 인식하고 차단하는 프로세스를 죽이는 것으로(별다른 문제없이 프로세스 kill을 수행할 수 있었는데, 이것도 상식적으로 권한으로 막아야 하지 않았을까 하는 것은 여전히 의문) 사지방 우회기의 기능을 구현할 수 있었다.

그때 당시에는 리버싱에 대해 자세히 알 지 못했었고, 사지방을 이용하면서 알아낸 몇가지 편법들을 본가의 PC에서 C#을 이용하여 자동화한 것이 현재 블로그에 작성해둔 "사지방 우회기" 이다. 그런 과정에서 리버싱에 대해 관심이 생겼고 리버싱을 좀더 본격적으로 배우고자 하여 병자기개발비 지원금을 이용해 리버싱 관련 문헌을 몇권 구매했다.

리버스 엔지니어링의 의미?

리버스 엔지니어링은 컴파일된 프로그램을 연구하는 일이다. 개발자가 C/C++ 등의 언어로 프로그램을 작성하면 컴파일러는 해당 코드를 해석하여 이를 바이너리로 빌드하는데 이를 "컴파일" 이라고 한다. 컴파일된 프로그램(바이너리)에서는 원래의 C/C++ 코드가 담겨있지 않다. 리버스 엔지니어링은 이러한 바이너리 코드를 분석하여 원래의 코드 구조와 동작을 이해하려는 시도이다. 바이너리 코드를 어셈블리어로 변환(디스어셈블리) 하거나 더 높은 수준의 언어로 변환(디컴파일) 하는 것 모두 리버스 엔지니어링에 포함된다.

어셈블리어란?

어셈블리어는 특정 CPU 아키텍처의 기계어 명령을 사람이 이해할 수 있는 형태로 표현한 것이다. 프로그래밍 언어는 고급 언어와 저급 언어로 구분할 수 있는데, 어셈블리 언어는 저급 언어에 해당하며 인간의 언어보다 기계의 언어에 더 가까우며 어셈블리어의 명령어는 기계어 코드와 1:1 대응한다. 즉, 기계어를 그저 사람이 읽을 수 있는 형태로 변환한것에 불과한 언어이다.

어셈블리어를 배워야 하는 이유

요즘의 일반적인 개발자는 어셈블리어로 코드를 작성할 일은 거의 없다. C, C++, C#, JAVA, Java Script, Python등의 코드를 비교하는 유튜브 영상에서 어셈블리어 코드에 대해 "시급을 받으며 일하는 개발자에게 적합" 한 언어라고 소개할 정도이다. 어셈블리어는 같은 동작을 수행하기 위해 고급 언어 대비 더 복잡하고 긴 코드를 작성하게 된다. 심지어 최신 컴파일러는 사람이 직접 최적화를 수행하는 것 보다 최적화 수행 능력이 좋다.

어셈블리에 대한 이해가 도움이 되는 경우는 보안/악성 프로그램을 연구하거나 디버깅 하는 동안 컴파일된 코드를 확인하는 경우이다. 이미 바이너리로 빌드된 프로그램을 분석할 때에는 어셈블리어 외에는 방도가 없으며 어셈블리어를 이해하는 능력이 필수적이다.

어셈블리어 학습에 유용한 사이트 - godbolt.org

여러가지 언어로 작성한 프로그램 코드를 다양한 컴파일러를 이용해 어셈블리어로 변환하여 확인해볼 수 있는 사이트가 있다. https://godbolt.org/ 에 접속하면 온라인 환경에서 코드를 작성해보고 이를 컴파일러를 이용해 어셈블리어로 변환해볼 수 있다.

int형 정수를 입력받아 그대로 반환하는 간단한 함수를 작성하였다. 실시간으로 어셈블리어로 변환해주고 있다.

num:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

동일한 코드라도 CPU 아키텍처의 명령어 집합에 따라 조금씩 어셈블리어가 달라질 수 있다. 또한 어셈블리언어는 인텔 구문과 AT&T 구문의 두 가지 문법을 가지고 있다. 위 구문은 인텔 구문의 문법으로 작성되어 있다. 어셈블리어에 대한 내용은 추후 포스트에서 더 자세히 다루겠으나 일단 위의 명령어를 간단하게 살펴보자면

push rbp
push : 오퍼랜드(피연산자, 즉 연산에 사용할 데이터 / 포인터)의 내용을 스택에 쌓는다.
rbp : 베이스 포인터 레지스터로, 스택의 바닥을 가르키는 포인터이다. 스택프레임으로 관리하기 위해 사용되는 포인터이다

mov rbp, rsp
mov A, B: A를 B값으로 덮어씌운다, 즉 rbp를 rsp의 값으로 덮어씌운다.
rsp :  스택 포인터 레지스터로 스택의 맨 꼭대기를 가르키는 포인터이다.

여기 까지 했을 때, rbp는 현재 함수의 스택프레임을 가리키게 된다.

mov DWORD PTR [rbp-4], edi
edi : x86-64 호출 규약에 따라 함수의 첫번째 인수(int a)가 전달되는 32비트 레지스터다. 
rbp-4 위치에 edi의 값을 저장한다

mov eax, DWORD PTR [rbp-4]
eax: 함수의 반환값을 저장하는데 사용되는 32비트 레지스터다. 

pop rbp
pop : 스택으로부터 값을 뽑아낸다. 여기서는 스택 맨 위의 값을 뽑아서 rbp에 집어 넣는다. 스택 프레임을 정리하는 과정 중 일부다.

ret
호출된 함수에서 호출한 함수로 복귀한다.