정말 오래간만에 기술적인 포스팅을 하는 것 같네요. 사실 포스팅하고 싶은 것들은 쌓여 있는데... 시간 내기가 어렵네요.

오늘은 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


이때 인자들을 잠시 살펴보겠습니다.

일단 파일명 정보는 다음과 같이 들어 있구요...

kd> ?? pFileNameInfo
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를 한번 보겠습니다.


kd> dt fltmgr!_FLT_IO_PARAMETER_BLOCK 0x863466b8
   +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) 이군요...


kd> ?? pFltObjects->FileObject
struct _FILE_OBJECT * 0x85f052f8
   +0x000 Type             : 5
   +0x002 Size             : 112
   +0x004 DeviceObject     : 0x86523900 _DEVICE_OBJECT   // 이 값이 Step 1에서 얻은 FileObject의 DeviceObject와 다르면 서로 다른 볼륨간의 MoveFile임!!
   +0x008 Vpb              : (null)
// 생략
   +0x02c Flags            : 0
   +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를 확인해보면 다음과 같습니다.

kd> ?? FltObjects->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

ntdll!_UNICODE_STRING
"\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...
e1c23fd8  68 00 77 00 70 00 00 00-94 57 2a fe c1 7a 7e 01  h.w.p....W*..z~.

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를 걸어보겠습니다.

kd> k
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 구조체의 내용은 다음과 같습니다.


kd> ?? Data->Iopb->Parameters.SetFileInformation
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로 진행한다는 것을 알 수 있습니다.






결론적으로, MoveFileEx(L"ExistingFileName", L"NewFileName", MOVEFILE_COPY_ALLOWED) 가 수행되는 과정을 간단하게 요약하면 다음의 세 단계로 정리할 수 있겠습니다.
  1. "ExistingFileName"을 오픈하여 FILE_OBJECT 획득
  2. "NewFileName" 을 SL_OPEN_TARGET_DIRECTORY 를 지정한 채 오픈하여 "NewFileName"의 부모 디렉토리에 대한 FILE_OBJECT 획득
  3. 만약 위의 두 FILE_OBJECT에 들어있는 DeviceObject가 동일하다면, (동일 볼륨 간의 MoveFile이므로) SetInformationFile 이 호출되어 Rename 진행, 만약 다르다면 (다른 볼륨간 MoveFile이므로) CopyFile로 진행.

그림으로 정리해보면 다음과 같이 되겠네요...


다음 포스트에서는... File Rename IO를 Redirect 하는 방법에 대해 써보려고 합니다. ^^

참고.




Posted by kuaaan
,


사랑합니다. 편안히 잠드소서