[플밍노트] 윈도우에서 llvm-obfuscator 사용하기

Clang은 LLVM을 백엔드로 사용하는 C 계통의 언어를 컴파일해주는 컴파일러다. LLVM을 백엔드로 사용하기 때문에 컴파일 타임에 뭔가를 해줄 수 있다는 장점이 있다. 그런 것 중에 하나로 난독화를 들 수 있다. obfuscator-llvm은 LLVM에 난독화 기능을 추가한 버전이라고 보면 된다. 소개 페이지에 나와 있는 것처럼 LLVM IR레벨에서 작업하고 있기 때문에 LLVM을 백엔드로 사용하는 언어와 타겟 머신을 모두 지원한다는 장점이 있다. 제작자들이 첨에는 학교에서 논문으로 쓰다가 아이디어가 괜찮아서 스타트업을 차린 것으로 보여진다. 어쨌든 이걸 쓰면 컴파일 타임에 나의 코드에 난독화를 적용할 수 있는 것이다. 응당 사용하지 않을 이유가 전혀 없다. 그럼 한 번 살포시 시작해보자.

우선은 Clang을 윈도우에서 어떻게 쓸지부터 알아야 한다. 정말 많은 글들이 있지만 이 글에 나와있는 방법이 가장 좋았다. Clang에다 런타임라이브러리와 링커는 mingw-w64껄 쓰는 방법이다. 글에 나와 있는 방법대로 하면 대체로 모두 잘 된다. 내가 조금 골탕먹은 것은 mingw-w64를 선택할 때 스레딩 모델을 Win32로 선택했던 것이었다. Win32로 선택하면 C++11부터 추가된 스레드 기능을 사용하지 못한다. 그러니 POSIX로 선택하는 것이 바람직하다 하겠다. 예외의 경우 64는 SEH로 32는 Dwarf를 받아서 썼다. 그렇게 했더니 아래 코드가 깔끔하게 컴파일이 되었다. "clang++ -std=c++14 hello.cpp -lpthread"라는 명령어로 아래 코드가 깔끔하게 컴파일이 되었다.

#include <thread>
#include <chrono>
#include <stdio.h>

void PrintMsg(const char *msg, int try_cnt, int sec)
{
	for(int i = 0; i < try_cnt; ++i)
	{
		printf("[%03d] %s\n", i, msg);
		std::this_thread::sleep_for(std::chrono::seconds(sec));
	}
}

int main()
{
	std::thread thread1(&PrintMsg, "dog", 10, 1);
	std::thread thread2(&PrintMsg, "cat", 10, 2);
	std::thread thread3(&PrintMsg, "monkey", 10, 3);

	thread1.join();
	thread2.join();
	thread3.join();

	return 0;
}

이렇게 컴파일한 결과물을 사용하면 별 문제는 없는데 덕지덕지 온갖 이상한 DLL을 많이 임포트하는 경향이 있다. 그래서 용량이 큰 C++ 라이브러리를 제외하고 정적으로 컴파일하고 싶으면 "clang++ -std=c++14 hello.cpp -static -static-libgcc -lpthread -Wl,-Bdyanmic -lstdc++"와 같이 입력하면 된다. 여튼 뭐 컴파일은 그렇다. 그렇다면 이제 llvm-obfuscator를 써보자.

난독화 함수를 찾기 쉽도록 아래와 같이 익스포트 되도록 함수를 선언해주자. 뒤에 속성으로 따라 붙는 것이 fla, bcf, sub 난독화 기법을 이 함수에 적용하겠다는 것을 의미한다. 오픈소스 버전은 세 가지 기능만 제공하고 있다.

__declspec(dllexport)
void PrintMsg(const char *msg, int try_cnt, int sec)
__attribute((__annotate__(("fla"))))
__attribute((__annotate__(("bcf"))))
__attribute((__annotate__(("sub"))))
;
normal
Figure 1. 난독화가 적용되지 않은 PrintMsg 함수. 코드만큼 어셈도 단순하다.
obfuscated
Figure 2. 난독화가 적용된 PrintMsg 함수. 함수가 점프 코드로 분산되고 길고 어려워진다.

내가 사용한 버전은 llvm-3.6.1 브랜치 코드를 기준으로 작업했다. 사용해보면 윈도우에서 /dev/random을 찾을 수 없다는 버그가 있는데 lib/Transform/Obfuscation/CryptoUtils.cpp 파일의 prng_seed 함수를 아래와 같이 수정해주면 정상 동작하도록 만들 수 있다.

#ifdef _WIN32
#define _WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <Ntsecapi.h>
#endif

void CryptoUtils::prng_seed()
{

#ifdef _WIN32

	LLVMContext &ctx = llvm::getGlobalContext();

	HCRYPTPROV prov;

	if (CryptAcquireContext(&prov, 0, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT | CRYPT_SILENT))
	{
		if(!CryptGenRandom(prov, 16, (BYTE *) key))
			ctx.emitError(Twine("CryptGenRandom fail"));

		CryptReleaseContext(prov, 0);

		memset(ctr, 0, 16);

		// Once the seed is there, we compute the
		// AES128 key-schedule
		aes_compute_ks(ks, key);

		seeded = true;
	}
	else
	{
		ctx.emitError(Twine("CryptAcquireContext fail"));
	}

#else

	// ... 기존 코드 ...

#endif
}