Now let's look at a function that has to do with looking data up in the table. Remember, it doesn't matter which functions we reverse first. I'm choosing based on what I think will be a good order to go in.
There are multiple functions that might get something from the table: RtlEnumerateGenericTable
, RtlGetElementGenericTable
, RtlLookupElementGenericTable
, and some others. Based on the names, I think RtlGetElementGenericTable
or RtlLookupElementGenericTable
will be the easiest. Out of those two I'd guess RtlGetElementGenericTable
will be the simplest. I'd guess that it simply looks in the table at the index passed to the function. This is just a guess, I don't even know what parameters it takes yet.
Disassembly of RtlGetElementGenericTable
:
7FF8BBD1C1B0 MOV QWORD PTR SS:[RSP+0x8], RBX
7FF8BBD1C1B5 MOV R10D, DWORD PTR DS:[RCX+0x20]
7FF8BBD1C1B9 LEA R11D, QWORD PTR DS:[RDX+0x1]
7FF8BBD1C1BD MOV R8, QWORD PTR DS:[RCX+0x18]
7FF8BBD1C1C1 OR EBX, 0xFFFFFFFF
7FF8BBD1C1C4 MOV R9D, R11D
7FF8BBD1C1C7 CMP EDX, EBX
7FF8BBD1C1C9 JE ntdll.7FF8BBD1C21C
7FF8BBD1C1CB MOV EAX, DWORD PTR DS:[RCX+0x24]
7FF8BBD1C1CE CMP R11D, EAX
7FF8BBD1C1D1 JA ntdll.7FF8BBD1C21C
7FF8BBD1C1D3 CMP R11D, R10D
7FF8BBD1C1D6 JE ntdll.7FF8BBD1C200
7FF8BBD1C1D8 JB ntdll.7FF8BBD66C7A
7FF8BBD1C1DE SUB EAX, R11D
7FF8BBD1C1E1 MOV EDX, R11D
7FF8BBD1C1E4 SUB EDX, R10D
7FF8BBD1C1E7 INC EAX
7FF8BBD1C1E9 CMP EDX, EAX
7FF8BBD1C1EB JA ntdll.7FF8BBD1C20A
7FF8BBD1C1ED TEST EDX, EDX
7FF8BBD1C1EF JE ntdll.7FF8BBD1C1F8
7FF8BBD1C1F1 MOV R8, QWORD PTR DS:[R8]
7FF8BBD1C1F4 ADD EDX, EBX
7FF8BBD1C1F6 JNE ntdll.7FF8BBD1C1F1
7FF8BBD1C1F8 MOV QWORD PTR DS:[RCX+0x18], R8
7FF8BBD1C1FC MOV DWORD PTR DS:[RCX+0x20], R11D
7FF8BBD1C200 LEA RAX, QWORD PTR DS:[R8+0x10]
7FF8BBD1C204 MOV RBX, QWORD PTR SS:[RSP+0x8]
7FF8BBD1C209 RET
7FF8BBD1C20A LEA R8, QWORD PTR DS:[RCX+0x8]
7FF8BBD1C20E TEST EAX, EAX
7FF8BBD1C210 JE ntdll.7FF8BBD1C1F8
7FF8BBD1C212 MOV R8, QWORD PTR DS:[R8+0x8]
7FF8BBD1C216 ADD EAX, EBX
7FF8BBD1C218 JE ntdll.7FF8BBD1C1F8
7FF8BBD1C21A JMP ntdll.7FF8BBD1C212
7FF8BBD1C21C XOR EAX, EAX
7FF8BBD1C21E JMP ntdll.7FF8BBD1C204
This function is clearly going to be more of a challenge than the others.
When a function is pretty involved, I always like going through it and identifying the obvious stuff, then going back through and analyzing more closely. The first thing we'll do is check for function parameters.
- RCX is used like a data structure (
MOV EAX, DWORD PTR DS:[RCX+0x24]
), so it's probably the table. - RDX is also used, but with an offset of +0x1 (
LEA R11D, QWORD PTR DS:[RDX+0x1]
). RDX is probably not a data structure, maybe a pointer, we'll figure that out later. Just note that RDX is used. - R8 and R9 are overwritten, so they were probably not parameters. Now we know that this function only takes 2 parameters. The first one being the table, the second one is only a guess but it could be an index into the table. That quick analysis is going to make things easier, but let's keep looking around.
Something I like doing is identifying loops. Just by scanning this function I can tell it's very likely there is a loop because of the JMP ntdl.###
instructions.
The first set of jumps are JCC ntdll.7FF8BBD1C21C
. ntdll.7FF8BBD1C21C
zeroes EAX, then jumps to ntdll.7FF8BBD1C204
which does something to RBX then returns. This is probably because of a condition "failing", so we'll keep that in mind. I say this is may be a fail condition because it's early on in the function and it jumps immediately to a ret
. Also the return value is set to 0.
There is some other mildly interesting jumps but nothing to note. We'll just examine those when we get there. With that said, I think this is a good time to get started with the full analysis.
If you don't already, I'd highly recommend writing notes and/or pseudo code as you are reverse engineering.
Let's focus on the following code:
MOV QWORD PTR SS:[RSP+0x8], RBX
MOV R10D, DWORD PTR DS:[RCX+0x20]
LEA R11D, QWORD PTR DS:[RDX+0x1]
MOV R8, QWORD PTR DS:[RCX+0x18]
OR EBX, 0xFFFFFFFF
MOV R9D, R11D
CMP EDX, EBX
JE ntdll.7FF8BBD1C21C
MOV EAX, DWORD PTR DS:[RCX+0x24]
CMP R11D, EAX
JA ntdll.7FF8BBD1C21C
- RBX is preserved/saved on the stack.
- Member4 of the table is moved into R10D.
LEA R11D, QWORD PTR DS:[RDX+0x1]
is a very important instruction. It's essentiallyR11D = RDX + 1
. This might be done instead of using theINC
instruction to preserve the value of RDX.- Next, the fourth member of the table is moved into R8.
OR EBX, 0xFFFFFFFF
sets EBX to -1.- R11D (RDX + 1) is moved into R9D.
- EDX (second parameter) is then compared to EBX (-1). This tells us a few things. First, we can be even more sure that EDX is an index. Also, previously the NumberOfElements member in the table data structure was accessed with the offset of +0x24 in
RtlNumberGenericTableElements
. You would expect it to be accessed with the offset of +0x20 because it's the fifth member. The fact that EDX is used instead of RBX could mean that the fifth parameter is not 8 bytes, instead it's only 4 bytes. We still can't be certain, but it's nice to know that it's less likely that we're wrong. - If EDX (the second parameter) is -1, then jump to
ntdll.7FF8BBD1C21C
which is the fail condition. MOV EAX, DWORD PTR DS:[RCX+0x24]
moves the NumberOfElements member into EAX.- R11D (RDX + 1) is then checked if it's greater than EAX (NumberOfElements). This is bounds checking to make sure the index isn't greater than the range of the table. This also tells us another piece of useful information, the index is zero-based. This means it starts from index zero, not 1, similar to a conventional array. This makes sense because the number of elements is not zero-based (0 elements would mean it's empty). Again, this is just like an array.
- If R11D is greater than EAX, then it jumps to the fail condition. Here is a pro tip, pay attention to the jump types.
JA
tells us that it's unsigned.
Here is some pseudo code to ease your mind:
ULONG adjustedIndex = index + 1;
if(index == -1 || adjustedIndex > Table->NumberOfElements){
return 0;
}