파일탐색기를 후킹해서 폴더를 숨겨보자

Osori·2021년 8월 13일
2


우리들은 누구나 남에게 보여주기 싫은 폴더가 있을 것이다.
그리고 상용 프로그램들도 많이 존재한다. 원리는 잘 모르겠지만.
누군가 컴퓨터의 파일에 접근할 일이 생기면 파일탐색기로 파일탐색을 하기 때문에 파일탐색기를 후킹해서
내가 숨기고 싶은 폴더를 숨긴다면( 숨김폴더 기능X) 꽤나 괜찮을 것 이라고 생각을 했다.
그리고 이 글은 완성을 한 뒤에 정리를 해서 올리는 글이고, 완성까지는 약 일주일정도 걸렸다.

자세한 글을 보고 싶으면 노션으로 https://www.notion.so/1fb78ce3493e45d2a71cae0ed1bd9f30#

윈도우에서 파일을 어떻게 관리할까?

후킹을 하기 위해서는 먼저 어떤 함수 또는 메소드들이 이용되는지 알아야 한다.
여기서 이 글을 보고 있는 사람들 중 WinAPI를 알고있는 사람들 중 몇몇은 이렇게 생각할 수도 있다.
"아니 그냥 msdn 들어가서 검색하면 되는거 아님?"
물론 나도 그랬고 FindFirstFileW 같은 함수들을 발견할 수 있었다. 하지만 윈도우가 근본적으로 파일을 관리하는 방법은 Absolute Path가 아니다!
물론 이를 알기까지는 상당히 고통스러운 시간이였다

윈도우 쉘

자 한번 생각해보자. 운영체제에서 쉘(Shell)이란 무엇인가? 커널과 유저를 이어주는 매개체라고 볼 수 있다. 윈도우에서는 Windows 탐색기(EXPLORER.exe) 가 GUI형태로 제공된다. 이 파일탐색기에는 작업표시줄, 바탕화면, 파일탐색기 기능이 포함되어있다.

생각해보면 당연하다. 윈도우10의 대부분은 GUI로 구현되어있는데 위에서 말한대로 바탕화면,작업표시줄, 제어판 등등.. 이 모두가 탐색기의 기능이라고 생각하면 우리는 프로그램을 실행할 때 파일탐색기를 통해서 파일을 실행하고, 실제로 무언가 프로세스를 실행하게 되면 파일탐색기에서 이벤트를 받을 수 있다. 쉘에서 단순하게 절대 경로로 파일을 관리할까? 더 효율적으로 관리를 하기 위해서 PIDL 이라는 ID를 이용해서 폴더 및 파일들(이제 객체라 지칭 하겠다)을 관리한다.

파일탐색기는 GUI 쉘이라고 하였는데 마이크로 소프트는 개발자들도 쉘 기능을 확장해서 쓸 수 있도록 API들을 제공한다. 가장 대표적인 것이 원드라이브 같은 가상폴더이다. 파일이 컴퓨터에 없지만 목록에 표시되는 것 같은 기능을 Shell Extension 을 이용해 만들 수 있다. 그리고 위에서 말했던 절대경로로 파일을 관리할 수 없는 이유가 이 가상객체들 때문이다.

PIDL을 이용한 파일 탐색

typedef struct _SHITEMID { 
    USHORT cb; 
    BYTE   abID[1]; 
} SHITEMID, * LPSHITEMID;

위 구조체는 _SHITMID 구조체다. cb 는 자기 자신을 포함한 구조체 전체의 크기를 가지고 있고 소스코드와 다르게 abID 는 가변적인 크기를 가진다.

typedef struct _ITEMIDLIST {
  SHITEMID mkid;
} ITEMIDLIST;

그리고 SHITEMID 구조체 여러개가 뭉쳐서 PIDL을 이루게 된다. 위 구조체 또한 가변길이이다.
결과적으로는 2byte NULL로 끝을 구분하며 아래같은 구조를 가지게 된다.

그러면 이 PIDLs 을 순회하는 방법도 있을 것이다. msdn에서는 친절하지는 않지만 어쨌든 방법들을 대강 설명해놓았다.

  1. SHGetDesktopFolder 함수를 이용해서 데스크탑의 IShellFolder 인터페이스를 획득
  2. 폴더의 경로를 이용해서 PIDL 을 획득. 이는 ILCreateFromPath 로 가능하다.
  3. 2에서 얻은 PIDL 에 1번에서 얻은 IShellFolder 를 바인딩. IShellFolder::BindToObject 이용
  4. IShellFolder::EnumObjects 를 이용해서 IEnumIDList 인터페이스 획득
  5. IEnumIDList::Next 를 이용해서 child의 PIDL 획득

단 여기서 획득한 자식 객체의 PIDL은 상대적이기 때문에 이를 절대경로로 바로 변환할 수는 없다.
ILCombine 등의 함수를 이용해서 절대경로로 바꾸어줄 필요가 있다.

