티스토리 뷰

본 문서는 원문 저자인 Massimiliano Tomassoli의 허락 하에 번역이 되었으며, 원문과 관련된 모든 저작물에 대한 저작권은 원문 저자인 Massimiliano Tomassoli에 있음을 밝힙니다. 


http://expdev-kiuhnm.rhcloud.com



최신 윈도우즈 익스플로잇 개발 05. 윈도우즈 기초


hackability.kr (김태범)

hackability_at_naver.com or ktb88_at_korea.ac.kr

2016.07.19



윈도우즈 기초


여기서는 윈도우 개발자들은 기본적으로 알지만 리눅스 개발자는 모를수도 있는 몇가지를 소개하려고 합니다.



Win32 API


윈도우즈 API는 몇몇 DLL (Dynamic Link Libraries)로 부터 제공됩니다. 응용 프로그램은 이런 DLL 로 부터 함수를 가져올 수 있고 호출 할 수 있습니다. 이는 커널의 내부 API의 버전에 따라 유저 모드 응용프로그램들의 행위가 바뀔 수 있음을 뜻합니다.



PE 파일 구조


실행 파일과 DLL 들은 PE (Portable Executable) 파일 입니다. 각각의 PE import table, export table 을 포함하고 있습니다. Import table은 등록된 함수들과 어디에 위치해 있는지 명시합니다. Export table은 다른 PE 파일에서 import 하여 사용될 함수에 대해 명시합니다.

 

PE 파일은 코드 영역, 데이터 영역과 같이 다양한 영역들로 구성되어 있습니다. .reloc 영역은 메모리에서 실행 파일이나 DLL 들에 대한 재배치 정보를 가지고 있습니다. 코드의 몇몇 주소는 상대적인 위치를 갖습니다만 다수의 주소들이 메모리에 불려진 모듈의 위치에 의존적이거나 절대적인 주소를 갖습니다.

 

윈도우즈 로더는 현재 디렉토리에서 부터 DLL 들을 검색하기 때문에 시스템 루트에 있는 DLL 들과 다른 DLL 들을 응용 프로그램에 포함하여 배포할 수 있습니다. 이는 몇몇 사람들에 의해 DLL 지옥이라 불리는 버전 이슈가 있긴 합니다.

 

한 가지 중요한 개념은 RVA (Relative Virtual Address) 입니다. PE 파일들은 RVA들을 이용하여 모듈의 기본 주소에 대해 요소들의 상대적인 위치를 명시합니다. 만약 어떤 모듈이 주소 B에 로드 되었고 RVA X 라 하면, 해당 요소의 절대 주소는 간단히 B + X 가 됩니다.



스레딩


만약 당신이 윈도우즈를 사용한다면 스레드에 대해 특별히 다른 이상한 것은 없습니다만 만약 리눅스를 사용하시다가 왔다면 명심해야 할 것이 윈도우즈는 리눅스처럼 프로세스별로 CPU 시간을 제공하는 것이 아닌 스레드 별 CPU 시간이 주어진다는 것 입니다. 윈도우즈에는 fork() 같은 함수가 없습니다. 새로운 프로세스를 생성하려면 CreateProcess() 를 이용하고 스레드를 생성하려면 CreateThreads() 를 이용합니다. 스레드는 프로세스 주소 공간에서 실행되며 메모리를 공유합니다.

 

스레드는 TLS (Thread Local Storage)라 불리는 방식을 이용하여 제한적으로 비공유 메모리를 지원합니다. 기본적으로 각각의 스레드의 TEB (Thread Environment Block) 64 개의 main TLS 배열을 갖으며 선택적으로 가능하다면 최대 1024 개의 TLS 배열을 할당 될 수 있습니다. 첫 번째로 인덱스는 두 개의 배열 중 하나의 위치에 대응되는데 이는 할당 되어 있거나 할당된 인덱스를 반환하는 TlsAlloc()으로 예약되어야 합니다. 그러면 각각의 스레드는 인덱스가 할당된 두개의 TLS 배열중 하나의 DWORD에 접근할 수 있습니다. 해당 DWORD TlsGetValue(indx)로 읽을 수 있으며 TlsSetValue(index, newValue)로 쓸수 있습니다. 예를들어, TlsGetValue(7) 은 현재 스레드의 TEB에 있는 main TLS 배열의 7 번째 인덱스의 DWORD를 읽습니다.

 

우리는 GetCurrentThreadId()를 이용하여 이 방식을 에뮬레이트 할 수 있지만 이 방식은 그렇게 효율적이지 않습니다.



토큰과 사칭(Impersonation)


토큰은 올바른 접근에 대해 표현합니다. 토큰은 32 비트 정수로 구현되었으며 파일 핸들러와 비슷합니다. 각각의 프로세스는 토큰에 연관된 올바른 접근에 대한 정보를 갖고 있는 내부 구조를 갖고 있습니다.

 

여기에는 2가지 종류 (primary token: 주 토큰, secondary token: 부 토큰) 토큰이 존재합니다. 프로세스가 생성되면 primary token이 할당됩니다. 프로세스의 각각의 스레드는 프로세스의 토큰을 갖거나 다른 프로세스에서 secondary token 또는 적절한 자격을 갖고 있다면 LoginUser() 함수로 새로운 토큰을 갖을 수 있습니다.

 

현재 스레드에 토큰을 설정하고 싶다면 SetThreadToken(newToken)을 이용하거나 RevertToSelf()를 이용하여 삭제 할 수 있는데 이는 현재 스레드의 토큰을 primary token으로 되돌립니다.

 

윈도우즈에서 유저가 서버에 접속하여 유저 이름과 암호를 전달한다고 가정 해봅시다. 서버는 SYSTEM 으로 동작하며 제공된 자격 (유저 이름, 암호)를 이용하여 LogonUser()를 호출합니다. 만약 적절한 자격을 갖고 있다면 새로운 토큰을 발급합니다. 그러면 서버는 새로운 스레드를 생성하고 해당 스레드는 LogonUser()에서 제공받은 토큰으로 SetThreadToken(new_token)을 호출합니다. 이 방식을 통해 스레드는 해당 자격을 갖는 유저와 동일한 권한으로 실행됩니다. 만약 스레드가 종료거나 revertToSelf()가 호출된다면 free threads pool에 추가됩니다. 만약 서버를 장악할 수 있다면 ReverToSelf()나 메모리에서 다른 토큰을 이용하여 SetThreadToken()을 이용하여 현재 스레드에 적용하여 SYSTEM 으로 바꿀 수 있습니다.

 

한 가지 명심해야 할 것은 CreateProcess()는 새로운 프로세스의 토큰을 primary token으로 사용합니다. 이는 CreateProcess()를 호출하는 스레드는 primary token 보다 더 많은 권한을 갖는 secondary token 를 갖기 때문에 문제가 됩니다. 이 경우, 새로운 프로세스는 생성된 스레드보다 낮은 권한을 갖도록 해야 합니다.

 

해결책으로는 DuplicateTokenEx()를 이용하여 현재 스레드의 secondary token으로부터 primary token을 생성하고 새로운 primary token으로 CreateProcessAsUser()를 이용하여 새로운 프로세스를 생성하는 것 입니다.

댓글