Now that we know what requests look like, let’s try creating and posting some of our own. We’ve already captured one that gives us versions, checksums, and links to various game files. Now it’s just a matter of periodically re-issuing it and checking for changes...
...right? Hah, if only it were that simple. Remember that signature in the request query string that I totally ignored last time? Yeah, turns out it’s quite important. Take a look at what happened when I tried to re-submit a captured request a few hours later:
That first session was captured from the launcher a long time ago. The second is the same, but re-issued just now.
Misleading status code aside, the response to the second request only contained an error message. After a bit more testing, it became clear that the signatures were “expiring”, and I’d have to re-sign the requests for them to be considered valid.
Here’s a quick demo you can try at home. What happens if you open the updater, close it, wait a minute, then open it again?
While the contents of all these requests are identical, the signature in the query string is constantly changing. This is true for all but the 5th and 6th requests, which occur within the same minute, and thus, share the same signature.
Something worth mentioning was that the GetServerClock
request and everything before it could be re-issued, even though a few hours had passed since the original request. Everything following that request resulted in the same ‘status 9’ error.
From this, let’s assume that the GetServerClock
request is used to “synchronize” your local system time with a time value provided by the server, which is what ensures all subsequent requests will have valid signatures. That’s another thing we’ll have to keep in mind, but for now, let’s just focus on generating some signatures programmatically.
First things first, let’s see how the updater does it. If you have a really good memory, you’ll remember seeing a few mentions of makeRequestStringAndSignature
in the previous post, which showed up while searching for strings containing “request”.
Ignore those and search for “failed to makeRequestStringAndSignature
“ instead. If you look a bit above where the string is referenced, you should see a call
, immediately followed by a test
instruction. Set a breakpoint on the call
.
The breakpoint will hit on the initial getServices
request, so the address in the rcx
register should be pointing to an instance of EacnetRequestServices
, which inherits from EacnetRequestPost
and EacnetRequestBase
. I’ve partially mapped out the class structure below by looking through various virtual functions.
As this is before the call occurs, we can see the ‘request
’ and ‘signature
’ members are currently empty:
Stepping over the call, both request
and signature
variables are now populated. Honestly, that’s… pretty much all there is to it. If you copied these strings out from the class and (quickly) sent off a request, you’d get a valid response back.
Time to step through that function, reverse engineer and reimplement it, right? Nah… I mean, you could, but I think it’d be more fun, but mostly a lot easier, to hijack the game process and turn it into a server for encoding and decoding messages instead.
Okay, with my thinly veiled excuse to try out some REST libraries out of the way, let’s waste no time and get straight into it. We’ll start simple and work our way up, beginning with creating a request, and using the aforementioned function to do all the signing stuff for us. This will be compiled as a dynamic-link library and, for now, manually injected into the game process.
That said, creating a request and calling makeRequestStringAndSignature
from the entrypoint won’t work. Instead, we’ll have to create a new thread using an exported function from avs2-core.dll
and move our code into there. If you’ve looked around the directories of any Konaste game, you’ve probably seen this library (or its 32-bit variant under a different name) before.
What does AVS stand for? Who knows? I’ve just noticed that all the Konaste games use it, and it provides stuff like configuration, filesystem, networking, threading, memory management, and so on. All the export names are obfuscated, but you can get a general idea of what they do by looking at the error strings. I found these thread functions without too much hassle:
#define thread_create XCgsqzn0000004
#define thread_join XCgsqzn000000b
#define thread_destroy XCgsqzn0000006
extern "C"
{
std::int32_t thread_create(void* proc, void* param, std::uint32_t sz_stack, std::uint32_t priority);
void* thread_join(std::int32_t thread, void* = nullptr);
void* thread_destroy(std::int32_t thread);
}
You could resolve these with GetModuleHandle
and GetProcAddress
, but I’ve recently started using gendef
and dlltool
instead, which can be installed in MSYS2 via the mingw-w64-x86_64-tools and mingw-w64-x86_64-binutils packages.
These tools allow you to generate a .lib
file from avs2-core.dll
, which you can link against when building your library.
$ gendef avs2-core.dll
* [avs2-core.dll] Found PE+ image
$ dlltool --input-def=avs2-core.def --dllname=avs2-core.dll --output-lib=avs2-core.lib
Here’s how you can use it with CMake. Just make sure to copy avs2-core.lib
into the “lib
” directory after generating it.
target_link_directories(${PROJECT_NAME} PRIVATE lib)
target_link_libraries(${PROJECT_NAME} avs2-core)
Now we’ve got the preparation stuff out of the way, let’s continue writing our library code.
Here’s an extremely simplified structure for EacnetRequestBase
based on what I found in ReClass earlier:
struct EacnetRequestBase
{
virtual ~EacnetRequestBase() = 0;
virtual void* setup(bool*) = 0;
auto get_request() { return *reinterpret_cast<const char**>(std::uintptr_t(this) + 0x88); }
auto get_signature() { return *reinterpret_cast<const char**>(std::uintptr_t(this) + 0xA8); }
};
There are a few more important fields, such as the request property, but we’ll add those as the need arises. In the meantime, we’ll need to call the setup
virtual function after creating the request object, but before making the request string and signature.
To create a request object, we simply need to call createRequestObject
– a function that can be found by… you guessed it, searching for the “createRequestObject
” string. If you see a huge switch
statement, you’ve found the right function.
The first case
in the switch
corresponds to EacnetRequestServices
, so we pass 0
as the second argument. The first argument, a1
, doesn’t appear to be used for anything, so it’s fine to just pass nullptr
there. Here’s what all this looks like so far:
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID)
{
if (reason != DLL_PROCESS_ATTACH)
return FALSE;
AllocConsole();
freopen_s(reinterpret_cast<FILE**>(stderr), "CONOUT$", "w", stderr);
freopen_s(reinterpret_cast<FILE**>(stdout), "CONOUT$", "w", stdout);
auto thread = thread_create(reinterpret_cast<void*>(+[] ()
{
auto const createRequestObject = reinterpret_cast<void* (*) (void*, std::int32_t)>(0x1402A6760);
auto const makeRequestStringAndSignature = reinterpret_cast<bool (*) (void*)>(0x1402C51E0);
auto request = reinterpret_cast<EacnetRequestBase*>(createRequestObject(nullptr, 0));
auto param = true;
request->setup(¶m);
makeRequestStringAndSignature(request);
printf("request: %s\n", request->get_request());
printf("signature: %s\n", request->get_signature());
request->~EacnetRequestBase();
}), nullptr, 0x1000, 1);
thread_join(thread);
thread_destroy(thread);
FreeConsole();
return FALSE;
}
One down, four to go. As a quick reminder, here’s the full list of requests we’re going to be making:
EacnetRequestServices
– Contains URLs for all subsequent requests.EacnetRequestServerState
– Used to check if the server is currently in maintenance mode. If it is, we should avoid making any further requests until the maintenance period ends. (see:mainte_start_clock
&mainte_end_clock
)EacnetRequestServerClock
– Returnsserver_clock
, a Unix timestamp in milliseconds. We can use this to set the Eacnet clock, which will ensure subsequently generated signatures will be considered valid by the server.EacnetRequestLogCheck
– Not really used for anything... ¯\_(ツ)_/¯EacnetRequestResourceInfo
– The end goal. Contains everything needed to track game updates.
In case you were wondering, yes, you could definitely skip most of these. I just thought it would be a bit underwhelming if I only created one request, but by all means, feel free to go straight to EacnetRequestResourceInfo
if you want.
Moving on, the next request is EacnetRequestServerState
... but there’s a problem.
So far I’ve been targeting the game process, bm2dx.exe
, but after looking through all the cases in createRequestObject
, it turns out you can’t actually get a EacnetRequestResourceInfo
request from it. Likewise, it doesn’t show up in Class Informer either.
As shown above, an ID of 7 can yield two completely different requests depending on which binary you’re loaded into. It makes sense that the game wouldn’t have requests exclusive to the launcher and vice versa, but it’s pretty inconvenient for us.
…or it would be, but we’ve already got all we need with our single instance of EacnetRequestServices
!
As it turns out, the request body is determined by the request property, which can be found 0x50
bytes in to the class.
It’s nullptr
until the setup
virtual function is called, which is what we did for EacnetRequestServices
, but this isn’t necessary if we create a custom property, swap the pointer in the class to use it, then call makeRequestStringAndSignature
.
So that’s exactly what we’ll do. All the necessary functions are provided by AVS2, so we’ll need to expand our definitions a bit.
#define property_create XCgsqzn0000090
#define property_destroy XCgsqzn0000091
#define property_set_flag XCgsqzn000009a
#define property_mem_write XCgsqzn00000b8
#define property_query_size XCgsqzn000009f
#define property_insert_read XCgsqzn0000094
#define property_read_query_memsize XCgsqzn00000b0
enum property_flag
{
PROP_XML = 0x00,
PROP_READ = 0x01,
PROP_WRITE = 0x02,
PROP_CREATE = 0x04,
PROP_BINARY = 0x08,
PROP_APPEND = 0x10,
};
extern "C"
{
void* property_create(std::uint32_t flags, void* buffer, std::uint32_t size);
std::int32_t property_destroy(void* property);
std::int32_t property_set_flag(void* property, std::uint32_t set_flags, std::uint32_t clear_flags);
std::int32_t property_mem_write(void* property, void* buffer, std::uint32_t size);
std::int32_t property_query_size(void* property);
std::int32_t property_insert_read(void* property, void* node, void* reader, std::uint32_t file);
std::int32_t property_read_query_memsize(void* fn, std::int32_t file, void* = nullptr, void* = nullptr);
}
We’ll also need to add another function to EacnetRequestBase
for when we’re ready to swap the property:
auto set_request_property(void* property) { *reinterpret_cast<void**>(std::uintptr_t(this) + 0x50) = property; }
And now for the star of the show, we’ve got this helper class to handle property creation:
struct xml_property
{
explicit xml_property(std::string_view src): body(src)
{
auto const index = TlsAlloc();
TlsSetValue(index, this);
auto static reader = +[] (std::uint32_t param, void* dst, std::size_t nbytes) -> std::size_t
{
auto property = reinterpret_cast<xml_property*>(TlsGetValue(param));
auto start = property->body.data() + property->position;
if (property->position + nbytes > property->body.size())
nbytes = property->body.size() - property->position;
if (nbytes == 0)
return 0;
std::memcpy(dst, start, nbytes);
property->position += nbytes;
return nbytes;
};
auto const size = property_read_query_memsize(reader, index, nullptr, nullptr);
auto const flags = PROP_XML | PROP_READ | PROP_WRITE | PROP_CREATE | PROP_APPEND;
buffer = std::make_unique<std::uint8_t[]>(size);
instance = property_create(flags, buffer.get(), size);
position = 0;
property_insert_read(instance, nullptr, reader, index);
property_set_flag(instance, PROP_BINARY, PROP_XML);
TlsFree(index);
}
~xml_property() { property_destroy(instance); }
xml_property(const xml_property&) = delete;
xml_property& operator=(const xml_property&) = delete;
void* instance = nullptr;
std::string_view body = {};
std::size_t position = 0;
std::unique_ptr<std::uint8_t[]> buffer = nullptr;
};
To give a quick rundown of how this works, we call property_read_query_memsize
to determine how much memory we’ll need to allocate for the property. The reader function only accepts a 32-bit value (probably because AVS2 file handles are 32-bit), so we copy the address of our xml_property
to a TLS slot and pass the index of that instead.
Next, we allocate the memory and create the property. It’ll begin as XML, so we set PROP_XML
when creating it. The position
was previously advanced by property_read_query_memsize
, so we need to reset it before calling property_insert_read
.
Finally, we convert from PROP_XML
to PROP_BINARY
instead, allowing us to call makeRequestStringAndSignature
.
With that out of the way, it’s time to make some more requests. How about all of them in one go?
auto const samples = std::vector<std::tuple<std::string, std::string>> {
{"EacnetRequestServices", "<p2d>"
" <method __type=\"str\">getServices</method>"
" <params>"
" <dummy __type=\"u8\">0</dummy>"
" </params>"
"</p2d>"},
{"EacnetRequestServerState", "<p2d>\n"
" <method __type=\"str\">getServerState</method>\n"
"</p2d>"},
{"EacnetRequestServerClock", "<p2d>\n"
" <method __type=\"str\">getServerClock</method>\n"
" <params>\n"
" <dummy __type=\"u8\">0</dummy>\n"
" </params>\n"
"</p2d>"},
{"EacnetRequestLogCheck", "<p2d>\n"
" <method __type=\"str\">checkSendLogAvailable</method>\n"
" <params>\n"
" <dummy __type=\"u8\">0</dummy>\n"
" </params>\n"
"</p2d>"},
{"EacnetRequestResourceInfo", "<p2d>\n"
" <method __type=\"str\">getResourceInfo</method>\n"
" <params>\n"
" <full __type=\"bool\">1</full>\n"
" </params>\n"
"</p2d>"},
};
I captured these requests from the updater process, then decoded them to XML with my Fiddler inspector plugin.
We can re-use our existing instance from createRequestObject
by swapping out the request property between each call to makeRequestStringAndSignature
. Just be careful to set this back to nullptr
before calling the virtual destructor!
auto thread = thread_create(reinterpret_cast<void*>(+[] ()
{
auto const createRequestObject = reinterpret_cast<void* (*) (void*, std::int32_t)>(0x1402A6760);
auto const makeRequestStringAndSignature = reinterpret_cast<bool (*) (void*)>(0x1402C51E0);
auto request = reinterpret_cast<EacnetRequestBase*>(createRequestObject(nullptr, 0));
for (auto const& [name, body]: samples)
{
auto property = xml_property(body);
request->set_request_property(property.instance);
makeRequestStringAndSignature(request);
printf("[%s]\n", name.c_str());
printf("request: %s\n", request->get_request());
printf("signature: %s\n\n", request->get_signature());
}
request->set_request_property(nullptr);
request->~EacnetRequestBase();
}), nullptr, 0x1000, 1);
Just one thing left now before we can actually start submitting these – setting the Eacnet clock.
Once again, it’s... a little underwhelming. Search for the “eacne_clock initialized
” string, follow the reference, copy the address of the function, then call it with the 64-bit value you got from EacnetRequestServerClock
and, yeah, that’s it.
auto const initializeClock = reinterpret_cast<void* (*) (std::uint64_t)>(0x140297D80);
For example, this capture from January returned a server clock of 1642772720510
. If we pass that to initializeClock
, the signature for the newly generated EacnetRequestResourceInfo
should match the one captured back then.
I suppose that’s everything I wanted to cover in this post. If you’re looking for a more ‘preassembled’ version of the above, check out my simple REST service “infapi” over on GitHub. Some things worth checking out in that repository are:
The pattern scans which replace the hardcoded offsets seen in this post.
A launcher executable to start the game suspended, load
infapi.dll
, then resume it.A Dockerfile that builds the project and uses WINE to run it under Linux.
Lastly, there’s also a Python script which performs the requests above and produces an aria2 download list as a result.