1. DLL 주소 재배치와 ASLR의 관계

Windows XP 내지 Vista 이후로 (1) 커널 API와 C 런타임 라이브러리 함수 심벌들이 한 DLL 몰빵이 아니라 분야별로 재분류되어 배치되기 시작한 것, (2) 시스템 DLL들이 이제 전혀 rebase되지 않고 고정된 단일 preferred base 주소를 갖기 시작한 것을 보면 참 격세지감이 느껴진다.

위의 둘은 (1) 자잘한 DLL 여러 개보다 큰 DLL 하나가 더 효율적이다(선박처럼??). (2) DLL들은 로딩되는 주소가 겹치지 않게 빌드 후에 반드시 rebasing을 해 줘라
이런 전통적인 고정관념을 역행하는 변화이기 때문이다.

보안 강화를 위해 10여 년 전 Windows Vista 때부터 ASLR (시작 주소 랜덤화)이 도입되면서 DLL은 물론이고 EXE조차도 반드시 자기 preferred base에 고정적으로 로딩이 되지 않게 되었다. 이 때문에 요즘은 EXE도 과거 Win32s 프로그램들처럼 끝에 재배치 정보가 다시 포함돼 들어가고 있다.

하지만 이런 ASLR을 위한 재배치 때는 말 그대로 메모리 오프셋 수정만 행해질 뿐, 재배치의 치명적인 페널티라고 여겨지는 가상 메모리 페이지 파일 재기록이라든가 재사용 불가(여러 프로세스에서 동일 DLL 로딩 시에도 shallow가 아닌 deep copy 발생) 까지 발생하지는 않는다. 운영체제의 보안 기능이 그 정도로 바보는 아니다.

그러므로 오늘날은 DLL을 미리 rebase 하건 안 하건 실행 성능이 달라지는 것은 없다. rebase를 해도 이익을 얻는 것은 없지만 반대로 손해를 보는 것 역시 없다. rebase라는 게 빌드 타임이 아닌 런타임의 영역으로 바뀐 셈이다.

정말 재수가 없어서 엄청 많은 자잘한 DLL들이 로딩되다 보니 한 DLL이 프로세스 A에서는 ASLR 배당 주소로 로딩됐지만 프로세스 B에서는 그 주소로 로딩이 못 되게 됐다면.. 그때는 통상적인 페널티가 부과되는 재배치가 발생할 것이다. 하지만 광대한 주소 공간을 자랑하는 64비트 환경에서는 그럴 가능성이 더욱 희박해졌다.

2. EXE를 LoadLibrary 하기

LoadLibrary 함수는 실행 가능한 코드가 담긴 DLL을 불러오거나 혹은 EXE/DLL로부터 리소스를 얻고자 할 때 즐겨 쓰인다.
그런데 여기서 의문이 든다. LoadLibrary를 호출해서 exe의 단순 리소스가 아니라 코드를 내 프로세스 공간에 가져와 실행하는 게 가능할까?

사실, 기술적으로 볼 때 EXE와 DLL의 차이는 그리 크지 않다. 심지어 EXE도 DLL처럼 심벌 export를 할 수 있다.
그리고 EXE를 LoadLibrary로 그냥 쌩으로 불러와도, 의외로 일단 성공은 한다. GetProcAddress를 해서 심벌을 요청하면 주소값이 돌아오기까지 한다.
하지만 그 함수를 호출해 보면 십중팔구 access violation 에러가 난다. 여기서 대부분의 사람들은 '안 되나 보다'라고 생각하고 단념하게 된다. 왜 이런 현상이 발생하는 것이며, 문제를 해결할 방법은 없는 걸까?

DLL이 아닌 EXE를 LoadLibrary 하면 운영체제는 얘를 반쯤 데이터로 취급하는가 보다. GetProcAddress를 호출했을 때 심벌 검색 결과를 되돌려 주지만 그 포인터가 가리키는 코드를 실행 가능한 상태로 만들어 놓지는 않는다.
특히 (1) 주소 재배치와 관련된 그 어떤 조치도 취하지 않는다. 구체적으로는.. EXE가 사용하는 import table의 주소를 패치하지 않기 때문에 그 EXE의 코드가 실행되면서 Windows API 같은 걸 호출하면 그대로 뻑이 나게 된다.

