Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

windbg/sos - Hashtable의 buckets 배열 내용을 모두 덤프하는 방법 (do_hashtable.py)

windbg에서 프로그램을 분석할 때 Hashtable이 나오면 기본 windbg + sos 명령어로는 분석이 (어려운 게 아니고) 지겹습니다. 예를 하나 들어 볼까요? ^^

using System;
using System.Collections;

class Program
{
    static unsafe void Main(string[] args)
    {
        Hashtable hash = new Hashtable();
        hash["TEST"] = "1abc";
        hash["qwer"] = "2def";

        TypedReference tr = __makeref(hash);
        IntPtr ptr = **(IntPtr**)(&tr);

        Console.WriteLine(ptr.ToInt64().ToString("x")); // 1bfd8672f40

        Console.WriteLine(hash);

        Console.WriteLine("debug this...");
        Console.ReadLine();
    }
}

저렇게 TypedReference를 활용하면 객체의 GC Heap 위치(주소)를 알 수 있습니다. 물론 현실적으로 메모리 덤프 파일을 분석하는 경우에는 직접 저 주소를 구해야 하지만,

windbg - x64 덤프 분석 시 메서드의 인자 또는 로컬 변수의 값을 확인하는 방법
; https://www.sysnet.pe.kr/2/0/12069

이번에는 어찌어찌 구했다고 가정하고 진행해 보겠습니다. 일단 windbg로 (1bfd8672f40로 출력된) 객체를 덤프해 보면,

0:005> .loadby sos clr

0:005> !do 1bfd8672f40
Name:        System.Collections.Hashtable
MethodTable: 00007ffdc3079118
EEClass:     00007ffdc3184b88
Size:        80(0x50) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffdc3074628  4001830        8 ...ashtable+bucket[]  0 instance 000001bfd8672f90 buckets
00007ffdc30780f8  4001831       30         System.Int32  1 instance                2 count
00007ffdc30780f8  4001832       34         System.Int32  1 instance                1 occupancy
00007ffdc30780f8  4001833       38         System.Int32  1 instance                2 loadsize
00007ffdc307e7f0  4001834       3c        System.Single  1 instance 0.720000 loadFactor
00007ffdc30780f8  4001835       40         System.Int32  1 instance                2 version
00007ffdc307e598  4001836       44       System.Boolean  1 instance                0 isWriterInProgress
00007ffdc3079aa8  4001837       10 ...tions.ICollection  0 instance 0000000000000000 keys
00007ffdc3079aa8  4001838       18 ...tions.ICollection  0 instance 0000000000000000 values
00007ffdc2fede18  4001839       20 ...IEqualityComparer  0 instance 0000000000000000 _keycomparer
00007ffdc3075f88  400183a       28        System.Object  0 instance 0000000000000000 _syncRoot

Hashtable에 저장된 buckets 필드를 확인할 수 있습니다. 그리고 그 값을 다시 덤프해 보면,

0:005> !DumpObj /d 000001bfd8672f90
Name:        System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass:     00007ffdc3183448
Size:        96(0x60) bytes
Array:       Rank 1, Number of elements 3, Type VALUETYPE (Print Array)
Fields:
None

위와 같이 나오는데 원래 windbg에서는 "Debugger Markup Language" 덕분에 "Print Array"가 링크로 제공되므로 사용자는 이후의 작업을 그냥 "Print Array"를 눌러 다음과 같이 bucket [] 배열의 원소 값을 확인할 수 있습니다.

0:005> !DumpArray /d 000001bfd8672f90
Name:        System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass:     00007ffdc3183448
Size:        96(0x60) bytes
Array:       Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0

그러니까, 다음과 같은 식으로 링크를 따라 이동하므로 편리하게 디버깅을 할 수 있습니다.

dump_hashtable_1.png

그런데, 저 링크에서 배열 요소의 값을 확인하기 위해 링크를 타고 들어가도 "Fields:" 영역은 비어서 나옵니다.

0:005> !DumpVC /d 00007ffdc3074628 000001bfd8672fa0
Name:        System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass:     00007ffdc3183448
Size:        24(0x18) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:

왜냐하면, 올바르지 않게 DML이 구성되어 있기 때문입니다. !DumpVC 명령어는 원래 !DumpObject 명령과 출력은 비슷하지만 부가적으로 "MethodTable"을 함께 지정해 주소의 값을 해석하는 기능을 가집니다.

!dumpvc [methodtable] [object_address]

그런데 위의 DumpVC 명령어가 받아들인 methodtable == 00007ffdc3074628 값은 "System.Collections.Hashtable+bucket"이 아닌 "System.Collections.Hashtable+bucket[]" 타입을 가리키기 때문에 정상적으로 배열 요소의 값이 해석이 안 된 것입니다. 따라서 이것을 제대로 해석하려면 "!DumpArray"의 출력 결과에 "Element Methodtable: ...." 값이 나오는데 바로 그 값(예제의 경우 00007ffdc30746a8)이 System.Collections.Hashtable+bucket 타입의 MethodTable이므로 다음과 같이 명령을 내리면 요솟값을 확인할 수 있습니다.

0:005> !DumpVC /d 00007ffdc30746a8 000001bfd8672fa0
Name:        System.Collections.Hashtable+bucket
MethodTable: 00007ffdc30746a8
EEClass:     00007ffdc32b42c0
Size:        40(0x28) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffdc3075f88  4003483        0        System.Object  0 instance 000001bfd8672e48 key
00007ffdc3075f88  4003484        8        System.Object  0 instance 000001bfd8672e70 val
00007ffdc30780f8  4003485       10         System.Int32  1 instance      -1371534266 hash_coll

그리고 출력된 key나 val의 값을 덤프해 들어가면 저장한 값을 개별적으로 확인할 수 있습니다.

0:005> !DumpObj /d 000001bfd8672e48
Name:        System.String
MethodTable: 00007ffdc3075b70
EEClass:     00007ffdc2fd6808
Size:        34(0x22) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      TEST
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffdc30780f8  4000283        8         System.Int32  1 instance                4 m_stringLength
00007ffdc30769e8  4000284        c          System.Char  1 instance               54 m_firstChar
00007ffdc3075b70  4000288       e0        System.String  0   shared           static Empty
                                 >> Domain:Value  000001bfd69d5d50:NotInit  <<
0:005> !DumpObj /d 000001bfd8672e70
Name:        System.String
MethodTable: 00007ffdc3075b70
EEClass:     00007ffdc2fd6808
Size:        34(0x22) bytes
File:        C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String:      1abc
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffdc30780f8  4000283        8         System.Int32  1 instance                4 m_stringLength
00007ffdc30769e8  4000284        c          System.Char  1 instance               31 m_firstChar
00007ffdc3075b70  4000288       e0        System.String  0   shared           static Empty
                                 >> Domain:Value  000001bfd69d5d50:NotInit  <<




그런데, Hashtable에 보관한 값이 몇 개 이하라면 저런 식으로 일일이 확인을 해보겠지만 수십 개 이상이 되면 저렇게 확인하는 것이 매우 불편합니다. 따라서 이런 경우에는 pykd를 이용해,

windbg에서 python 스크립트 실행하는 방법 - pykd
; https://www.sysnet.pe.kr/2/0/11227

자동화할 수 있을 것입니다. 그전에, GC Heap 객체가 저장된 특성을 이용하면 pykd를 이용한 텍스트 파싱 작업이 꽤나(그나마) 간단해질 수 있습니다. 우선, DumpArray의 명령에서 본 것처럼,

0:005> !DumpArray /d 000001bfd8672f90
Name:        System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass:     00007ffdc3183448
Size:        96(0x60) bytes
Array:       Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0

bucket 타입은 struct 형이기 때문에 값의 요소가 GC Heap 메모리에 연속적으로 보관되어 있습니다. 따라서, bucket 타입의 3개 필드 값을 다음과 같은 식으로 한 라인에 요소 하나씩 출력할 수 있습니다.

