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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12720정성태7/19/20216972Linux: 43. .NET Core/5+ 응용 프로그램의 Ubuntu (Debian) 패키지 준비
12719정성태7/19/20216132오류 유형: 737. SharePoint 설치 시 "0x800710D8 The object identifier does not represent a valid object." 오류 발생
12718정성태7/19/20216752개발 환경 구성: 581. Windows에서 WSL로 파일 복사 시 root 소유권으로 적용되는 문제파일 다운로드1
12717정성태7/18/20216726Windows: 195. robocopy에서 파일의 ADS(Alternate Data Stream) 정보 복사를 제외하는 방법
12716정성태7/17/20217512개발 환경 구성: 580. msbuild의 Exec Task에 robocopy를 사용하는 방법파일 다운로드1
12715정성태7/17/20219234오류 유형: 736. Windows - MySQL zip 파일 버전의 "mysqld --skip-grant-tables" 실행 시 비정상 종료 [1]
12714정성태7/16/20217938오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/20218487.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/20217889개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/20218235개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/202110002개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/20216678Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202110028Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202177239오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/20218556.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [2]파일 다운로드1
12705정성태7/13/20218677VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/20217823개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/202113452개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/20218584오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/20218247.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/20218632.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/20217459VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/202111431오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/20217328개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/20217881VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/20217964VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...