Ordinal Values, Windows Functions, and C#
There's many different techniques that an offensive security professional could use to try to have their code avoid detection by various AV and EDR products. Various options include encrypting part of their code, code obfuscation, guardrails, and a lot more. But one cool trick that seems to keep surviving the test of time is utilizing ordinal values when working with the API. What are ordinal values? Let's check this out.
The easiest way for me to visualize and understand what ordinal values are is to think of finding them as an array. If I want to reference a specific value stored in an array, I have to do so by referencing its position in the array (in the above image, it might be something like array). The fun thing is this same concept can be applied to functions called within various Windows DLLs, such as kernel32, etc.
Let's say I want to use QueueUserAPC within code that I am writing. Normally, I would use P/Invoke to define QueueUserAPC so that it can be utilized throughout my code. The code would look something similar to the following:
So we can easily see that QueueUserAPC resides within kernel32.dll. The next step would be trying to figure out how to define QueueUserAPC via its ordinal value (position) within kernel32.dll. Thankfully, there's multiple tools that can help you find this information, such as PEView. All you need to do is download PEView, have it open up kernel32.dll (or any dll you want to figure this information out for), and look at the Export Address Table. Finally, just look for the specific function call you are interested in (in this case, QueueUserAPC).
In this screenshot, we can see that QueueUserAPC has a value of "045A". You'll likely quickly recognize that this is the hexidecimal representation of its position, so in base 10 QueueUserAPC's position is "1114". Great, now that you have its position, the last step is defining the function call via its ordinal value.
To do so, you should define the EntryPoint value when defining QueueUserApc to specify the position within kernel32.dll that you want to call. Your code should look pretty similar to the following screenshot.
You can see that the ordinal value is used in the QueueUserAPC function definition. Additionally, you likely noticed that rather than giving the function name its standard name of "QueueUserAPC", in this case it's being defined as "ChrisQueueUserAPC". The name that you specify does not matter, it could be called any name because it's not the name that is important, but the ordinal value which ensures that the correct function is ultimately called.
It is worth noting that there can be some drawbacks when using ordinal values, the biggest of which is that you have to be very targeted with your code. Ordinal values of functions can change from version to version, service pack to service pack, or major version to major version. There's no guarantee that they will have the same ordinal value. Therefore you likely want to maintain a running database that you can quickly lookup and find the ordinal value of the function call you want to run with respect to the operating system version it is running on.
Hopefully this helps to show how you can not only look up the ordinal value of a function you want to use, but also how to leverage ordinal values within your code. If you have any questions about the topic, don't hesitate to reach out and contact us. We're happy to help answer any questions you may have!