[컴][Hack][디버그] disassembly code 분석 방법 - 3



RtlInsertElementGenericTable.


56DF4FE6  /. 55             PUSH EBP
56DF4FE7  |. 8BEC           MOV EBP,ESP
56DF4FE9  |. 57             PUSH EDI 56DF4FEA  |. 8B7D 08        MOV EDI,DWORD PTR SS:[EBP+8] ; edi = *(ebp + 8)56DF4FED  |. 8D45 08        LEA EAX,DWORD PTR SS:[EBP+8]  ; eax = ebp + 8
56DF4FF0  |. 50             PUSH EAX                                  ; 2nd parameter56DF4FF1  |. FF75 0C        PUSH DWORD PTR SS:[EBP+C]  ; 1st parameter
56DF4FF4  |. E8 4DE1FDFF    CALL ntdll_1.56DD3146
56DF4FF9  |. 50             PUSH EAX
56DF4FFA  |. FF75 08        PUSH DWORD PTR SS:[EBP+8]
56DF4FFD  |. FF75 14        PUSH DWORD PTR SS:[EBP+14]
56DF5000  |. FF75 10        PUSH DWORD PTR SS:[EBP+10]
56DF5003  |. FF75 0C        PUSH DWORD PTR SS:[EBP+C]
56DF5006  |. 57             PUSH EDI
56DF5007  |. E8 0A000000    CALL ntdll_1.RtlInsertElementGenericTableFull>
56DF500C  |. 5F             POP EDI
56DF500D  |. 5D             POP EBP
56DF500E  \. C2 1000        RETN 10

일반적으로 PUSH 가 쓰이는 곳

  1. register 를 사용하기 위해, register 가 가지고 있는 값을 stack 에 보관 할 때,(보통 이 경우에는 push 로 register A 의 값을 저장했다고, 함수 끝날 때 다시 pop 을 해서 해당 register A 에 값을 복원한다.)
  2. function call 에서 parameter 를 넘길 때
  3. 아주 가끔, register 의 값을 복사할 때, push 하고 바로 pop 을 통해 register 의 값을 다른 register 로 복사한다.
std calling convention 에서는 a(1,2,3) 이라는 함수가 있으면 3, 2, 1 의 순으로 parameter 가 stack 에 push 된다.

ntdll_1.56DD3146

56DD3146   $ 8BFF           MOV EDI,EDI ; 의미 없다. windows 에서 trapping 을 위한 mark 이다. 56DD3148   . 55             PUSH EBP
56DD3149   . 8BEC           MOV EBP,ESP
56DD314B   . 56             PUSH ESI
56DD314C   . 8B37           MOV ESI,DWORD PTR DS:[EDI] ; esi = *edi, (첫번째 element)
56DD314E   . 85F6           TEST ESI,ESI
56DD3150   . 0F84 5E1F0200  JE ntdll_1.56DF50B4
56DD3156   > 8D46 18        LEA EAX,DWORD PTR DS:[ESI+18]
56DD3159   . 50             PUSH EAX                                ; 3rd parameter56DD315A   . FF75 08        PUSH DWORD PTR SS:[EBP+8] ; 2nd parameter56DD315D   . 57             PUSH EDI                              ; 1st parameter56DD315E   . FF57 18        CALL DWORD PTR DS:[EDI+18] 56DD3161   . 85C0           TEST EAX,EAX             ; callback function 의 return value 를 test 하고56DD3163   . 0F84 8D200200  JE ntdll_1.56DF51F6 ; '0' 이면 jump 한다.56DD3169   . 83F8 01        CMP EAX,1                ; callback function 의 return value 를 '1' 과 비교
56DD316C   . 0F84 93200200  JE ntdll_1.56DF5205 ;  '1' 이 맞으면 jump
56DD3172   . 33C0           XOR EAX,EAX            ; return value 가 '1' or '0' 이 아닐 때, eax 를 0 으로56DD3174   . 40             INC EAX                    ; eax 를 '1'로 , 이 함수의 return value 는 '1' 이 될 것이다.
56DD3175   > 8B4D 0C        MOV ECX,DWORD PTR SS:[EBP+C] ; ecx = 2nd parameter from caller of ntdll.56DD3146
56DD3178   . 8931           MOV DWORD PTR DS:[ECX],ESI         ;  *ecx = esi
56DD317A   > 5E             POP ESI
56DD317B   . 5D             POP EBP
56DD317C   . C2 0800        RETN 8
56DF51F6 > 8B46 04       MOV EAX,DWORD PTR DS:[ESI+4]; eax = *(esi + 4)
56DF51F9 . 85C0             TEST EAX,EAX
56DF51FB . 75 79            JNZ SHORT ntdll_1.56DF527656DF51FD . 6A 02            PUSH 2
56DF51FF > 58                POP EAX
56DF5200 .^E9 70DFFDFF JMP ntdll_1.56DD3175

