목차

0. 개요

1. SEH

2. PEB

3. DLL 및 API 함수 주소 얻기

4. 안티디버깅

5. 기타

7. 참고





0. 개요

  32비트 윈도우의 유저 모드 기준으로 FS 레지스터는 TEB(Thread Environment Block) 또는 TIB(Thread Information Block)이라고 불리는 구조체를 가리킨다. 이 구조체는 현재 실행 중인 스레드에 대한 정보를 저장하고 있다. 주로 API 함수를 호출하지 않고도 정보를 얻을 수 있기 때문에 이러한 목적으로 사용된다. 참고로 FS 레지스터는 TEB의 첫 번째 주소를 가리키므로 여기에 위치만큼 값을 더해서 원하는 필드에 접근할 수 있다.


  그리고 마찬가지로 PEB(Process Environment Block)를 살펴보도록 하겠다. PEB의 경우 몇 개의 필드는 문서화되어 있다. 바로 참조할 수는 없고 FS 레지스터를 이용하여 TEB에서 PEB의 주소를 얻은 후에 사용된다.


  아래에는 자주 사용되는 부분들을 정리할 것이며 차례대로 어떻게 보여지는지와 어떻게 사용되는지에 대하여 예제와 함께 설명하도록 하겠다.


FS:[0x00] : 현재 SEH 프레임

FS:[0x18] : TEB

FS:[0x20] : PID

FS:[0x24] : TID

FS:[0x30] : PEB

FS:[0x34] : Last Error Value


  참고로 위를 보면 오프셋 0x18의 주소가 TEB 자체를 가리키는 것을 볼 수 있다. 즉 만약 현재 스레드에서 TEB의 위치가 0x00379000인 경우 0x00379018에는 0x00379000이 들어있는 것이다.


  x64 환경에서는 FS 레지스터 대신 GS 레지스터가 사용된다.


GS:[0x30] : TEB

GS:[0x40] : PID

GS:[0x48] : TID

GS:[0x60] : PEB


  궁극적으로 이 문서의 목적은 리버스 엔지니어링을 수행하면서 TEB 또는 PEB를 참조하는 루틴을 봤을 때 직접 찾아볼 필요 없이 정리된 내용을 제공하는 것이다. 물론 모든 것을 제공하지는 않지만 자주 사용되는 것들과 어떻게 사용되는지에 대한 내용을 쓰도록 하겠다.