그리고 (2) EXE의 진입점 함수를 전혀 실행하지 않는다.
EXE건 DLL이건 무조건 맨 먼저 실행할 부분을 가리키는 진입점이란 게 있는데.. 그게 EXE는 int func() 형태이고, DLL은 BOOL func(HMODULE, UINT, PVOID) 형태이다.

즉, EXE는 처음엔 아무 인자 없이 실행됐고 C 라이브러리가 GetStartupInfo 같은 API 함수를 호출해서 실행 인자를 준비한 뒤에 main이나 WinMain을 또 호출하는 형태이다. 그러나 DLL은 진입점 함수의 형태가 DllMain과 완전히 동일하다. 즉, DLL_PROCESS_ATTACH 같은 이벤트 명칭은 이 함수의 호출 인자가 아니면 딴 데서 알아낼 곳이 없다.
LoadLibrary는 원래 DllMain을 호출하게 돼 있는데 EXE는 받아들이는 함수 prototype이 다르므로 아예 호출을 안 하는 것이다.

그러므로 LoadLibrary된 exe의 코드를 강제로 실행한다면 IAT 테이블의 주소가 패치되지 않고 C 라이브러리가 전혀 초기화되지 않은 상태에서 덥석 실행된다. 그 함수에서 내부적으로 전역변수 C++ 객체 같은 걸 사용한다면.. 역시나 제대로 실행되지 못하고 높은 확률로 뻑나게 된다.

IAT 주소를 패치하는 방법까지는 어느 용자가 찾아낸 게 인터넷에 이미 굴러다닌다. (☞ 링크) 이거 패치가 제대로 되려면 EXE는 애초부터 재배치 정보가 들어간 상태로 빌드돼야 한다.
하지만 각종 부작용 없이 C 라이브러리만 감쪽같이 초기화하고 EXE의 export 함수를 실행하는 건.. 굉장히 삽질스럽고 가성비가 낮다. 그냥 EXE와 DLL의 차이가 이러하며 LoadLibrary(EXE)가 기술적으로 왜 권장되지 않는지 이론으로만 알고 넘어가면 될 듯하다.

3. 재빠르게 대체된 파일에 대한 creation date 보정

응용 프로그램 중에는 안전을 위해 문서 저장 기능을 임시 파일을 생성하는 형태로 구현한 것이 있다.
기존 파일을 곧장 덮어써서 저장하는 게 아니라.. 임시 파일에다가 저장을 한 뒤, 기존 파일을 지우고 임시 파일을 기존 파일의 이름으로 바꾼다. 이렇게 하면 저장하는 중에 컴퓨터에 전기가 나가는 등의 이상 현상이 발생하더라도 최소한 기존 자료가 송두리째 날아가는 일은 막을 수 있다.

그런데 이렇게 기존 파일을 덮어쓰는 게 아니라 파일 자체를 딴 것으로 대체하는 식으로 저장을 하면 기존 파일이 갖고 있는 creation time이 보존되지 않게 된다. 그렇기 때문에 기존 파일의 creation time을 따로 얻어 놓은 뒤, 저장을 마친 새 파일에 대해서 creation time을 SetFileTime 함수로 따로 지정해 줘야 한다.

단, Windows NT 계열의 경우, 놀랍게도 보정 동작을 진작부터 지원하고 있었다. 어떤 프로그램이 A라는 파일을 삭제한 뒤에 다른 파일의 이름을 A로 신속하게 거의 곧장 변경한 경우, 그 파일에다가 삭제된 A의 creation time을 자동으로 지정해 줬던 것이다~!