0:005> dq /c3 000001d500002fa0 L9
000001d5`00002fa0  000001d5`00002e48 000001d5`00002e70 00000000`ae400c46
000001d5`00002fb8  00000000`00000000 00000000`00000000 00000000`00000000
000001d5`00002fd0  000001d5`00002e98 000001d5`00002ec0 00000000`34a6e611

즉, 첫 번째 요소([1] 000001bfd8672fa0)의 값은 key == 000001d5`00002e48, value == 000001d5`00002e70이 됩니다.

그리고 key와 value가 string 타입이라는 것을 알고 있으면, 우리는 string 객체가 GC Heap에 저장될 때 sizeof(int) * 3 만큼의 값이 Object Header에 해당한다는 것을 알고 있기 때문에 개별 문자열은 [주소 + 0x0c] 위치를 이용해 곧바로 덤프할 수 있습니다. 따라서 key, value의 문자열은 이렇게 구할 수 있습니다.

0:005> du 000001d5`00002e48+c
000001d5`00002e54  "TEST"

0:005> du 000001d5`00002e70+c
000001d5`00002e7c  "1abc"

이것을 종합하면, Hashtable의 요소가 key = string, value = string으로 이뤄져 있다면 다음의 python 스크립트를 이용해 일괄적으로 bucket 배열의 값을 덤프할 수 있습니다.

import argparse

from pykd import *

def getItem(text, index):
    result = text.split(" ")[index + 1].strip()
    return result

def isValidAddress(text):
    text = text.replace("`", "")
    if (int(text, 16) == 0):
        return False
    return True

def ToValidText(output):
    if (len(output.split("\"??????????")) >= 2):
        return "(non-string)"
    return output

def action(elem0Address, elemCount):
    totalCount = elemCount * 3
    outputText = pykd.dbgCommand("dq /c3 " + elem0Address + " L" + hex(totalCount))
    index = -1

    for line in outputText.splitlines():
        index = index + 1

        keyAddress = getItem(line, 1)
        valueAddress = getItem(line, 2)

        if (isValidAddress(keyAddress) == False or isValidAddress(valueAddress) == False):
            print("[" + str(index) + "] (empty)")
            continue

        keyText = pykd.dbgCommand("du " + keyAddress + "+c").split("  ")[1].strip()
        valueText = pykd.dbgCommand("du " + valueAddress + "+c").split("  ")[1].strip()

        keyText = ToValidText(keyText).split("\n")[0]
        valueText = ToValidText(valueText).split("\n")[0]

        dprintln("[" + str(index) + "] key: <link cmd=\"!do " + keyAddress + " \">" + keyText + "</link>, value:  <link cmd=\"!do " + valueAddress + " \">" + valueText + "</link>", True)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('address', type=str, help='hashtable.Element[0] address')        
    parser.add_argument('count', type=int, help='# of Element')        
    args = parser.parse_args()
    
    action(args.address, args.count)

if __name__ == "__main__":
    main()

다음은 위의 스크립트를 이용해 덤프한 결과입니다.

0:005> .load d:\pykd\x64\pykd.dll

0:005> !DumpArray /d 000001bfd8672f90
Name:        System.Collections.Hashtable+bucket[]
MethodTable: 00007ffdc3074628
EEClass:     00007ffdc3183448
Size:        96(0x60) bytes
Array:       Rank 1, Number of elements 3, Type VALUETYPE
Element Methodtable: 00007ffdc30746a8
[0] 000001bfd8672fa0
[1] 000001bfd8672fb8
[2] 000001bfd8672fd0

0:005> !py d:\pykd\do_hashtable.py 000001bfd8672fa0 3
[0] key: "TEST", value: "1abc"
[1] (empty)
[2] key: "qwer", value: "2def"

게다가 출력 결과에 DML을 사용했기 때문에 다음과 같이 개별 값에 대한 "!do"를 실행할 수 있습니다.

dump_hashtable_2.png




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

[연관 글]