1. SEH

  FS:[0x00] 다시 말해서 fs:0은 현재 SEH 핸들러의 주소를 나타낸다. SEH는 안티 디버깅 기법으로 사용되기도 하므로 아마 가장 많이 보이는 형태 중 하나일 것이다. 예외 처리 관련 내용은 다음의 문서를 [ http://sanseolab.tistory.com/16 ] 통해 더 공부하기로 하고 여기서는 간단하게만 정리하겠다. 예외 발생 시 SEH 체인의 Top 부터 찾아나가는데 이 주소가 FS:[0x0]에 들어있는 것이다. 물론 SEH 프레임의 구조 상 핸들러의 주소 및 다음 핸들러의 주소를 가리키는 포인터로 구성되어 있기 때문에 SEH를 참조할 때는 이렇게 Top의 주소만 알면 충분할 것이다.


PUSH 0040184B

XOR EAX, EAX

PUSH DWORD PTR FS:[EAX]

MOV DWORD PTR FS:[EAX], ESP


  또는 다른 방식으로 사용될 수도 있다.


PUSH 0040184B

PUSH DWORD PTR FS:[0]    ; [FS:00000000] = [00379000] = 0019FFCC

..

MOV DWORD PTR FS:[0], EAX


  위는 Ollydbg로 본 SEH 핸들러 설치 루틴이다. 핸들러의 주소를 PUSH한 후 SEH 체인의 Top을 나타내는 주소를 FS[0x0]을 통해 얻어와서 PUSH한다. 왜냐하면 현재 SEH 핸들러가 체인의 Top이 될 것이기 때문에 이전에 Top이었던 SEH 프레임을 가리켜야 하기 때문이다. 물론 마지막으로 방금 설치한 SEH 프레임의 주소를 다시 FS:[0x0]에 넣어서 Top은 방금 설치한 SEH 프레임을 가리키게 된다. 참고로 FS 레지스터의 주소로는 FS:[0]처럼 직접 0을 넣을 수도 있고 XOR EAX, EAX 명령을 통해 EAX를 0으로 초기화한 후에 FS:[EAX] 형태를 사용할 수도 있다. 참고로 x64에서는 예외 처리 시에 이러한 메커니즘을 사용하지 않는다.





2. PEB

  PEB의 경우 뒤에서 꾸준히 나올 것이기 때문에 미리 정리하도록 한다. PEB는 직접 참조할 수 없고 32비트의 경우 FS 레지스터를 통해 TEB를 얻은 후 여기서 오프셋 0x30 즉 PEB 필드에 들어있는 값을 통하여 PEB의 주소를 얻는 방식이 사용된다.


MOV EAX, DWORD PTR FS:[18]

MOV EAX, DWORD PTR DS:[EAX+30]


  위에서는 먼저 FS:[18]을 통해 TEB의 주소를 직접 얻은 후 이 주소 즉 EAX에 오프셋 0x30만큼의 값을 더하여 PEB를 얻는다. 또는 다음과 같이 직접 사용할 수도 있다.


MOV EAX, DWORD PTR FS:[30]


  이제 PEB의 주소가 EAX에 들어가 있으므로 PEB의 필드들 또한 EAX에 주소를 더해가며 원하는 필드를 찾는 방법이 사용된다. 다음으로 64비트 환경에서 설명하겠다. 64비트에서는 FS 레지스터 대신 GS 레지스터가 사용되며 오프셋 0x30이 TEB 주소를 가진다. 또한 PEB는 TEB 기준 오프셋 0x60에 위치한다.


MOV RAX, QWORD PTR GS:[30]

MOV RAX, QWORD PTR DS:[RAX+60]


  이것도 아래와 같이 직접 사용할 수 있다.


MOV RAX, QWORD PTR GS:[60]


  x86 기준 자주 사용되는 PEB의 멤버들은 다음과 같다.


0x002 BYTE BeingDebugged;

0x008 void* ImageBaseAddress;

0x00C _PEB_LDR_DATA* Ldr;

0x018 void* ProcessHeap

0x064 DWORD NumberOfProcessors;

0x068 DWORD NtGlobalFlag;





3. DLL 및 API 함수 주소 얻기

  일반적으로 프로그램은 개발자는 모든 기능을 구현할 필요 없이 제공되는 함수들을 사용할 것이다. 사용하는 함수가 써드파티에서 제공되는 라이브러리에 포함된 것이라고 하더라도 내부적으로는 결국 운영체제에서 제공해주는 API 함수를 이용하게 된다. 물론 악성코드들 같이 직접 API 함수들을 이용하여 개발된 프로그램도 많다. 어쨌든 사용하는 함수들이 들어있는 라이브러리를 정적으로 링크하지 않는 이상 DLL을 임포트하고 필요한 함수들의 주소를 얻어와 호출한다. 이쪽 내용을 위해서는 PE 그리고 임포트 테이블 같이 기본적인 내용이 당연히 필요하며 여기서는 이러한 내용들을 숙지하고 있다는 가정 하에 적도록 한다.


  우리는 악성코드 분석 시에 악성코드가 임포트하고 있는 함수들을 통해 어떠한 방식으로 동작할지를 예측할 수 있으며 정적 분석에서도 많이 사용된다. 물론 패킹된 경우에는 LoadLibrary()와 GetProcAddress()를 이용하여 사용할 API들을 실행 중에 임포트함으로써 이러한 정적 분석을 방해하기도 한다. 여기서는 위의 두 함수조차도 없이 즉 아무런 API 호출 없이 API 함수의 주소를 알아내는 방식을 설명한다. 이 방식은 셸코드에서 자주 사용되는 것으로 보이며 PEspin이라는 프로텍터에서도 비슷한 메커니즘이 사용되었다.


  다음 링크에 [ http://ezbeat.tistory.com/283 ] 관련 내용이 매우 자세하게 나와있으며 여기서는 간단하게 설명하도록 한다. PEB의 Ldr이라는 필드는 PEB_LDR_DATA 구조체를 가리키는 포인터로서 PEB에서 오프셋 0xC에 위치한다. 이 구조체는 로드된 모듈에 대한 정보를 제공하는데 0x1C만큼 떨어진 필드는 InInitializationOrderModuleList 즉 PEB->Ldr.InInitializationOrderModuleList.Flink로서 초기화된 모듈 순서로 구성된 리스트(_LIST_ENTRY)를 가리킨다. 리스트를 구성하는 각 구조체는 _LDR_MODULE로서 첫 번째 필드는 NextModule이며 다음 리스트를 가리킨다. 오프셋 0x8에 위치 필드는 해당 모듈의 BaseAddress이며 오프셋 0x20은 유니코드 문자열로 저장된 해당 모듈의 이름에 대한 포인터이다.


  이제 위의 내용을 가지고 차례대로 관련 루틴을 분석해 보겠다.


MOV EAX, DWORD PTR FS:[30]

; EAX에는 PEB의 주소가 들어가 있다.

MOV EAX, DWORD PTR DS:[EAX+0C]

; EAX에는 PEB_LDR_DATA 구조체의 주소가 들어가 있다.

MOV ESI, DWORD PTR DS:[EAX+1C]

; ESI는 PEB->Ldr.InInitializationOrderModuleList.Flink의 주소이다.

MOV EAX, PTR DS:[ESI+20]

; EAX에는 해당 유니코드 문자열 형태를 가진 해당 모듈의 이름에 대한 주소가 들어간다. 이 이름을 가지고 비교하여 원하는 DLL을 찾을 수 있을 것이다.


MOV ESI, PTR DS:[ESI]

MOV EAX, PTR DS:[ESI+20]

; 참고로 ESI가 PEB->Ldr.InInitializationOrderModuleList.Flink인 경우에 해당 주소에는 리스트의 다음 _LDR_MODULE를 가리키는 값이 들어가 있다. 그러므로 위와 같이 사용하면 ESI는 새로운 모듈을 가리키게 된다. 이후 0x20만큼을 더하여 새로운 모듈의 이름에 대한 주소를 얻을 수 있다. 즉 해당 주소를 저장하고 반복문을 통해서 리스트의 다음으로 계속 이동하면서 이름을 구할 수 있는 것이다.


  이러한 방식과 반복문을 통하여 우리가 원하는 DLL의 이름을 구할 수 있을 것이고 이름이 동일하다면 BaseAddress를 구해야 한다. 이것은 다음 명령어를 통해 구할 수 있다.


MOV EAX, PTR DS:[ESI+8]

; ESI가 PEB->Ldr.InInitializationOrderModuleList.Flink인 경우 BaseAddress는 오프셋 0x8이다. 참고로 오프셋 0x20은 이름을 나타내는 문자열에 대한 포인터였다.


  이제 해당 모듈의 BaseAddress를 구했으니 마지막으로 Export Table을 찾은 후 AddressOfNames 테이블에서 찾는 API 함수의 이름을 구한 후 AddressOfFunctions 테이블에서 API 함수의 주소를 구한다. 그리고 RVA 형태의 주소를 VA 형태로 변환하면 해당 API 함수의 주소를 구하게 된다.



3.1 구조체들에 대한 정리

  그림으로 만드려고 했지만 검색을 위해서 보기 불편하더라도 다음과 같이 표시하기로 한다. 기본적인 그림은 위의 링크를 참고하여 보면 이해하기 쉬울 것이다. 아래의 것들은 문서화되어 있지는 않지만 윈도우 XP SP3와 윈도우 10에서 확인한 결과 동일해서 찾은 내용을 정리하려고 한다.



TEB

0x30 : PEB


PEB

0x0C : PEB_LDR_DATA


PEB_LDR_DATA

0x0C : ?? ( InLoadOrderModuleList )

0x14 : ?? ( InMemoryOrderModuleList )

0x1C : LDR_MODULE ( InInitializationOrderModuleList )



  먼저 가장 많이 사용되는 InInitializationOrderModuleList를 기반으로 설명하겠다.


LDR_MODULE ( InInitializationOrderModuleList )

( ntdll.dll  ->  kernelbase.dll  ->  kernel32.dll )

0x00 : Next Module

0x04 : Previous Module

0x08 : ImgBase

0x0C : EP

0x10 : Size of Img

0x20 : Name



  TEB에서 오프셋 0x30은 PEB를 의미한다. PEB에서 오프셋 0x0C는 PEB_LDR_DATA 구조체를 의미한다. PEB_LDR_DATA 구조체에서 오프셋 0x1C는 InInitializationOrderModuleList를 가리킨다.

정확히 말하자면 어떠한 구조체들이 특정 목적에 따라 링크드 리스트로 연결되어 있다. 링크드 리스트로 연결된 각 구조체들은 이름과 완벽한 구조는 찾지 못했지만 위의 링크에서는 LDR_MODULE로 나와있어서 그대로 사용하기로 한다.


  어쨌든 각 구조체들은 이름에 따르면 모듈들의 초기화된 순서에 따른 것으로 확인할 수 있고, 

직접 살펴보면 ntdll, kernelbase, kernel32 순이다. 참고로 이것은 윈도우 7 이상이며 윈도우 xp에서는 ntdll 다음이 kernel32이다. 악성코드에서 주로 사용하는 구조체의 멤버는 아마도 Name(0x20)과 ImageBase(0x08)일 것이다.


  앞의 것이 대표적으로 사용되지만 PEB_LDR_DATA의 오프셋 0x0C, 0x14도 사용할 수 있다. 0x0C 즉 InLoadOrderModuleList부터 보겠다. 이 구조체는 이름을 알 수 없어서 물음표로 표시하였다.


?? ( InLoadOrderModuleList )

( 바이너리  ->  ntdll.dll  ->  kernel32.dll  ->  kernelbase.dll )

0x00 : Next Module

0x04 : Previous Module

0x18 : ImgBase

0x1C : EP

0x20 : Size of Img

0x30 : Name


  이것은 이전의 것과는 달리 로드 순서라서 그런지 바이너리가 가장 먼저 있는 것을 확인할 수 있다.


  다음으로  0x14 즉 InMemoryOrderModuleList를 보겠다.


?? ( InMemoryOrderModuleList )

( 바이너리  ->  ntdll.dll  ->  kernel32.dll  ->  kernelbase.dll )

0x00 : Next Module

0x04 : Previous Module

0x10 : ImgBase

0x14 : EP

0x18 : Size of Img

0x20 : Path


  앞과 순서는 같아 보인다. 하지만 특징이 있는데 0x20의 값이 단순한 이름이 아니라 해당 모듈의 경로명이라는 점이다.





4. 안티디버깅

4.1 PEB.BeingDebugged

  IsDebuggerPresent() API 함수는 PEB의 BeingDebugged 필드를 사용한다. 이 필드는 오프셋 0x2에 위치한 바이트 값으로서 현재 디버깅 중이라면 0x1을 갖으며 일반적인 경우에는 0x0이 설정되어 있다. 이 함수가 반환하는 값도 마찬가지이다. 참고로 직접 라이브러리를 확인해보면 알겠지만 단지 3줄의 어셈블리 명령어를 갖는다. 그렇기 때문에 API를 호출하는 대신 직접 구현하는 방식이 사용되는 경우가 있다고도 한다. 32비트에서 해당 루틴은 다음과 같다.


MOV EAX, DWORD PTR FS:[30]

MOVZX EAX, BYTE PTR DS:[EAX+2]

RETN


  아래는 64비트에서의 루틴이다.


MOV RAX, QWORD PTR GS:[60]

MOVZX EAX, BYTE PTR DS:[RAX+2]

RET



4.2 PEB.NtGlobalFlag

  이 필드는 일반적인 경우에 0x0이며 디버깅 시에 0x70으로 설정된다. 즉 FLG_HEAP_ENABLE_TAIL_CHECK (0x10), FLG_HEAP_ENABLE_FREE_CHECK(0x20), FLG_HEAP_VALIDATE_PARAMETERS(0x40)가 설정되는 것이다. 참고로 실행 중인 프로세스를 Attach한 경우에는 변경되지 않는다고 한다.


  32비트 환경의 경우 PEB에서 오프셋 0x68에 있으며 다음을 통해 검사할 수 있다.


MOV EAX, DWORD PTR FS:[30]

MOV AL, DWORD PTR DS:[EAX+68]

AND AL, 70h

CMP AL, 70h

JE being_debugged


  64비트의 환경의 경우 PEB에서 오프셋 0xBC에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV RAX, QWORD PTR GS:[60]

MOV AL, BYTE PTR DS:[RAX+BC]

AND AL, 70h

CMP AL, 70h

JE being_debugged


  64비트 윈도우에서 32비트 애플리케이션 실행 시 즉 Wow64를 통해 실행 중인 경우에는 각각에 대한 PEB가 존재한다. x64 TEB는 x86 TEB보다 0x2000만큼 아래에 위치하므로 이만큼을 뺸 후에 0x60만큼 더하여 x64 PEB를 얻을 수 있다. 그리고 NtGlobalFlag 필드는 위와 마찬가지로 오프셋 0xBC에 위치한다.


MOV EAX, DWORD PTR FS:[18]

SUB EAX, 2000h

MOV EAX, DWORD PTR DS:[EAX+60]

MOV AL, BYTE PTR DS:[EAX+BC]

AND AL, 70h

CMP AL, 70h

JE being_debugged



4.3 PEB.ProcessHeap

  이 필드는 힙 구조체를 가리킨다. 참고로 GetProcessHeap() API도 이런 역할을 하는데 해당 함수의 루틴을 보겠다. 먼저 32비트 환경의 경우 PEB에서 오프셋 0x18에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]


  64비트 환경의 경우 PEB에서 오프셋 0x30에 위치해 있으며 다음을 통해 검사할 수 있다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]


  중요한 것은 이 구조체의 멤버들 중에서 Flags와 ForceFlags이다. 프로세스가 디버깅 중이지 않을 경우 Flags는 0x2를 ForceFlags는 0x0의 값을 갖는다. 참고로 비스타 이후로 조금 달라졌는데 여기서는 비스타 이후의 버전만 보도록 한다. 


  먼저 Flags 필드를 보겠다. 디버거가 존재할 때 Flags 필드는 일반적으로 아래의 플래그들의 조합으로 설정된다.


