Microsoft MVP성태의 닷넷 이야기
VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석 [링크 복사], [링크+제목 복사]
조회: 406
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석

이전 글에서,

ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법
; https://www.sysnet.pe.kr/2/0/12676

ILxssSession, ILxInstnace를 소개했는데요, 아쉽게도 동작은 하지 않았지만 어쨌든 4개의 함수 signature를 공개했다는 것이 중요합니다. 그럼, 혹시 우리도 알 수 있지 않을까요? ^^

일단, CoCreateInstance로 반환한 ILxssSession 인스턴스를,

hr = CoCreateInstance(CLSID_LxssManager, nullptr, CLSCTX_LOCAL_SERVER, 
            __uuidof(ILxssSession), (PVOID*)&pSession);

// pSession == 00000267BD58AB98

해당 주솟값을 참조해서 들어가면 vtable 위치를 알 수 있는데요,

[00000267BD58AB98] == 00007ff9ae572180 

vtable의 항목에 대해 windbg에서 symbol을 확인할 수 있습니다. 아래는 vtable에 대해 정리한 것입니다.

C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거
; https://www.sysnet.pe.kr/2/0/12114

00007ff9`ae572180 00007ff9ae571490 LxssManagerProxyStub!IUnknown_QueryInterface_Proxy: // @0 IUnknown::QueryInterface
00007ff9`ae572188 00007ff9ae571320 LxssManagerProxyStub!IUnknown_AddRef_Proxy: // @8 IUnknown::AddRef
00007ff9`ae572190 00007ff9ae5713d0 LxssManagerProxyStub!IUnknown_Release_Proxy: // @10 IUnknown::Release
00007ff9`ae572198 00007ff9ae571340 LxssManagerProxyStub!ObjectStublessClient3: // @18 GetCurrentInstance
00007ff9`ae5721a0 00007ff9ae5714c0 LxssManagerProxyStub!ObjectStublessClient4: // @20 StartDefaultInstance
00007ff9`ae5721a8 00007ff9ae571390 LxssManagerProxyStub!ObjectStublessClient5: // @28 SetState
00007ff9`ae5721b0 00007ff9ae5713e0 LxssManagerProxyStub!ObjectStublessClient6: // @30 QueryState
00007ff9`ae5721b8 00007ff9ae571370 LxssManagerProxyStub!ObjectStublessClient7: // @38 InitializeFileSystem
00007ff9`ae5721c0 00007ff9ae5713f0 LxssManagerProxyStub!ObjectStublessClient8: // @40 Destroy
00007ff9`ae5721c8 00007ff9ae571400 LxssManagerProxyStub!ObjectStublessClient9:
00007ff9`ae5721d0 00007ff9ae571430 LxssManagerProxyStub!ObjectStublessClient10:
00007ff9`ae5721d8 00007ff9ae571310 LxssManagerProxyStub!ObjectStublessClient11:
00007ff9`ae5721e0 00007ff9ae5712f0 LxssManagerProxyStub!ObjectStublessClient12:
00007ff9`ae5721e8 00007ff9ae571380 LxssManagerProxyStub!ObjectStublessClient13:
00007ff9`ae5721f0 00007ff9ae571300 LxssManagerProxyStub!ObjectStublessClient14:
00007ff9`ae5721f8 00007ff9ae571360 LxssManagerProxyStub!ObjectStublessClient15:
00007ff9`ae572200 00007ff9ae571470 LxssManagerProxyStub!ObjectStublessClient16:
00007ff9`ae572208 00007ff9ae571420 LxssManagerProxyStub!ObjectStublessClient17:
00007ff9`ae572210 00007ff9ae571350 LxssManagerProxyStub!ObjectStublessClient18:
00007ff9`ae572218 00007ff9ae5713b0 LxssManagerProxyStub!ObjectStublessClient19:

아쉽게도 ObjectStublessClient3부터 시작해 ObjectStublessClient19까지의 함수는 추적해 들어가는 경우 얻을 수 있는 정보가 없습니다. ^^; 솔직히 없다기보다는 INPROC COM 개체였다면 쉽게 분석을 들어갈 수 있었겠지만 DCOM 서버라서 RPC 호출이 엮어져서 분석이 매우 어렵습니다.

그럼 접근법을 달리해서 wslapi.dll에 있는 함수 호출을 분석해 들어가면 될 듯합니다. 이를 위해 적당한 API 하나를 골라야 하는데, 함수 인자가 1개인 WslIsDistributionRegistered가 후보로 좋을 듯합니다. ^^

WslIsDistributionRegistered function (wslapi.h)
; https://docs.microsoft.com/en-us/windows/win32/api/wslapi/nf-wslapi-wslisdistributionregistered

사용은 이렇게 하고,

typedef BOOL(__stdcall* pIsDistroRegistered)(PCWSTR distroName);

