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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13448정성태11/20/20232620닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20232485닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20232419닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232699Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232454닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232492개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232321개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232651닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232351Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232842닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232462닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232656닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232892닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232827닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232618스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232341스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232378오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232706스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232596닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232856닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20232916닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233085닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233266스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233084닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233062스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/20233203닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...