Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

C# - x86 실행 환경에서 SECURITY_ATTRIBUTES 구조체를 CreateEvent에 전달할 때 예외 발생

예전에 null dacl을 생성해서 테스트하는 방법에 관해 설명했었는데요.

커널 객체를 위한 null DACL 생성 방법
; https://www.sysnet.pe.kr/2/0/1749

위의 코드를 다음과 같이 간략화해서 각각 x86, x64로 빌드해 실행해 보면,

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
using System.Security.AccessControl;

class Program
{
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern SafeWaitHandle CreateEvent(SECURITY_ATTRIBUTES lpSecurityAttributes, bool isManualReset, bool initialState, string name);

    static void Main(string[] args)
    {
        SafeWaitHandle swh = CreateEventWithNullDacl();
    }

    // http://codemortem.blogspot.kr/2006/01/creating-null-dacl-in-managed-code.html
    public static SECURITY_ATTRIBUTES GetNullDacl()
    {
        // Build NULL DACL (Allow everyone full access)
        RawSecurityDescriptor gsd = new RawSecurityDescriptor(ControlFlags.DiscretionaryAclPresent, null, null, null, null);

        // Construct SECURITY_ATTRIBUTES structure
        SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
        sa.nLength = (uint)Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
        sa.bInheritHandle = false;

        // Get binary form of the security descriptor and copy it into place
        byte[] desc = new byte[gsd.BinaryLength];
        gsd.GetBinaryForm(desc, 0);
        sa.lpSecurityDescriptor = Marshal.AllocHGlobal(desc.Length); // This Alloc is Freed by the Disposer or Finalizer
        Marshal.Copy(desc, 0, sa.lpSecurityDescriptor, desc.Length);

        return sa;
    }

    private static SafeWaitHandle CreateEventWithNullDacl()
    {
        SECURITY_ATTRIBUTES sa = GetNullDacl();

        SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT");
        return swh;
    }
}

x64의 경우 문제가 없는 반면, x86 환경에서는 CreateEvent 호출 시 다음과 같은 예외가 발생합니다.

System.AccessViolationException was unhandled
  Message=Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
  Source=NullDaclEvent
  StackTrace:
       at Program.CreateEvent(SECURITY_ATTRIBUTES lpSecurityAttributes, Boolean isManualReset, Boolean initialState, String name)
       at Program.CreateEventWithNullDacl() in C:\nulldacl\NullDaclEvent\Program.cs:line 48
       at Program.Main(String[] args) in C:\nulldacl\NullDaclEvent\Program.cs:line 21
  InnerException: 

또한, "Enable native code debugging" 옵션을 켜 놓으면 다음과 같은 예외를 볼 수 있습니다.

Exception thrown at 0x77900A4B (ntdll.dll) in NullDaclEvent.exe: 0xC0000005: Access violation reading location 0x00000001.

If there is a handler for this exception, the program may be safely continued.

그런데, 한번 더 생각해 보면 x64가 이상하고 x86이 정상이라는 결론이 나옵니다. 왜냐하면 CreateEvent 함수는,

CreateEvent function
; https://msdn.microsoft.com/en-us/library/windows/desktop/ms682396(v=vs.85).aspx

첫 번째 인자가 포인터 형이기 때문입니다.

HANDLE WINAPI CreateEvent(
  _In_opt_?LPSECURITY_ATTRIBUTES lpEventAttributes,
  _In_?????BOOL ?????????????????bManualReset,
  _In_?????BOOL ?????????????????bInitialState,
  _In_opt_?LPCTSTR ??????????????lpName
);

하지만 우리가 사용한 C# 코드를 보면,

// ...[생략]...

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static extern SafeWaitHandle CreateEvent(SECURITY_ATTRIBUTES lpSecurityAttributes, bool isManualReset, bool initialState, string name);

// ...[생략]...

    SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();

// ...[생략]...

    CreateEvent(sa, true, false, "TEST_EVENT");