이를 코드로 구현하면 아래와 같다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>
#include <shlobj_core.h>
#include <string.h>
#include <locale>
int main()
{
	_wsetlocale(LC_ALL, L"korean"); //한글 출력 용도
	PIDLIST_ABSOLUTE fuzzerPIDL = ILCreateFromPath(L"C:\\Users\\kunsh\\Desktop\\fuzzer");//폴더의 경로로 pidl을 획득 
	IShellFolder* ShellFolder; 
	IEnumIDList* list; // enumlator 
	PITEMID_CHILD child; //폴더 순회하면서 받아올 자식 pidl
	LPWSTR path = (LPWSTR)new wchar_t[0x500]; //폴더 경로 출력할 버퍼 
	if (path == NULL)
		return 0;
	if (SHGetDesktopFolder(&ShellFolder) == S_OK) // 셸 네임스페이스의 루트인 데스크탑 인터페이스를 먼저 획득 
	{
		if (ShellFolder->BindToObject(fuzzerPIDL, NULL, IID_IShellFolder, (void**)&ShellFolder) == S_OK) //획득한 인터페이스로 내가 원하는 경로에 바인딩
		{
			if (ShellFolder->EnumObjects(NULL, SHCONTF_NONFOLDERS | SHCONTF_FOLDERS, &list) == S_OK) //폴더와 파일을 찾겠다
			{
				while (list->Next(1, &child, NULL) == S_OK) //계속 순회 
				{
					SHGetPathFromIDListW(child, path); //pidl->path 변환
					printf("%ws\n", path); //유니코드 형식으로 출력
					CoTaskMemFree(child); //받아온 pidl은 다시 반환
				}
			}
			//메모리 정리
			ShellFolder->Release();
			list->Release();
			delete[] path;
			ILFree(fuzzerPIDL);
		}
		else
		{
			puts("Binding fail");
		}	
	}
	else
	{
		puts("GetDesktopFolder fail");
	}
	
}


그리고 결과는 위에서 말한대로 상대경로로 출력된다. 경로가 바탕화면 바로 밑에 있는 것을 알 수 있다. 사실 개발자 입장에서는 Binding을 할때 경로를 알고 있기 때문에 문제가 되지 않지만 우리입장에서는 문제가 된다. 이에 대해서는 뒤에서 자세히 다루겠다.

후킹 시나리오

사용혹은 후킹해줄 함수 목록들은 아래와 같다. 후킹은 frida를 이용하였다.

SHGetPathFromIDListW : pidl을 파일이름으로 바꾸는 용도로 사용.
CoTaskMemFree : pidl을 해제해주어야 한다.
IShellFolder::EnumObjects : 아래에서 설명
IShellFolder::BindToObject : 폴더의 전체경로를 알아오는 용도이다. 우리가 파일탐색기에서 폴더에 진입했다고 그 폴더에만 바인딩되는 것이 아니라 다양한 정보를 얻기 위해서 여러곳에 바인딩하기 때문에 이를 EnumObjects 가 호출될 때까지 모두 저장해서 가지고 있다가 EnumObjects 호출시 IShellFolder 객체가 동일한지 판단해서 루트디렉토리를 추출한다.
IEnumIDList::Next : 실제로 pidl을 가지고 오고, 이를 후킹해서 반환값을 변조해주어야 한다.
LoadLibraryA : 파일탐색기를 종료하고 창을 다시열면 후킹이 잘 안되는데 다시 열때 이 API가 호출되기 때문에 후킹해서 호출시에는 frida를 재실행해주었다.

숨김설정한 폴더의 루트폴더에 접근시 frida에서 미리 pidl들을 모두 다 가지고 와서 다음에 IShellFolder::Next 호출시 비밀 폴더의 pidl을 제외하고 반환해주도록 프로그래밍 하였다.

결과

코드