56DF5205   > 8B46 08        MOV EAX,DWORD PTR DS:[ESI+8] ; eax = *(esi + 8)
56DF5208   . 85C0           TEST EAX,EAX
56DF520A   . 75 6A          JNZ SHORT ntdll_1.56DF5276
56DF520C   . 6A 03          PUSH 3
56DF520E   .^EB EF          JMP SHORT ntdll_1.56DF51FF


56DF5276   > 8BF0             MOV ESI,EAX
56DF5278   .^E9 D9DEFDFF    JMP ntdll_1.56DD3156

이 함수에서 edi 에 어떤 값을 할당하는 부분이 전혀 없다. 그런데 call dword ptr [edi+18] 을 하는 것은 함수가 호출되기 이전에 이미 edi 에 어떤 값을 할당했다는 것을 알 수 있다.
그렇다면, 어떤 calling convention 이 EDI 를 통해 parameter 를 전달하는 것일까? 안타깝게 그런 calling convention 은 없다. 이 경우에는 static 일 가능성이 있다.
static 이 아닌 경우에 컴파일러는 보통 "알 수 없는 caller" 를 위해서 calling convention 이 잘 유지시키지만, 함수가 static 을 사용하면서 현재 object 파일에 대해 명백하게 local 일 때는 calling convention 을 유지 하지 않기도 한다. 위의 함수의 경우처럼 register 를 이용해서 parameter 를 전달하기도 한다.
잘 보면 함수를 호출하는 caller 쪽에서는 push 를 통해 parameter 를 stack 에 넣고 있긴 하지만, 실제로 callee 쪽에서 그냥 register 의 값을 가져와서 사용해 버린다.
RETN 8
를 통해서 8 bytes 의 paramter 를 전달받는 다는 것을 알 수 있고, 또, EDI 를 통해서 parameter 를 전달 받았으니, 총 3개의 parameter 를 전달받았다.
EDI 는 caller 쪽에서 확인을 해 보면 아래와 같다.
MOV EDI,DWORD PTR SS:[EBP+8] ; edi = *(ebp + 8)
아마도 ebp+8 이니까 1st parameter 일 것이다.

working-set tuning

JE ntdll_1.56DF50B4
위의 주소처럼 function 이 현재 주소 보다 멀리 떨어져서 분포되어 있는 경우가 있다. 이것은 windows 의 memory management 이슈 때문이다.
executable module 을 만들 때, 가장 중요한 점 중에 하나는 모듈을 어떤 식으로 배치시키는 것이냐 이다. 그래서 모듈이 메모리로 load 될 때 물리적인 memory 를 가장 적게 사용하는 쪽으로 배치를 시키려 한다. 이것을 working-set tuning 이라고 부른다.
Windows 는 실제 사용하는 영역에 대해서만 physical memory 를 할당하기 때문에 잘 쓰이는 code 영역(popular code sections)은 모듈의 맨 앞쪽 부분에 있게 되고 잘 안 쓰이는 code들은 뒤쪽으로 놓이게 된다.
그러므로 위에서 보이는 56DF50B4 는 우리가 분석하고 있는 code 주소인 56DD3146 보다 훨씬 뒤의 주소이기 때문에, 이 녀석은 훨씬 덜 쓰이는 녀석이라고 추측해 볼 수 있다. 이 녀석을 대충 따라가 보면 알겠지만, error handling code 이다. error handling code는 실제로 error 가 발생했을 때만 쓰이기에 보통 이렇게 뒤쪽에 놓일 가능성이 높다.

