우리들은 누구나 남에게 보여주기 싫은 폴더가 있을 것이다.
그리고 상용 프로그램들도 많이 존재한다. 원리는 잘 모르겠지만.
누군가 컴퓨터의 파일에 접근할 일이 생기면 파일탐색기로 파일탐색을 하기 때문에 파일탐색기를 후킹해서
내가 숨기고 싶은 폴더를 숨긴다면( 숨김폴더 기능X) 꽤나 괜찮을 것 이라고 생각을 했다.
그리고 이 글은 완성을 한 뒤에 정리를 해서 올리는 글이고, 완성까지는 약 일주일정도 걸렸다.
후킹을 하기 위해서는 먼저 어떤 함수 또는 메소드들이 이용되는지 알아야 한다.
여기서 이 글을 보고 있는 사람들 중 WinAPI를 알고있는 사람들 중 몇몇은 이렇게 생각할 수도 있다.
"아니 그냥 msdn 들어가서 검색하면 되는거 아님?"
물론 나도 그랬고 FindFirstFileW
같은 함수들을 발견할 수 있었다. 하지만 윈도우가 근본적으로 파일을 관리하는 방법은 Absolute Path가 아니다!
물론 이를 알기까지는 상당히 고통스러운 시간이였다
자 한번 생각해보자. 운영체제에서 쉘(Shell)이란 무엇인가? 커널과 유저를 이어주는 매개체라고 볼 수 있다. 윈도우에서는 Windows 탐색기(EXPLORER.exe) 가 GUI형태로 제공된다. 이 파일탐색기에는 작업표시줄, 바탕화면, 파일탐색기 기능이 포함되어있다.
생각해보면 당연하다. 윈도우10의 대부분은 GUI로 구현되어있는데 위에서 말한대로 바탕화면,작업표시줄, 제어판 등등.. 이 모두가 탐색기의 기능이라고 생각하면 우리는 프로그램을 실행할 때 파일탐색기를 통해서 파일을 실행하고, 실제로 무언가 프로세스를 실행하게 되면 파일탐색기에서 이벤트를 받을 수 있다. 쉘에서 단순하게 절대 경로로 파일을 관리할까? 더 효율적으로 관리를 하기 위해서 PIDL
이라는 ID를 이용해서 폴더 및 파일들(이제 객체라 지칭 하겠다)을 관리한다.
파일탐색기는 GUI 쉘이라고 하였는데 마이크로 소프트는 개발자들도 쉘 기능을 확장해서 쓸 수 있도록 API들을 제공한다. 가장 대표적인 것이 원드라이브 같은 가상폴더이다. 파일이 컴퓨터에 없지만 목록에 표시되는 것 같은 기능을 Shell Extension 을 이용해 만들 수 있다. 그리고 위에서 말했던 절대경로로 파일을 관리할 수 없는 이유가 이 가상객체들 때문이다.
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에서는 친절하지는 않지만 어쨌든 방법들을 대강 설명해놓았다.
SHGetDesktopFolder
함수를 이용해서 데스크탑의 IShellFolder
인터페이스를 획득PIDL
을 획득. 이는 ILCreateFromPath
로 가능하다.PIDL
에 1번에서 얻은 IShellFolder
를 바인딩. IShellFolder::BindToObject
이용IShellFolder::EnumObjects
를 이용해서 IEnumIDList
인터페이스 획득 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을 제외하고 반환해주도록 프로그래밍 하였다.