Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

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

비밀번호

댓글 쓴 사람
 




... [46]  47  48  49  50  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
11211정성태5/26/20174914오류 유형: 391. BeginInvoke에 전달한 람다 함수에 CS1660 에러가 발생하는 경우
11210정성태5/25/20174895기타: 65. ActiveX 없는 전자 메일에 사용된 "개인정보 보호를 위해 암호화된 보안메일"의 암호화 방법
11209정성태5/25/201717435Windows: 143. Windows 10의 Recovery 파티션을 삭제 및 새로 생성하는 방법 [6]
11208정성태5/25/20177761오류 유형: 390. diskpart의 set id 명령어에서 "The specified type is not in the correct format." 오류 발생
11207정성태5/24/20178353Windows: 142. Windows 10의 복구 콘솔로 부팅하는 방법
11206정성태5/24/20175987오류 유형: 389. DISM.exe - The specified image in the specified wim is already mounted for read/write access.
11205정성태5/24/20174895.NET Framework: 658. C#의 tail call 구현은?
11204정성태3/8/202013069개발 환경 구성: 316. 간단하게 살펴보는 Docker for Windows [5]
11203정성태5/19/20176693오류 유형: 388. docker - Host does not exist: "default"
11202정성태5/19/20174867오류 유형: 387. WPF - There is no registered CultureInfo with the IetfLanguageTag 'ug'.
11201정성태5/16/20177255오류 유형: 386. WPF - .NET 3.5 이하에서 TextBox에 한글 입력 시 TextChanged 이벤트의 비정상 종료 문제 [1]파일 다운로드1
11200정성태5/16/20174818오류 유형: 385. WPF - 폰트가 없어 System.IO.FileNotFoundException 예외가 발생하는 경우
11199정성태5/16/20175783.NET Framework: 657. CultureInfo.GetCultures가 반환하는 값
11198정성태5/10/20176916.NET Framework: 656. Windows Forms의 오류(Exception) 처리 방법에 대한 차이점 설명
11197정성태5/8/20175047개발 환경 구성: 315. VHD 파일의 최소 크기파일 다운로드1
11196정성태5/4/20176184오류 유형: 384. Msvm_ImageManagementService WMI 객체를 사용할 때 오류 상황 정리 [1]
11195정성태8/19/20176288.NET Framework: 655. .NET Framework 4.7 릴리스
11194정성태5/3/20177565오류 유형: 383. net use 명령어로 네트워크 드라이브 연결 시 "System error 67 has occurred." 오류 발생
11193정성태5/3/20176333Windows: 141. 설치된 Windows로부터 설치 이미지를 만드는 방법
11192정성태5/2/20177216Windows: 140. unattended.xml/autounattend.xml 파일을 마련하는 방법
11191정성태5/2/20177380Windows: 139. Dell Venue 8 Pro 태블릿에 USB를 이용한 윈도우 운영체제 설치 방법
11190정성태5/2/20179162Windows: 138. Windows 운영체제의 ISO 설치 파일에 미리 Device driver를 준비하는 방법
11189정성태5/2/20175495Windows: 137. Windows 7 USB/DVD DOWNLOAD TOOL로 98%에서 실패하는 경우
11188정성태4/27/20176181VC++: 118. Win32 HANDLE 자료형의 이모저모
11187정성태4/26/20177734개발 환경 구성: 314. C# - PowerPoint 확장 Add-in 만드는 방법 [1]파일 다운로드1
11186정성태4/24/20176186VS.NET IDE: 117. Visual Studio 확장(VSIX)을 이용해 사용자 매크로를 추가하는 방법 [1]파일 다운로드1
... [46]  47  48  49  50  51  52  53  54  55  56  57  58  59  60  ...