CreateEvent에는 스택에 할당된 sa 구조체(struct) 인스턴스의 값이 통째로 넘어가게 되어 있습니다. 포인터가 아니므로 당연히 문제가 발생해야 합니다. 실제로 이것이 포인터라는 것을 명시하면 x86에서의 문제가 사라집니다. 그 방법은 대략 다음과 같은 2가지 정도가 됩니다.

// ==== 1. 구조체를 Pinning하고 주소를 전달

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static unsafe extern SafeWaitHandle CreateEvent(IntPtr lpSecurityAttributes, bool isManualReset, bool initialState, string name);

GCHandle gcHandle = GCHandle.Alloc(sa, GCHandleType.Pinned);
SafeWaitHandle swh = CreateEvent(gcHandle.AddrOfPinnedObject(), true, false, "TEST_EVENT");
gcHandle.Free();

// ==== 2. 스택에 있는 구조체의 포인터를 직접 전달

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static unsafe extern SafeWaitHandle CreateEvent(SECURITY_ATTRIBUTES *lpSecurityAttributes, bool isManualReset, bool initialState, string name);

SECURITY_ATTRIBUTES* pSec = &sa;
SafeWaitHandle swh = CreateEvent(pSec, true, false, "TEST_EVENT");




자, 그럼 왜 x86에서 문제가 발생했는지 추적해 보겠습니다. 이를 위해 정상적으로 x86에서 실행될 때의 환경을 살펴볼 텐데요, 다음은 위에서 소개한 2번 방법을 적용한 코드입니다.

using Microsoft.Win32.SafeHandles;
using System;
using System.Runtime.InteropServices;
using System.Security.AccessControl;

class Program
{
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public int bInheritHandle;
    }

    // ...[생략]...

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    internal static unsafe extern SafeWaitHandle CreateEvent(SECURITY_ATTRIBUTES* lpSecurityAttributes, bool isManualReset, bool initialState, string name);

    // http://codemortem.blogspot.kr/2006/01/creating-null-dacl-in-managed-code.html
    private static unsafe SafeWaitHandle CreateEventWithNullDacl()
    {
        // ...[생략]...

        SECURITY_ATTRIBUTES* pSec = &sa;
        SafeWaitHandle swh = CreateEvent(pSec, true, false, "TEST_EVENT");

        return swh;
    }
}

Visual Studio에서 디버깅을 시작해 disassembly 창으로 CreateEvent의 호출 코드를 확인하니 다음과 같이 나옵니다.

        SafeWaitHandle swh = CreateEvent(pSec, true, false, "TEST_EVENT");
050701EF  push        0  
050701F1  push        dword ptr ds:[3CC2030h]  
050701F7  mov         ecx,dword ptr [ebp-50h]  
050701FA  mov         edx,1  
050701FF  call        00DEC034  

그런데, 왠지 이상합니다. ecx, edx를 쓰는 걸로 봐서 __fastcall 규약으로 인자가 넘어가고 있는데 원래 x86에서의 DllImport의 기본 호출 규약은 __stdcall이기 때문입니다. 어쨌든 __fastcall 규약으로 보고, 처음 2개의 인자는 ecx, edx로 넘어가니 "SECURITY_ATTRIBUTES *" 인자는 [ebp-50h]에 해당합니다. Registers 윈도우를 통해 ebp == 0x0076ee00 임을 확인하고, 그럼 넘어가는 주소 값은 0x0076ee00 - 0x50 == 0x0076edb0이 됩니다. 이어서 메모리 창을 통해 0x0076edb0 주소의 값을 확인해 보니, 0xb8 0xed 0x76 0x00이 나옵니다. Little Endian이므로, 조립해 보면 0x0076edb8로 나오고 이 값은 pSec 변수의 주소와 일치합니다. (당연한 결과입니다.)

그래서 0x0076edb8 주소를 다시 메모리 창으로 보면, 다음과 같이 나옵니다.

0c 00 00 00 80 53 b8 00 00 00 00 00