HEAP_GROWABLE (2)

HEAP_TAIL_CHECKING_ENABLED (0x20)

HEAP_FREE_CHECKING_ENABLED (0x40)

HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)


  32비트 환경의 경우 PEB에서 오프셋 0x18에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x40에 위치한 것이 Flags 필드이다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]

MOV EAX, DWORD PTR DS:[EAX+40]


  EAX를 비교하여 0x2보다 큰 경우 디버깅 중으로 판단할 수 있다. 64비트 환경의 경우 PEB에서 오프셋 0x30에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x70에 위치한 것이 Flags 필드이다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]

MOV EAX, DWORD PTR DS:[RAX+70]


  다음으로 ForceFlags 필드를 보자. 디버거가 존재할 때 ForceFlags 필드는 일반적으로 이 플래그들의 조합으로 설정된다.


HEAP_TAIL_CHECKING_ENABLED (0x20)

HEAP_FREE_CHECKING_ENABLED (0x40)

HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)


  32비트 환경의 경우 PEB에서 오프셋 0x18에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x44에 위치한 것이 ForceFlags 필드이다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+18]

MOV EAX, DWORD PTR DS:[EAX+44]


  EAX를 비교하여 0x0보다 큰 경우 디버깅 중으로 판단할 수 있다. 64비트 환경의 경우 PEB에서 오프셋 0x30에 위치한 ProcessHeap를 구한 후 여기에서 오프셋 0x74에 위치한 것이 ForceFlags 필드이다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+30]

