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 가 쓰이는 곳
- register 를 사용하기 위해, register 가 가지고 있는 값을 stack 에 보관 할 때,(보통 이 경우에는 push 로 register A 의 값을 저장했다고, 함수 끝날 때 다시 pop 을 해서 해당 register A 에 값을 복원한다.)
- function call 에서 parameter 를 넘길 때
- 아주 가끔, register 의 값을 복사할 때, push 하고 바로 pop 을 통해 register 의 값을 다른 register 로 복사한다.
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]그렇다면 우리가 GenericTable 을 초기화 할 때 넘겨주는 함수 포인터이기에, user-defined function pointer 가 될 것이다.
mov dword ptr ds:[eax+18], ecx
이 user-defined 함수를 호출할 때 3개의 parameter 를 넘겨주는데, 첫 번째는 table data structure 이고, 2번 째는 이 함수(우리가 분석중인)로 넘어온 parameter 이고, 3번째는 ESI+18 이다.
56DD3156 > 8D46 18 LEA EAX,DWORD PTR DS:[ESI+18]ESI 는 "EDI 가 가진 address 가 가리키는 값(*edi)"을 가지고 있다. 위의 table 의 구조를 가지는 structure 이다.(앞으로 이 structure 를 TS 라고 부르자.)
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]
이 3개의 parameter 가 어떤 값이 될지는 밑에서 얘기하자.
56DF51F6
56DF51F6를 살펴보면,ESI+4(offset+4)의 값(value)이
- '0' 이 아니면,
- ESI 로 값을 load 하고 : esi = *(esi+4)
- callback 함수를 call 하는 부분으로 돌아간다.
- '0' 이면
- 호출했던 곳으로 돌아간다.
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 함수를 호출
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'
이 녀석이 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]callback 에서 사용하는 parameter 에 대해 알아보자. 위에서 설명했듯이
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]
- 첫 번째는 root table data structure(TS) 이고,
- 2번 째는 이 함수(우리가 분석중인)로 넘어온 parameter 이고
- 3번째는 ESI+18 이다.
그럼 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
- RtlInsertElementGenericTable, Chapter 5, Beyond documentation, Reversing: Secrets of Reverse Engineering
- Effects of Working-Set Tuning on Reversing, Appendix A, Reversing: Secrets of Reverse Engineering
댓글 없음:
댓글 쓰기