이런 보정을 위해서는 파일 삭제와 개명 알고리즘에다가 삭제된 파일의 생성 시각을 백업해 놓고, 시간차를 감지해서 이 renaming이 기존 파일을 승계하는 동작인지 판단하는 등 여러 귀찮은 작업이 필요할 것이다. 하지만 마소에서는 임시 파일 방식으로 저장하면서 creation time을 관리하지 않는 프로그램이 많은 것을 감안하여 운영체제 차원에서 이런 보정 기능을 구현했다고 한다.

이 보정은 NT 계열에서만 지원되어 왔으며, 9x 계열에서는 존재하지 않는다.

4. 스레드 동기화 deadlock 자동 감지

복잡한 메모리 문제를 잡아내기 위해 C 라이브러리 차원에서 저런 다양한 안전 장치와 디버깅 편의 기능이 제공되듯, 멀티스레드 동기화 오브젝트에도 디버그 버전용은 데드락 정도는 assertion failure 에러를 내면서 곧장 감지하는 기능이 있으면 좋겠다는 생각이 든다.

“당신이 지금 취득을 위해 대기하려는 뮤텍스는 현재 다른 스레드가 잡고 있는데, 문제는 그 스레드도 지금 당신이 요 스레드에서 잡고 있는 뮤텍스를 얻으려고 대기 중이다. 그러니 이 상태로는 상호 무한 대기 교착 상태가 됨.”

이건 레퍼런스 카운트 기반인 오브젝트에서 순환 참조 오류를 감지하는 기능을 구현하는 것과 기술적으로 완전히 동급이다.
Hash 같은 컨테이너를 둬서 스레드 ID별로 각각 현재 진입해 있는 뮤텍스에 대한 기록을 관리하고, 뮤텍스 오브젝트를 감싸는 클래스에다가 현재 자신을 잡고 있는 스레드 정보도 같이 보관하는 정도의 수고만 하면 큰 어려움 없이 구현 가능하다.

하지만 PC용 프로그램에서 돌아가는 스레드의 개수가 무슨 할당된 동적 메모리 블록 개수처럼 많을 리는 없을 것이고, 프로그램의 응답이 멎었을 때 데드락 부위를 찾는 것은 도구의 도움 없이 도저히 못 할 일은 아닐 것이다. 유용성에 비해 저런 기능을 갖추는 건 속도와 메모리 오버헤드가 너무 커서 가성비가 맞지 않으니 데드락 자동 감지 기능은 운영체제나 프로그래밍 언어 런타임이 제공해 주지 않는 듯하다.

개인적으로 직장에서는 심지어 자기 스레드 자신의 실행이 끝나기를 기다리는.. C++ 오브젝트로 치면 delete this.. 무슨 자살이나 다름없는 deadlock도 경험한 적이 있었다.
프로그램의 실행이 종료되어 UnInit() 함수가 호출될 때는 백그라운드 작업 스레드에 대해서도 작업을 중단시키고 작업 스레드의 실행이 끝날 때까지 기다리게 했는데, 뭔가 로직이 꼬여서 작업 스레드에서 UnInit()를 호출하는 상황이 발생한 것이다.

Uninit이 무슨 loop 안에서 1초에 수십, 수백 번씩 실행되어서 성능이 중요한 함수인 건 아니다. 그러니 자기 자신이 무슨 스레드 문맥에서 실행되었는지 검사해서 deadlock을 피할 수도 있다.
하지만 그것보다는 Uninit이 스레드 함수가 아니라 의도했던 대로 main thread에서만 실행되도록 프로그램 구조를 고치는 것이 훨씬 더 나은 해결책이었다.

Posted by 사무엘

2021/02/03 19:36 2021/02/03 19:36
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1850