HMODULE dll = LoadLibraryExW(L"wslapi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
pIsDistroRegistered IsDistroRegistered = (pIsDistroRegistered)GetProcAddress(dll, "WslIsDistributionRegistered");

BOOL result = IsDistroRegistered(L"Ubuntu20.04");
printf("IsDistroRegistered: %d\n", result);

windbg로 IsDistroRegistered 호출부터 BP를 걸어 들어가면 됩니다. 한번 따라가볼까요? ^^




아래는 windbg에서 본 wslapi!WslIsDistributionRegistered 코드입니다.

struct _LX_DIST_INFO
{
    wchar_t *pName;
    int *pValue;
}

wslapi!WslIsDistributionRegistered:
00007ff9`b9ff23a0 48895c2418      mov     qword ptr [rsp+18h],rbx
00007ff9`b9ff23a5 48894c2408      mov     qword ptr [rsp+8],rcx ss:00000094`eacffae0={ConsoleApplication1!`string' (00007ff7`54dead00)}
                                     // rsp + 8 == 00000094`eacffae0
                                     // [rsp + 8] == 00007ff754dead58 == L"Ubuntu20.04"
00007ff9`b9ff23aa 57              push    rdi
00007ff9`b9ff23ab 4883ec30        sub     rsp,30h
                                     // int parent_sp_2; // rsp+48h == 00000094`eacffae8
                                     // void *ptr_2; // rsp+28h == 00000094`eacffac8
00007ff9`b9ff23af 33ff            xor     edi,edi
00007ff9`b9ff23b1 897c2448        mov     dword ptr [rsp+48h],edi
                                     // parent_sp_2 = 0; // [rsp + 48] == (00007ff9)00000000
00007ff9`b9ff23b5 8d4f10          lea     ecx,[rdi+10h] // rcx == 0x10
00007ff9`b9ff23b8 e8db360000      call    wslapi!operator new (00007ff9`b9ff5a98) // 0x10바이트만큼 공간 확보
00007ff9`b9ff23bd 4885c0          test    rax,rax // rax == 00000267`bd5a5820
                                     // 00000267`bd5a5820 38 01 00 00 00 00 00  8......
                                     // 00000267`bd5a5827 00 01 00 00 00 10 16  .......
00007ff9`b9ff23c0 7413            je      wslapi!WslIsDistributionRegistered+0x35 (00007ff9`b9ff23d5)
00007ff9`b9ff23c2 488d4c2440      lea     rcx,[rsp+40h] // L"Ubuntu20.04"
00007ff9`b9ff23c7 488908          mov     qword ptr [rax],rcx
00007ff9`b9ff23ca 488d4c2448      lea     rcx,[rsp+48h]
00007ff9`b9ff23cf 48894808        mov     qword ptr [rax+8],rcx

                                     // _LX_DIST_INFO pInfo = new();
                                     // info->pName = [1st_arg];
                                     // info->pValue = &parent_sp_2;

00007ff9`b9ff23d3 eb03            jmp     wslapi!WslIsDistributionRegistered+0x38 (00007ff9`b9ff23d8)
00007ff9`b9ff23d5 488bc7          (mov     rax,rdi)
00007ff9`b9ff23d8 4885c0          test    rax,rax
00007ff9`b9ff23db 747b            je      wslapi!WslIsDistributionRegistered+0xb8 (00007ff9`b9ff2458)
00007ff9`b9ff23dd 48897c2428      mov     qword ptr [rsp+28h],rdi
                                     // ptr_2 = nullptr; // rsp+28h == 00000094`eacffac8

00007ff9`b9ff23e2 4c8d0577050000  lea     r8,[wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl> (00007ff9`b9ff2960)]
00007ff9`b9ff23e9 4c8bc8          mov     r9,rax
00007ff9`b9ff23ec 897c2420        mov     dword ptr [rsp+20h],edi // DWORD dwCreationFlags = 0
00007ff9`b9ff23f0 33d2            xor     edx,edx
00007ff9`b9ff23f2 33c9            xor     ecx,ecx
00007ff9`b9ff23f4 48ff15054f0000  call    qword ptr [wslapi!_imp_CreateThread (00007ff9`b9ff7300)]
                                    // CreateThread(0, 0, [wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>, pInfo, dwCreationFlags, ptr_2);
00007ff9`b9ff23fb 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff2400 488bd8          mov     rbx,rax
                                    // rax == 1bc
                                    // !handle 1bc ==> Type Thread
00007ff9`b9ff2403 4885c0          test    rax,rax
00007ff9`b9ff2406 7439            je      wslapi!WslIsDistributionRegistered+0xa1 (00007ff9`b9ff2441)
00007ff9`b9ff2408 83caff          or      edx,0FFFFFFFFh
00007ff9`b9ff240b 488bc8          mov     rcx,rax
00007ff9`b9ff240e 48ff15534f0000  call    qword ptr [wslapi!_imp_WaitForSingleObject (00007ff9`b9ff7368)]
                                   // WaitForSingleObject(rax, 4294967295);

