NSIS 패커 악용, 샌드박스 우회 기법은…
안랩시큐리티대응센터(AhnLab Security Emergency response Center, 이하 ASEC)는 블로그를 통해 ‘NSIS 설치 파일 형태의 악성코드에서 사용 중인 탐지 우회 기법’을 소개한 바 있다. 해당 블로그에서는 악성코드의 과거와 현재의 차이를 다루었으며, 가장 큰 특징이라고 할 수 있는 탐지 우회 기법을 중점으로 소개하였다. 이번에 다루고자 하는 내용은 ‘NSIS 패커가 실제 내부 악성코드를 어떠한 과정을 통해 실행하는지’이다. 이전 ASEC 블로그에서 소개하지 못한 복호화 과정과 여기에 사용되는 알고리즘을 포함하여 NSIS 패커의 전체적인 특징을 상세하게 소개한다.

악성코드 제작자는 안티바이러스 제품으로부터 진단되지 않기 위해 다양한 시도를 해왔다. 진단을 방해하는 방식으로는 파일 외형에 대한 진단을 방해하는 방식 외에도, 파일의 행위에 대한 진단을 방해하는 방식이 존재한다.
이 글에서 소개하는 널소프트 스크립터블 인스톨 시스템(Nullsoft Scriptable Install System, 이하 NSIS)은 일반적으로 다른 응용 프로그램을 설치하는데 사용되는 인스톨 프로그램이다. 어떠한 프로그램이 개발되었을 때 만들어지는 다수의 실행 파일 및 데이터 파일을 묶어서 하나의 실행 파일 형태로 만들어 줌으로써, 프로그램의 배포 및 설치를 유용하게 해주는 도구이다. 이렇게 만들어진 NSIS 인스톨러 프로그램의 경우, 외형 이 다른 정상적인 NSIS 인스톨러 프로그램들과 굉장히 유사할 수밖에 없다. 여기에서 다루는 악성코드는 안티바이러스 제품으로부터 파일 외형에 대한 진단을 회피하기 위한 목적으로 NSIS에 의해 패킹되어 있다.
참고로 이렇게 만들어진 NSIS 실행 파일 내부에는 다수의 실행 파일뿐만 아니라 해당 파일들이 어느 위치에 설치되어야 하는지와 같은 설정, 그리고 NSIS에서
제공하는 다양한 기능들이 포함될 수 있다. 이러한 설정 및 기능들은 내부에 포함된 *.nsi 스크립트에 저장되어 있다. 문제는 NSIS로 패킹될 경우 외형상의 진단 방해뿐만 아니라, 내부에 존재하는 이 *.nsi 스크립트의 기능들이 악용될 수 있다는 점이다. 과거에는 악성코드 분석가의 분석을 방해하기 위해 안티 디버깅 및 안티 VM 기법의 비중이 높았다면, 최근에는 자동으로 악성코드를 분석하여 결과를 제공해 주는 샌드박스(SandBox) 환경을 우회하기 위한 시간 지연 방법
등이 자주 사용된다. 여기에서는 이 스크립트의 기능을 악용한 시간 지연 기법이 사용되며, 실행된 이후에도 유저모드 후킹 우회 기법과 같은 추가적인 안티 샌드박스 기법들이 사용된다.
1. NSIS(Nullsoft Scriptable Install System) 구성
7zip(v15.05 이하)을 이용해 NSIS 파일에 대한 압축을 해제할 경우, 내부 파일뿐만 아니라 [그림 1]과 같이 .nsi 스크립트를 확인할 수 있다.
1-1. 내부 파일
악성 행위와 연관된 샘플은 [그림 1]에서 빨간색 테두리로 표시한 3개의 파일이며, 해당 파일에 대한 자세한 내용은 아래 본문에서 설명하겠다. 추출한 결과로
보았을 때 과거와 현재의 차이점은 정상 파일로 위장하기 위해 무의미한 파일을 다수 포함시켰다는 점과 ‘system.dll’이 존재하지 않는다는 점이다.
1-2. NSIS 스크립트
스크립트 코드는 크게 시간 지연 코드와 실행 방법 2가지로 나뉜다.
1) 시간 지연 코드
현재 샘플은 과거와 동일하게 시간 지연 코드를 가지고 있다. [그림 2]의 하단 샘플로 예시를 들자면 디렉터리나 레지스트리 키에 접근하는 무의미한 행위를
$R7이 0이 될 때까지 32,998번을 반복 후 악성코드에 필요한 파일을 생성한다.
2) 실행 방법
결과부터 말하자면, 로더 역할을 담당하던 ‘System.dll’이 ‘varicella.exe’로 변경되었다. 과거에는 CallInstDLL 명령을 이용하여 system.dll의 call 함수로 악성 DLL의 Export 함수를 실행했던 반면, 최근에는 ExecWait 명령을 이용해 직접적으로 로더(varicella.exe)를 실행한다. 이 ‘varicella.exe’는 악성 DLL(ChiliadWoodshed.dll)의 엑스포트(Export) 함수를 실행하는 기능이 전부이다. 참고로 ‘varicella.exe’의 파일명은 악성 샘플마다 다양한 이름으로 유포되고 있다.
2. 바이너리의 특징
지금부터 앞서 언급한 NSIS에서 추출된 내부 파일 중 악성 행위와 연관된 3가지 파일에 대해 본격적으로 살펴보자.
‘varicella.exe’를 통해 ChiliadWoodshed.dll의 엑스포트(Export) 함수가 실행되면, 이 악성 DLL은 먼저 .rdata 섹션의 쉘코드를 복호화한다([그림 5] 참고). 이 쉘코드의 기능은 또 다른 바이너리 파일인 ‘Poundal’의 앞 부분을 디코딩하여 실행하는 것이다.
2-1. 바이너리의 구성
DLL이 읽어오는 바이너리 파일(파일명: Poundal)은 [그림 6]과 같이 크게 코드와 데이터로 구성된다.
해당 바이너리 파일은 실제 악성코드가 실행되기까지 과정이 복잡하여, 바이너리 요소의 구성과 특징을 먼저 다루고 이 뒤에 모든 실행 과정을 설명한다.
[그림 6]의 1번은 쉘코드 및 DLL 파일로 인코딩된 데이터가 풀리기 전에 먼저 실행된다. 쉘코드는 내부 DLL 파일을 실행하는 역할을 한다. DLL 파일은 시간
지연 기능 후에 바이너리 파일을 다시 읽어와 [그림 6]의 2번의 데이터를 디코딩한다.
[그림 6]의 2번에는 인코딩된 데이터와 압축된 데이터 그리고 키 테이블 등으로 구성된다. 인코딩된 데이터는 쉘코드와 인젝션 행위에 필요한 API와 문자열 그리고 압축된 PE(실행파일)를 한 번 더 인코딩한 데이터가 들어 있다. 디코딩 과정은 MOD 연산 및 XOR 연산을 통해 이뤄지며, 이 디코딩 과정에 사용될 키
테이블도 존재한다. 별개로 묶어진 3개의 쉘코드는 LZNT1 압축 알고리즘을 사용하여 압축되었다. 이 쉘코드는 최종 인젝션 단계에서 사용된다. 마지막으로
실제 악성 기능을 수행하는 PE 파일이 LZMA 압축 형태로 존재한다.
이처럼 NSIS 패커의 가장 큰 특징은 여러 방법으로 데이터를 디코딩한 것이다.여기서 사용된 디코딩 방법을 자세히 살펴보자.
2-2. 바이너리의 인코딩 특징
인코딩 혹은 압축된 데이터의 특징은 다음과 같다.
1) 0x186C 크기를 갖는 키 테이블
[그림 6]의 2번 0x186C 키 테이블은 인코딩된 것으로, 가장 먼저 복호화가 이루어져야 다른 데이터를 풀어낼 수 있다. 이 인코딩된 키 테이블은 [그림 6]의 1번 DLL의 내부 코드에 의해 복호화가 이루어진다. 1번 DLL에는 특정 문자열(‘Poundal’)이 존재하며 이 문자열을 가지고 임시 키를 생성한다. 특정 문자열은 [그림 7]과 같이 코드 내부에 문자열 그대로 저장되어 있다.
[그림 7]과 같이 해당 샘플에서는 ‘Poundal’ 문자열이 코드 내부에 존재하며, 이는 바이너리 파일명과 동일하다. 먼저 해당 문자열과 문자열의 길이를 이용해
MOD 연산을 진행한다. 이후 비트 연산자인 Left Shift 3(* 8과 동일)을 통해 1차로 값을 가공한 후 해당 값과 반복 횟수에 따라 증가하는 숫자를 XOR하여 [그림 8]과 같이 0x11f 길이의 키를 만들어 낸다. 참고로 악성코드마다 문자열 및 바이너리 파일명은 다르다.
해당 키는 2번의 인코딩된 키를 복호화하는데 사용되며, 이후에는 사용되지 않는다.
이제부터 디코딩 시 사용되는 ‘키 테이블’이란 위의 복호화 과정을 통해 생성된 0x186C의 길이를 가지고 있는 키를 의미한다.
2) 산술 연산을 이용한 디코딩 알고리즘
해당 패커가 사용하는 대다수의 디코딩 방법은 산술 연산으로 MOD 연산과 XOR 연산을 사용한다.
하나의 예시로서 [그림 9]는 API 이름을 복호화하는 루틴이다. 이 쉘코드에서는 IAT(Import Address Table)에 존재하지 않는 API를 사용하기 위해 GetModuleHandle() 및 LoadLibray() API를 통해 API의 주소를 획득하는데, 다음 코드는 여기에서 사용되는 API 이름을 복호화하는 루틴이다.
[그림 9] 코드는 0x186C값으로 증감 식에 사용되는 변수를 MOD 연산을 수행한다. 이후 키 테이블의 인덱스에 결과 값을 넣고 해당 요소를 인코딩할 데이터
와 XOR을 연산하여 복호화한다. 결과적으로는 [그림 10]과 같이 API 문자열을 확인할 수 있다.
3) LZNT1 알고리즘으로 압축한 데이터
해당 패커는 LZNT1 알고리즘을 사용하여 쉘코드를 복호화한다. RtlDecompressBuffer라는 API를 사용해 데이터를 풀어내기 때문에 이는 비교적 간단하다.
RtlDecompressBuffer API의 첫 번째 인자에 따라 알고리즘을 선택할 수 있다. 첫 번째 인자는 ‘2’로 MSDN의 아래 [표 1]에 따르면 LZNT1 알고리즘인 것
을 확인할 수 있다.
4) LZMA 형태의 압축된 악성 PE 파일
모든 디코딩 과정을 거치면 바이너리 파일 내부에 LZMA 압축 알고리즘으로 압축된 악성 PE가 존재하며, 해당 악성 PE가 실질적으로 악성 행위를 하는 본체이다. 압축된 PE의 형태를 살펴보면 [그림 12]의 빨간색 테두리는 LZMA 헤더로 5Byte를 차지한다. 첫 번째 byte는 0x5D이며, 나머지 4byte는 아래 [표 2]와 같이 세 번째와 네 번째 값에 따라 압축 레벨 확인할 수 있다.
해당 패커는 LZMA 형태인 악성 PE를 압축 해제하기 위해 [그림 13]의 우측 코드가 존재한다. 흔히 알려진 윈도우 압축 소프트웨어 7Zip(7z.exe)의 LMZA 해제 코드를 살펴보면 [그림 13]의 좌측 코드 루틴을 확인할 수 있다. 이는 두 개의 코드를 비교했을 때 유사성이 매우 높은 것을 확인할 수 있다.
패커에서 압축 해제 코드를 따라가 보면 [그림 12]의 빨간색 테두리부터 읽어내어 압축 레벨을 확인하는 것을 볼 수 있다. 이후에는 미리 저장 공간을 만들어
두고 압축 해제된 PE를 저장한다. 저장 공간의 크기는 압축된 파일 4Byte 앞 위치(주황색 선)에 존재한다. 즉, 해당 패커로 실질적인 악성코드를 패킹할 때
악성코드의 크기를 확인하여 앞에 4Byte를 붙이고, 바로 뒤에 PE 파일을 저장하는 것이다.
압축 해제가 완료되면 아래 [그림 14]와 같이 실질적인 악성 PE 형태를 확인할 수 있다.
3. 바이너리의 디코딩 과정과 내부 사용된 기법
3-1. 바이너리 디코딩 과정
NSIS 악성코드 실행 시 다수의 파일들이 드롭(Drop)되며, 드롭되어 실행된 악성코드는 정상 프로그램(cmd.exe)에 악성 코드를 인젝션(Injection)하여 악성 행위를 수행한다.
자세한 설명은 아래와 같으며, 굵은 글씨로 표현한 것은 [그림 15]의 상단의 4가지 항목(Drop, Load, Shellcode, Injection)을 뜻한다.
1) NSIS 악성코드를 실행 시, NSIS 스크립트에 의해 시간 지연을 발생시킨다. 이후 악성 행위에 필요한 중요 파일(Varicella.exe, ChiliadWoodshed.dll, Poundal)과 나머지 사용하지 않는 더미 파일을 Drop한다.
Varicella.exe는 ChiliadWoodshed.dll의 특정 API를 임포트(Import)한다. 즉, Varicella.exe는 악성 dll을 메모리에 로드하여 실행하는 로더이다.
2) Load된 ChiliadWoodshed.dll은 .rdata 섹션의 쉘코드를 실행하여, Poundal 바이너리 일부를 리드(Read) 및 디코딩(Decoding)을 수행한다. 따라서, ChiliadWoodshed.dll 또한 Poundal을 로드하는 기능이 중점이다.
3) 이 디코딩된 데이터는 ShellCode로 Poundal의 내부 DLL을 디코딩 및 실행(Execute)하기 위해 사용된다. 여기서 디코딩된 내부 DLL은 [그림 15]
ShellCode의 ‘악성 DLL’이다.
4) Poundal 바이너리 내부의 ‘악성 DLL’에는 2개의 코드가 존재한다. 첫 번째는 시간 지연 코드로서 WriteProfileStringA API를 이용해 시간 지연을 수행한다. 해당 코드는 3-2의 ‘[그림 16] DLL 내부의 시간 지연 코드’에서 확인할 수 있다.
5) ‘악성 DLL’의 두 번째 코드는 Poundal의 특정 오프셋(Offset)을 디코딩하는 코드로서, 특정 알고리즘(압축 포함)을 사용해 특정 크기만큼의 바이너리를 디코딩한다. 이 과정으로 다수의 ShellCode 및 문자열과 API 등이 디코딩되며, 이후 디코딩했던 ShellCode가 실행된다.
6) LZMA 형태로 압축된 악성 PE를 압축 해제한 후 정상 프로세스(cmd.exe 혹은 msbuild.exe)를 실행하여, Injection한다. Injection 방법은 3-2의 ‘[표 3] 인젝션 기법 순서’에서 자세히 설명한다.
7) 5번 과정에서 디코딩했던 ShellCode를 정상 프로세스로 위장한 Malware.exe에 Injection을 한다. 해당 ShellCode의 주요 행위는 ntdll의 후킹(Hooking) 여부 검사이며, 후킹되어 있다고 판단될 경우 후킹을 제거하는 기능까지 포함되어 있다.
3-2. 바이너리 내부 기법
1) 시간 지연 코드
바이너리 파일 Poudal에서 디코딩된 DLL은 시간 지연 코드를 가지고 있다. 해당 코드는 WriteProfileStringA API를 이용해 %WINDIR%win.ini 파일에 10,000번 접근하는 코드이다. 참고로 프로세스가 관리자 권한 이상인 경우에만 WriteProfileStringA API를 이용해 win.ini에 접근할 수 있어서 일반적인 사용자 PC에서의 시간 지연은 미비하다.
그러나 샌드박스 환경에서는 사용자 PC와 다르게 시간이 더 지연될 수 있다. 샌드박스 환경에서는 분석 대상 샘플을 관리자 권한으로 실행시키는 경우가 많으며 이 경우 win.ini에 대한 접근이 가능하게 된다. 또한 프로세스 행위에 대한 로깅으로 인해 일반적인 환경보다 더 많은 시간 지연이 발생할 수 있기 때문이다.
2) 유저 모드 후킹 우회 기법
해당 기법은 유저 모드 후킹을 우회하는 방법이다. 구체적으로는 악성코드가 ntdll을 프로세스 메모리 상에 다시 로드한 후 기존에 로드된 ntdll이 아닌 새로 로드된 ntdll의 API를 사용하는 방식이다. 즉, 모니터링 모듈이 기존 ntdll을 후킹 중이라고 하더라도 후킹되어 있는 기존 ntdll의 API를 호출하지 않아, 유저 모드 후킹을 사용하는 샌드박스에서는 행위가 기록될 수 없다.
위의 기법을 사용하기 위해 사용된 API 리스트 및 주요 인자 값들이다.
● kernel32.CreateFileW() - ntdll.dll 핸들 획득
● kernel32.CreateFileMappingW()
● kernel32.MapViewOfFile()
참고로 이 기법에 대한 자세한 설명과 다른 기법은 ASEC Report Vol.97에서 소개되어 있다.
https://www.ahnlab.com/kr/site/securityinfo/asec/asecReportView.do
3) 공유 메모리 인젝션 기법
실제 악성코드는 정상 프로세스에 인젝션하여 동작한다. 쉘코드는 악성코드가 .NET으로 컴파일 된 경우라면 MSBuild.exe를 실행하며, .NET 이외의 다른 컴파
일 언어로 만들어진 악성코드는 CMD.exe를 실행해 인젝션한다. 아래는 인젝션 기법을 대략적으로 설명하기 위한 의사 코드(Pseudo Code), 즉 실행되지 않고 로직만 표현한 소스코드다.
4) UAC 바이패스(bypass) 기법
바이너리(‘Poundal’)의 쉘코드 내부에는 CMSTPLUA
COM 인터페이스를 이용하여 UAC를 우회하는 코드가 [그림 19]와 같이 존재한다.
UAC 우회 기법은 UAC 알림 창을 띄우지 않으면서 프로세스를 관리자 권한으로 실행시키는 기법이다. 관리자 권한으로 실행될 경우 더 많은 악성 행위를 수행
할 수 있지만, 동시에 UAC 알림 창이 떠서 사용자가 악성코드의 실행을 인지할 수 있기 때문에 사용되는 방식이다.
여기에서 사용되는 UAC 우회 기법에서는 CMSTPLUACOM 컴포넌트의 ICMLuaUtil 인터페이스를 이용한다. 참고로 COM 컴포넌트를 식별하기 위해서 사용되는 CLSID와 인터페이스를 식별하기 위해 사용되는 IID는 아래와 같다.
● CLSID: {3E5FC7F9-9A51-4367-9063-A120244FBEC7}
● IID: {6EDD6D74-C007-4E75-B76A-E5740995E24C}
구체적으로 [그림 20]과 같이 위의 인자를 이용해 CoGetObject API를 호출하여 해당 객체를 얻어오며, cmlua.dll 및 cmutil.dll을 메모리에 로드한다. 이후 쉘엑서큐트(ShellExecute)와 유사한 문서화되지 않은 메소드(Method)를 통해 특정 프로세스를 실행시킨다. 실행될 프로세스 경로명은 [그림 20]의 9번째 줄인 ‘arg[2]’ 인자에 들어간다.
참고로 해당 코드는 존재함에도 불구하고 대부분의 샘플에서 실행되지 않는 것으로 보아, 이 패커의 경우 패킹 과정에서 옵션을 설정할 수 있을 것으로 추정된다.
4. 프로세스 트리
정리하자면 NSIS 악성 샘플을 실행할 경우 드롭된 파일들 중 DLL 로더(varicella.exe)가 악성 DLL을 로드하며, 이 DLL이 Poundal 바이너리를 읽어와 복호화하여 메모리 상에서 실행한다. 복호화된 해당 바이너리(‘Poundal’)는 정상 프로세스(cmd.exe)를 실행시키고, 여기에 악성코드를 인젝션(Injection)하여 동작하게 된다.
5. 결론
악성코드 유포자는 안티바이러스 제품으로부터 진단되지 않도록 다양한 패커를 사용한다. 이번에 소개된 NSIS 설치 파일 형태의 패커는 외형 진단을 우회하려는 목적뿐만 아니라, 안티 샌드박스 및 시간 지연 코드 그리고 유저 모드 후킹 우회 기법 등 탐지 회피를 목적으로 다양한 기법을 사용했다. 앞으로도 탐지 우회를 시도하는 새로운 패커가 유포될 것으로 보이며, 사용자는 이러한 공격의 피해를 최소화하기 위해 출처가 불분명한 파일은 실행하지 말아야 한다. 또 V3와 같은 백신 프로그램의 엔진 버전을 최신 상태로 유지하고 실시간 감시 기능을 활성화해야 감염을 사전 방지할 수 있다.
- AhnLabASEC 분석팀 김준석 연구원