CALL DWORD PTR DS:[EDI+18]

CALL DWORD PTR DS:[EDI+18]
EDI 는 이전 글에서 봤던 structure 이다.(RtlInsertElementGenericTable 이 Generic Table 을 insert 하기 위한 것이니까 parameter 중에 하나는 확실히 generic table 의 structure 일 것이다.) 그런데 이 녀석의 7 번째 member (EDI+18) 를 call 하는 것을 통해 이 녀석이 함수의 주소인 것을 알 수 있다. 그러면 table 은 아래와 같아진다.

ecx(= ebp+8) param 1 offset + 0 member 1 pointer
ecx+4 offset + 4 member 2 pointer
offset + 8 member 3 pointer
offset + c member 4 pointer
offset + 10 member 5
offset + 14 member 6 element 총 개수
offset + 18 member 7 function pointer
offset + 1c member 8
offset + 20 member 9
offset + 24 member 10

RtlInitializeGenericTable 를 보면 아래처럼 memeber 7 은 2번째 parameter 에서 얻어온 값이다.
mov ecx, dword ptr ss:[ebp+c]
mov dword ptr ds:[eax+18], ecx
그렇다면 우리가 GenericTable 을 초기화 할 때 넘겨주는 함수 포인터이기에, user-defined function pointer 가 될 것이다.

이 user-defined 함수를 호출할 때 3개의 parameter 를 넘겨주는데, 첫 번째는 table data structure 이고, 2번 째는 이 함수(우리가 분석중인)로 넘어온 parameter 이고, 3번째는 ESI+18 이다.
56DD3156 > 8D46 18 LEA EAX,DWORD PTR DS:[ESI+18]
56DD3159 . 50          PUSH EAX ; 3rd parameter56DD315A . FF75 08   PUSH DWORD PTR SS:[EBP+8] ; 2nd parameter56DD315D . 57         PUSH EDI ; 1st parameter56DD315E . FF57 18 CALL DWORD PTR DS:[EDI+18]
ESI 는 "EDI 가 가진 address 가 가리키는 값(*edi)"을 가지고 있다. 위의 table 의 구조를 가지는 structure 이다.(앞으로 이 structure 를 TS 라고 부르자.)
이 3개의 parameter 가 어떤 값이 될지는 밑에서 얘기하자.


56DF51F6

56DF51F6를 살펴보면,
ESI+4(offset+4)의 값(value)이
  • '0' 이 아니면,
    1. ESI 로 값을 load 하고 : esi = *(esi+4)
    2. callback 함수를 call 하는 부분으로 돌아간다.
  • '0' 이면
    1. 호출했던 곳으로 돌아간다.
이 모습은 linked list 를 traverse 하면서 user-defined function 을 호출하는 것처럼 보인다.

3rd parameter 로 ESI+18 을 넘겼다. ESI 가 TS 의 root 를 가지고 있기 때문에, TS의 크기는 적어도 0x1c(0x18 + 4) bytes 이상일 것이라 추측할 수 있다.


JE ntdll_1.56DF5205
JE ntdll_1.56DF5205
esi+8 값을 TEST 해서 
  • esi+8 이 '0' 이면 '3' 을 return 한다.
  • esi+8 이 '0' 이 아니면, esi 에 esi+8 값을 넣고 다시 callback 함수를 호출
esi + 8 은 위의 TS 의 table 에서 보듯이 offset + 8 에는 pointer 가 있다. 이 pointer 를 esi 에 넣고 다시 같은 방법으로 callback 함수 호출을 하는 것으로 보아, 아마도 esi+8 에 있는 pointer 도 TS 일 수 있다. 물론 아닐 수도 있다.


High-level perspective

이제 이 함수를 좀 더 추측 해 보자.