MOV EAX, DWORD PTR DS:[RAX+74]



4.4 기타

  TEB를 통해서 프로그램의 PID 등의 정보를 쉽게 구할 수 있으므로 이러한 것들을 인지하고 있다면 분석에 도움이 될 수 있다. 안티 디버깅 루틴을 직접 구현한 경우 특정 함수를 호출할 때 인자로 PID가 필요할 때 TEB를 통해 쉽게 값을 얻어올 수 있어서 이러한 방식으로 자주 사용되기 때문이다. 


  이렇게 간접적으로 사용되는 방식으로서 TEB의 LastErrorValue도 있다. Kernel32.dll의 GetLastError() API 함수 대신 직접 사용할 수 있는 것이다. 비스타 이전에 사용되는 안티 디버깅 방식 중에서 OutputDebugString()을 이용하는 것이 있다. Last Error Value를 설정한 후 OutputDebugString()을 호출하는데 만약 디버거가 붙어있다면 해당 값은 변화가 없을 것이며 디버거가 붙어있지 않아서 실패한다면 거기에 해당하는 에러 코드가 Last Error Value에 설정되어 이전 값과 차이가 발생할 것이다. 안티 디버깅으로서 전과 후의 값을 비교하여 변화가 없을 때 디버깅 중으로 판단한다. Last Error Value를 설정/확인할 때 SetLastError()와 GetLastError()를 이용할 수도 있지만 직접 FS:[0x34]를 사용할 수도 있다. 


  마지막으로 PEB의 ImageBaseAddress를 통해 ImageBase 주소를 얻을 수 있다. 참고로 GetModuleHandle() API 함수는 특정 모듈의 ImageBase 주소를 반환해주는 함수인데 인자로 NULL을 넣은 경우에는 현재 실행 중인 모듈의 ImageBase 주소를 반환해준다. 즉 32비트의 경우 내부를 보면 인자로 NULL 즉 0이 들어올 때 실행되는 부분은 다음과 같으며 PEB에서 오프셋 0x8에 위치한다.