SECURITY_ATTRIBUTES 구조체로 대입해 보면,

nLength == 0x0000000c
lpSecurityDescriptor == 0x00b85380
bInheritHandle == 0x00000000

와 같이 됩니다. 다시 lpSecurityDescriptor 주소(0x00b85380)의 값을 보면, 다음과 같이 20 바이트의 값이 나옵니다.

01 00 04 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

null dacl을 표현하는 것이므로 위의 값은 고정입니다.

이것으로 정상 동작할 때의 환경을 살펴봤습니다. 이제 다시 비정상적으로 동작했을 때의 코드로 분석해 보겠습니다.

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static unsafe extern SafeWaitHandle CreateEvent(SECURITY_ATTRIBUTES lpSecurityAttributes, bool isManualReset, bool initialState, string name);

        SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
        // ...[생략]...
        SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT");
02E101E4  lea         eax,[ebp-48h]  
02E101E7  push        dword ptr [eax+8]
02E101EA  push        dword ptr [eax+4]  
02E101ED  push        dword ptr [eax]  
02E101EF  push        dword ptr ds:[42D2030h]  
02E101F5  xor         edx,edx  
02E101F7  mov         ecx,1  
02E101FC  call        0143C034  

역시 __fastcall 규약을 따르는 데 ecx 레지스터(즉, 첫 번째 인자)에 1이 넣어져서 전달되고 있습니다. 이는 위에서 예시했던 오류 메시지와 일치합니다.

Exception thrown at 0x77900A4B (ntdll.dll) in NullDaclEvent.exe: 0xC0000005: Access violation reading location 0x00000001.

재미있는 것은, SECURITY_ATTRIBUTES가 구조체이고 DllImport에 정의한 CreateEvent가 구조체로 첫 번째 인자를 받는다고 되어 있으므로 다음과 같이 SECURITY_ATTRIBUTES에 대한 구조체 복사가 이뤄지고 있다는 점입니다.

02E101E4  lea         eax,[ebp-48h]  
02E101E7  push        dword ptr [eax+8]  
02E101EA  push        dword ptr [eax+4]  
02E101ED  push        dword ptr [eax]  

그러니까, 인자 전달이 뒤죽박죽인 것입니다. "mov ecx, 1"은 CreateEvent의 2번째 인자인 true 값을 의미하고, "xor edx, edx"가 false 임을 감안할 때 sa 인자가 가장 마지막에 전달된 것입니다.

어쨌든, 왜 "Access violation" 오류가 발생했는지에 대한 답은 나왔습니다.




그렇다면 x64 버전에서는 왜 잘 실행이 된 것인지 마저 짚고 넘어가겠습니다. 역시 CreateEvent 호출 부분을 보면,

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static unsafe extern SafeWaitHandle CreateEvent(<span style='color: blue; font-weight: bold'>SECURITY_ATTRIBUTES lpSecurityAttributes</span>, bool isManualReset, bool initialState, string name);

        SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
        // ...[생략]...

        SafeWaitHandle swh = CreateEvent(sa, true, false, "TEST_EVENT");
00007FFB4FED0356  mov         r9,13CE3050h  
00007FFB4FED0360  mov         r9,qword ptr [r9]  
00007FFB4FED0363  lea         rcx,[rsp+38h]  
00007FFB4FED0368  mov         rax,qword ptr [rcx]  
00007FFB4FED036B  mov         qword ptr [rsp+70h],rax  
00007FFB4FED0370  mov         rax,qword ptr [rcx+8]  
00007FFB4FED0374  mov         qword ptr [rsp+78h],rax  
00007FFB4FED0379  mov         rax,qword ptr [rcx+10h]  
00007FFB4FED037D  mov         qword ptr [rsp+80h],rax  
00007FFB4FED0385  xor         r8d,r8d  
00007FFB4FED0388  mov         dl,1  
00007FFB4FED038A  lea         rcx,[rsp+70h]  
00007FFB4FED038F  call        00007FFB4FD9C050  