1. 프로그래밍 용어· 명칭과 타 분야 비교

  • 2의 제곱근 vs 제곱근(루트) 2: 어순의 차이로 의미가 달라지는 new operator/operator new, 함수 템플릿 vs 템플릿 함수 같은 C++ 용어와 상황이 비슷하다. 특히 다들 전자가 후자를 포함하는 편이기도 하다.
  • 전철역에서 논현, 신길, 양평: namespace가 필요한 좋은 예이다..;;; (1) 얘들은 고유명사 지명이 지역간에 충돌하는 사례이고 (2) 월드컵경기장이나 시청은 보통명사 시설명이 충돌하는 예이다.
    (3) 수색, 광명, 신촌 같은 건 지역이 아니라 일반열차 역과 지하철역이 충돌하는 경우이다. 그리고 (4) '회송'은 역의 이름으로는 쓰이지 않는(쓰일 수 없는) reserved word 정도에 대응할 것이다.
  • 배의 명칭 A급 B함/호(포항급 초계함 천안함, 올림픽급 타이타닉 호, 베헤모스급 배틀크루저 히페리온): 프로그래밍으로 치면 A는 타입명이고 B는 변수명이다.

2. 마침표와 세미콜론

우리는 년월일 날짜를 간략하게 표기할 때 2020. 10. 5. 같은 식으로 숫자 뒤에 마침표를 찍곤 한다. 이때 일을 나타내는 마지막 마침표도 생략하지 말고 반드시 다 찍는 것이 한글 맞춤법에 규정된 원칙이다. 각각의 점이 년, 월, 일을 나타내기 때문이다.

이는 마치 파스칼 언어에서는 세미콜론이 문장의 구분자(separator)인 반면, C/C++에서는 문장의 종결자(terminator)인 것과 비슷하다.
파스칼에서는 end나 else 직전에 등장하는 구문의 끝에 ; 이 생략되지만 C/C++은 그렇지 않다. 날짜 숫자의 뒤에다 찍는 .는 파스칼이 아닌 C/C++의 세미콜론과 같은 성격이라고 생각해야 한다.

3. 병렬화

같은 용량의 데이터가 있을 때 압축을 하는 것은 압축을 푸는 것보다 계산량이 훨씬 더 많은 어려운 작업임이 주지의 사실이다.
그 대신, 압축 "하기"는 CPU 멀티코어를 활용해서 속도를 쭉쭉 끌어올릴 수도 있는 반면, "풀기"는 그런 병렬화는 안 되고 그냥 단일 코어에서 linear한 작업에 의존할 수밖에 없다. 기껏해야 풀어야 하는 압축 파일 자체가 여러 개일 때에나 여러 CPU에다가 던져줄 수 있을 것이다.

C/C++ 파일을 빌드하는 절차도 이와 비슷해 보인다. '컴파일'은 아무래도 분산 처리와 병렬화가 가능하지만, 모든 결과물이 하나로 집약되는 '링크'는 그게 불가능한 최종 병목이 될 수밖에 없다.

4. 버퍼의 크기

일상적으로 무슨 모임 같은 데서(본인의 경우는 교회에서 청년부 모임 같은..) 인원수대로 프린트물이나 간식 같은 것을 준비해야 할 때가 있다. 평소에 모임 참석자가 얼마 정도 되는지에 대한 대략의 데이터는 있지만 딱 정확하게 몇 명인지는 알 수 없을 때는 준비물을 몇 개 정도 챙겨야 너무 남거나 모자라는 일이 없이 최대한 딱 맞을 수 있을까?

이건 나름 통계적인 노하우가 필요한 일이다. 가끔 모자라는 일이 발생해도 괜찮은지, 아니면 모자라는 일은 절대 없어야 하는지에 따라서도 전략이 달라진다. 프로그래밍으로 치면 static한 배열의 크기를 잡는 것과 매우 비슷해 보인다는 생각을 본인은 오래 전부터 했다. ㅎㅎ

문자열 클래스의 경우, 사소한 문자열까지 늘 동적 메모리를 할당하는 건 번거로우니 자체적으로 자그마한 배열도 갖고 있고, 그 배열 크기를 초월하는 긴 문자열을 배당할 때만 동적 메모리를 사용하게 하는 구현체도 존재한다. C++의 표준 string 클래스도 반드시 저렇게 동작해야 한다는 조건은 없지만 대체로 이런 식으로 구현된 걸로 본인은 알고 있다.