MOV EAX, DWORD PTR FS:[30]

MOV EAX, DWORD PTR DS:[EAX+8]


  64비트의 경우 PEB에서 오프셋 0x10에 위치한다.


MOV RAX, QWORD PTR GS:[60]

MOV RAX, QWORD PTR DS:[RAX+10]





5. 기타

  먼저 올리디버거에서 관련 내용을 보겠다. 올리디버거의 메뉴 중에서 "Memory map"을 선택하고 확인해보면 Contains에 설명이 "Data block of main thread"라고 적혀있는 영역이 보일 것이다. 이곳이 TEB의 위치이며 해당 라인을 더블클릭해 보면 자세한 내용을 볼 수 있다. 찾지 못한 것인지 모르겠지만 PEB에서는 특별한 설명이 제공되지는 않는 것 같다. 참고로 올리디버거로 분석할 때 처음 화면에서 EBX를 보면 PEB 주소가 들어있는 것을 확인할 수 있다.


  Windbg의 경우 !teb와 !peb라는 명령어가 제공된다. 이름과 같이 TEB 및 PEB에 대한 정보를 보여주는 명령어이다. 참고로 의사 레지스터로도 제공되는데 $proc은 PEB를 의미하며, $thread는 TEB를 의미한다.





7. 참고

* The "Ultimate" Anti-Debugging Reference - Peter Ferrie

* 리버싱 핵심원리 - 이승원

* http://ezbeat.tistory.com/283

* TEB x86 : [ https://www.aldeid.com/wiki/TEB-Thread-Environment-Block ]

* PEB x86 : [ https://www.aldeid.com/wiki/PEB-Process-Environment-Block ]

'악성코드 분석' 카테고리의 다른 글

COM, OLE, .NET Frame work 등의 개념 및 사용  (0) 2017.12.18
exeinfo PE 사용법  (1) 2017.12.17
EFLAGS 상태 레지스터  (0) 2017.11.22
USB 악성코드 분석  (0) 2017.11.09
윈도우에서 스크립트 악성코드  (0) 2017.11.02
Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.3
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31

최근에 올라온 글

최근에 달린 댓글

글 보관함