callback 의 return 에 대한 logic
  • callback 이 '0' 을 return 하면, offset+8 에 있는 pointer 를 가져와서
    • pointer 가 '0' 이 아니면, callback 호출하는 부분으로 돌아가고,
    • pointer 가 '0' 이면 return '3'
  • callback 이 '1' 을 return 하면, offset+4 에 있는 pointer 를 가져와서
    • pointer 가 '0' 이 아니면, callback 호출하는 부분으로 돌아가고,
    • pointer 가 '0' 이면 return '2'
  • callback 이 '1', '0' 이 아닌 값을 return 하면,
    • return '1'
56DD3175 로 jump 하는 녀석들은 함수가 끝나는 지점인데, 여기서 보면 return value (EAX)는 1, 2, 3 의 3가지가 있다. 이것은 "같다", "작다", "크다" 등의 의미가 될 수 있다.
이 녀석이 linked list 의 구조에서 traverse 를 하는 것이라면, 한 방향으로 가다가 한번은 반대 방향으로 틀어서 값을 찾는 부분이 존재할 것인데, 여기서는 한방향으로 계속 가다가도, 갑자기 다른 방향으로 계속 가기도 하고 한다. 이런 구조는 tree 구조를 traverse 하는 데에 알맞다.
그러면 callback 에서 return 하는 값 '0', '1' 은 '작은 경우', '큰 경우' 로 생각 해 볼 수 있다. 그러면 '0'(작은) 경우에 offset + 8 을 가져오는 것으로 보아, offset + 8 이 left-node 라고 추측할 수 있고, '1' 인 경우에 가져오는 offset + 4 가 right-node 라고 생각 해 볼 수 있다.
이렇게 특정 element 를 찾을 때까지 tree 를 traverse 하는 것은 binary search algorithm 이 있다. 이 assembly 함수가 이 binary search 구조를 닮았다. binary search 의 방법으로 원하는 위치를 찾는 것으로 볼 수 있겠다.

Callback 의 parameters
56DD3156 > 8D46 18 LEA EAX,DWORD PTR DS:[ESI+18]
56DD3159 . 50 PUSH EAX ; 3rd parameter56DD315A . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; 2nd parameter56DD315D . 57 PUSH EDI ; 1st parameter56DD315E . FF57 18 CALL DWORD PTR DS:[EDI+18]
callback 에서 사용하는 parameter 에 대해 알아보자. 위에서 설명했듯이
  1. 첫 번째는 root table data structure(TS) 이고,
  2. 2번 째는 이 함수(우리가 분석중인)로 넘어온 parameter 이고
  3. 3번째는 ESI+18 이다.
먼저 첫 번째 parameter 를 보자. InsertElementGenericTable 이 쓰이는 일반적인 방법을 고민해 보자. 지금의 함수는 이 InsertElementGenericTable 을 완성해 주는 것인데, 우리가 위에서 이 자료구조는 tree 같을 것이라 추측했다. 그러면 분명 어디에선가 root node 를 넘겨줬을 것이다. 그것이 바로 EDI 일 것이다. (우리는 이전 글에서 TS 의 첫 번째 element 가 root 일 것이라 추측했다.) 이것은 이전의 다른 함수들에서도 첫번째 parameter 로 root node 를 넘겨주는 것과 비슷한 경향을 보인다.
그럼 3번째 parameter 를 한 번 보자. 3rd parameter 는 ESI+18이 가리키는 값인데, 우리는 위에서 loop 을 돌면서 ESI 를 바꿔주면서 traverse 를 하는 것을 봤다. 그렇기 때문에 이 [ESI+18] 은 아마도 현재 우리가 traverse 하려고 하는 current node 일 것이다.
이제 2번째 parameter 만 알아내면 되는데, 이것은 상식적으로 유추 해 보자. 이 함수는 insert 에서 호출하고 있으며, binary search 를 사용하고 있다. 그렇다면, 이 2번째 값이 아마도 "insert 하려는 값"일 것이다.
이 내용을 토대로 prototype 을 써본다면 아래와 비슷할 것이다.
int (*stdcall function_name)(TS root_node, void* new_element, void* element_of_current_node)




Reference

  1. RtlInsertElementGenericTable, Chapter 5, Beyond documentation, Reversing: Secrets of Reverse Engineering
  2. Effects of Working-Set Tuning on Reversing, Appendix A, Reversing: Secrets of Reverse Engineering

댓글 없음:

댓글 쓰기