이런 것 말고도

  • 건물을 지을 때 이 정도 건물에서는 화장실에 변개를 몇 개 설치하는 게 좋을까?
  • 엘리베이터는 어느 정도 크기로 몇 개 설치하는 게 좋을까?
  • 이 정도 도로의 교차로 내지 횡단보도에서는 신호 주기를 어느 정도로 주는 게 좋을까?

같은 문제도 치밀한 공학 및 통계 계산의 산물이지 싶다. 동시에 사용하는 사람의 수가 최대인 시간대에 대기 시간이 너무 길어지지 않게 하는 한편으로, 나머지 한산한 시간대에 시설들이 사용자 없이 놀면서 발생하는 비효율도 최소화해야 하기 때문이다.

5. 훅킹

훅(hook) 내지 훅킹이란 컴퓨터 소프트웨어가 돌아가는 과정을 몰래 들여다보고 필요하면 변조도 하는 메커니즘을 말한다. 훅킹은 대체로 시스템 프로그래밍 분야에 속하며 꽤 강력한 고급 테크닉으로 간주된다.

(1) 메시지 훅
Windows에는 SetWindowsHookEx라는 엄청난 함수가 있어서 시스템과 응용 프로그램 사이에서 오가는 메시지들을 매우 수월하게 들여다볼 수 있다. 그러니 Spy++ 같은 프로그램을 만들 수 있다.
권한 문제만 없다면 심지어 다른 프로그램의 메시지를 들여다볼 수도 있다. 이 경우, 훅 프로시저가 내 프로세스가 아니라 그 메시지를 받은 프로세스의 문맥에서 실행된다는 점을 주의할 것. 32비트와 64비트별로 DLL을 따로 만들고, 프로세스 간의 통신 같은 잡다한 수고만 좀 해 주면 된다.

(2) API 훅
다른 프로그램이 그냥 기계어 수준에서 운영체제의 특정 함수를 호출하는 것을 감지하고, 그 함수 대신 내가 심은 함수가 호출되게 할 수 있다. C 언어 형태의 클래식 API가 제일 쉽고, COM도 결국은 CoCreateInstance 같은 함수를 훅킹하면 이론적으로 가능하다. 실행되는 기계어 코드를 변조하는 게 아니라 import 섹션 주소를 변조하는 고전적인 테크닉이 있다.

16비트 시절에는 API 훅을 시스템 전체에다 걸어서 운영체제 외형을 통째로 마개조 할 수도 있었지만 32비트 이후부터는 그 정도까지는 어렵다. 다만, 시스템 전체에다가 설치한 메시지 훅과 CreateRemoteThread 등 다른 어려운 테크닉들과 연계하면 API 훅도 어느 정도 global하게 설치하는 게 가능은 하다.
과거에 한컴사전이 GDI 그래픽 API에다가 훅을 걸어서 단어 자동 인식 기능을 제공했던 적이 있다. 마우스 포인터 주변의 화면 캡처 + 필기 인식이 아니다~!

(3) 패킷 훅
심지어 시스템 전체에서 오고 가는 네트워크 패킷을 모니터링 할 수도 있다. 이게 기술적으로 가능하니까 packet sniffer이라고 불리는 유틸리티들도 존재 가능할 것이다. 이에 대해서는 본인도 더 아는 게 없다.
macOS는 Windows와 달리 메시지 훅이고 API 훅 같은 건 존재하지 않는 것으로 본인은 알고 있다. 하지만 macOS라도 패킷 모니터링은 아마 가능할 것이다.

packet sniffer이라든가 심지어 VPN 툴 같은 건 어떤 API를 써서 어떻게 만드나 모르겠다. 신기한 물건이다.

Posted by 사무엘

2021/02/01 08:34 2021/02/01 08:34
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1849


블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2021/02   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28            

Site Stats

Total hits:
2666162
Today:
1400
Yesterday:
1937