rcx에 [rsp + 70h] 위치의 주솟값이 전달되고 있습니다. 이때의 RSP 값이 0x0000000000DFE7B0이고, + 0x70 하여 구한 0000000000DFE820 주소를 lea로 로드했기 때문에 바로 그 주솟값을 메모리 창으로 확인해 보면 24바이트 짜리 SECURITY_ATTRIBUTES 구조체의 값이 이렇게 나옵니다.

18 00 00 00 00 00 00 00 e0 5e fb 00 00 00 00 00 00 00 00 00 00 00 00 00

nLength == 0x0000000000000018
lpSecurityDescriptor == 0x0000000000fb5ee0 
bInheritHandle == 0x0000000000000000

당연히 0x0000000000fb5ee0 주소 값을 보면 이전의 x86 포인터 버전에서 확인했던 null dacl 값이 나옵니다.

01 00 04 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

x64 환경에서는 호출 규약이 처음 4개의 인자를 rcx, rdx, r8, r9에 전달하도록 되어 있습니다. rcx에는 SECURITY_ATTRIBUTES의 주소 값이 맞게 들어갔고, dl(즉, rdx)에는 1로 들어갔으니 true를 의미하므로 역시 정상입니다. 3번째인 false는 r8에 "xor r8d, r8d"로 되어 있으니 0 값이 잘 들어갔고, 4번째 인자인 "TEST_EVENT" 문자열 주소가 13CE3050h인데 "mov r9, 13CE3050h", "mov r9, qword ptr [r9]" 2개의 명령어로 잘 처리되었습니다.

재미있는 것은, SECURITY_ATTRIBUTES의 인자 전달 방식입니다. 원래는 [rsp + 38h] 주소에 SECURITY_ATTRIBUTES의 내용이 있음에도 불구하고 다음의 명령어로 [rsp+70h] 위치에 값을 그대로 복사하고 있습니다.

00007FFB4FED0363  lea         rcx,[rsp+38h]  
00007FFB4FED0368  mov         rax,qword ptr [rcx]  
00007FFB4FED036B  mov         qword ptr [rsp+70h],rax  
00007FFB4FED0370  mov         rax,qword ptr [rcx+8]  
00007FFB4FED0374  mov         qword ptr [rsp+78h],rax  
00007FFB4FED0379  mov         rax,qword ptr [rcx+10h]  
00007FFB4FED037D  mov         qword ptr [rsp+80h],rax  

따라서, 이런 경우에는 주소를 넘겼음에도 불구하고 Win32 API 내부에서 그 값을 변경한다 해도 호출자 측에는 반영되지 않습니다.




사실, 이것을 어떻게 해석해야 할지 모르겠습니다.

원래는 x64에서도 문제가 발생했어야 하는데 웬일인지 DllImport에 명시한 구조체로서의 SECURITY_ATTRIBUTES 인자를 넘기지 않고 주소 값으로 일부러 넘겨주는 처리를 하기 때문에 x64에서도 잘 동작한 것입니다. 반면, x86은 (정상적으로) 구조체로써 SECURITY_ATTRIBUTES 인자를 넘겼기 때문에 문제가 당연히 발생한 것이고!

따라서, 어쨌든 잘못된 코드이기 때문에 애당초 이 글에서 제시한 2가지 방식으로 인자를 넘기도록 처리하는 것이 바람직합니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]