[최초 등록일: ]
[최종 수정일: 12/21/2019 ]

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

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12115정성태1/14/2020545디버깅 기술: 157. C# - PEB.ProcessHeap을 이용해 디버깅 중인지 확인하는 방법파일 다운로드1
12114정성태1/13/2020755디버깅 기술: 156. C# - PDB 파일로부터 심벌(Symbol) 및 타입(Type) 정보 열거 [1]파일 다운로드3
12113정성태1/12/20201067오류 유형: 590. Visual C++ 빌드 오류 - fatal error LNK1104: cannot open file 'atls.lib' [1]
12112정성태1/12/2020440오류 유형: 589. PowerShell - 원격 Invoke-Command 실행 시 "WinRM cannot complete the operation" 오류 발생
12111정성태3/23/20201115디버깅 기술: 155. C# - KernelMemoryIO 드라이버를 이용해 실행 프로그램을 숨기는 방법(DKOM: Direct Kernel Object Modification) [11]
12110정성태6/23/2020734디버깅 기술: 154. Patch Guard로 인해 블루 스크린(BSOD)가 발생하는 사례파일 다운로드1
12109정성태1/10/2020601오류 유형: 588. Driver 프로젝트 빌드 오류 - Inf2Cat error -2: "Inf2Cat, signability test failed."
12108정성태1/10/2020482오류 유형: 587. Kernel Driver 시작 시 127(The specified procedure could not be found.) 오류 메시지 발생
12107정성태1/10/2020586.NET Framework: 877. C# - 프로세스의 모든 핸들을 열람 - 두 번째 이야기
12106정성태1/8/2020648VC++: 136. C++ - OSR Driver Loader와 같은 Legacy 커널 드라이버 설치 프로그램 제작 [1]
12105정성태1/8/2020541디버깅 기술: 153. C# - PEB를 조작해 로드된 DLL을 숨기는 방법
12104정성태1/9/2020915DDK: 9. 커널 메모리를 읽고 쓰는 NT Legacy driver와 C# 클라이언트 프로그램 [3]
12103정성태4/23/20201623DDK: 8. Visual Studio 2019 + WDK Legacy Driver 제작- Hello World 예제 [1]파일 다운로드2
12102정성태1/6/2020628디버깅 기술: 152. User 권한(Ring 3)의 프로그램에서 _ETHREAD 주소(및 커널 메모리를 읽을 수 있다면 _EPROCESS 주소) 구하는 방법
12101정성태1/8/2020652.NET Framework: 876. C# - PEB(Process Environment Block)를 통해 로드된 모듈 목록 열람
12100정성태1/3/2020447.NET Framework: 875. .NET 3.5 이하에서 IntPtr.Add 사용
12099정성태1/3/2020652디버깅 기술: 151. Windows 10 - Process Explorer로 확인한 Handle 정보를 windbg에서 조회
12098정성태1/2/2020608.NET Framework: 874. C# - 커널 구조체의 Offset 값을 하드 코딩하지 않고 사용하는 방법
12097정성태1/2/2020476디버깅 기술: 150. windbg - Wow64, x86, x64에서의 커널 구조체(예: TEB) 구조체 확인
12096정성태1/2/2020672디버깅 기술: 149. C# - DbgEng.dll을 이용한 간단한 디버거 제작
12095정성태12/27/2019805VC++: 135. C++ - string_view의 동작 방식
12094정성태12/26/2019679.NET Framework: 873. C# - 코드를 통해 PDB 심벌 파일 다운로드 방법
12093정성태12/26/2019836.NET Framework: 872. C# - 로딩된 Native DLL의 export 함수 목록 출력파일 다운로드1
12092정성태12/25/2019721디버깅 기술: 148. cdb.exe를 이용해 (ntdll.dll 등에 정의된) 커널 구조체 출력하는 방법
12091정성태12/25/2019996디버깅 기술: 147. pdb 파일을 다운로드하기 위한 symchk.exe 실행에 필요한 최소 파일 [1]
12090정성태12/24/2019674.NET Framework: 871. .NET AnyCPU로 빌드된 PE 헤더의 로딩 전/후 차이점
1  2  3  4  5  6  7  [8]  9  10  11  12  13  14  15  ...