console.log('[+] Hook Start');
const storage=Module.findBaseAddress("windows.storage.dll");
const FirstFile=Module.findExportByName("Kernel32.dll","FindFirstFileW");
const ILCreateFromPath=Module.findExportByName("Shell32.dll","ILCreateFromPathW");
const CreateProcessW=Module.findExportByName("Kernel32","LoadLibraryW");
const SHGetPathFromIDListW=Module.findExportByName("Shell32.dll","SHGetPathFromIDListW");
const CoTaskMemFree=Module.findExportByName("Ole32.dll","CoTaskMemFree");
const NativeCoTaskMemFree=new NativeFunction(CoTaskMemFree,"int",["pointer"]);
const EnumObjects=storage.add(0x142dd0);
const BindToObjects=storage.add(0xeb400);
const next=storage.add(0x1432c0);
const NextNative=new NativeFunction(next,"int",["pointer","int","pointer","uint64"]);
const ILCreateFromPathNative=new NativeFunction(ILCreateFromPath,"pointer",["pointer"]);
const SHGetPathFromIDListWNatvie=new NativeFunction(SHGetPathFromIDListW,"int",["pointer","pointer"]);
var path=Memory.alloc(0x10000); 
var is_first=true;
var child;
var root_path;
var IShellFolder;
var IDEnumList;
var PIDLs=[];
var lock=false;
var is_hooked=false;
Interceptor.attach(CreateProcessW,{
	onEnter: function(arg){
		send({'restart':'aaaaa'});
	}
});
Interceptor.attach(FirstFile,{
	onEnter: function(arg){
	}
})
Interceptor.attach(next,{
	onEnter: function(arg){
		child=arg[2];
		IDEnumList=arg[0];
	},
	onLeave: function(ret){
		if(hidden_file!='' && !lock && is_first){
			var pchild=Memory.readPointer(child);
			SHGetPathFromIDListWNatvie(pchild,path);
			var name=Memory.readUtf16String(path);
			name=name.substr(23,name.length-23);
			//첫번째 파일이 타겟이 아닌경우 동적 메모리 할당해서 리스트에 넣어줌 
			if (name!=hidden_file){
				PIDLs[PIDLs.length++]=pchild;
				console.log("[+] Add first pidl to array");
			}
			//이제 해당 폴더에 있는 나머지 폴더들을 모두 읽어서 PIDL을 저장
			lock=true;
			while(true){
				var pptr=Memory.alloc(8);
				var hr=NextNative(IDEnumList,1,pptr,0);
				if(hr!=0){
					break;
				}
				var ptr=Memory.readPointer(pptr);
				SHGetPathFromIDListWNatvie(ptr,path);
				var name=Memory.readUtf16String(path);
				name=name.substr(23,name.length-23);
				if (name!=hidden_file){
					PIDLs[PIDLs.length++]=ptr;
				}else{
					NativeCoTaskMemFree(ptr);
					console.log("[-] Skip file",name);
				}
			}
			is_first=false;
			lock=false;
			is_hooked=true;
		}	
		//console.log('[+] PIDLs length',PIDLs.length);
		//이제 리스트에서 하나씩 꺼내서 준다. 
		if(is_hooked){
			if(PIDLs.length!=0){
				var temp_mem=PIDLs.shift(); //넣은 순서대로 꺼낸다.
				child.writePointer(temp_mem);
				ret.replace(0);
			}
			else{
				is_hooked=false;
				ret.replace(1); //리스트의 끝이면 탐색 종료 
			}
		}
	}
});

var arg4;
var check=0;
var folders=[];
var hidden_file='';
Interceptor.attach(BindToObjects,{
	onEnter: function(arg){
		var path2=Memory.alloc(520);
		SHGetPathFromIDListWNatvie(arg[1],path2);
		folders[folders.length++]=[arg[4],path2];
	},
});


Interceptor.attach(EnumObjects,{
	onEnter: function(arg){
		hidden_file='';
		is_first=true;
		for(var i=0;i<folders.length;i++){
			var inptr=Memory.readPointer(folders[i][0]);
			if(inptr.toString().includes(arg[0].toString())){
				IShellFolder=arg[0];
				root_path=Memory.readUtf16String(folders[i][1]);
				if(root_path.includes("undefined")){
					send({'restart':'aaaaa'});
				}
				send({'root_path':root_path});
				//타겟폴더의 부모폴더를 탐색하려고 하는 경우 플래그 설정 
				recv(function(arg){
					if(arg.hasOwnProperty('hidden')){
						hidden_file=arg.hidden;
					}
				}).wait();

				break;
			}
		}
		folders=[];
	}
});
import frida
import sqlite3
import os
import time
hidden_path="C:\\Users\\kunsh\\Desktop\\개인 프로젝트\\파일탐색기 후킹"
root=os.path.dirname(hidden_path)
filename=os.path.basename(hidden_path)
flag=True
timepass=0
def on_message(message, data) :
	global flag
	if 'payload' in message and 'restart' in message['payload'] :
		print('restart')
		flag=False
	if 'payload' in message and 'root_path' in message['payload'] :
		if message['payload']['root_path'] == root :
			print("[+]Hidden root path")
			script.post({'hidden':filename})
		else:
			script.post({'nop':'nop'})


while True :
	flag=True
	session = frida.attach('EXPLORER.exe')
	script = session.create_script(open('explore.js','r',encoding='UTF-8').read())
	script.on("message",on_message)
	script.load()
	while flag==True :
		time.sleep(0.2)
		timepass+=0.2
		if timepass>=60 :
			timepass=0
			flag=False
	session.detach()

궁금한점이 있으면 댓글 환영합니다!!

profile
해킹, 리버싱, 게임 좋아합니다

0개의 댓글