![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
![[community profile]](https://www.dreamwidth.org/img/silk/identity/community.png)
Take a look at this folder:
This is an application for Wii Virtual Console ROM injection from 2008, but that's not really important. What I want to talk about is the DLLs in this folder - in particular, which executables need which DLLs - and how, when I wanted to use wadpacker myself, I took a different approach, just so I didn't have to learn how linking works.
Auto Injectuwad Injector v3.exe is the main GUI, which calls injectuwad.exe. There's also sha1.exe and the WAD tools (wadpacker, wadunpacker, imet_signer, and wadsigncheck). The dependency tree looks like this:
- Auto Injectuwad Injector v3
- MBVBM60.DLL (Visual Basic 6)
- injectuwad
- zlib1.dll
- kernel32.dll (Windows API)
- MSVCP80.dll (Visual C++ 8.0)
- MSVCR80.dll (Visual C++ 8.0)
- sha1
- kernel32.dll (Windows API)
- MSVCRT.dll (Visual C++ 6.0)
- wadpacker / wadunpacker / imet_signer / wadsigncheck
- cygcrypto-0.9.8.dll
- cygwin1.dll
- kernel32.dll (Windows API)
The Visual Basic 6 runtime is included with Windows. So is the "old" Windows C runtime in MSVCRT.dll, which MinGW uses as well. Later Visual C++ versions split their standard libraries from the OS - you can download them here, but they're so commonly used that you probably have some of them installed already.
The other libraries are included with the application. zlib1 implements the compression algorithm used in ZIP, gzip, and PNG, among others; it's only about 60 KB. The other two are much larger though, at about 3 MB combined: cygwin1 provides a POSIX API, and cygcrypto provides encryption and hashing functions from OpenSSL. Both come from the Cygwin project, which implements a Unix environment on top of Windows (as opposed to something like WSL, which runs alongside Windows in a hypervisor; Cygwin compiles apps to Windows binaries, albeit ones that reference its own runtime libraries).
Is this a problem? Not really. Besides, since their site calls out cygwin1.dll by the name right at the top, I think including it with an application is accepted practice. But it did cause a couple of issues for me when I wanted to tweak wadpacker by adding a couple of parameters. First, I like it when I'm able to have a bunch of EXEs in one folder, without worrying about conflicting DLLs, or trying to figure out which version of a library is newer. But also, I just think it's more elegant to take advantage of Microsoft's libraries when you can. If wadpacker were compiled for Linux, it would use the C and OpenSSL libraries from the distro's repository, and I wanted my Windows apps to do the same kind of thing.
So I knew I wanted to get wadpacker and the other WAD tools into Visual Studio. This would replace the dependency on cygwin1.dll with one on the Visual C++ redistributable - it doesn't come with Windows, but it does come with a lot of apps. (Starting with VS 2015, Microsoft made the Visual C++ libraries compatible with prior versions. I used VS 2019, so anyone with the redistributable from 2019 or later would be able to run my build.) I made four separate projects - one for each of the four WAD tools - and also included the required shared files (.h and .c) in each.
That left two things I had to do:
- Get the code to compile as C++ instead of C
- Get the code to compile with Visual C++ on Windows
- Replace all dependencies with calls to the .NET Framework
(That last one sounds kind of wild, but we'll get to that.)
Telling Visual Studio to compile the code as C++ was as easy as toggling a setting in the project properties:
As for turning the code into valid C++, that mostly consisted of putting casts in front of malloc calls; C++ doesn't allow implicit conversion from a void pointer to a typed one.
The other tasks were a little more involved. First, without something like Cygwin or MinGW, I didn't have the POSIX API (unistd.h) available to me for mkdir and chdir. I ended up just adding a check in the preprocessor to include direct.h instead when compiling with Visual C++:
#ifdef _MSC_VER #include <direct.h> #else #include <unistd.h> #endif
The other thing I really wanted was to avoid the dependency on OpenSSL; not only was I trying to avoid external DLLs whenever I could, but I just didn't know how to link it properly from Visual Studio. Luckily, I had another solution in mind. C++/CLI is an extension of Visual C++, adding features to the language to hook into the .NET platform. It's intended for interop situations between "managed" (.NET) and native code; it can be awkward to work with if not used sparingly, but it technically has access to most of what the .NET ecosystem has to offer; a C++/CLI assembly can define functions for other .NET code to call, or call .NET functions written in languages like C#. Essentially, a C++/CLI assembly is an assembly that can contain both native and .NET code.
.NET Framework - the "old" .NET - might not get new features anymore, but it's been included with Windows for a long time now. I decided to target .NET Framework 4.7.2, which has come with Windows since 2018. I tweaked my project properties again:
Then I added a check for C++/CLI at the top of tools.c and slapped a bunch of using statements inside it:
#ifdef __cplusplus_cli using System::IO::FileAccess; using System::IO::Stream; using System::IO::UnmanagedMemoryStream; using System::Runtime::InteropServices::Marshal; using System::Security::Cryptography::Aes; using System::Security::Cryptography::CipherMode; using System::Security::Cryptography::CryptoStream; using System::Security::Cryptography::CryptoStreamMode; using System::Security::Cryptography::HashAlgorithm; using System::Security::Cryptography::ICryptoTransform; using System::Security::Cryptography::MD5; using System::Security::Cryptography::PaddingMode; using System::Security::Cryptography::SHA1; #else #include <stddef.h> // to accommodate certain broken versions of openssl #include <stddef.h> // to accommodate certain broken versions of openssl #include <openssl/md5.h> #include <openssl/md5.h> #include <openssl/aes.h> #include <openssl/aes.h> #include <openssl/sha.h> #include <openssl/sha.h> #endif
(Whereas C#, Visual Basic .NET, and F# all use a period to separate namespace components, C++/CLI uses a pair of colons just like standard C++.)
Now I could rewrite the SHA1 function using dark magic the .NET standard libraries (my code is inside the #ifdef, not the #else):
void sha(u8 *data, u32 len, u8 *hash) { #ifdef __cplusplus_cli // interop UnmanagedMemoryStream^ inputStream = gcnew UnmanagedMemoryStream(data, len); HashAlgorithm^ algorithm = SHA1::Create(); // this is where we actually compute the hash! array<u8>^ hashArr = algorithm->ComputeHash(inputStream); // more interop pin_ptr<u8> pin = &hashArr[0]; memcpy(hash, pin, hashArr->Length); #else SHA1(data, len, hash); #endif }
Quick breakdown of how this works: the type signature UnmanagedMemoryStream^ indicates a handle to an UnmanagedMemoryStream object on the .NET heap. .NET objects are garbage collected as always, so the keyword to create a new one and get the handle is gcnew. UnmanagedMemoryStream itself is a .NET library type that implements the Stream interface and wraps a block of unmanaged (i.e. not garbage collected) memory. This means that inputStream is a simple wrapper around our block of memory, for other .NET objects that expect an input stream object.
Next, we create another .NET object from the standard library, one that implements the SHA1 hash algorithm by exposing the HashAlgorithm interface. HashAlgorithm has a method that computes a hash by reading all data from a stream, so we use that to compute the hash of the stream we just created. This comes back to us as a .NET array of bytes (what would be byte[] in C#); the C++/CLI type signature for this type is array<uint8_t>^. (u8 is simply an alias defined in tools.h as unsigned char, which Visual C++ also treats as 8-bit.)
Then we use the pin_ptr type to get a reference to the beginning of the hashArr array which we can use as a C pointer (I believe this pins the memory in place until the pointer goes out of scope), and finally, we copy the hash from the pin into the destination pointer.
I do something similar for the other cryptographic functions. Here's AES encryption (again, the #ifdef is mine):
#ifdef __cplusplus_cli array<u8>^ keyArr = gcnew array<u8>(16); pin_ptr<u8> keyPin = &keyArr[0]; memcpy(keyPin, key, 16); auto ivArr = gcnew array<u8>(16); pin_ptr<u8> ivPin = &ivArr[0]; memcpy(ivPin, iv, 16); Aes^ aes = System::Security::Cryptography::Aes::Create(); aes->Mode = System::Security::Cryptography::CipherMode::CBC; aes->Padding = System::Security::Cryptography::PaddingMode::None; ICryptoTransform^ encryptor = aes->CreateEncryptor(keyArr, ivArr); Stream^ inputStream = gcnew UnmanagedMemoryStream(in, len, len, System::IO::FileAccess::Read); Stream^ outputStream = gcnew UnmanagedMemoryStream(out, len, len, System::IO::FileAccess::Write); Stream^ cryptoStream = gcnew CryptoStream(inputStream, encryptor, CryptoStreamMode::Read); cryptoStream->CopyTo(outputStream); #else AES_KEY aes_key; AES_KEY aes_key; AES_set_encrypt_key(key, 128, &aes_key); AES_set_encrypt_key(key, 128, &aes_key); AES_cbc_encrypt(in, out, len, &aes_key, iv, AES_ENCRYPT); AES_cbc_encrypt(in, out, len, &aes_key, iv, AES_ENCRYPT); #endif
Here, I make .NET byte arrays, copy the key and IV bytes into them, then call the AES functions in .NET to set up the encryption. Once I do that, I wrap a CryptoStream around an input stream (which reads the input buffer) to perform the encryption, then tell .NET to read everything from that CryptoStream's output and write it to an output stream (which writes it to the output buffer).
The end result is that I can compile a fully working version of wadpacker as a single 90KB Windows executable. Instead of having to include my own DLLs, I just need the end user to install the newest Visual C++ redistributable - something that another one of their apps has likely already done.
I've done a couple other conversions like this, too. Here's a particularly fun one from ccf-tools, which uses .NET to replace zlib:
// Create input and output streams pointing to unmanaged memory // Streams will be flushed and disposed once out of scope UnmanagedMemoryStream inputStream(indata, inbufsize, inbufsize, FileAccess::Read); UnmanagedMemoryStream outputStream(outdata, outbufsize, outbufsize, FileAccess::Write); // Write zlib header outputStream.WriteByte(0x78); outputStream.WriteByte(0x9C); { // Compress data using DEFLATE algorithm // DeflateStream will be flushed and disposed once out of scope DeflateStream compressor(%outputStream, CompressionMode::Compress, true); inputStream.CopyTo(%compressor); } // Compute adler32 checksum uint32_t a = 1, b = 0; for (uint8_t* ptr = indata; ptr < indata + inbufsize; ptr++) { a = (a + *ptr) % 65521; b = (b + a) % 65521; } uint32_t checksum = b << 16 | a; // Write checksum in big endian outputStream.WriteByte((checksum & 0xFF000000) >> 24); outputStream.WriteByte((checksum & 0x00FF0000) >> 16); outputStream.WriteByte((checksum & 0x0000FF00) >> 8); outputStream.WriteByte((checksum & 0x000000FF) >> 0); datasize = outputStream.Position;
Note the DeflateStream (which performs compression much like how CryptoStream performs encryption), and how it's defined like a stack variable. In certain conditions, C++/CLI lets you "pretend" that a garbage-collected .NET object is on the stack, and it will actually dispose of it when it goes out of scope, analogously to a using statement in C#. And just like you use & to get the address of an actual stack variable, C++/CLI includes the % operator to get the handle of a .NET object with stack semantics, which you'll need to pass it to .NET functions.