This guide is stage 2 in a much larger workflow process. "
" was stage 1, so please be sure to have at least read that thread to understand the prerequisites for this one.To quickly summarize, "GFXRC_Generator" is a tool people can use to dump the GFX_RUNTIME_CLASS information from the client. This information can be used to reconstruct a lot of important class layouts, which is of great value as this guide will illustrate. In order to use those structures though, we must find the relevant pointers in the client to read. This guide will cover the basics of that process, as well as create a few simple tools to help people get started.
This guide requires x32dbg. If you're still using Ollydbg, I'd strongly suggest you invest the time upgrading to x32dbg. It's 100% worth it, and you'll be more prepared for when the 32-bit era comes to an end, and you have to acquire 64-bit skills (x64dbg). Most modern games have stopped including 32-bit clients, and a lot of games that supported legacy 32-bit clients, are dropping support as well.
I'll be using C#/.Net 5 to make the custom tools in this guide. Typically, I'd have some tools written in C++ for better performance or because they have to be (for example, for shellcode related stuff), but this is the starter guide. I'm going to try to keep things as simple as possible so people can at least understand what they need to be able to do or know the skills they need to acquire in order to actually make use of this stuff.
While I cannot teach you the basics of using x32dbg or even reversing, I will at least mention basic things time to time, just so the guide is a little more complete in helping the less experienced who want to jump in and start learning new things. You will need to at least have basic skills in using a debugger, programming, and reversing though.
Lastly, this guide is still on the advanced side, and only usable by a small number of devs. Like the previous guide, this is a foundational building block someone will use in order to create/update another project from, and community involvement/participation will come in at a later, more accessible stage. As I mentioned previously though, these initial guides building up the workflow process have to be completed and released piece by piece to keep everything manageable and easy to understand.
[Part 1 - Debugging]
Before we begin, always keep a version named backup copy of every sro_client you work on. I use the format "[version]sro_client.exe", but you can use whatever works for you. There's a few reasons why you want to do this. First, you never want to lose your x32dbg label/comment database between updates due to the same filename being used. Second, you will often need to refer to previous versions to check changes or find something you missed. Third, sometimes you might just need to have a copy of x32dbg open to refer to, while you work from another copy named differently. If you're going to be reverse engineering, this practice is a must. For Path of Exile, I saved every client from 2012, so I have over 300 saved exes to refer to. Having so many clients also opens the doors to new and interesting tools, but that will be covered in a much later guide if I get to it.
For this guide, I'll be creating a new x32dbg database to work from (see reason 3 above), so my client name will be: "[TR-1.013]sro_client.exe". Once again, I'll be working from TRSRO just because these concepts are generic to SRO, so understanding how it works for one version, means you'll be able to apply most of it to others. This is an advanced guide, so nothing different will result if I were to target VSRO instead. After this foundation is built, the general framework must be done next, and that is still something for only a tiny set of developers in the community. It's not until after the foundation and supporting frameworks are completed, that more people in the community can start making use of everything.
Getting started, the first thing we want to do is load up our sro_client in x32dbg. By default, x32dbg breaks on TLS entries, so for SRO, it's best to change the debugger settings to only break on the OEP. To change this: Options->Preferences->Events and only select "Entry Breakpoint*". Restart x32dbg using "CTRL + F2" so the debugger breaks at the OEP of sro_client, which for TRSRO, will look like this:
Code:
00BA0584 <EntryPoint> | E8 F2060000 | call <sub_BA0C7B> | 00BA0589 | E9 69FEFFFF | jmp 0xBA03F7 |
If you've not already analyzed it, hit "CTRL + A" to analyze it. Depending on your PC's specs, this might take a minute or two. At this point, we'll also want to change the commandline of the process. That can be done through the "File->Change Command Line" menu. For TRSRO, we'll want to add "0 /56 0 0 0" after the quoted process name. For other SRO versions, you'll use the normal "0 /locale 0 0" format everyone should know by now. I also always load strings for the client, so right click on the disassembly view: "Search for->Current Region->String references".
We don't want to start the client yet, because we need to apply our "x32dbg-labelscript.txt" generated from "GFXRC_Generator". If you've not already ran "GFXRC_Generator" for your sro_client, do so now. To run the labeling script in x32dbg, click on the Script tab button (or hit "ALT + S") and right click somewhere inside the window body and expand Load Script. You can either choose to open the file or paste it in if you copy it to the clipboard. Once it's loaded, you can hit "Space" to run it, or right click the window body again and choose "Run". It will take a few moments to run, but when it's finished, you'll get a messagebox.
In order to maximize our ability to reverse sro_client, we have to run in a debugger. This is so memory debug heaps get used. That allows us to easily see uninitialized memory, which makes a massive difference when trying to identify memory patterns to find known C++ containers or other objects. In addition, other memory markers for allocations will exist, which helps us know when we mess up something size wise, because we'll be reading into memory with distinct values that should not exist naturally.
The downside to running a debugger is the performance loss. It will tank, but I've noticed Silkroad is still more than playable enough due to it not being super graphics intensive. In other games like Path of Exile, running a debugger makes the game almost unplayable, which is a bit problematic when trying to figure things out, but you can work around it still. The biggest issue I've seen that you can run into with in SRO, is you can timeout when joining the game world. It might take you a few tries to get connected as the debugger settles down, so it's important you are testing on a server you can reconnect easily to.
At this point, we're almost ready to run sro_client. However, we'll need to do a little manual work to bypass the launcher check. Click on the "References" tab to be able to switch to the "Referenced strings" list we found earlier (or just search for them again if you've lost that tab due to a restart). To bypass the launcher check, we:
- Find the string "Please Execute the \"Silkroad.exe.\"" by typing "please exe" in the "Search" bar at the bottom of the string list and double click the string to follow it in the disassembler.
- Set a breakpoint on the conditional jump above it. We'll manually take the jump to bypass the launcher check when we run the client each time. It is possible to write a plugin/tool to make the patch for us at runtime, but I tend to just do these things manually to keep things simple.
Now, we're ready to run the client, so hit F9 (or Debug->Run via the menu) and our breakpoint should be hit. Now, we can just hit enter to follow the JNE to it's destination, and "hit CTRL + NUMPAD *" to set the new origin. Alternatively, we can right click the line of where the new origin should be and choose "Set New Origin Here". When reversing, we'll end up debugging a lot, so it's best to learn all the hotkeys since you'll be performing these actions 1000s of times. Now, we can hit F9 to resume, and the launcher check has been bypassed.
At this point, the debugger is going to process modules as the client loads them, and slowly start up. If you get a connection failed error messagebox, you most likely forgot to set the commandline! Even with a super beefy machine, the client might take a few minutes to fully load now. When you need to restart the debugging process (CTRL + F2) you'll have to endure this waiting time, so it's best to try to avoid the need to restart. This is the downside to the debug heaps, it really hurts.
Once everything is loaded, login, and try to join the game world. This might take a few tries, but I've only really had back-to-back disconnects on ISRO, and that might just have been due to the server being overcrowded. Once you're in game, get to a safe spot, as we'll not be doing anything for a while.
[Part 2 - Reversing Static Addresses]
Thanks to "GFXRC_Generator", we've got a bunch of labeled things in the client to explore. To view labels, hit "CTRL + ALT + L" (or View->Labels via the menu). One quick note, the label window is a snapshot of your labels when you generated the list. It does not auto-update, so you'll need to close it and view labels again to refresh it. I tend to manually backup my labels time to time, just to prevent any unfortunate accidents. Losing work is never fun, so keep this in mind so you don't do a bunch of labeling, then end up not copying the new stuff because you're viewing an old list!
The biggest question is always "where to start". Everyone has a different process they follow for reversing. There's nothing to really teach in regard to this, because you're just searching for needles in a haystack. I could give specific tips, like "search for common keywords", but there's no real way to replicate what I experience when I reverse. As a result, I'm going to give a guided approach of what to do in this guide, so readers have something to follow, but normally, you'd not know exactly what to look for. Reversing is like exploration, so you'll never know what you'll find and where you end up as you freely roam a target. It goes without saying, you'll rarely have a tool like "GFXRC_Generator" that gives you a bunch of things to work from at the start.
We're going to start with the "CGame" class. In the label window, search for "CGame". We should see 4 results: "Allocate_GFXRuntimeClass_CGame", "Constructor_GFXRuntimeClass_CGame", "Register_GFXRuntimeClass_CGame", "GFXRuntimeClass_CGame". Here's an explanation of each and their importance:
- "Register_GFXRuntimeClass_" functions get called to register class information. This is the starting point to find everything else and is what "" processes.
- "GFXRuntimeClass_" addresses are the constants that get checked to verify if an object is a specific class or not.
- "Allocate_GFXRuntimeClass_" functions get called to allocate memory for a class instance and construct it afterwards.
- "Constructor_GFXRuntimeClass_" are the class constructors. These contain all the juicy initialization information we need to understand. These are primarily referenced from "Allocate_GFXRuntimeClass_" functions, but it's possible other functions in the client invoke them. These "other functions" are of great interest to us as well!
Right click on the "Constructor_GFXRuntimeClass_CGame" label and "Follow in Disassembler". Alternativly from the CPU view, we can just press "CTRL + G" to go to an address and paste in our "Constructor_GFXRuntimeClass_CGame" label. The labels are long and verbose in order to help keep things as obvious as possible. Typically, I'd prefer short names to cut down on typing, but you can always copy/paste labels or use the label list to get to things quicker.
We're now inside the CGame constructor. The first thing we're going to look for, is a static variable assignment. To put another way, we're trying to find any instructions that assign the "this" pointer to a fixed memory address. Not all classes will use this pattern, only a few. It's our job to find any interesting static variables like this though, because we'll use them for Part 3 of this guide with our memory reading tool that uses the structures "GFXRC_Generator" generates.
The code pattern we're looking for can be in the constructor directly, or it possibly could be in a parent constructor. In this case, CGame's static variable is actually in "Constructor_GFXRuntimeClass_CGFXMainFrame". Having the labels is great, without them, we'd know it was a constructor related function (because that's how MSVC generates constructors), but we'd not know what it was for.
Follow the call into "Constructor_GFXRuntimeClass_CGFXMainFrame" (you can either hit enter on the call itself or ctrl+g and use the label again). If we scroll down, we'll find what we're looking for:
Code:
mov dword ptr ds:[0x011A86B8], esi
This means we can now label the address "0x011A86B8" as "g_pCGame" (which is just my naming convention). To set a label, press "SHIFT + ;" or right click the address and choose "Label Current Address". I use "g_" to denote global (which also implies a static address, not to be confused with the static keyword in C/C++) and then "p" for a pointer type followed by the class name.
The important part is that we know if we read a 32-bit pointer from "0x011A86B8", we'll have a pointer to a CGame pointer. To verify, go to the address (CTRL + G) in a dump tab. Right click on the first byte and choose "Follow DWORD in Current Dump". We're now viewing the memory of a CGame object, but how can we know? The first DWORD is going to be the VTable, as you should know (if you don't, you need to read about VTables and how MSVC generates C++ code). Right click the first byte again and follow the DWORD in the current dump. Now we have all our object's virtual functions. Right click the first byte once more, but this time choose "Follow DWORD in Disassembler".
We should see the following function:
Code:
006866B0 <sub_6866B0> | B8 98E31701 | mov eax, <[tr-1.013]sro_client.GFXRuntimeClass_CGame> | 117E398:&"CGame" 006866B5 | C3 | ret |
First, we followed the static pointer "0x011A86B8" in the dump. This is like having the C++ code "CGame** g_pCGame = (CGame**)0x011A86B8;". In order to get our CGame pointer, we need to dereference it, so we follow the DWORD in the dump a second time. We essentially executed the c++ code "CGame* pGame = *g_pCGame". With the CGame instance, we want to read the VTable next, which is the first 4 bytes. To do that, the C++ code would be something like "DWORD* VTable = *(DWORD**)pGame". Lastly, we want to access the first function in the VTable, so we follow the first pointer in the disassembler, since we want the first function. The C++ code for that would be "DWORD func = VTable[0]".
Hopefully that makes sense, we're just following the chain of pointers to our desired destination. I only know that the first table in the VTable is the function that tells the runtime class from having already reversed it. Typically, you'd check all functions and see if any of them have something useful in them. In this case, the first one contains the class, and the second one contains the parent class. This information actually overlaps with the RTTI information florian posted about on his blog (
), but the GFX_RUNTIME_CLASS is a separate system that mirrors the RTTI layout.There's a few more things to mention to be on the lookout for. We know the address "0x011A86B8" contains a pointer to CGame*, but what type of memory is the CGame* instance? Typically, objects are allocated from the heap, so their address will change execution to execution. However, and I'm simply giving the answer since this guide is about understanding what you need to know, this CGame pointer is actually in the .data section of the exe. That means this CGame object is going to be at the same address, much like our static variable. This is unusual, but worth noting, because we can then search for this address to gain another label and unlock more things to explore.
The address "0x117D9B8" can be labeled as g_CGame. If we search for all constants and use that address, we'll find the following interesting reference:
Code:
004C3B70 <sub_4C3B70> | B9 B8D91701 | mov ecx, <[tr-1.013]sro_client.g_CGame> | 117D9B8:&"°fh" 004C3B75 | E8 86771A00 | call <Constructor_GFXRuntimeClass_CGame> | 004C3B7A | 68 5062D800 | push <sub_D86250> | 004C3B7F | E8 6BB76D00 | call <sub_B9F2EF> | 004C3B84 | 59 | pop ecx | 004C3B85 | C3 | ret |
If we go back to "Constructor_GFXRuntimeClass_CGFXMainFrame", and look down a little from where we found our static pointer, we'll see another static pointer assignment. That code is as follows:
Code:
00BAC0FB | 85C0 | test eax, eax | 00BAC0FD | 74 09 | je 0xBAC108 | 00BAC0FF | 8BC8 | mov ecx, eax | 00BAC101 | E8 4A8B0000 | call <Constructor_GFXRuntimeClass_CControler> | 00BAC106 | EB 02 | jmp 0xBAC10A | 00BAC108 | 33C0 | xor eax, eax | 00BAC10A | A3 BC861A01 | mov dword ptr [0x11A86BC], eax | 011A86BC:&"ŒÞâ"
We've gained a new static address, so label "0x11A86BC" as "g_pCControler". One thing I should note is that we don't know if these class instances are singletons or not. We can assume CGame is, naturally, but what about this new instance? If we view the memory region where the address is at, we'll see it's not in the .data section like the CGame object was, so it's just a normal object. There may be more allocated or might not, there's no way to know really unless we do more debugging or find code that points us to something only having one instance possible. This is just something to be aware of, as it's easy to miss things when reversing because there's so many little things to check each time.
I'm not going to walk through any others because I'd never finish this guide in my lifetime, but a few known static addresses to search for would be: "CGWorld", "CEntityManagerClient", "CICPlayer" and "CGInterface". Find the static addresses for those, because we'll be using them in Part 3!
[Part 3 - Tool Development]
Now that we have quite a few static addresses to use, and we've verified each has a pointer to a GFX_RUNTIME_CLASS type by checking the vtable's first function, we can now make use of the class layouts "GFXRC_Generator" generated. This part is really simple, because "GFXRC_Generator" has done all the work of generating the layouts (although they won't be 100% accurate yet) and you've done the work of finding the memory locations that need to be read. As a result, I've created a simple C# project that can be used to get started. The offsets are for TRSRO 1.013 as noted.
Like "GFXRC_Generator", I've stripped down this project to the bare minimal required. Code is duplicated in part from the other project, intentionally, because this tool is also a standalone project. The goal is to give a very small yet concise picture of how everything works together. For a real project, common code would be organized in different projects in the solution, and the way the programs are coded would change a little.
Source:

Example output:
CEntityManagerClient.txt:

CGame.txt:

CGWorld.txt:

When you create your own project, you'll have to replace the files in the GFX_RUNTIME_CLASS folder. Make sure to add them normally so a copy gets made into the project, as opposed to adding them as linked files (which is not the default behavior, but something commonly done in C#). If you were to run "GFXRC_Generator" again from the location where you linked the files from, you'd overwrite all modifications, and then lose a lot of work.
For TRSRO, there's over 1200 files total (only 1/2 will need user modifications) to add to the project, so hopefully you can understand the issue with trying to provide a complete working project for people to just dive into for this guide for every version. Besides the fact there's a lot of files, everyone starting from scratch due to the guide makes no sense, when some basic effort can be made to setup a common community project people can contribute to. This is what I meant from my earlier guide when talking about community involvement and setting my projects up to support people leading those efforts.
For example, if someone wants to reverse CSRO-R and share the work, they'd add their GFX_RUNTIME_CLASS folder to a repository, and then be in charge of applying any community updates. Anyone interested in having labeled structures for their projects would then simply be able to sync to the repo and get updates time to time as more progress is made. Then, reversers of other versions can reference any work done and see if it's applicable to theirs. Fixed versions like VSRO 188 are obviously the easiest to manage, because there won't be another a client update ever again, but if you're targeting an official version of SRO, then you'll have a bit more work to do merging changes between client updates. Those sorts of things should be easy to work out if you're capable of reversing anyways, but it's worth mentioning before people rush to manage something they might not have the time to commit to.
I think it'd make sense to have a class library dedicated solely to the updated GFX_RUNTIME_CLASS files, so end users can just reference a single dll rather than have to copy a ton of files. Developers would then check out the source code tree, and perform updates on the individual files.
The last topic to cover in this section, is using the memory dumps. I use Notepad++ and Devart's Code Compare. Using Notepad++, I tend to search for values in a folder (or opened files) and using code compare, I can diff two folders to figure out what changed between an action. While you do not have to use these exact tools, an advanced text editor and diffing tool is highly recommended to make use of the memory dumps.
Since we've enabled debug memory heaps, we'll be able to see uninitialized memory. This allows us to visually see common C++ containers a lot easier. For example, here are what a few common containers look like in TRSRO (keep in mind older SRO versions use an older MSVC, so container sizes can be different).
std::string / std::wstring
The easy way to distinguish between empty strings/wstring is checking the Reserved size. If you see 15 (0xF), then it's most likely going to be a std::string. std::wstring's Reserved size is 7 (as the fixed buffer needs + 1 for the null terminator). If the size is set, or it looks like there's a pointer in the first 4 bytes, then you'll just have to visually inspect the string to see if it's using a char (single bytes) or wchar_t (each character is two bytes, so you see a character followed by 00 for normal ascii values). The image shows various examples of a std::string in memory and what to be on the lookout for.
std::vector
std::vector stands out because you'll have three memory addresses in a row that are always linear. They either all be increasing in value, or the last 2 addresses remain the same (but come after the first) which means the vector has not been resized larger yet. When viewing vector memory, you typically are able to see the uninitialized memory bytes before the address of the third value. The exception is if the vector has been resized smaller, and that memory was previously used so it's no longer initialized.
std::list / std::map / std::set
std::list, std::map, and std::set all look the same in memory when you first see them. They will be a head pointer followed by the container size, so the pattern stands out. However, you have to explore the head pointer to know which container you actually have. I'll make note of it again in the pictures, but the Head node is always uninitialized data, and the address of the Head node represents the "end" of the container for linked nodes. When a child node points to the Head node, we know there's no more pointers to traverse from the child node.
A list node will have two pointers and then the data of the list entry. A map and set will 3 pointers instead, followed by a few bytes and then padding bytes. A set node will only have data for the value type of the set, while a map node will have data for the key type followed by the value type.
It just takes experience to be able to easily identify these, so don't worry if it doesn't seem clear at first. Typically, primitive types are used in sets, so when you find something that looks like a map, but you can only find 1 data type, such as an uint, it's most likely a set instead.
std::hash_map
std::hash_map (std::unordered_map in new C++) is very easy to identify because of how many parts it has. You'll always see the Proxy value of 0x3F800000, followed by a list, and then a vector, and finally two uints where the first is smaller than the second.
In an attempt to keep this example project as simple as possible, no C++ container structures or logic is included. In case the workflow of how to use "GFXRC_MemoryDumper" is not clear, I'll quickly explain. First, run the program to generate memory logs. Then pick a log to work from, and start looking for things to label (more on this in the next section as well). After you make changes, simply re-run again and check the new log to make sure everything looks right and the validation did not fail.
If validation fails, you'll see an error like this:
I went with a basic console program to demostrate how things work. However, you're not limited to such primative tools. For Path of Exile, I made a lot of gui based programs to neatly show memory and more easily change structs and generate logs to compare things to. You can create something that uses scripting as well, but for most people certain things like that might just be too impractical vs just editing files and running the program. I at least want to mention it, because once you understand how things work, then you can develop a workflow that is best for you.
[Part 4 - Snowball Effect]
The last part of this guide will talk about the snowball effect. This is the most exciting aspect of reversing SRO right now for me.
We started out with a bunch of labels from "GFXRC_Generator". By checking out the constructors, we were able to identify important static addresses. Using those addresses with the generated structures, we also have memory dumps of important objects. As we reverse the object layouts, we'll be able to associate various fields/offsets with code in the client.
That in turn, allows us to find even more interesting things. Since SRO has been around for so long, there's lots of information about packets. One thing we can now do is start looking through all S2C packet handlers, and have way more information about what is going on thanks to knowing the static addresses of things like CGame, CGInterface, CICPlayer, etc...
I'm going to just dump a bunch of examples of things to look for, so everyone has an idea of how to start associating things. This is in addition to the C++ container patterns talked about at the end of Part 3, except we're now finding stuff in the client code instead of just memory.
1. Client's window handle
Code:
00519A7D | 8B0D B8861A01 | mov ecx, dword ptr [<g_pCGame>] | 011A86B8:&" GÙ" 00519A83 | E8 082E6900 | call <sub_BAC890> | 00519A88 | 50 | push eax | 00519A89 | FF15 20D5D800 | call dword ptr [<&_OpenClipboard@4>] | 00519A8F | 85C0 | test eax, eax | 00519A91 | 74 2F | je 0x519AC2 | ... 00BAC890 <sub_BAC890> | 8B41 30 | mov eax, dword ptr [ecx+0x30] | 00BAC893 | C3 | ret |
2. Finding GFX_RUNTIME_CLASS::IsSame
Search for the referenced string "pObj->IsSame( GFX_RUNTIME_CLASS(CICCos) )"
3. Finding GFX_RUNTIME_CLASS::IsKindOf
Search for the referenced string "IsKindOf(GFX_RUNTIME_CLASS(CICUser)) || IsKindOf(GFX_RUNTIME_CLASS(CICGuard))"
4. Finding CGlobalDataManager::GetTIDFromObjectID
Search for the referenced string "CGlobalDataManager::GetTIDFromObjectID ( 0x%X )"
5. Finding CGlobalDataManager
Check xrefs to CGlobalDataManager::GetTIDFromObjectID
Find the static address of CGlobalDataManager referenced from one of them
Those are just a few things to start with. My final tips are to label the packet reading/writing functions as well as all S2C packet handlers. By referencing known packet opcodes/formats, you can then see where labels give more context to packet handlers, and where data is read into the major classes.
For example, the time packet,
, directly reads the data into the CPSMission object. Make sure to read the
blog post by florian, as it's super useful and important information to understand! Using posts like that, we can also learn new things to label, as now we'll have the CGame::CurrentProcess field we can then dump using the right GFX_RUNTIME_CLASS structure.[Conclusion]
Phew, another massive guide. I know these can be hard to get through, but as you can see, there's a lot of stuff that goes into this side of things. Most of the time, the community gets to use the end result, but doesn't quite know or see all of what it took to get to that point.
I'll recap where we are in our workflow process. Stage 1 was the development of "GFXRC_Generator" to generate GFX_RUNTIME_CLASS layouts. Stage 2 (this guide) is finding the memory pointers to use those layouts on. Once we know where to read everything from, we can begin labeling things and fixing the member types. Stage 3 would be using a labeled set of GFX_RUNTIME_CLASS layouts in order to accomplish some goal.
For example, a stage 3 project might be running a C++ code generation tool on the results of stage 2 in order to have exact object layouts for the SRO_DevKit. All the "unknown padding" can be replaced with the appropriate members. In addition, more client knowledge can be obtained from just being able to associate more things in the client. In theory, the next phase of advancements in SRO_DevKit should now be possible as people have the tools and knowledge of what they can do to start filling in the blanks.
There's always a lot of work to do, whether it’s improving the workflow, the tools being used to reverse, or just the time required in figuring out what is what across 600+ classes. Thanks to the layouts though, a lot of wasted work can be avoided since any commonly used base classes are "reverse once, reuse everywhere".
Anyways, that's it for now, hopefully this guide and the last will prove useful to people. There won't be a stage 3 guide anytime soon, as the only thing I could write about would be creating an end user project that used GFX_RUNTIME_CLASS stuff to achieve some goal. That's not going to be possible until more stuff is reversed first anyways.
Since there's a lot of stuff in this guide and the previous for the community to digest first, some time is certainly needed for people to get new ideas of how they could possibly use this. I'll be answering questions or talking about things people discover and share, while working on my own personal projects.
VirusTotal:
(Once again, no binaries in the attachment, but just saving a snapshot to epvp for archiving. It is nice all my projects from a decade ago are still accessible here!)NOTE: Since I'm attaching the repo snapshot before the thread is created, it doesn't contain the link to this thread yet. That's not a problem though, since you have to read this thread to get the attachment anyways, so you know where it came from.