[최초 등록일: ]
[최종 수정일: 1/11/2017 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 




... 31  32  33  34  35  36  37  38  39  40  41  42  [43]  44  45  ...
NoWriterDateCnt.TitleFile(s)
11161정성태3/24/20174828오류 유형: 379. ICOMAdminCatalog.GetCollection 호출 시 0x80070422 예외 발생
11160정성태3/23/20175733.NET Framework: 649. ASP.NET - Server cannot append header after HTTP headers have been sent. (HTTP 헤더를 보낸 후에는 서버에서 헤더를 추가할 수 없습니다.)파일 다운로드1
11159정성태3/23/20174819Windows: 136. Memory-mapped File은 Private Bytes 크기에 포함될까요?파일 다운로드1
11158정성태3/22/20174834디버깅 기술: 85. Windbg - SOS 디버깅 사례 System.NullReferenceException 예외 추적
11157정성태3/22/20175304.NET Framework: 648. Dictionary&lt;TKey, TValue&gt;를 deep copy하는 방법파일 다운로드1
11156정성태3/21/20175070.NET Framework: 647. 닷넷(C#) 코드로 인증서 요청 코드 만드는 방법파일 다운로드1
11155정성태3/21/20175121.NET Framework: 646. SslStream의 CipherAlgorithm 선택이 가능할까요?파일 다운로드1
11154정성태3/5/201712990VC++: 109. DLL에서 STL 객체를 인자/반환값으로 갖는 함수를 제공할 때, 그 함수를 외부에서 사용하는 경우 비정상 종료한다면? [2]파일 다운로드1
11153정성태3/5/201712347VC++: 108. DLL에 정의된 C++ template 클래스의 복사 생성자 문제파일 다운로드1
11152정성태10/19/20196226VC++: 107. VirtualAlloc, HeapAlloc, GlobalAlloc, LocalAlloc, malloc, new의 차이점
11151정성태10/19/20196308VC++: 106. DLL 개발자가 주의해야 할 Secure CRT 함수 사용파일 다운로드1
11150정성태2/21/20174600.NET Framework: 645. Visual Studio Fakes 기능에서 Shim... 클래스가 생성되지 않는 경우
11149정성태2/21/20176804오류 유형: 378. A 64-bit test cannot run in a 32-bit process. Specify platform as X64 to force test run in X64 mode on X64 machine.
11148정성태2/20/20175898.NET Framework: 644. AppDomain에 대한 단위 테스트 시 알아야 할 사항
11147정성태2/19/20177277오류 유형: 377. Windows 10에서 Fake 어셈블리를 생성하는 경우 빌드 시 The type or namespace name '...' does not exist in the namespace 컴파일 오류 발생
11146정성태2/19/20176431오류 유형: 376. Error VSP1033: The file '...' does not contain a recognized executable image. [2]
11145정성태2/16/20176130.NET Framework: 643. 작업자 프로세스(w3wp.exe)가 재시작되는 시점을 알 수 있는 방법 - 두 번째 이야기 [4]파일 다운로드1
11144정성태2/6/20178960.NET Framework: 642. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (부록 1) - CallingConvention.StdCall, CallingConvention.Cdecl에 상관없이 왜 호출이 잘 될까요?파일 다운로드1
11143정성태2/5/20176462.NET Framework: 641. [Out] 형식의 int * 인자를 가진 함수에 대한 P/Invoke 호출 방법파일 다운로드1
11142정성태10/19/201910453.NET Framework: 640. 닷넷 - 배열 크기의 한계 [2]파일 다운로드1
11141정성태3/21/20177422.NET Framework: 639. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (4) - CLR JIT 컴파일러의 P/Invoke 호출 규약 [1]파일 다운로드1
11140정성태1/27/20175998.NET Framework: 638. RSAParameters와 RSA파일 다운로드1
11139정성태2/6/20176414.NET Framework: 637. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (3) - x64 환경의 __fastcall과 Name mangling [1]파일 다운로드1
11138정성태1/20/20176070VS.NET IDE: 113. 프로젝트 생성시 부터 "Enable the Visual Studio hosting process" 옵션을 끄는 방법 - 두번째 이야기 [3]
11137정성태1/20/20175489Windows: 135. AD에 참여한 컴퓨터로 RDP 연결 시 배경 화면을 못 바꾸는 정책
11136정성태3/12/20206222오류 유형: 375. Hyper-V 내에 구성한 Active Directory 환경의 시간 구성 방법 - 두 번째 이야기
... 31  32  33  34  35  36  37  38  39  40  41  42  [43]  44  45  ...