물리 메모리
물리 메모리라 하면 우리는 흔히 RAM만을 생각한다. 하지만 실제 4GB램을 장착하더라도 사용할 수 있는 메모리는 4GB 이하이다. 이는 시스템이 관리하는 모든 메모리란 램 하나만을 의미하는 것이 아닌 장치 메모리(Device Memory)가 존재하기 때문인데, 이로 인해 우리가 사용할 수 있는 공간은 4GB 램일 경우 [4GB-장치 메모리]가 된다. 안 그래도 부족한 4GB 메모리가 이로 인해 더욱 부족하게 되는 것이다. 그렇기에 가상 메모리라는 개념을 사용하게 되었는데, 가상 메모리라 해도 결국 이 실제 메모리에서 활동하게 되는 것이다.
하지만 가상 메모리라 해도 결국 실제 메모리에서 활동한다고 하였는데, 여러 프로세스를 실행하면 이 주소 공간이 부족하지 않을까? 이를 위해 존재하는 것이 바로 페이징 기법이다. 페이징 기법은 메모리 관리자가 현재 사용하는 프로세스를 메모리에 올리고 만약 메모리에서 어떠한 프로세스가 놀거나 쉬고 있다면 이를 페이지 파일로 내리는 것이다.
이를 통해 컴퓨터는 더 넓은 메모리 공간을 갖고 있는 것처럼 사용할 수 있다. 단, 페이지 스왑이 일어날 때 디스크를 읽고 쓰는 작업이 발생하기 때문에 메모리만으로 프로세스가 동작할 때보다 많은 성능 저하가 발생한다는 것 또한 알아야 한다. 그렇다면 이제 각 프로세스마다 갖는 가상 메모리에 대하여 알아보자.
가상 메모리
가상 메로리란 단어 그 자체와 같이 물리적으로 존재하지 않는 가상적인 메모리 공간을 뜻한다. 메모리 공간 외에 하드 디스크에 파일 형태로 따로 준비하는 가상의 메모리 공간으로, 부족한 시스템 메모리를 보조해주는 역할을 한다. 보조해준다는 것은 실제로 존재하는 것이 아님과 같이 위 물리 메모리에서 언급한 바와 같이 실제 메모리가 부족할 경우 페이징을 통해 실제 메모리에 공간을 확보하여 실행함을 뜻한다. 만약 페이징 되어 있는 요소가 다시 메모리에서 활동해야 한다면 이를 다시 페이징 시켜 메모리에 올려 활동할 수 있도록 메모리 관리자가 이를 조정할 수 있도록 한다.
가상 메모리의 구조는 크게 나누어 아래 그림(좌측)과 같이 두 부분으로 나눌 수가 있다. 4GB의 가상 메모리가 할당되면 2GB의 사용자 영역과 2GB의 커널 영역으로 나누어지며 커널 영역은 윈도가 사용하는 공간이며, 사용자 영역은 우리가 보통 사용하는 일반 응용 프로그램들이 사용하는 공간이다. 이러한 가상 메모리는 하나의 프로세스마다 할당이 되어 독립적으로 제공된 자신만의 가상공간에서 작업할 수 있게 된다. 단, 여기서 커널 공간은 단일 공간으로 커널 모드를 사용하는 모든 프로세스에서 공유되며, 커널 영역은 공유되면서 시스템 운영에 필수적이기 때문에 주로 페이지 파일보다는 RAM에 존재하고 있다.
프로세스가 사용하는 가상 주소 공간의 내용도 결국 실행되기 위해서는 실제 메모리에 올라가야 한다. 이러한 조정을 메모리 관리자가 진행하게 되며, 위 물리 메모리의 그림에서와 같이 스왑이 일어나 메모리를 조정하는 것이다. 쉽게 말해, 사용하는 프로세스만 메모리에 올리고, 사용하지 않는 것은 디스크에 저장하는 방식이다.
이제 좀 더 세분화된 메모리 구조에 대하여 알아보자. 위 그림(우측)을 보면 code영역부터 시작하여 kernel 영역까지 나타나 있다. 이 또한 대략적인 형태이며, 이외에도 더 많은 요소들이 존재하고 있지만 너무 많은 내용들을 다루어야 하기 때문에 위에 나타난 항목에 대해서만 알아보자.
code 영역은 코드 자체를 구성하는 메모리 영역으로 메모리에서 실행하고자 하는 명령어들이 위치해 있으며 data 영역에는 초기화된 변수들이 존재하는 곳으로, 전역 변수, 정적 변수, 베열, 구조체 등이 저장된다. 그 외에 초기화되지 않은 변수들은 bss영역에 저장되며, 이러한 변수들은 프로그램이 실행될 때 생성되고 프로그램이 종료되면 시스템에 반환된다.
위의 변수들은 프로그램의 실행과 함께 생성된다 하였다. 그렇다면 동적으로 할당되는 변수들은 어디에 존재할까? 바로 Heap 영역이다. 동적으로 할당된 변수들은 낮은 주소의 힙 영역부터 생성되어 높은 주소로 쌓이는 형태로 존재하고 있으며 만약 동적으로 할당된 변수를 해제할 경우 이는 힙 영역에서 사라지게 된다.
마지막으로 Stack 영역의 경우 프로그램이 자동으로 사용하는 임시 메모리 영역으로 지역변수, 매개변수, 반환 값 등을 위해 필요할 때마다 생성하고 지우는 등의 작업이 이루어진다. 다른 영역들과는 다르게 함수가 호출될 시 생성되며, 함수가 끝나면 제거된다. 스택 영역은 높은 주소에서부터 낮은 주소로 쌓이는 형태로 진행되며 우리가 흔히 알고 있는 LIFO(Last Input First Out)로 동작한다.
흔히 메모리 구조라 하면 위와 같이 나타나지만, 이보다 한 단계 나아가 좀 더 상세한 메모리 구조에 대하여 한 번 알아보자. DLL이 어떻게 어느 주소에 위치하는지, 어떠한 프로세스인지, 다음 프로세스는 어떠한 것이 있는지 등을 확인하기 위한 구조라 생각하면 된다. 아래의 그림이 이를 표현한 것으로 메모리 포렌식과 관련하여 공부를 해본 사람이라면 어떠한 요소들이 어디에 사용되는지 대략적인 감이 올 것이다.
커널 영역의 EPROCESS를 먼저 확인하. 아마 리버싱에 입문한지 얼마 안 된 사람이라면 이것이 어떠한 것인지 모를 수가 있다. EPROCESS 구조체는 프로세스에 관한 많은 정보를 담고 있는 구조체로 KPROCESS (PCB)를 가리키거나 프로세스의 생성 시간, 다른 프로세스가 가진 EPROCESS 구조, 토큰, 그리고 PEB의 주소 등 많은 정보를 가지고 있다. 프로세스는 이러한 EPROCESS 구조로부터 많은 정보를 얻을 수가 있는데 우선 PCB에 대하여 알아보자.
PCB는 Process Contorl Block으로 이들을 통해 ETHREAD와 KTHREAD 구조체를 참고하여 TEB(FS:0)을 찾을 수가 있다. TEB에는 스레드와 관련된 정보들을 포함하고 있으며, 이중에는 해당 스레드가 어떠한 프로세스에 속해있는지 알 수 있는 PEB(Process EnvironmentBlock)를 찾아갈 수가 있다. PEB에는 현재 프로세스의 Image Base Address에 대한 정보, LDR에 관한 정보 등에 대하여 알 수 있다. 이렇게 LDR을 확인하므로 어떠한 DLL이 어느 주소에 위치하였는지 확인할 수가 있다.
간략하게나마 메모리 구조로부터 얻을 수 있는 정보들에 대하여 확인해보았다. 간략하게나마 이러한 구조에 대해 이야기한 것은, 프로세스가 존재하고 있는 가상 메모리 하나로부터 많은 정보들을 확인할 수가 있기 때문이며, 이를 토대로 발전한 것이 바로 메모리 포렌식 분석 기법이다. 따라서 메모리에 대해 이해하는 것은 악성코드를 분석할 때 좀 더 주도면밀하게 분석할 수 있는 여건을 형성해 줄 것이다.
메모리 분석
메모리 구조에 대하여 간략히 알아보았으니 메모리 분석 도구를 통해 이에 대하여 한 번 실습을 진행해보자. 메모리 분석도구로 Volatility와 Rekall 등 다양한 도구가 존재하고 있지만, 필자는 Rekall을 통해 실습을 진행할 것이다. 우선 pslist 명령에 대하여 알아보자. pslist는 현재 메모리에서 실행되고 있거나 종료된 프로세스의 목록을 나열해준다. 그렇다면 어떠한 프로세스가 존재하고 있는지 어떻게 알 수 있을까? pslist 명령어는 EPROCESS 구조에서 ActiveProcessLinks를 통해 현재 실행 중인 프로세스의 목록을 가지고 온다. 해당 구조는 링크형태로 다음 프로세스와 이전 프로세스를 가리키고 있다.
가장 우측의 "Start" 항목은 프로세스가 시작된 시간으로 이 또한 EPROCESS 구조체가 가지고 있는 CreateTime의 값을 통해 알 수가 있다. 그리고 만약 프로세스가 종료되어있지만 메모리에 아직 남아있는 경우 ExitTime을 통해 프로세스가 종료된 시간까지 확인할 수 있다.
이번에는 프로세스가 가지고 있는 스레드에 대하여 알아보자. 스레드 목록 또한 EPROCESS 구조로부터 확인할 수가 있는데, 바로 ThreadListHead를 확인하면 된다. 해당 값은 위와 유사하게 어떠한 스레드가 존재하고 있는지 링크 형태로 존재하고 있다. 아래 결과를 확인하면 현재 해당 프로세스 DarkSeoul.exe에는 하나의 스레드가 존재하고 있는 것을 확인할 수 있다.
프로세스 목록과 스레드 목록에 대해 알아보았으니 이제 DLL의 목록에 대하여 알아보자. DLL 목록의 경우 EPROCESS 구조에서 PEB 구조의 위치를 확인한 다음, PEB 구조에서 LDR( _LDR_DATA_TABLE_ENTRY)의 "InMemoryOrderModuleList"를 통해 확인할 수가 있다. 해당 값에는 링크의 형태로 DLL의 목록을 가지고 있으며 이를 통해 어떠한 DLL들이 각 프로세스에 사용되고 있는지 확인할 수가 있다.
메모리로부터 프로세스가 가진 핸들 또한 확인할 수 있는데, 이는 쉽게 handles 명령을 사용하면 된다. 이러한 핸들의 목록은 위 큰 그림에서는 포함되지 않았지만, 마찬가지로 EPROCESS 구조로부터 ObjectTable (_HANDLE_TABLE)에 포함되어 있는 HandleTableList를 통해 핸들의 목록을 확인할 수가 있다.
이렇게 메모리의 구조에 대하여 알아보았고, 메모리 분석도구를 통해 실제 메모리를 분석해보며 어떠한 구조를 참고하는지에 대하여 알아보았다. 사실 이러한 메모리 구조를 모르더라도 Volatility나 Rekall을 사용할 수가 있다. 하지만 알고 쓰는 것과 모르고 쓰는 것은 엄연히 다르기 때문에 많이 알고 있어서 나쁠 건 없다고 생각한다. 여기서 다루지 않은 요소 중, 어떠한 정보를 어떻게 찾는지에 대해선 https://github.com/volatilityfoundation/volatility/wiki/Command-Reference를 참고하면 된다.
출처: https://kali-km.tistory.com/entry/윈도우-메모리구조와-메모리분석-기초 [Kali-KM_Security Study]
'쿼리큘럼 개인적 정리' 카테고리의 다른 글
성능 측정 프로파일러 (0) | 2021.09.11 |
---|---|
메모리 누수 추적 라이브러리 (0) | 2021.09.11 |
CPU 캐시 메모리 (0) | 2021.09.10 |
C 언어의 어셈블리 분석 (0) | 2021.09.10 |
어셈블리어의 기본 정리 (0) | 2021.06.09 |