Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)

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://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa

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

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(SECURITY_ATTRIBUTES lpSecurityAttributes, 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가지 방식으로 인자를 넘기도록 처리하는 것이 바람직합니다.

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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 10/15/2020]

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

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12240정성태6/23/202013571오류 유형: 622. 소켓 바인딩 시 "System.Net.Sockets.SocketException: An attempt was made to access a socket in a way forbidden by its access permissions" 오류 발생
12239정성태6/21/20209938Linux: 30. (윈도우라면 DLL에 속하는) .so 파일이 텍스트로 구성된 사례 [1]
12238정성태6/21/20209987.NET Framework: 913. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 라이브러리
12237정성태6/20/20209751.NET Framework: 912. 리눅스 환경의 .NET Core에서 "test".IndexOf("\0")가 0을 반환
12236정성태6/19/202010141오류 유형: 621. .NET Standard 대상으로 빌드 시 dynamic 예약어에서 컴파일 오류 - error CS0656: Missing compiler required member 'Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create'
12235정성태6/19/20209801오류 유형: 620. Windows 10 - Inaccessible boot device 블루 스크린
12234정성태6/19/20209496개발 환경 구성: 494. NuGet - nuspec의 패키지 스키마 버전(네임스페이스) 업데이트 방법
12233정성태6/19/20209183오류 유형: 619. SQL 서버 - The transaction log for database '...' is full due to 'LOG_BACKUP'. - 두 번째 이야기
12232정성태6/19/20208156오류 유형: 618. SharePoint - StoreBusyRetryLater 오류
12231정성태6/15/202010549.NET Framework: 911. Console/Service Application을 위한 SynchronizationContext - AsyncContext
12230정성태6/15/20209935오류 유형: 617. IMetaDataImport::GetMethodProps가 반환하는 IL 코드 주소(RVA) 문제
12229정성태6/13/202011796.NET Framework: 910. USB/IP PROJECT를 이용해 C#으로 USB Keyboard + Mouse 가상 장치 만들기 [1]
12228정성태6/12/202011865.NET Framework: 909. C# - Source Generator를 적용한 XmlCodeGenerator파일 다운로드1
12227정성태6/12/202015816오류 유형: 616. Visual Studio의 느린 업데이트 속도에 대한 원인 분석 [5]
12226정성태6/11/202013164개발 환경 구성: 493. OpenVPN의 네트워크 구성 [4]파일 다운로드1
12225정성태6/11/202012101개발 환경 구성: 492. 윈도우에 OpenVPN 설치 - 클라이언트 측 구성
12224정성태6/11/202019926개발 환경 구성: 491. 윈도우에 OpenVPN 설치 - 서버 측 구성 [1]
12223정성태6/9/202013990.NET Framework: 908. C# - Source Generator 소개 [10]파일 다운로드2
12222정성태6/3/20209888VS.NET IDE: 146. error information: "CryptQueryObject" (-2147024893/0x80070003)
12221정성태6/3/20209718Windows: 170. 비어 있지 않은 디렉터리로 symbolic link(junction) 연결하는 방법
12220정성태6/3/202012059.NET Framework: 907. C# DLL로부터 TLB 및 C/C++ 헤더 파일(TLH)을 생성하는 방법
12219정성태6/1/202011203.NET Framework: 906. C# - lock (this), lock (typeof(...))를 사용하면 안 되는 이유파일 다운로드1
12218정성태5/27/202011171.NET Framework: 905. C# - DirectX 게임 클라이언트 실행 중 키보드 입력을 감지하는 방법 [3]
12217정성태5/24/20209673오류 유형: 615. Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.
12216정성태5/15/202012761.NET Framework: 904. USB/IP PROJECT를 이용해 C#으로 USB Keyboard 가상 장치 만들기 [14]파일 다운로드1
12215정성태5/12/202017716개발 환경 구성: 490. C# - (Wireshark의) USBPcap을 이용한 USB 패킷 모니터링 [10]파일 다운로드1
... 46  47  48  49  50  51  52  53  54  [55]  56  57  58  59  60  ...