00007ff9`b9ff2415 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff241a 8b7c2448        mov     edi,dword ptr [rsp+48h]
00007ff9`b9ff241e 4883fbff        cmp     rbx,0FFFFFFFFFFFFFFFFh
00007ff9`b9ff2422 740f            je      wslapi!WslIsDistributionRegistered+0x93 (00007ff9`b9ff2433)
00007ff9`b9ff2424 488bcb          mov     rcx,rbx
00007ff9`b9ff2427 48ff15f24d0000  call    qword ptr [wslapi!_imp_CloseHandle (00007ff9`b9ff7220)]
00007ff9`b9ff242e 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff2433 488b5c2450      mov     rbx,qword ptr [rsp+50h]
00007ff9`b9ff2438 8bc7            mov     eax,edi
00007ff9`b9ff243a 4883c430        add     rsp,30h
00007ff9`b9ff243e 5f              pop     rdi
00007ff9`b9ff243f c3              ret

코드를 보면, 다른 건 별로 의미가 없고 WslIsDistributionRegistered의 본래 기능은 CreateThread에 전달된 lambda_invoker_cdecl 함수에서 실제 동작을 하는 걸로 보입니다. lambda_invoker_cdecl은 다음의 코드로 이뤄집니다.

wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>:
    // DWORD WINAPI ThreadProc(_In_ LPVOID lpParameter);
00007ff9`b9ff2960 48895c2408      mov     qword ptr [rsp+8],rbx
00007ff9`b9ff2965 57              push    rdi
00007ff9`b9ff2966 4883ec40        sub     rsp,40h
                                       // _LX_CERATE_INFO *pCerateInfo;
                                       // __int128* pField1; // [rsp + 28h]
00007ff9`b9ff296a 488b059f760000  mov     rax,qword ptr [wslapi!_security_cookie (00007ff9`b9ffa010)]
00007ff9`b9ff2971 4833c4          xor     rax,rsp
00007ff9`b9ff2974 4889442438      mov     qword ptr [rsp+38h],rax

00007ff9`b9ff2979 488bf9          mov     rdi,rcx // rdi = lpParameter
00007ff9`b9ff297c 488d542420      lea     rdx,[rsp+20h] // rdx = 94eb3ff900
00007ff9`b9ff2981 488b09          mov     rcx,qword ptr [rcx] // rcx == 94eacffae0, [94eacffae0] == 7ff754dead58 == L"Ubuntu20.04"
00007ff9`b9ff2984 33db            xor     ebx,ebx
00007ff9`b9ff2986 48895c2420      mov     qword ptr [rsp+20h],rbx
                                       // pCerateInfo = nullptr;

00007ff9`b9ff298b 488b09          mov     rcx,qword ptr [rcx] // rcx = [rcx] == 7ff754dead58
00007ff9`b9ff298e e889220000      call    wslapi!WslSession::s_Create (00007ff9`b9ff4c1c)
                                         // WslSession::s_Create(L"Ubuntu20.04", ptr_1);
                                         // pCreateInfo에 pSession을 담아 반환
00007ff9`b9ff2993 85c0            test    eax,eax
00007ff9`b9ff2995 7919            jns     wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x50 (00007ff9`b9ff29b0) // 00007ff9`b9ff29b0로 점프
...[생략]...
00007ff9`b9ff29b0 488b4c2420      mov     rcx,qword ptr [rsp+20h] // rcx =  pCerateInfo;
00007ff9`b9ff29b5 488d542428      lea     rdx,[rsp+28h] // rdx = &pField1;
00007ff9`b9ff29ba 0f57c0          xorps   xmm0,xmm0 
00007ff9`b9ff29bd 0f11442428      movups  xmmword ptr [rsp+28h],xmm0 // pField1 = nullptr;
00007ff9`b9ff29c2 e829250000      call    wslapi!WslSession::_GetDistroId (00007ff9`b9ff4ef0)
                                        // WslSession::_GetDistroId(pCreateInfo, pField1);
00007ff9`b9ff29c7 85c0            test    eax,eax
00007ff9`b9ff29c9 7821            js      wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x8c (00007ff9`b9ff29ec)
00007ff9`b9ff29cb 488b442428      mov     rax,qword ptr [rsp+28h]
00007ff9`b9ff29d0 483b05514e0000  cmp     rax,qword ptr [wslapi!GUID_NULL (00007ff9`b9ff7828)]
00007ff9`b9ff29d7 750e            jne     wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x87 (00007ff9`b9ff29e7)
00007ff9`b9ff29d9 488b442430      mov     rax,qword ptr [rsp+30h]
00007ff9`b9ff29de 483b054b4e0000  cmp     rax,qword ptr [wslapi!GUID_NULL+0x8 (00007ff9`b9ff7830)]
00007ff9`b9ff29e5 7405            je      wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x8c (00007ff9`b9ff29ec)
00007ff9`b9ff29e7 bb01000000      mov     ebx,1
00007ff9`b9ff29ec 488b4708        mov     rax,qword ptr [rdi+8]
00007ff9`b9ff29f0 8918            mov     dword ptr [rax],ebx
00007ff9`b9ff29f2 488d4c2420      lea     rcx,[rsp+20h]
00007ff9`b9ff29f7 e8c4ecffff      call    wslapi!wistd::unique_ptr<WslSession,wistd::default_delete<WslSession> >::~unique_ptr<WslSession,wistd::default_delete<WslSession> > (00007ff9`b9ff16c0)
00007ff9`b9ff29fc 488bcf          mov     rcx,rdi
00007ff9`b9ff29ff e888300000      call    wslapi!operator delete (00007ff9`b9ff5a8c)
00007ff9`b9ff2a04 33c0            xor     eax,eax
00007ff9`b9ff2a06 488b4c2438      mov     rcx,qword ptr [rsp+38h]
00007ff9`b9ff2a0b 4833cc          xor     rcx,rsp
00007ff9`b9ff2a0e e8cd340000      call    wslapi!_security_check_cookie (00007ff9`b9ff5ee0)
00007ff9`b9ff2a13 488b5c2450      mov     rbx,qword ptr [rsp+50h]
00007ff9`b9ff2a18 4883c440        add     rsp,40h
...[생략]...

위의 호출에서 s_Create 함수를 먼저 들어가 보면,

struct _LX_CREATE_UNK16
{
    wchar_t *pDistName;
    ILxssSession *pSession;
}

struct _LX_CERATE_INFO
{
    _LX_CREATE_UNK16 f1;
    __int64 flag2;
}

wslapi!WslSession::s_Create: // WslSession::s_Create(L"Ubuntu20.04", ptr_1);
00007ff9`b9ff4c1c 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000094`eb3ff8e0=0000000000000000
00007ff9`b9ff4c21 4889742418      mov     qword ptr [rsp+18h],rsi
00007ff9`b9ff4c26 57              push    rdi
00007ff9`b9ff4c27 4883ec20        sub     rsp,20h
00007ff9`b9ff4c2b 488bfa          mov     rdi,rdx  // rdi == ptr_1 == 94eb3ff900
00007ff9`b9ff4c2e 488bf1          mov     rsi,rcx  // rsi == "Ubuntu20.04"
00007ff9`b9ff4c31 488d15182a0000  lea     rdx,[wslapi!std::nothrow (00007ff9`b9ff7650)]
00007ff9`b9ff4c38 b918000000      mov     ecx,18h
00007ff9`b9ff4c3d e89a180000      call    wslapi!operator new (00007ff9`b9ff64dc)
                                          // _LX_CERATE_INFO pCreateInfo = new [0x18] // 24bytes [267bd5a5660]
00007ff9`b9ff4c42 4885c0          test    rax,rax
00007ff9`b9ff4c45 7410            je      wslapi!WslSession::s_Create+0x3b (00007ff9`b9ff4c57)
00007ff9`b9ff4c47 0f57c0          xorps   xmm0,xmm0
00007ff9`b9ff4c4a 33c9            xor     ecx,ecx
00007ff9`b9ff4c4c 0f1100          movups  xmmword ptr [rax],xmm0
                                          // pCreateInfo->f1 = 0;
00007ff9`b9ff4c4f 48894810        mov     qword ptr [rax+10h],rcx
                                          // pCreateInfo->flag2 = 0;
00007ff9`b9ff4c53 c6401001        mov     byte ptr [rax+10h],1
                                          // pCreateInfo->flag2 = 1;
00007ff9`b9ff4c57 488b1f          mov     rbx,qword ptr [rdi] // rbx == 부모가 준 ptr_1 스택 영역 (아마도 구조체)
                                                              // rbx = 0
00007ff9`b9ff4c5a 488364243800    and     qword ptr [rsp+38h],0
00007ff9`b9ff4c60 488907          mov     qword ptr [rdi],rax // ptr_1 = pCreateInfo;
00007ff9`b9ff4c63 4885db          test    rbx,rbx
00007ff9`b9ff4c66 7464            je      wslapi!WslSession::s_Create+0xb0 (00007ff9`b9ff4ccc) // rbx != null이므로 00007ff9`b9ff4ccc로 점프
...[생략]...
00007ff9`b9ff4ccc 488d4c2438      lea     rcx,[rsp+38h] // rcx = 94eb3ff8e8
00007ff9`b9ff4cd1 e8eac9ffff      call    wslapi!wistd::unique_ptr<WslSession,wistd::default_delete<WslSession> >::~unique_ptr<WslSession,wistd::default_delete<WslSession> > (00007ff9`b9ff16c0)
00007ff9`b9ff4cd6 488b0f          mov     rcx,qword ptr [rdi]  // rcx = pCreateInfo
00007ff9`b9ff4cd9 4885c9          test    rcx,rcx
00007ff9`b9ff4cdc 7520            jne     wslapi!WslSession::s_Create+0xe2 (00007ff9`b9ff4cfe) // rcx != null이므로 00007ff9`b9ff4cfe로 점프
...[생략]...
00007ff9`b9ff4cfe 488bd6          mov     rdx,rsi
00007ff9`b9ff4d01 e826000000      call    wslapi!WslSession::_Initialize (00007ff9`b9ff4d2c)
                                          // wslapi!WslSession::_Initialize(pCreateInfo, "Ubuntu20.04");
                                          // pCreateInfo에 pSession을 담아 반환
00007ff9`b9ff4d06 8bd8            mov     ebx,eax
00007ff9`b9ff4d08 85c0            test    eax,eax
00007ff9`b9ff4d0a 7907            jns     wslapi!WslSession::s_Create+0xf7 (00007ff9`b9ff4d13)
...[생략]...
00007ff9`b9ff4d13 33c0            xor     eax,eax // 반환값 0
00007ff9`b9ff4d15 488b5c2430      mov     rbx,qword ptr [rsp+30h]
00007ff9`b9ff4d1a 488b742440      mov     rsi,qword ptr [rsp+40h]
00007ff9`b9ff4d1f 4883c420        add     rsp,20h
00007ff9`b9ff4d23 5f              pop     rdi
00007ff9`b9ff4d24 c3              ret

실제 작업은 "WslSession::_Initialize" 함수에서 이뤄지는 것을 알 수 있습니다.

wslapi!WslSession::_Initialize:
00007ff9`b9ff4d2c 48895c2410      mov     qword ptr [rsp+10h],rbx ss:00000094`eb3ff8b8=0000000000000000
00007ff9`b9ff4d31 48896c2418      mov     qword ptr [rsp+18h],rbp
00007ff9`b9ff4d36 4889742420      mov     qword ptr [rsp+20h],rsi
00007ff9`b9ff4d3b 57              push    rdi
00007ff9`b9ff4d3c 4883ec60        sub     rsp,60h
00007ff9`b9ff4d40 488d7910        lea     rdi,[rcx+10h]
00007ff9`b9ff4d44 488bea          mov     rbp,rdx
00007ff9`b9ff4d47 488bf1          mov     rsi,rcx
00007ff9`b9ff4d4a c60700          mov     byte ptr [rdi],0
00007ff9`b9ff4d4d 33d2            xor     edx,edx
00007ff9`b9ff4d4f 33c9            xor     ecx,ecx
00007ff9`b9ff4d51 48ff1538240000  call    qword ptr [wslapi!_imp_CoInitializeEx (00007ff9`b9ff7190)]
                                          // CoInitializeEx(nullptr, 0);
00007ff9`b9ff4d58 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff4d5d 85c0            test    eax,eax
00007ff9`b9ff4d5f 0f8874010000    js      wslapi!WslSession::_Initialize+0x1ad (00007ff9`b9ff4ed9)
00007ff9`b9ff4d65 488364244000    and     qword ptr [rsp+40h],0
00007ff9`b9ff4d6b 4533c9          xor     r9d,r9d
00007ff9`b9ff4d6e c744243820000000 mov     dword ptr [rsp+38h],20h
00007ff9`b9ff4d76 4533c0          xor     r8d,r8d
00007ff9`b9ff4d79 488364243000    and     qword ptr [rsp+30h],0
00007ff9`b9ff4d7f 83caff          or      edx,0FFFFFFFFh
00007ff9`b9ff4d82 c744242803000000 mov     dword ptr [rsp+28h],3
00007ff9`b9ff4d8a 33c9            xor     ecx,ecx
00007ff9`b9ff4d8c 8364242000      and     dword ptr [rsp+20h],0
00007ff9`b9ff4d91 48ff1500240000  call    qword ptr [wslapi!_imp_CoInitializeSecurity (00007ff9`b9ff7198)]
                                          // CoInitializeSecurity(0, -1, 0, 0, RpcAuthnLevel.Default /* 0 */,
                                                        RpcImpLevel.Impersonate /* 3 */, 0, EoAuthnCap.StaticCloaking /* 0x20 */, 0);
00007ff9`b9ff4d98 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff4d9d ba00000080      mov     edx,80000000h
00007ff9`b9ff4da2 8d0c10          lea     ecx,[rax+rdx]
00007ff9`b9ff4da5 85ca            test    edx,ecx
00007ff9`b9ff4da7 750b            jne     wslapi!WslSession::_Initialize+0x88 (00007ff9`b9ff4db4)
00007ff9`b9ff4da9 3d19010180      cmp     eax,80010119h
00007ff9`b9ff4dae 0f850e010000    jne     wslapi!WslSession::_Initialize+0x196 (00007ff9`b9ff4ec2) // RCP_E_TOO_LATE
00007ff9`b9ff4db4 488d5e08        lea     rbx,[rsi+8] // rbx == &pCreateInfo->f1.pSession;
00007ff9`b9ff4db8 488b0b          mov     rcx,qword ptr [rbx]  // rcx = pCreateInfo->f1.pSession; // == 0
00007ff9`b9ff4dbb 48832300        and     qword ptr [rbx],0  // pCreateInfo->f1.pSession = 0;
00007ff9`b9ff4dbf 4885c9          test    rcx,rcx
00007ff9`b9ff4dc2 740d            je      wslapi!WslSession::_Initialize+0xa5 (00007ff9`b9ff4dd1) // 00007ff9`b9ff4dd1 점프
...[생략]...
00007ff9`b9ff4dd1 33d2            xor     edx,edx
00007ff9`b9ff4dd3 48895c2420      mov     qword ptr [rsp+20h],rbx
00007ff9`b9ff4dd8 4c8d0db12c0000  lea     r9,[wslapi!GUID_536a6bcf_fe04_41d9_b978_dcaca9a9b5b9 (00007ff9`b9ff7a90)]
00007ff9`b9ff4ddf 488d0d9a2c0000  lea     rcx,[wslapi!`string'+0x4 (00007ff9`b9ff7a80)]
00007ff9`b9ff4de6 448d4204        lea     r8d,[rdx+4]
00007ff9`b9ff4dea 48ff15af230000  call    qword ptr [wslapi!_imp_CoCreateInstance (00007ff9`b9ff71a0)]
                                        // CoCreateInstance(CLSID_LxssManager, nullptr, CLSCTX_LOCAL_SERVER, __uuidof(ILxssSession), &pCreateInfo->f1.pSession);
00007ff9`b9ff4df1 0f1f440000      nop     dword ptr [rax+rax]
00007ff9`b9ff4df6 8bd8            mov     ebx,eax
00007ff9`b9ff4df8 b89e010780      mov     eax,8007019Eh // The Windows Subsystem for Linux has not been enabled. 
00007ff9`b9ff4dfd 81fb54010480    cmp     ebx,80040154h // Class not registered - Retrieving the COM class factory for component with CLSID
00007ff9`b9ff4e03 0f44d8          cmove   ebx,eax
00007ff9`b9ff4e06 85db            test    ebx,ebx
00007ff9`b9ff4e08 7907            jns     wslapi!WslSession::_Initialize+0xe5 (00007ff9`b9ff4e11) [br=1] // 00007ff9`b9ff4e11로 점프
...[생략]...
00007ff9`b9ff4e11 4983c8ff        or      r8,0FFFFFFFFFFFFFFFFh // r8 = ffffffffffffffff
00007ff9`b9ff4e15 488d4c2470      lea     rcx,[rsp+70h]  // 00000094`eb3ff8b0 (이 위치는 WslSession::_initialize 반환 주소 값을 담고 있는 그다음 부모의 rsp 마지막 기준 주소)
00007ff9`b9ff4e1a 488bd5          mov     rdx,rbp  // rdx == "Ubuntu20.08"
00007ff9`b9ff4e1d e8c20a0000      call    wslapi!wil::make_unique_string_nothrow<wil::unique_any_t<wil::details::unique_storage<wil::details::resource_policy<unsigned short * __ptr64,void (__cdecl*)(void * __ptr64),&CoTaskMemFree,wistd::integral_constant<unsigned __int64,0>,unsigned short * __ptr64,unsigned short * __ptr64,0,std::nullptr_t> > > > (00007ff9`b9ff58e4)
                                            // [rsp+70h] 00000094`eb3ff8b0 == 00000267bd5a57e0 == "Ubuntu20.04"
00007ff9`b9ff4e22 488d542470      lea     rdx,[rsp+70h] // rdx == "Ubuntu20.04"
                                                        // rdx == 94eb3ff8b0, [rdx] == 00000267bd5a57e0 == "Ubuntu20.04"
00007ff9`b9ff4e27 488bce          mov     rcx,rsi  // rcx == pCreateInfo
00007ff9`b9ff4e2a e8b5050000      call    wslapi!wil::unique_any_t<wil::details::unique_storage<wil::details::resource_policy<unsigned short * __ptr64,void (__cdecl*)(void * __ptr64),&CoTaskMemFree,wistd::integral_constant<unsigned __int64,0>,unsigned short * __ptr64,unsigned short * __ptr64,0,std::nullptr_t> > >::operator= (00007ff9`b9ff53e4)
                                         // 호출 후,
                                         // rax == 267bd5a5660, pCreateInfo
                                         // [rsp+70h] = 0
00007ff9`b9ff4e2f 488b4c2470      mov     rcx,qword ptr [rsp+70h] // rcx = 0
00007ff9`b9ff4e34 4885c9          test    rcx,rcx
00007ff9`b9ff4e37 740c            je      wslapi!WslSession::_Initialize+0x119 (00007ff9`b9ff4e45) // 00007ff9`b9ff4e45로 점프
...[생략]...
00007ff9`b9ff4e45 48833e00        cmp     qword ptr [rsi],0
00007ff9`b9ff4e49 752c            jne     wslapi!WslSession::_Initialize+0x14b (00007ff9`b9ff4e77) // 00007ff9`b9ff4e77로 점프
...[생략]...
00007ff9`b9ff4e77 488d442450      lea     rax,[rsp+50h] // rax == 00000094`eb3ff890 fffffffffffffffe
00007ff9`b9ff4e7c 483bf8          cmp     rdi,rax // rdi == 267bd5a5670, pCreateInfo->flag2
00007ff9`b9ff4e7f 741a            je      wslapi!WslSession::_Initialize+0x16f (00007ff9`b9ff4e9b)
00007ff9`b9ff4e81 8a07            mov     al,byte ptr [rdi]  // pCreateInfo->flag2 == 0 (1로 초기화했는데 언제 0으로???)
00007ff9`b9ff4e83 c60700          mov     byte ptr [rdi],0  // 어쨌든 pCreateInfo->flag2 = 0으로 초기화
00007ff9`b9ff4e86 84c0            test    al,al
00007ff9`b9ff4e88 740c            je      wslapi!WslSession::_Initialize+0x16a (00007ff9`b9ff4e96) // 00007ff9`b9ff4e96으로 점프
...[생략]...
00007ff9`b9ff4e96 c60701          mov     byte ptr [rdi],1  // 어쨌든 pCreateInfo->flag2 = 1로 초기화
00007ff9`b9ff4e99 eb0c            jmp     wslapi!WslSession::_Initialize+0x17b (00007ff9`b9ff4ea7) // 00007ff9`b9ff4ea7로 점프
...[생략]...
00007ff9`b9ff4ea7 33db            xor     ebx,ebx
00007ff9`b9ff4ea9 4c8d5c2460      lea     r11,[rsp+60h]
00007ff9`b9ff4eae 8bc3            mov     eax,ebx // 리턴 값 0
00007ff9`b9ff4eb0 498b5b18        mov     rbx,qword ptr [r11+18h]
00007ff9`b9ff4eb4 498b6b20        mov     rbp,qword ptr [r11+20h]
00007ff9`b9ff4eb8 498b7328        mov     rsi,qword ptr [r11+28h]
00007ff9`b9ff4ebc 498be3          mov     rsp,r11
00007ff9`b9ff4ebf 5f              pop     rdi
00007ff9`b9ff4ec0 c3              ret

익숙한 함수 호출들을 볼 수 있는데요, 결국 하는 것은 ILxssSession 인스턴스를 구하는 것입니다. 위의 함수를 호출한 lambda_invoker_cdecl 측에서는 저렇게 ILxssSession 인스턴스를 구한 후 WslSession::_GetDistroId 함수를 호출하게 됩니다. 그리고 마침내 이 함수에서 ILxssSession 인터페이스의 (QueryState로 보이는) 7번째 함수의,

MIDL_INTERFACE("536A6BCF-FE04-41D9-B978-DCACA9A9B5B9") ILxssSession : IUnknown
{
    STDMETHOD(GetCurrentInstance)(ILxInstnace** pInstanceOut);
    STDMETHOD(StartDefaultInstance)(_In_ const IID& InstanceIid, _Out_ PVOID* InstanceOut);
    STDMETHOD(SetState)(/* unknown */);
    STDMETHOD(QueryState)(/* unknown */);
    STDMETHOD(InitializeFileSystem)(/* unknown */);
    STDMETHOD(Destroy)(/* unknown */);
};

사용 코드가 나옵니다.

wslapi!WslSession::_GetDistroId:
00007ff9`b9ff4ef0 4053            push    rbx
00007ff9`b9ff4ef2 4883ec30        sub     rsp,30h
00007ff9`b9ff4ef6 4c8b5108        mov     r10,qword ptr [rcx+8] // r10 == pCreateInfo->f1.pSession;
00007ff9`b9ff4efa 4c8bca          mov     r9,rdx // r9 == pField1 // 94eb3ff908
00007ff9`b9ff4efd 488b11          mov     rdx,qword ptr [rcx] // rdx == 267bd5a57e0 == "Ubuntu20.04"
00007ff9`b9ff4f00 4533c0          xor     r8d,r8d // r8 = 0
00007ff9`b9ff4f03 498bca          mov     rcx,r10 // rcx = 267bd58ab98, [267bd58ab98] == pSession;
00007ff9`b9ff4f06 498b02          mov     rax,qword ptr [r10] // rax = pSesion.vptr
00007ff9`b9ff4f09 488b4030        mov     rax,qword ptr [rax+30h] // rax = pSession의 QueryState
00007ff9`b9ff4f0d ff15ad250000    call    qword ptr [wslapi!_guard_dispatch_icall_fptr (00007ff9`b9ff74c0)]
                                       // pSession->QueryState("Ubuntu20.04", 0, &pField1);
00007ff9`b9ff4f13 8bd8            mov     ebx,eax // 80070005
00007ff9`b9ff4f15 85c0            test    eax,eax
00007ff9`b9ff4f17 791d            jns     wslapi!WslSession::_GetDistroId+0x46 (00007ff9`b9ff4f36)
00007ff9`b9ff4f19 488b4c2438      mov     rcx,qword ptr [rsp+38h]
00007ff9`b9ff4f1e 4c8d05bb2a0000  lea     r8,[wslapi!`string' (00007ff9`b9ff79e0)]
00007ff9`b9ff4f25 448bc8          mov     r9d,eax
00007ff9`b9ff4f28 ba67000000      mov     edx,67h
00007ff9`b9ff4f2d e836cbffff      call    wslapi!wil::details::in1diag3::Return_Hr (00007ff9`b9ff1a68)
00007ff9`b9ff4f32 8bc3            mov     eax,ebx
00007ff9`b9ff4f34 eb02            jmp     wslapi!WslSession::_GetDistroId+0x48 (00007ff9`b9ff4f38)
00007ff9`b9ff4f36 33c0            xor     eax,eax
00007ff9`b9ff4f38 4883c430        add     rsp,30h
00007ff9`b9ff4f3c 5b              pop     rbx
00007ff9`b9ff4f3d c3              ret

여기까지 보면 QueryState의 signature를 이렇게 정의할 수 있는데요,

STDMETHOD(QueryState)(const wchar_t* distroName, int unknownField, __int64 *pField);

마지막 인자에 대한 힌트를 lambda_invoker_cdecl의 WslSession::_GetDistroId에서 QueryState를 호출한 이후 다행히 그것을 다루는 코드에서 찾아볼 수 있습니다.

00007ff9`b9ff29c2 e829250000      call    wslapi!WslSession::_GetDistroId (00007ff9`b9ff4ef0)
                                        // WslSession::_GetDistroId(pCreateInfo, pField1);
00007ff9`b9ff29c7 85c0            test    eax,eax
00007ff9`b9ff29c9 7821            js      wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x8c (00007ff9`b9ff29ec)
00007ff9`b9ff29cb 488b442428      mov     rax,qword ptr [rsp+28h]
00007ff9`b9ff29d0 483b05514e0000  cmp     rax,qword ptr [wslapi!GUID_NULL (00007ff9`b9ff7828)]
00007ff9`b9ff29d7 750e            jne     wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x87 (00007ff9`b9ff29e7)
00007ff9`b9ff29d9 488b442430      mov     rax,qword ptr [rsp+30h]
00007ff9`b9ff29de 483b054b4e0000  cmp     rax,qword ptr [wslapi!GUID_NULL+0x8 (00007ff9`b9ff7830)]
00007ff9`b9ff29e5 7405            je      wslapi!<lambda_d62d766b87cfe9fe43d92b02b0dda941>::<lambda_invoker_cdecl>+0x8c (00007ff9`b9ff29ec)
00007ff9`b9ff29e7 bb01000000      mov     ebx,1

GUID_NULL이 나오죠. ^^ 따라서 이렇게 마무리할 수 있습니다.

STDMETHOD(QueryState)(const wchar_t* distroName, int unknownField, _In_ _Out_ IID* pGuid);

아울러, 이 값이 GUID라는 것에서 이전에 사용한 StartDefaultInstance의 사용법이 잘못되었음을 인지하게 됩니다.

hr = pSession->StartDefaultInstance(__uuidof(ILxInstance), (PVOID *)&pInstance);
printf("StartDefaultInstance hr == %d(0x%x)\n", hr, hr);

__uuidof(ILxInstance)가 아마도 QueryState로부터 받아온 GUID 값으로 보입니다.

IID instanceId;
hr = pSession->QueryState(L"Ubuntu20.04", 0, &instanceId);
hr = pSession->StartDefaultInstance(instanceId, (PVOID *)&pInstance);

(그래도 crash되는 것은 마찬가지입니다. 아마도 다른 인자가 있을 듯합니다.)




마지막으로, 이것을 이용해 다음과 같이 WslIsDistributionRegistered 함수를 QueryState로 다시 작성할 수 있습니다.

#include <Windows.h>
#include <stdio.h>
#include "lxssmanager.h"

HRESULT OleMain();
BOOL WslIsDistributionRegistered(ILxssSession* pSession, const wchar_t* distroName);

int main()
{

    HRESULT hr = CoInitializeEx(nullptr, 0);
    if (hr != S_OK)
    {
        return 1;
    }

    hr = OleMain();

    CoUninitialize();

    return (hr == S_OK) ? 0 : hr;
}

HRESULT OleMain()
{
    HRESULT hr = S_FALSE;

    hr = CoInitializeSecurity(nullptr,
        -1,
        nullptr,
        nullptr,
        RPC_C_AUTHN_LEVEL_DEFAULT,
        RPC_C_IMP_LEVEL_IMPERSONATE,
        nullptr,
        EOAC_STATIC_CLOAKING,
        nullptr);

    if (hr != S_OK)
    {
        return hr;
    }

    ILxssSession* pSession = nullptr;

    do
    {
        hr = CoCreateInstance(CLSID_LxssManager, nullptr, CLSCTX_LOCAL_SERVER, 
            __uuidof(ILxssSession), (PVOID*)&pSession);
        if (hr != S_OK)
        {
            break;
        }

        // -2147024891 == 0x80070005 E_ACCESSDENIED (Access is denied.)
        // -2147024894 == 0x80070002 (The system cannot find the file specified.)
        // -2147024809 == 0x80070057 E_INVALIDARG (The parameter is incorrect.)
        // -2147220734 == 0x80040302

        printf("IsRegistered: %d\n", WslIsDistributionRegistered(pSession, L"Ubuntu20.04"));

    } while (false);

    if (pSession != nullptr)
    {
        pSession->Release();
    }

    return hr;
}

BOOL WslIsDistributionRegistered(ILxssSession* pSession, const wchar_t* distroName)
{
    IID iid;
    HRESULT hr = pSession->QueryState(distroName, 0, &iid);

    if (hr != S_OK)
    {
        return FALSE;
    }

    /*
    OLECHAR* guidString;
    StringFromCLSID(iid, &guidString);

    printf("%ls\n", guidString);
    CoTaskMemFree(guidString);
    */

    return TRUE;
}

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




그나저나, 이런 어려움은 언젠가 마이크로소프트 측에서 (c:\windows\system32\lxss\)lxssmanager.dll에 대한 Type Library를 배포하면 자연스럽게 해결될 것입니다. 그런데 이게 그다지 희망적이지가 않습니다.

왜냐하면, lxssmanager.dll에서 제공하는 COM 개체의 호스팅 서비스를 찾아보면 마이크로소프트가 이 부분을 얼마나 비공개로 두고 싶은지 느낄 수 있기 때문입니다. 심심한데 이것도 기록을 남겨 볼까요? ^^

우선 lxssmanager.dll의 export 함수에서,

ServiceMain
SvchostPushServiceGlobals
DllCanUnloadNow
DllGetClassObject
InprocRegister
InprocUnregister

DllCanUnloadNow
DllGetClassObject

svchost.exe로 호스팅되는 서비스 프로세스가 아닐까 예상을 하게 되는데요, 실제로 레지스트리에는 다음과 같은 서비스 정보가 있습니다.

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LxssManager]
DependOnService: RPCSS staterepository
Description: @%systemroot%\\system32\\lxss\\LxssManager.dll,-101
DisplayName: @%systemroot%\\system32\\lxss\\LxssManager.dll,-100
ErrorControl=0x00000001
Group: LxssManagerGroup
ImagePath: %systemroot%\system32\svchost.exe -k netsvcs -p
LaunchProtected: 0x00000002
ObjectName: LocalSystem
RequiredPrivileges: SeChangeNotifyPrivilege SeDebugPrivilege SeImpersonatePrivilege SeAssignPrimaryTokenPrivilege SeLoadDriverPrivilege SeTcbPrivilege
ServiceSidType: 0x00000001
Start: 0x00000003
Type: 0x00000020

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LxssManager\parameters]
ServiceDll: %SystemRoot%\system32\lxss\LxssManager.dll
ServiceDllUnloadOnStop: 0x00000001

"C:\Windows\System32\lxss\en-US\LxssManager.dll.mui" 리소스 파일의 Strings 테이블에 따라,

100: LXSSMANAGER
101: The LXSS Manager service supports running native ELF binaries. The service provides the infrastructure necessary for ELF binaries to run on Windows. If the service is stopped or disabled, those binaries will no longer run.
102: LxssManagerUser

DisplayName은 LXSSMANAGER이므로 서비스 관리자에서 해당 이름과 "Path to executable"을 보면 다음과 같이 잘 나와 있습니다.

Path to executable: C:\WINDOWS\system32\svchost.exe -k netsvcs -p
Status: Running

오호~~~ 그럼 분명히 실행 중이기 때문에 process explorer를 통해 lxssmanager.dll을 로딩한 프로세스를 검색할 수 있을 것입니다. 그런데 애석하게도 검색이 안 됩니다. 할 수 없이, 작업 관리자를 이용해 프로세스 실행/중지를 반복하며 어떤 svchost.exe인지 애써 찾아보면 Process Explorer에서조차 DLL 로딩 정보를 볼 수 없는 특별한 프로세스가 나옵니다.

lxssmanager_service_1.png

찾긴 찾았지만, 관리자 권한의 windbg로 attach시켜도 권한이 없다는 오류 메시지(Could not attach to process 9840, Win32 error 0n5 Access is denied)와 함께 더 이상 할 수 있는 것이 아무것도 없습니다. ^^;

이 정도로 보안에 신경 쓴다는 것은, Type Library를 공개하지 않을 충분한 이유가 있는 걸로 보입니다. ^^;




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 6/17/2021]

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)
12500정성태1/21/2021976.NET Framework: 1014. ASP.NET Core(Kestrel)의 HTTP/2 지원 여부파일 다운로드1
12499정성태1/20/20211219.NET Framework: 1013. .NET Core Kestrel 호스팅 - 포트 변경, non-localhost 접속 지원 및 https 등의 설정 변경파일 다운로드1
12498정성태1/20/20211038.NET Framework: 1012. .NET Core Kestrel 호스팅 - 비주얼 스튜디오의 Kestrel/IIS Express 프로파일 설정
12497정성태1/20/20211323.NET Framework: 1011. C# - OWIN Web API 예제 프로젝트 [1]파일 다운로드2
12496정성태1/19/20211220.NET Framework: 1010. .NET Core 콘솔 프로젝트에서 Kestrel 호스팅 방법 [1]
12495정성태1/19/20211655웹: 40. IIS의 HTTP/2 지원 여부 - h2, h2c [1]
12494정성태1/19/20211367개발 환경 구성: 522. WSL2 인스턴스와 호스트 측의 Hyper-V에 운영 중인 VM과 네트워크 연결을 하는 방법 [2]
12493정성태1/18/20211113.NET Framework: 1009. .NET 5에서의 네트워크 라이브러리 개선 (1) - HTTP 관련 [1]파일 다운로드1
12492정성태1/17/2021921오류 유형: 695. ASP.NET 0x80131620 Failed to bind to address
12491정성태1/16/20211216.NET Framework: 1008. 배열을 반환하는 C# COM 개체의 메서드를 C++에서 사용 시 메모리 누수 현상 [1]파일 다운로드1
12490정성태1/15/20211399.NET Framework: 1007. C# - foreach에서 열거 변수의 타입을 var로 쓰면 object로 추론하는 문제 [1]파일 다운로드1
12489정성태1/13/20211779.NET Framework: 1006. C# - DB에 저장한 텍스트의 (이모티콘을 비롯해) 유니코드 문자가 '?'로 보인다면?
12488정성태1/13/20211363.NET Framework: 1005. C# - string 타입은 shallow copy일까요? deep copy일까요?파일 다운로드1
12487정성태1/13/20211070.NET Framework: 1004. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법파일 다운로드1
12486정성태1/12/20211212.NET Framework: 1003. x64 환경에서 참조형의 기본 메모리 소비는 얼마나 될까요?
12485정성태1/11/20211478Graphics: 38. C# - OpenCvSharp.VideoWriter에 BMP 파일을 1초씩 출력하는 예제파일 다운로드1
12484정성태1/9/20211534.NET Framework: 1002. C# - ReadOnlySequence<T> 소개파일 다운로드1
12483정성태1/8/20211130개발 환경 구성: 521. dotPeek - 훌륭한 역어셈블 소스 코드 생성 도구
12482정성태1/8/20211105.NET Framework: 1001. C# - 제네릭 타입/메서드에서 사용 시 경우에 따라 CS8377 컴파일 에러
12481정성태1/7/20211110.NET Framework: 1000. C# - CS8344 컴파일 에러: ref struct 타입의 사용 제한 메서드파일 다운로드1
12480정성태1/6/20211522.NET Framework: 999. C# - ArrayPool<T>와 MemoryPool<T> 소개파일 다운로드1
12479정성태1/6/20211070.NET Framework: 998. C# - OWIN 예제 프로젝트 만들기
12478정성태1/5/20211323.NET Framework: 997. C# - ArrayPool<T> 소개파일 다운로드1
12477정성태1/5/20212454기타: 79. github 코드 검색 방법 [1]
12476정성태1/5/20211323.NET Framework: 996. C# - 닷넷 코어에서 다른 스레드의 callstack을 구하는 방법파일 다운로드1
12475정성태1/5/20211386.NET Framework: 995. C# - Span<T>와 Memory<T> [1]파일 다운로드1
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...