File이 Rename (MoveFile)될 때 커널에서 벌어지는 일들
정말 오래간만에 기술적인 포스팅을 하는 것 같네요. 사실 포스팅하고 싶은 것들은 쌓여 있는데... 시간 내기가 어렵네요.
오늘은 File Rename시 발생하는 파일 I/O와 내부적인 동작 방식들을 한번 살펴보고자 합니다.
분석 방법은 대충 이렇습니다.
1. 유저레벨에서 다음과 같은 간단한 코드를 실행
2. 간단한 미니필터를 붙여서 Pre/PostCreate, Pre/PostSetInformationFile 을 필터링하면서 MoveFileEx로 인한 I/O에 BP를 건 후 인자 등을 분석
테스트 결과에 따르면, File Rename은 대략 다음과 같은 세 단계로 이루어 지게 됩니다.
Step 1. ExistingFileName에 대한 IRP_MJ_CREATE
우선... 대상파일의 핸들을 얻어야겠죠? MoveFile은 다음과 같이 NtOpenFile을 호출하여 1.hwp 파일을 엽니다.
kd> k
ChildEBP RetAddr
ed87d9a0 f7419888 MyFsFt!MyFltPreCreate+0x23f
WARNING: Frame IP not in any known module. Following frames may be wrong.
ed87da60 804f0199 0xf7419888
ed87da70 8057a822 nt!IopfCallDriver+0x31
ed87db50 805b6f26 nt!IopParseDevice+0xa12
ed87dbd8 805b32cf nt!ObpLookupObjectName+0x56a
ed87dc2c 8056d415 nt!ObOpenObjectByName+0xeb
ed87dca8 8056dd8c nt!IopCreateFile+0x407
ed87dd04 805715b3 nt!IoCreateFile+0x8e
ed87dd44 8053f854 nt!NtOpenFile+0x27
ed87dd44 7c93e514 nt!KiSystemServicePostCall
0012fe68 7c93d5aa ntdll!KiFastSystemCallRet
0012fe6c 7c7ee876 ntdll!NtOpenFile+0xc
0012ff44 7c805712 kernel32!MoveFileWithProgressW+0x108
0012ff60 00401d41 kernel32!MoveFileExW+0x17
0012ff78 00402879 TestProject!main+0x21
0012ffc0 7c7e6037 TestProject!__tmainCRTStartup+0x10b
0012fff0 00000000 kernel32!BaseProcessStart+0x23
이때 인자들을 잠시 살펴보겠습니다.
일단 파일명 정보는 다음과 같이 들어 있구요...
struct _FLT_FILE_NAME_INFORMATION * 0xe2cacd6c
+0x000 Size : 0x40
+0x002 NamesParsed : 0xf
+0x004 Format : 1
+0x008 Name : _UNICODE_STRING "\Device\HarddiskVolume1\Temp\1.hwp"
+0x010 Volume : _UNICODE_STRING "\Device\HarddiskVolume1"
+0x018 Share : _UNICODE_STRING ""
+0x020 Extension : _UNICODE_STRING "hwp"
+0x028 Stream : _UNICODE_STRING ""
+0x030 FinalComponent : _UNICODE_STRING "1.hwp"
+0x038 ParentDir : _UNICODE_STRING "\Temp\
파일오브젝트와 나머지 인자들은 다음과 같습니다.
accessMask = 0x110080
shareMode = 7
createOption = 0x200020
disposition = 1
kd> ?? pFltObjects->FileObject
struct _FILE_OBJECT * 0x860ed8a0
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86523900 _DEVICE_OBJECT
+0x008 Vpb : (null)
/// 중간 생략
+0x02b SharedDelete : 0 ''
+0x02c Flags : 2
+0x030 FileName : _UNICODE_STRING "\temp\1.hwp"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
// 이하 생략
일단 FileObject의 DeviceObject는 그 파일이 존재하는 Device(볼륨)을 의미한다는 걸 언급하고, 다음으로 넘어가겠습니다. ^^
Step 2. NewFileName에 대한 IRP_MJ_CREATE
그 다음엔 Move할 대상 경로의 핸들을 얻어야 합니다.
그런데 이 대상 경로에는 아직 파일이 존재하지 않기 때문에... 대상 파일명을 지정한 상태에서 Data->Iopb->OperationFlags에 SL_OPEN_TARGET_DIRECTORY 를 지정하여 대상 파일이 생성될 디렉토리를 대신 오픈합니다.
대상 파일 경로(의 부모 디렉토리)에 대해 핸들을 여는 것은 대충 다음과 같은 목적이 있습니다. (... 있을 것 같습니다. ''a)
- 대상 디렉토리를 확인 (존재하는지?)
- MoveFile이 진행되는 동안 디렉토리가 삭제되거나 Rename되지 않도록 Lock
- 대상 디렉토리가 현재 파일과 동일한 볼륨에 존재하는지 확인
아시다시피 File Rename은 동일 볼륨 내에서만 지원됩니다. 서로 다른 볼륨일 경우 파일을 Copy한 후 원본 파일을 삭제하는 식으로 진행되죠. (MoveFileEx에 MOVEFILE_COPY_ALLOWED 플래그가 지정되었을 때만 가능합니다.)
그렇다면 현재 파일과 대상 Dir이 동일 볼륨에 존재한다는 것을 어떻게 확인하느냐?
두 파일 오브젝트의 DeviceObject를 비교해보면 알 수 있습니다. 이 DeviceObject 값이 다르면 서로 다른 볼륨이라는 거죠. :)
일단 NewFileName에 대한 PreCreate를 한번 보겠습니다.
+0x000 IrpFlags : 0x884
+0x004 MajorFunction : 0 ''
+0x005 MinorFunction : 0 ''
+0x006 OperationFlags : 0x5 '' // SL_OPEN_TARGET_DIRECTORY
+0x007 Reserved : 0 ''
+0x008 TargetFileObject : 0x862c6b70 _FILE_OBJECT
+0x00c TargetInstance : 0x8625e008 _FLT_INSTANCE
+0x010 Parameters : _FLT_PARAMETERS
일단... SL_OPEN_TARGET_DIRECTORY가 지정되었다는 걸 알수 있네요.
kd> k
ChildEBP RetAddr
ed87d874 f7419888 MyFsFt!MyFltPreCreate+0x23f
WARNING: Frame IP not in any known module. Following frames may be wrong.
ed87d934 804f0199 0xf7419888
ed87d944 8057a822 nt!IopfCallDriver+0x31
ed87da24 805b6f26 nt!IopParseDevice+0xa12
ed87daac 805b32cf nt!ObpLookupObjectName+0x56a
ed87db00 8056d415 nt!ObOpenObjectByName+0xeb
ed87db7c 8056dd8c nt!IopCreateFile+0x407
ed87dbd8 805769dc nt!IoCreateFile+0x8e
ed87dc88 80572b1f nt!IopOpenLinkOrRenameTarget+0x11a
ed87dd48 8053f854 nt!NtSetInformationFile+0x6a9
ed87dd48 7c93e514 nt!KiSystemServicePostCall
0012fe6c 7c93dc6a ntdll!KiFastSystemCallRet
0012fe70 7c7ee956 ntdll!NtSetInformationFile+0xc
0012ff44 7c805712 kernel32!MoveFileWithProgressW+0x3b4
0012ff60 00401d41 kernel32!MoveFileExW+0x17
0012ff78 00402879 TestProject!main+0x21
0012ffc0 7c7e6037 TestProject!__tmainCRTStartup+0x10b
0012fff0 00000000 kernel32!BaseProcessStart+0x23
음... 두번째 경로에 대해 IRP_MJ_CREATE를 날린 건 NetSetInformationFile (-> IopOpenLinkOrRenameTarget) 이군요...
struct _FILE_OBJECT * 0x85f052f8
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86523900 _DEVICE_OBJECT // 이 값이 Step 1에서 얻은 FileObject의 DeviceObject와 다르면 서로 다른 볼륨간의 MoveFile임!!
+0x008 Vpb : (null)
// 생략
+0x030 FileName : _UNICODE_STRING "\test\2.hwp"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
// 생략
kd> ?? pFileNameInfo
struct _FLT_FILE_NAME_INFORMATION * 0xe191522c
+0x000 Size : 0x40
+0x002 NamesParsed : 0xf
+0x004 Format : 1
+0x008 Name : _UNICODE_STRING "\Device\HarddiskVolume1\test"
+0x010 Volume : _UNICODE_STRING "\Device\HarddiskVolume1"
+0x018 Share : _UNICODE_STRING ""
+0x020 Extension : _UNICODE_STRING ""
+0x028 Stream : _UNICODE_STRING ""
+0x030 FinalComponent : _UNICODE_STRING "test"
+0x038 ParentDir : _UNICODE_STRING "\"
일단 Step 1 에서 ExistingFile에 대해 만들어진 FileObject의 DeviceObject 값과 Step 2에서 NewFileName의 디렉토리에 대해 새로 만들어진 File Object의 DeviceObject 값이 동일하다는 걸 알수 있습니다. (0x86523900) 동일한 드라이브 내에서의 MoveFile이란 걸 알 수 있네요... ^^
그리고, 파일 오브젝트는 "\test\2.hwp"를 가리키고 있는데요... FltParseFileNameInformation 해서 얻은 FLT_FILE_NAME_INFORMATION 구조체는 그 파일의 부모 디렉토리인 "\test"를 가리키고 있다는 것을 알수 있습니다. 물론 SL_OPEN_TARGET_DIRECTORY 이 지정되었기 때문이겠죠.
이 상태에서... 이 IRP의 PostCreate 콜백에 BP를 걸어서 FileObject를 확인해보면 다음과 같습니다.
struct _FILE_OBJECT * 0x85f052f8
+0x000 Type : 5
+0x002 Size : 112
+0x004 DeviceObject : 0x86523900 _DEVICE_OBJECT
+0x008 Vpb : 0x865048e0 _VPB
// 중간 생략
+0x02b SharedDelete : 0 ''
+0x02c Flags : 0
+0x030 FileName : _UNICODE_STRING "\test"
+0x038 CurrentByteOffset : _LARGE_INTEGER 0x0
// 이하 생략
일단, FileObject의 FileName이 파일이름을 제거한 "\test"로 변경되어 File이 열렸다는 것을 알 수 있습니다.
한가지 재미있는 것은... 파일 이름을 제거한 것이... 진짜로 이름을 제거한 게 아니라 UNICODE_STRING 의 Length만 줄였을 뿐이라는 것이죠. 즉, 파일 이름 부분은 사실 그대로 남아있다는 겁니다.
kd> dt _UNICODE_STRING 0x85f052f8+0x030
"\test"
+0x000 Length : 0xa
+0x002 MaximumLength : 0x16
+0x004 Buffer : 0xe1c23fc8 "\test"
kd> db 0xe1c23fc8
e1c23fc8 5c 00 74 00 65 00 73 00-74 00 5c 00 32 00 2e 00 \.t.e.s.t.\.2...
e1c23fe8 00 00 00 00 00 00 00 00-00 00 00 60 e9 42 f7 d0 ...........`.B..
Step 3-1. IRP_MJ_SET_INFORMATION
그 다음엔 실제로 파일 이름을 변경하는 단계입니다.
이 작업은 IRP_MJ_SET_INFORMATION 에서 Data->Iopb->Parameters.SetFileInformation.FileInformationClass 가 FileRenameInformation 지정된 IRP를 날림으로서 이루어집니다.
IRP_MJ_SET_INFORMATION 의 PreCallback에 BP를 걸어보겠습니다.
ChildEBP RetAddr
ed87dbc8 f7419888 MyFsFt!MyFltPreSetInformation+0x130
WARNING: Frame IP not in any known module. Following frames may be wrong.
ed87dc88 804f0199 0xf7419888
ed87dc98 805729fb nt!IopfCallDriver+0x31
ed87dd48 8053f854 nt!NtSetInformationFile+0x585
ed87dd48 7c93e514 nt!KiSystemServicePostCall
0012fe6c 7c93dc6a ntdll!KiFastSystemCallRet
0012fe70 7c7ee956 ntdll!NtSetInformationFile+0xc
0012ff44 7c805712 kernel32!MoveFileWithProgressW+0x3b4
0012ff60 00401d41 kernel32!MoveFileExW+0x17
0012ff78 00402879 TestProject!main+0x21
이번에는 NtSetInformationFile 에서 직접 IRP를 날렸군요. ^^
이때 Data->Iopb->Parameters.SetFileInformation.InfoBuffer 에 들어있는 FILE_RENAME_INFORMATION 구조체의 내용은 다음과 같습니다.
struct _FLT_PARAMETERS::<unnamed-tag>
+0x000 Length : 0x32
+0x004 FileInformationClass : a ( FileRenameInformation )
+0x008 ParentOfTarget : 0x85f052f8 _FILE_OBJECT // Step 2 에서 생성된 FILE_OBJECT
+0x00c ReplaceIfExists : 0 ''
+0x00d AdvanceOnly : 0 ''
+0x00c ClusterCount : 0
+0x00c DeleteHandle : (null)
+0x010 InfoBuffer : 0x8647d538
kd> ?? pFileRenameInformation
struct _FILE_RENAME_INFORMATION * 0x8647d538
+0x000 ReplaceIfExists : 0 ''
+0x004 RootDirectory : (null)
+0x008 FileNameLength : 0x22
+0x00c FileName : [1] "\"
kd> du 0x8647d538+0x00c
8647d544 "\??\C:\test\2.hwp "
사실 이건 다음 포스트에서 다룰 내용인데요... 파일시스템에서는 File Rename 시 NewFileName으로 Step 3의 FILE_RENAME_INFORMATION 구조체에 들어있는 FileName 이 아니라 Step 2에서 만들어진 FileObject의 FileName에 "\test" 뒤에 잘려나간 채 숨어있는 파일명 부분을 참조하게 됩니다.
그렇다면, Step 2에서 만들어진 FileObject가 SetFileInformation 콜백에 함께 전달된다는 뜻이겠네요? 네~ 그렇습니다.
위에서 보여주는 Data->Iopb->Parameters.SetFileInformation 의 ParentOfTarget 항목이 Step 2 에서 오픈된 NewFileName이 위치한 BaseDir이 FileObject입니다. (포인터 값을 비교해보시면 동일한 값이라는 것을 알수 있습니다.)
Step 3 - 2. 서로 다른 볼륨 간의 MoveFile인 경우
만약 Step 2에서 DeviceObject가 다른 경우 (즉, 서로 다른 드라이브 간의 MoveFile인 경우)라면... IRP_SET_INFORMATION 으로 진행하지 않고 NtSetInformationFile이 실패하게 됩니다. 이때의 NTSTATUS 값이 STATUS_NOT_SAME_DEVICE 라는 값으로 리턴되죠. 리턴된 시점에 BP를 걸어보면 다음과 같이 됩니다
kd> g
Breakpoint 0 hit
kernel32!MoveFileWithProgressW+0x3b4:
001b:7c7ee956 8bf8 mov edi,eax
kd> r eax
eax=c00000d4 // STATUS_NOT_SAME_DEVICE
그 다음엔 다시한번 IRP_MJ_CREATE가 날아오는데요.. 이때의 콜스택은 다음과 같이 되죠.
kd> k
ChildEBP RetAddr
ed91898c f7419888 MyFsFt!MyFltPreCreate+0x1fc
ed9189ec f741b2a0 fltmgr!FltpPerformPreCallbacks+0x2d4
ed918a00 f7428217 fltmgr!FltpPassThroughInternal+0x32
ed918a18 f7428742 fltmgr!FltpCreateInternal+0x63
ed918a4c 804f0199 fltmgr!FltpCreate+0x258
ed918a5c 8057a822 nt!IopfCallDriver+0x31
ed918b3c 805b6f26 nt!IopParseDevice+0xa12
ed918bc4 805b32cf nt!ObpLookupObjectName+0x56a
ed918c18 8056d415 nt!ObOpenObjectByName+0xeb
ed918c94 8056dd8c nt!IopCreateFile+0x407
ed918cf0 8057049e nt!IoCreateFile+0x8e
ed918d30 8053f854 nt!NtCreateFile+0x30
ed918d30 7c93e514 nt!KiSystemServicePostCall
0012f9d0 7c93d0ba ntdll!KiFastSystemCallRet
0012f9d4 7c7e0e8f ntdll!NtCreateFile+0xc
0012fa6c 7c7f67af kernel32!CreateFileW+0x35f
0012fe60 7c80d3cc kernel32!BasepCopyFileExW+0x153
0012ff44 7c805712 kernel32!MoveFileWithProgressW+0x444
0012ff60 00401d41 kernel32!MoveFileExW+0x17
WARNING: Stack unwind information not available. Following frames may be wrong.
0012ff78 00402899 TestProject1+0x1d41
즉, 서로 다른 볼륨 간 MoveFile인 경우 Step 1, Step 2 를 거쳐서 NtSetInformationFile이 실패한 후 MoveFile에서 다시 CopyFile로 진행한다는 것을 알 수 있습니다.
- "ExistingFileName"을 오픈하여 FILE_OBJECT 획득
- "NewFileName" 을 SL_OPEN_TARGET_DIRECTORY 를 지정한 채 오픈하여 "NewFileName"의 부모 디렉토리에 대한 FILE_OBJECT 획득
- 만약 위의 두 FILE_OBJECT에 들어있는 DeviceObject가 동일하다면, (동일 볼륨 간의 MoveFile이므로) SetInformationFile 이 호출되어 Rename 진행, 만약 다르다면 (다른 볼륨간 MoveFile이므로) CopyFile로 진행.
참고.