From de986fa3e683c63aed42fc65b5c25d211607f2d1 Mon Sep 17 00:00:00 2001 From: "Franc[e]sco" Date: Sun, 23 Mar 2014 02:02:18 +0100 Subject: [PATCH] initial commit --- README.md | 1 + wxPloiter.sln | 20 + wxPloiter/aes.cpp | 98 +++ wxPloiter/aes.hpp | 29 + wxPloiter/aobscan.cpp | 250 ++++++++ wxPloiter/aobscan.hpp | 68 +++ wxPloiter/common.h | 15 + wxPloiter/crypt.cpp | 214 +++++++ wxPloiter/crypt.hpp | 28 + wxPloiter/detours.h | 528 ++++++++++++++++ wxPloiter/detours.lib | Bin 0 -> 201168 bytes wxPloiter/dllmain.cpp | 13 + wxPloiter/headerdialog.cpp | 268 +++++++++ wxPloiter/headerdialog.hpp | 42 ++ wxPloiter/logging.cpp | 252 ++++++++ wxPloiter/logging.hpp | 63 ++ wxPloiter/mainform.cpp | 894 ++++++++++++++++++++++++++++ wxPloiter/mainform.hpp | 193 ++++++ wxPloiter/mem.cpp | 89 +++ wxPloiter/mem.h | 16 + wxPloiter/packet.cpp | 380 ++++++++++++ wxPloiter/packet.hpp | 165 +++++ wxPloiter/packethooks.cpp | 487 +++++++++++++++ wxPloiter/packethooks.cpp.bak.cpp | 340 +++++++++++ wxPloiter/packethooks.hpp | 63 ++ wxPloiter/packethooks.hpp.bak.hpp | 65 ++ wxPloiter/packetstruct.h | 49 ++ wxPloiter/resource.aps | Bin 0 -> 122984 bytes wxPloiter/resource.h | 16 + wxPloiter/resource.rc | Bin 0 -> 388 bytes wxPloiter/safeheaderlist.cpp | 148 +++++ wxPloiter/safeheaderlist.hpp | 56 ++ wxPloiter/utils.cpp | 191 ++++++ wxPloiter/utils.hpp | 106 ++++ wxPloiter/wsockhooks.cpp | 309 ++++++++++ wxPloiter/wsockhooks.hpp | 67 +++ wxPloiter/wxPloiter.vcxproj | 131 ++++ wxPloiter/wxPloiter.vcxproj.filters | 110 ++++ wxPloiter/wxPloiter.vcxproj.user | 6 + wxPloiter/zapp.ico | Bin 0 -> 23558 bytes 40 files changed, 5770 insertions(+) create mode 100644 README.md create mode 100644 wxPloiter.sln create mode 100644 wxPloiter/aes.cpp create mode 100644 wxPloiter/aes.hpp create mode 100644 wxPloiter/aobscan.cpp create mode 100644 wxPloiter/aobscan.hpp create mode 100644 wxPloiter/common.h create mode 100644 wxPloiter/crypt.cpp create mode 100644 wxPloiter/crypt.hpp create mode 100644 wxPloiter/detours.h create mode 100644 wxPloiter/detours.lib create mode 100644 wxPloiter/dllmain.cpp create mode 100644 wxPloiter/headerdialog.cpp create mode 100644 wxPloiter/headerdialog.hpp create mode 100644 wxPloiter/logging.cpp create mode 100644 wxPloiter/logging.hpp create mode 100644 wxPloiter/mainform.cpp create mode 100644 wxPloiter/mainform.hpp create mode 100644 wxPloiter/mem.cpp create mode 100644 wxPloiter/mem.h create mode 100644 wxPloiter/packet.cpp create mode 100644 wxPloiter/packet.hpp create mode 100644 wxPloiter/packethooks.cpp create mode 100644 wxPloiter/packethooks.cpp.bak.cpp create mode 100644 wxPloiter/packethooks.hpp create mode 100644 wxPloiter/packethooks.hpp.bak.hpp create mode 100644 wxPloiter/packetstruct.h create mode 100644 wxPloiter/resource.aps create mode 100644 wxPloiter/resource.h create mode 100644 wxPloiter/resource.rc create mode 100644 wxPloiter/safeheaderlist.cpp create mode 100644 wxPloiter/safeheaderlist.hpp create mode 100644 wxPloiter/utils.cpp create mode 100644 wxPloiter/utils.hpp create mode 100644 wxPloiter/wsockhooks.cpp create mode 100644 wxPloiter/wsockhooks.hpp create mode 100644 wxPloiter/wxPloiter.vcxproj create mode 100644 wxPloiter/wxPloiter.vcxproj.filters create mode 100644 wxPloiter/wxPloiter.vcxproj.user create mode 100644 wxPloiter/zapp.ico diff --git a/README.md b/README.md new file mode 100644 index 0000000..f64bb70 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +I'm currently in the process of writing this readme with instructions on how to compile this. Please wait for the next commit. \ No newline at end of file diff --git a/wxPloiter.sln b/wxPloiter.sln new file mode 100644 index 0000000..3d71818 --- /dev/null +++ b/wxPloiter.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "wxPloiter", "wxPloiter\wxPloiter.vcxproj", "{2C8BAC1D-B028-4557-9FBB-AE5935FBB79A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Release|Win32 = Release|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C8BAC1D-B028-4557-9FBB-AE5935FBB79A}.Debug|Win32.ActiveCfg = Debug|Win32 + {2C8BAC1D-B028-4557-9FBB-AE5935FBB79A}.Debug|Win32.Build.0 = Debug|Win32 + {2C8BAC1D-B028-4557-9FBB-AE5935FBB79A}.Release|Win32.ActiveCfg = Release|Win32 + {2C8BAC1D-B028-4557-9FBB-AE5935FBB79A}.Release|Win32.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/wxPloiter/aes.cpp b/wxPloiter/aes.cpp new file mode 100644 index 0000000..23b5899 --- /dev/null +++ b/wxPloiter/aes.cpp @@ -0,0 +1,98 @@ +#include "aes.hpp" + +#include +#include + +namespace maple +{ + const signed_dword aes::cb_aeskey = 32; + + const byte aes::aeskey[aes::cb_aeskey] = + { + 0x13, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, + 0xB4, 0x00, 0x00, 0x00, + 0x1B, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00 + }; + + const signed_dword aes::blocksize = 1460; + + aes *aes::get() + { + static aes inst; + return &inst; + } + + aes::aes() + : botankey(Botan::SymmetricKey(aeskey, cb_aeskey)) + { + // empty + } + + aes::~aes() + { + // empty + } + + void aes::encrypt(byte *buffer, byte *iv, signed_dword cb) + { + signed_dword pos = 0; + byte first = 1; + signed_dword tpos = 0; + signed_dword cbwrite = 0; + Botan::InitializationVector initvec(iv, 16); + + while (cb > pos) + { + tpos = blocksize - first * 4; + cbwrite = (cb > (pos + tpos) ? tpos : (cb - pos)); + + Botan::Pipe pipe(Botan::get_cipher("AES-256/OFB/NoPadding", + botankey, initvec, Botan::ENCRYPTION)); + pipe.start_msg(); + pipe.write(buffer + pos, cbwrite); + pipe.end_msg(); + + // process the message and write it into the buffer + pipe.read(buffer + pos, cbwrite); + + pos += tpos; + + if (first) + first = 0; + } + } + + void aes::decrypt(byte *buffer, byte *iv, signed_dword cb) + { + signed_dword pos = 0; + byte first = 1; + signed_dword tpos = 0; + signed_dword cbread = 0; + Botan::InitializationVector initvec(iv, 16); + + while (cb > pos) + { + tpos = blocksize - first * 4; + cbread = (cb > (pos + tpos) ? tpos : (cb - pos)); + + Botan::Pipe pipe(Botan::get_cipher("AES-256/OFB/NoPadding", + botankey, initvec, Botan::DECRYPTION)); + pipe.start_msg(); + pipe.write(buffer + pos, cbread); + pipe.end_msg(); + + // process the message and write it into the buffer + pipe.read(buffer + pos, cbread); + + pos += tpos; + + if (first) + first = 0; + } + } +} diff --git a/wxPloiter/aes.hpp b/wxPloiter/aes.hpp new file mode 100644 index 0000000..7bb3537 --- /dev/null +++ b/wxPloiter/aes.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "common.h" + +#include + +namespace maple +{ + // utility for maplestory's aes encryption + // note: botan must be initialized before using this class + // credits to vana for this slimmer encryption using botan + class aes + { + public: + static aes *get(); + virtual ~aes(); + void decrypt(byte *buffer, byte *iv, signed_dword cb); + void encrypt(byte *buffer, byte *iv, signed_dword cb); + + protected: + static const signed_dword cb_aeskey; // size of the aes key + static const byte aeskey[]; // aes key + static const signed_dword blocksize; // aes block size + + Botan::OctetString botankey; // symmetric key for AES encryption + + aes(); + }; +} diff --git a/wxPloiter/aobscan.cpp b/wxPloiter/aobscan.cpp new file mode 100644 index 0000000..85c52ab --- /dev/null +++ b/wxPloiter/aobscan.cpp @@ -0,0 +1,250 @@ +#include "aobscan.hpp" + +#include +#include + +namespace utils { +namespace mem +{ + aobscan::aobscan(const std::string &pattern, void *pimagebase, size_t imagesize, int index) + : pimagebase(reinterpret_cast(pimagebase)), + imagesize(imagesize), + pattern(pattern), + presult(NULL), + res(aobscan::none), + mask(reinterpret_cast(NULL)), // not sure why I have to cast here + data(reinterpret_cast(NULL)) + + { + bool invalid = false; + + countpatternbytes(); + invalid = invalid || !length; + + // initialize mask & data buffers + mask.reset(new char[length + 1]); + data.reset(new byte[length]); + memset(mask.get(), 0, length + 1); + memset(data.get(), 0, length); + + invalid = invalid || !makepatternmask(); + invalid = invalid || !makepatternbytes(); + + if (invalid) + res = aobscan::invalid; + + searchpattern(index); + + if (!presult) + res = aobscan::not_found; + + res = aobscan::none; + } + + aobscan::~aobscan() + { + // empty + } + + byte *aobscan::result() const + { + return presult; + } + + aobscan::error aobscan::geterror() const + { + return res; + } + + std::string aobscan::string() const + { + return pattern; + } + + size_t aobscan::bytecount() const + { + return length; + } + + boost::shared_array aobscan::bytearray() const + { + return data; + } + + boost::shared_array aobscan::maskstring() const + { + return mask; + } + + void aobscan::countpatternbytes() + { + size_t cb = 0; + bool firstnibble = false; + + for (size_t i = 0; i < pattern.length(); i++) + { + char c; + + c = std::toupper(pattern[i]); + + if (c == ' ') // ignore whitespace + continue; + + if (c == '?') // wildcard bytes + { + if (firstnibble) // unexpected ? after a nibble + { + length = 0; + return; + } + + cb++; // wildcard byte - increase count + } + + else // regular bytes + { + if (!std::isxdigit(c)) // invalid non-hex byte + { + length = 0; + return; + } + + if (firstnibble) // regular byte - increase count + cb++; + + firstnibble ^= true; + } + } + + if (firstnibble) // invalid truncated last nibble + { + length = 0; + return; + } + + length = cb; + } + + bool aobscan::makepatternmask() + { + bool firstnibble = false; + + for (size_t i = 0; i < pattern.length(); i++) + { + char c; + + c = std::toupper(pattern[i]); + + if (c == ' ') // ignore whitespace + continue; + + if (c == '?') // wildcard bytes + { + if (firstnibble) // invalid ? after a nibble + return false; + + strcat_s(mask.get(), length + 1, "?"); // wildcard + } + + else // regular bytes + { + if (!std::isxdigit(c)) // invalid non-hex byte + return false; + + if (firstnibble) + strcat_s(mask.get(), length + 1, "x"); // regular byte + + firstnibble ^= true; + } + } + + if (firstnibble) // invalid truncated last nibble + return false; + + return true; + } + + bool aobscan::makepatternbytes() + { + bool firstnibble = false; + size_t count = 0; + + for (size_t i = 0; i < pattern.length(); i++) + { + char c; + + c = toupper(pattern[i]); + + if (c == ' ') // ignore whitespace + continue; + + if (c == '?') + { + if (firstnibble) // invalid ? after a nibble + return false; + + data[count] = 0x00; // wildcard byte + count++; + } + + else + { + if (!std::isxdigit(c)) // invalid non-hex digit + return false; + + if (firstnibble) + { + char szbyte[3] = {0}; + + // convert byte string to byte + szbyte[0] = pattern[i - 1]; + szbyte[1] = c; + szbyte[2] = '\0'; + data[count] = static_cast(std::strtol(szbyte, NULL, 16)); + count++; + } + + firstnibble ^= true; + } + } + + if (firstnibble) + return false; + + return true; + } + + void aobscan::searchpattern(int index) + { + presult = NULL; + int n = 0; + + for (byte *i = pimagebase; i < pimagebase + imagesize; i++) + { + bool found = true; + + for (size_t j = 0; j < length; j++) + { + if (mask[j] != 'x') + continue; // whitespace or wildcard + + // check byte value + if (*reinterpret_cast(i + j) != data[j]) + { + found = false; + break; + } + } + + if (found) + { + if (n == index) + { + presult = i; + return; + } + else n++; + } + } + } +}} diff --git a/wxPloiter/aobscan.hpp b/wxPloiter/aobscan.hpp new file mode 100644 index 0000000..a80dda1 --- /dev/null +++ b/wxPloiter/aobscan.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "common.h" +#include +#include + +namespace utils { +// utilities for memory reading & editing +namespace mem +{ + // scans for a byte array in a given memory region + class aobscan + { + public: + enum error + { + none = 0, + invalid = 1, + not_found = 2 + }; + + // scans the given array of bytes in the given memory region + // pattern: a byte pattern with question marks as wildcards, for example "11 AA BB ? CC ? DD" + // pimagebase: the beginning address of the memory region to scan + // imagesize: the size of the memory region to scan + // index: number of occurences to skip + aobscan(const std::string &pattern, void *pimagebase, size_t imagesize, int index = 0); + virtual ~aobscan(); + + // retuns the address at which the aob was found + byte *result() const; + + // returns the result of the aob scan + // values: + // aobscan::none (0): pattern found correctly + // aobscan::invalid (1): the pattern is invalid + // aobscan::not_found (2): the pattern was not found + error geterror() const; + + // returns the byte array string that was passed at the constructor + std::string string() const; + + // returns the size of the pattern of bytes in number of bytes + size_t bytecount() const; + + // returns the byte pattern as a raw byte array with the wildcard bytes set to zero + boost::shared_array bytearray() const; + + // returns a mask string for the byte pattern which contains a series of x and ?, + // where the x stand for a non-wildcard byte and the ? stand for a wildcard byte + boost::shared_array maskstring() const; + + protected: + void countpatternbytes(); // counts the number of bytes in the aob + bool makepatternmask(); // generates a mask for the aob + bool makepatternbytes(); // generates the raw byte array for the aob + void searchpattern(int index); // searches the aob and stores the result in presult + + byte * const pimagebase; // the beginning address of the memory region to scan + const size_t imagesize; // the size of the memory region to scan + const std::string pattern; // a byte pattern with question marks as wildcards + size_t length; // length in bytes of the byte pattern + byte *presult; // the address at which the aob was found + error res; // result of the scan + boost::shared_array mask; // mask string for the byte pattern + boost::shared_array data; // byte pattern as a raw byte + }; +}} diff --git a/wxPloiter/common.h b/wxPloiter/common.h new file mode 100644 index 0000000..1f628be --- /dev/null +++ b/wxPloiter/common.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +typedef uint8_t byte; +typedef int8_t signed_byte; +typedef uint16_t word; +typedef int16_t signed_word; +typedef uint32_t dword; +typedef int32_t signed_dword; +typedef uint64_t qword; // my 64bit integers will pierce through the heavens! +typedef int64_t signed_qword; + +#define BLOCKED_HEADER -1 diff --git a/wxPloiter/crypt.cpp b/wxPloiter/crypt.cpp new file mode 100644 index 0000000..c1979c1 --- /dev/null +++ b/wxPloiter/crypt.cpp @@ -0,0 +1,214 @@ +#include "crypt.hpp" +#include "aes.hpp" +#include "utils.hpp" + +// this is all refactored code from titanms, credits to whoever made this +// TODO: refactor the unreadable encryption crap + +namespace maple +{ + namespace asmop = utils::asmop; + + crypt::crypt(word maple_version, byte *iv) + { + // the cypher key is repeated 4 times + for (byte i = 0; i < 4; i++) + std::copy(iv, iv + 4, this->iv + i * 4); + + this->maple_version = ((maple_version >> 8) & 0xFF) | + ((maple_version << 8) & 0xFF00); + } + + crypt::~crypt() + { + // empty + } + + word crypt::getmapleversion() + { + return maple_version; + } + + void crypt::encrypt(byte *buffer, signed_dword cb) + { + maplecrypt(buffer, cb); + aes::get()->encrypt(buffer, iv, cb); + } + + void crypt::decrypt(byte *buffer, signed_dword cb) + { + aes::get()->decrypt(buffer, iv, cb); + mapledecrypt(buffer, cb); + } + + bool crypt::check(byte *buffer) + { + // I have no idea what I'm doing but it works so don't touch this + return ((((buffer[0] ^ iv[2]) & 0xFF) == ((maple_version >> 8) & 0xFF)) && + (((buffer[1] ^ iv[3]) & 0xFF) == (maple_version & 0xFF))); + } + + void crypt::makeheader(byte *buffer, word cb) + { + // I have no idea what I'm doing + // this encryption shit was reversed from the game itself + word iiv = (iv[3]) & 0xFF; + iiv |= (iv[2] << 8) & 0xFF00; + + iiv ^= maple_version; + word mlength = ((cb << 8) & 0xFF00) | (cb >> 8); + word xoredIv = iiv ^ mlength; + + buffer[0] = static_cast((iiv >> 8) & 0xFF); + buffer[1] = static_cast(iiv & 0xFF); + buffer[2] = static_cast((xoredIv >> 8) & 0xFF); + buffer[3] = static_cast(xoredIv & 0xFF); + } + + word crypt::length(byte *header) + { + return (static_cast(header[0]) + static_cast(header[1]) * 0x100) + ^ (static_cast(header[2]) + static_cast(header[3]) * 0x100); + } + + void crypt::nextiv() + { + nextiv(iv); + } + + void crypt::nextiv(byte *vector) + { + // I have no idea what I'm doing + // this encryption shit was reversed from the game itself + // credits to vana for the key shuffle code + static const byte im12andwhatisthis[256] = { + 0xEC, 0x3F, 0x77, 0xA4, 0x45, 0xD0, 0x71, 0xBF, 0xB7, 0x98, 0x20, 0xFC, 0x4B, 0xE9, 0xB3, 0xE1, + 0x5C, 0x22, 0xF7, 0x0C, 0x44, 0x1B, 0x81, 0xBD, 0x63, 0x8D, 0xD4, 0xC3, 0xF2, 0x10, 0x19, 0xE0, + 0xFB, 0xA1, 0x6E, 0x66, 0xEA, 0xAE, 0xD6, 0xCE, 0x06, 0x18, 0x4E, 0xEB, 0x78, 0x95, 0xDB, 0xBA, + 0xB6, 0x42, 0x7A, 0x2A, 0x83, 0x0B, 0x54, 0x67, 0x6D, 0xE8, 0x65, 0xE7, 0x2F, 0x07, 0xF3, 0xAA, + 0x27, 0x7B, 0x85, 0xB0, 0x26, 0xFD, 0x8B, 0xA9, 0xFA, 0xBE, 0xA8, 0xD7, 0xCB, 0xCC, 0x92, 0xDA, + 0xF9, 0x93, 0x60, 0x2D, 0xDD, 0xD2, 0xA2, 0x9B, 0x39, 0x5F, 0x82, 0x21, 0x4C, 0x69, 0xF8, 0x31, + 0x87, 0xEE, 0x8E, 0xAD, 0x8C, 0x6A, 0xBC, 0xB5, 0x6B, 0x59, 0x13, 0xF1, 0x04, 0x00, 0xF6, 0x5A, + 0x35, 0x79, 0x48, 0x8F, 0x15, 0xCD, 0x97, 0x57, 0x12, 0x3E, 0x37, 0xFF, 0x9D, 0x4F, 0x51, 0xF5, + 0xA3, 0x70, 0xBB, 0x14, 0x75, 0xC2, 0xB8, 0x72, 0xC0, 0xED, 0x7D, 0x68, 0xC9, 0x2E, 0x0D, 0x62, + 0x46, 0x17, 0x11, 0x4D, 0x6C, 0xC4, 0x7E, 0x53, 0xC1, 0x25, 0xC7, 0x9A, 0x1C, 0x88, 0x58, 0x2C, + 0x89, 0xDC, 0x02, 0x64, 0x40, 0x01, 0x5D, 0x38, 0xA5, 0xE2, 0xAF, 0x55, 0xD5, 0xEF, 0x1A, 0x7C, + 0xA7, 0x5B, 0xA6, 0x6F, 0x86, 0x9F, 0x73, 0xE6, 0x0A, 0xDE, 0x2B, 0x99, 0x4A, 0x47, 0x9C, 0xDF, + 0x09, 0x76, 0x9E, 0x30, 0x0E, 0xE4, 0xB2, 0x94, 0xA0, 0x3B, 0x34, 0x1D, 0x28, 0x0F, 0x36, 0xE3, + 0x23, 0xB4, 0x03, 0xD8, 0x90, 0xC8, 0x3C, 0xFE, 0x5E, 0x32, 0x24, 0x50, 0x1F, 0x3A, 0x43, 0x8A, + 0x96, 0x41, 0x74, 0xAC, 0x52, 0x33, 0xF0, 0xD9, 0x29, 0x80, 0xB1, 0x16, 0xD3, 0xAB, 0x91, 0xB9, + 0x84, 0x7F, 0x61, 0x1E, 0xCF, 0xC5, 0xD1, 0x56, 0x3D, 0xCA, 0xF4, 0x05, 0xC6, 0xE5, 0x08, 0x49 }; + + byte newiv[4] = {0xF2, 0x53, 0x50, 0xC6}; + byte input; + byte valueinput; + dword fulliv; + dword shift; + + for (byte i = 0; i < 4; i++) + { + input = vector[i]; + valueinput = im12andwhatisthis[input]; + + newiv[0] += (im12andwhatisthis[newiv[1]] - input); + newiv[1] -= (newiv[2] ^ valueinput); + newiv[2] ^= (im12andwhatisthis[newiv[3]] + input); + newiv[3] -= (newiv[0] - valueinput); + + fulliv = (newiv[3] << 24) | (newiv[2] << 16) | (newiv[1] << 8) | newiv[0]; + shift = (fulliv >> 0x1D) | (fulliv << 0x03); + + newiv[0] = static_cast(shift & 0xFFu); + newiv[1] = static_cast((shift >> 8) & 0xFFu); + newiv[2] = static_cast((shift >> 16) & 0xFFu); + newiv[3] = static_cast((shift >> 24) & 0xFFu); + } + + for (byte i = 0; i < 4; i++) + std::copy(newiv, newiv + 4, vector + i * 4); + } + + void crypt::mapledecrypt(byte *buf, signed_dword size) + { + // I have no idea what I'm doing + // this encryption shit was reversed from the game itself + signed_dword j; + byte a, b, c; + + for (byte i = 0; i < 3; i++) + { + a = 0; + b = 0; + + for (j = size; j > 0; j--) + { + c = buf[j - 1]; + c = asmop::rol(c, 3); + c = c ^ 0x13; + a = c; + c = c ^ b; + c = (byte)(c - j); // shitty cast + c = asmop::ror(c, 4); + b = a; + buf[j - 1] = c; + } + + a = 0; + b = 0; + + for (j = size; j > 0; j--) + { + c = buf[size - j]; + c = c - 0x48; + c = c ^ 0xFF; + c = asmop::rol(c, j); + a = c; + c = c ^ b; + c = (byte)(c - j); // shitty cast + c = asmop::ror(c, 3); + b = a; + buf[size - j] = c; + } + } + } + + void crypt::maplecrypt(byte *buf, signed_dword size) + { + // I have no idea what I'm doing + // this encryption shit was reversed from the game itself + signed_dword j; + byte a, c; + + for (byte i = 0; i < 3; i++) + { + a = 0; + + for (j = size; j > 0; j--) + { + c = buf[size - j]; + c = asmop::rol(c, 3); + c = (byte)(c + j); // shitty cast + c = c ^ a; + a = c; + c = asmop::ror(a, j); + c = c ^ 0xFF; + c = c + 0x48; + buf[size - j] = c; + } + + a = 0; + + for (j = size; j > 0; j--) + { + c = buf[j - 1]; + c = asmop::rol(c, 4); + c = (byte)(c + j); // shitty cast + c = c ^ a; + a = c; + c = c ^ 0x13; + c = asmop::ror(c, 3); + buf[j - 1] = c; + } + } + } +} diff --git a/wxPloiter/crypt.hpp b/wxPloiter/crypt.hpp new file mode 100644 index 0000000..6e0b9d0 --- /dev/null +++ b/wxPloiter/crypt.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "common.h" + +namespace maple +{ + class crypt + { + public: + crypt(word maple_version, byte *iv); + virtual ~crypt(); + word getmapleversion(); + void encrypt(byte *buffer, signed_dword cb); // used to encrypt send packets + void decrypt(byte *buffer, signed_dword cb); // used to decrypt send packets + bool check(byte *buffer); // checks whether a packet is encrypted + void makeheader(byte *buffer, word cb); // create an encrypted header for a send packet + void nextiv(); // shuffles the encryption key + static word length(byte *header); // gets the length of the packet from the encrypted header + + protected: + word maple_version; + byte iv[16]; // cypher key + + static void nextiv(byte *vector); // shuffles the cypher key + static void mapledecrypt(byte *buf, signed_dword size); // custom maple decryption layer + static void maplecrypt(byte *buf, signed_dword size); // custom maple encryption layer + }; +} diff --git a/wxPloiter/detours.h b/wxPloiter/detours.h new file mode 100644 index 0000000..2773242 --- /dev/null +++ b/wxPloiter/detours.h @@ -0,0 +1,528 @@ +////////////////////////////////////////////////////////////////////////////// +// +// Core Detours Functionality (detours.h of detours.lib) +// +// Microsoft Research Detours Package, Version 2.1. +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// + +#pragma once +#ifndef _DETOURS_H_ +#define _DETOURS_H_ + +#define DETOURS_VERSION 20100 // 2.1.0 + +////////////////////////////////////////////////////////////////////////////// +// + +#if (_MSC_VER < 1299) +typedef LONG LONG_PTR; +typedef ULONG ULONG_PTR; +#endif + +#ifndef __in_z +#define __in_z +#endif + +////////////////////////////////////////////////////////////////////////////// +// +#ifndef GUID_DEFINED +#define GUID_DEFINED +typedef struct _GUID +{ + DWORD Data1; + WORD Data2; + WORD Data3; + BYTE Data4[ 8 ]; +} GUID; + +#ifdef INITGUID +#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + const GUID name \ + = { l, w1, w2, { b1, b2, b3, b4, b5, b6, b7, b8 } } +#else +#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \ + const GUID name +#endif // INITGUID +#endif // !GUID_DEFINED + +#if defined(__cplusplus) +#ifndef _REFGUID_DEFINED +#define _REFGUID_DEFINED +#define REFGUID const GUID & +#endif // !_REFGUID_DEFINED +#else // !__cplusplus +#ifndef _REFGUID_DEFINED +#define _REFGUID_DEFINED +#define REFGUID const GUID * const +#endif // !_REFGUID_DEFINED +#endif // !__cplusplus + +// +////////////////////////////////////////////////////////////////////////////// + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +/////////////////////////////////////////////////// Instruction Target Macros. +// +#define DETOUR_INSTRUCTION_TARGET_NONE ((PVOID)0) +#define DETOUR_INSTRUCTION_TARGET_DYNAMIC ((PVOID)(LONG_PTR)-1) +#define DETOUR_SECTION_HEADER_SIGNATURE 0x00727444 // "Dtr\0" + +extern const GUID DETOUR_EXE_RESTORE_GUID; + +#define DETOUR_TRAMPOLINE_SIGNATURE 0x21727444 // Dtr! +typedef struct _DETOUR_TRAMPOLINE DETOUR_TRAMPOLINE, *PDETOUR_TRAMPOLINE; + +/////////////////////////////////////////////////////////// Binary Structures. +// +#pragma pack(push, 8) +typedef struct _DETOUR_SECTION_HEADER +{ + DWORD cbHeaderSize; + DWORD nSignature; + DWORD nDataOffset; + DWORD cbDataSize; + + DWORD nOriginalImportVirtualAddress; + DWORD nOriginalImportSize; + DWORD nOriginalBoundImportVirtualAddress; + DWORD nOriginalBoundImportSize; + + DWORD nOriginalIatVirtualAddress; + DWORD nOriginalIatSize; + DWORD nOriginalSizeOfImage; + DWORD cbPrePE; + + DWORD nOriginalClrFlags; + DWORD reserved1; + DWORD reserved2; + DWORD reserved3; + + // Followed by cbPrePE bytes of data. +} DETOUR_SECTION_HEADER, *PDETOUR_SECTION_HEADER; + +typedef struct _DETOUR_SECTION_RECORD +{ + DWORD cbBytes; + DWORD nReserved; + GUID guid; +} DETOUR_SECTION_RECORD, *PDETOUR_SECTION_RECORD; + +typedef struct _DETOUR_CLR_HEADER +{ + // Header versioning + ULONG cb; + USHORT MajorRuntimeVersion; + USHORT MinorRuntimeVersion; + + // Symbol table and startup information + IMAGE_DATA_DIRECTORY MetaData; + ULONG Flags; + + // Followed by the rest of the header. +} DETOUR_CLR_HEADER, *PDETOUR_CLR_HEADER; + +typedef struct _DETOUR_EXE_RESTORE +{ + ULONG cb; + + PIMAGE_DOS_HEADER pidh; + PIMAGE_NT_HEADERS pinh; + PULONG pclrFlags; + DWORD impDirProt; + + IMAGE_DOS_HEADER idh; + IMAGE_NT_HEADERS inh; + ULONG clrFlags; +} DETOUR_EXE_RESTORE, *PDETOUR_EXE_RESTORE; + +#pragma pack(pop) + +#define DETOUR_SECTION_HEADER_DECLARE(cbSectionSize) \ +{ \ + sizeof(DETOUR_SECTION_HEADER),\ + DETOUR_SECTION_HEADER_SIGNATURE,\ + sizeof(DETOUR_SECTION_HEADER),\ + (cbSectionSize),\ + \ + 0,\ + 0,\ + 0,\ + 0,\ + \ + 0,\ + 0,\ + 0,\ + 0,\ +} + +///////////////////////////////////////////////////////////// Binary Typedefs. +// +typedef BOOL (CALLBACK *PF_DETOUR_BINARY_BYWAY_CALLBACK)(PVOID pContext, + PCHAR pszFile, + PCHAR *ppszOutFile); + +typedef BOOL (CALLBACK *PF_DETOUR_BINARY_FILE_CALLBACK)(PVOID pContext, + PCHAR pszOrigFile, + PCHAR pszFile, + PCHAR *ppszOutFile); + +typedef BOOL (CALLBACK *PF_DETOUR_BINARY_SYMBOL_CALLBACK)(PVOID pContext, + ULONG nOrigOrdinal, + ULONG nOrdinal, + ULONG *pnOutOrdinal, + PCHAR pszOrigSymbol, + PCHAR pszSymbol, + PCHAR *ppszOutSymbol); + +typedef BOOL (CALLBACK *PF_DETOUR_BINARY_COMMIT_CALLBACK)(PVOID pContext); + +typedef BOOL (CALLBACK *PF_DETOUR_ENUMERATE_EXPORT_CALLBACK)(PVOID pContext, + ULONG nOrdinal, + PCHAR pszName, + PVOID pCode); + +typedef VOID * PDETOUR_BINARY; +typedef VOID * PDETOUR_LOADED_BINARY; + +//////////////////////////////////////////////////////////// Detours 2.1 APIs. +// + +LONG WINAPI DetourTransactionBegin(); +LONG WINAPI DetourTransactionAbort(); +LONG WINAPI DetourTransactionCommit(); +LONG WINAPI DetourTransactionCommitEx(PVOID **pppFailedPointer); + +LONG WINAPI DetourUpdateThread(HANDLE hThread); + +LONG WINAPI DetourAttach(PVOID *ppPointer, + PVOID pDetour); + +LONG WINAPI DetourAttachEx(PVOID *ppPointer, + PVOID pDetour, + PDETOUR_TRAMPOLINE *ppRealTrampoline, + PVOID *ppRealTarget, + PVOID *ppRealDetour); + +LONG WINAPI DetourDetach(PVOID *ppPointer, + PVOID pDetour); + +VOID WINAPI DetourSetIgnoreTooSmall(BOOL fIgnore); + +////////////////////////////////////////////////////////////// Code Functions. +// +PVOID WINAPI DetourFindFunction(PCSTR pszModule, PCSTR pszFunction); +PVOID WINAPI DetourCodeFromPointer(PVOID pPointer, PVOID *ppGlobals); + +PVOID WINAPI DetourCopyInstruction(PVOID pDst, PVOID pSrc, PVOID *ppTarget); +PVOID WINAPI DetourCopyInstructionEx(PVOID pDst, + PVOID pSrc, + PVOID *ppTarget, + LONG *plExtra); + +///////////////////////////////////////////////////// Loaded Binary Functions. +// +HMODULE WINAPI DetourEnumerateModules(HMODULE hModuleLast); +PVOID WINAPI DetourGetEntryPoint(HMODULE hModule); +ULONG WINAPI DetourGetModuleSize(HMODULE hModule); +BOOL WINAPI DetourEnumerateExports(HMODULE hModule, + PVOID pContext, + PF_DETOUR_ENUMERATE_EXPORT_CALLBACK pfExport); + +PVOID WINAPI DetourFindPayload(HMODULE hModule, REFGUID rguid, DWORD *pcbData); +DWORD WINAPI DetourGetSizeOfPayloads(HMODULE hModule); + +///////////////////////////////////////////////// Persistent Binary Functions. +// + +PDETOUR_BINARY WINAPI DetourBinaryOpen(HANDLE hFile); +PVOID WINAPI DetourBinaryEnumeratePayloads(PDETOUR_BINARY pBinary, + GUID *pGuid, + DWORD *pcbData, + DWORD *pnIterator); +PVOID WINAPI DetourBinaryFindPayload(PDETOUR_BINARY pBinary, + REFGUID rguid, + DWORD *pcbData); +PVOID WINAPI DetourBinarySetPayload(PDETOUR_BINARY pBinary, + REFGUID rguid, + PVOID pData, + DWORD cbData); +BOOL WINAPI DetourBinaryDeletePayload(PDETOUR_BINARY pBinary, REFGUID rguid); +BOOL WINAPI DetourBinaryPurgePayloads(PDETOUR_BINARY pBinary); +BOOL WINAPI DetourBinaryResetImports(PDETOUR_BINARY pBinary); +BOOL WINAPI DetourBinaryEditImports(PDETOUR_BINARY pBinary, + PVOID pContext, + PF_DETOUR_BINARY_BYWAY_CALLBACK pfByway, + PF_DETOUR_BINARY_FILE_CALLBACK pfFile, + PF_DETOUR_BINARY_SYMBOL_CALLBACK pfSymbol, + PF_DETOUR_BINARY_COMMIT_CALLBACK pfCommit); +BOOL WINAPI DetourBinaryWrite(PDETOUR_BINARY pBinary, HANDLE hFile); +BOOL WINAPI DetourBinaryClose(PDETOUR_BINARY pBinary); + +/////////////////////////////////////////////////// Create Process & Load Dll. +// +typedef BOOL (WINAPI *PDETOUR_CREATE_PROCESS_ROUTINEA) + (LPCSTR lpApplicationName, + LPSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCSTR lpCurrentDirectory, + LPSTARTUPINFOA lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation); + +typedef BOOL (WINAPI *PDETOUR_CREATE_PROCESS_ROUTINEW) + (LPCWSTR lpApplicationName, + LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation); + +BOOL WINAPI DetourCreateProcessWithDllA(LPCSTR lpApplicationName, + __in_z LPSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCSTR lpCurrentDirectory, + LPSTARTUPINFOA lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, + LPCSTR lpDetouredDllFullName, + LPCSTR lpDllName, + PDETOUR_CREATE_PROCESS_ROUTINEA + pfCreateProcessA); + +BOOL WINAPI DetourCreateProcessWithDllW(LPCWSTR lpApplicationName, + __in_z LPWSTR lpCommandLine, + LPSECURITY_ATTRIBUTES lpProcessAttributes, + LPSECURITY_ATTRIBUTES lpThreadAttributes, + BOOL bInheritHandles, + DWORD dwCreationFlags, + LPVOID lpEnvironment, + LPCWSTR lpCurrentDirectory, + LPSTARTUPINFOW lpStartupInfo, + LPPROCESS_INFORMATION lpProcessInformation, + LPCSTR lpDetouredDllFullName, + LPCSTR lpDllName, + PDETOUR_CREATE_PROCESS_ROUTINEW + pfCreateProcessW); + +#ifdef UNICODE +#define DetourCreateProcessWithDll DetourCreateProcessWithDllW +#define PDETOUR_CREATE_PROCESS_ROUTINE PDETOUR_CREATE_PROCESS_ROUTINEW +#else +#define DetourCreateProcessWithDll DetourCreateProcessWithDllA +#define PDETOUR_CREATE_PROCESS_ROUTINE PDETOUR_CREATE_PROCESS_ROUTINEA +#endif // !UNICODE + +BOOL WINAPI DetourCopyPayloadToProcess(HANDLE hProcess, + REFGUID rguid, + PVOID pvData, + DWORD cbData); +BOOL WINAPI DetourRestoreAfterWith(); +BOOL WINAPI DetourRestoreAfterWithEx(PVOID pvData, DWORD cbData); + +// +////////////////////////////////////////////////////////////////////////////// +#ifdef __cplusplus +} +#endif // __cplusplus + +//////////////////////////////////////////////// Detours Internal Definitions. +// +#ifdef __cplusplus +#ifdef DETOURS_INTERNAL + +#ifndef __deref_out +#define __deref_out +#endif + +#ifndef __deref +#define __deref +#endif + +////////////////////////////////////////////////////////////////////////////// +// +#if (_MSC_VER < 1299) +#include +typedef IMAGEHLP_MODULE IMAGEHLP_MODULE64; +typedef PIMAGEHLP_MODULE PIMAGEHLP_MODULE64; +typedef IMAGEHLP_SYMBOL SYMBOL_INFO; +typedef PIMAGEHLP_SYMBOL PSYMBOL_INFO; + +static inline +LONG InterlockedCompareExchange(LONG *ptr, LONG nval, LONG oval) +{ + return (LONG)::InterlockedCompareExchange((PVOID*)ptr, (PVOID)nval, (PVOID)oval); +} +#else +#include +#endif + +#ifdef IMAGEAPI // defined by DBGHELP.H +typedef LPAPI_VERSION (NTAPI *PF_ImagehlpApiVersionEx)(LPAPI_VERSION AppVersion); + +typedef BOOL (NTAPI *PF_SymInitialize)(IN HANDLE hProcess, + IN LPCSTR UserSearchPath, + IN BOOL fInvadeProcess); +typedef DWORD (NTAPI *PF_SymSetOptions)(IN DWORD SymOptions); +typedef DWORD (NTAPI *PF_SymGetOptions)(VOID); +typedef DWORD64 (NTAPI *PF_SymLoadModule64)(IN HANDLE hProcess, + IN HANDLE hFile, + IN PSTR ImageName, + IN PSTR ModuleName, + IN DWORD64 BaseOfDll, + IN DWORD SizeOfDll); +typedef BOOL (NTAPI *PF_SymGetModuleInfo64)(IN HANDLE hProcess, + IN DWORD64 qwAddr, + OUT PIMAGEHLP_MODULE64 ModuleInfo); +typedef BOOL (NTAPI *PF_SymFromName)(IN HANDLE hProcess, + IN LPSTR Name, + OUT PSYMBOL_INFO Symbol); + +typedef struct _DETOUR_SYM_INFO +{ + HANDLE hProcess; + HMODULE hDbgHelp; + PF_ImagehlpApiVersionEx pfImagehlpApiVersionEx; + PF_SymInitialize pfSymInitialize; + PF_SymSetOptions pfSymSetOptions; + PF_SymGetOptions pfSymGetOptions; + PF_SymLoadModule64 pfSymLoadModule64; + PF_SymGetModuleInfo64 pfSymGetModuleInfo64; + PF_SymFromName pfSymFromName; +} DETOUR_SYM_INFO, *PDETOUR_SYM_INFO; + +PDETOUR_SYM_INFO DetourLoadDbgHelp(VOID); + +#endif // IMAGEAPI + +#ifndef DETOUR_TRACE +#if DETOUR_DEBUG +#define DETOUR_TRACE(x) printf x +#define DETOUR_BREAK() DebugBreak() +#include +#include +#else +#define DETOUR_TRACE(x) +#define DETOUR_BREAK() +#endif +#endif + +#ifdef DETOURS_IA64 +__declspec(align(16)) struct DETOUR_IA64_BUNDLE +{ + public: + union + { + BYTE data[16]; + UINT64 wide[2]; + }; + + public: + struct DETOUR_IA64_METADATA; + + typedef BOOL (DETOUR_IA64_BUNDLE::* DETOUR_IA64_METACOPY) + (const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + + enum { + A_UNIT = 1u, + I_UNIT = 2u, + M_UNIT = 3u, + B_UNIT = 4u, + F_UNIT = 5u, + L_UNIT = 6u, + X_UNIT = 7u, + UNIT_MASK = 7u, + STOP = 8u + }; + struct DETOUR_IA64_METADATA + { + ULONG nTemplate : 8; // Instruction template. + ULONG nUnit0 : 4; // Unit for slot 0 + ULONG nUnit1 : 4; // Unit for slot 1 + ULONG nUnit2 : 4; // Unit for slot 2 + DETOUR_IA64_METACOPY pfCopy; // Function pointer. + }; + + protected: + BOOL CopyBytes(const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + BOOL CopyBytesMMB(const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + BOOL CopyBytesMBB(const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + BOOL CopyBytesBBB(const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + BOOL CopyBytesMLX(const DETOUR_IA64_METADATA *pMeta, DETOUR_IA64_BUNDLE *pDst) const; + + static const DETOUR_IA64_METADATA s_rceCopyTable[33]; + + public: + // 120 112 104 96 88 80 72 64 56 48 40 32 24 16 8 0 + // f. e. d. c. b. a. 9. 8. 7. 6. 5. 4. 3. 2. 1. 0. + + // 00 + // f.e. d.c. b.a. 9.8. 7.6. 5.4. 3.2. 1.0. + // 0000 0000 0000 0000 0000 0000 0000 001f : Template [4..0] + // 0000 0000 0000 0000 0000 03ff ffff ffe0 : Zero [ 41.. 5] + // 0000 0000 0000 0000 0000 3c00 0000 0000 : Zero [ 45.. 42] + // 0000 0000 0007 ffff ffff c000 0000 0000 : One [ 82.. 46] + // 0000 0000 0078 0000 0000 0000 0000 0000 : One [ 86.. 83] + // 0fff ffff ff80 0000 0000 0000 0000 0000 : Two [123.. 87] + // f000 0000 0000 0000 0000 0000 0000 0000 : Two [127..124] + BYTE GetTemplate() const; + BYTE GetInst0() const; + BYTE GetInst1() const; + BYTE GetInst2() const; + BYTE GetUnit0() const; + BYTE GetUnit1() const; + BYTE GetUnit2() const; + UINT64 GetData0() const; + UINT64 GetData1() const; + UINT64 GetData2() const; + + public: + BOOL IsBrl() const; + VOID SetBrl(); + VOID SetBrl(UINT64 target); + UINT64 GetBrlTarget() const; + VOID SetBrlTarget(UINT64 target); + VOID SetBrlImm(UINT64 imm); + UINT64 GetBrlImm() const; + + BOOL IsMovlGp() const; + UINT64 GetMovlGp() const; + VOID SetMovlGp(UINT64 gp); + + VOID SetInst0(BYTE nInst); + VOID SetInst1(BYTE nInst); + VOID SetInst2(BYTE nInst); + VOID SetData0(UINT64 nData); + VOID SetData1(UINT64 nData); + VOID SetData2(UINT64 nData); + BOOL SetNop0(); + BOOL SetNop1(); + BOOL SetNop2(); + BOOL SetStop(); + + BOOL Copy(DETOUR_IA64_BUNDLE *pDst) const; +}; +#endif // DETOURS_IA64 + +////////////////////////////////////////////////////////////////////////////// + +#endif // DETOURS_INTERNAL +#endif // __cplusplus + +#endif // _DETOURS_H_ +// +//////////////////////////////////////////////////////////////// End of File. diff --git a/wxPloiter/detours.lib b/wxPloiter/detours.lib new file mode 100644 index 0000000000000000000000000000000000000000..fde5612ab09fa07f798d2652dee6341f1de27e90 GIT binary patch literal 201168 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 +#include +#include + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) +{ + if (dwReason == DLL_PROCESS_ATTACH) + boost::shared_ptr t = boost::make_shared( + wxPloiter::app::rundll, reinterpret_cast(hModule)); + + return TRUE; +} diff --git a/wxPloiter/headerdialog.cpp b/wxPloiter/headerdialog.cpp new file mode 100644 index 0000000..50bfd4f --- /dev/null +++ b/wxPloiter/headerdialog.cpp @@ -0,0 +1,268 @@ +#include "headerdialog.hpp" + +#include +#include +#include +#include +#include + +#include "common.h" +#include "packet.hpp" +#include "utils.hpp" + +namespace wxPloiter +{ + // {{{ + // headerlist begin + headerlist::headerlist(wxWindow *parent) + : wxListView(parent, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL) + { + SetItemCount(0); // initialize the listview as empty + + const int tacos = 70; + AppendColumn("header", wxLIST_FORMAT_LEFT, tacos); + AppendColumn("direction", wxLIST_FORMAT_LEFT, tacos); + AppendColumn("action", wxLIST_FORMAT_LEFT, tacos); + + SetFont(wxFont(8, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, + wxFONTWEIGHT_NORMAL, false, "Consolas")); + } + + headerlist::~headerlist() + { + // empty + } + + wxString headerlist::OnGetItemText(long item, long column) const + { + wxString dir, action; + safeheaderlist::ptr list; + + // iterates: + // - blocked recv + // - blocked send + // - ignored recv + // - ignored send + + size_t s1 = safeheaderlist::getblockedrecv()->size(); + size_t s2 = s1 + safeheaderlist::getblockedsend()->size(); + size_t s3 = s2 + safeheaderlist::getignoredrecv()->size(); + size_t s4 = s3 + safeheaderlist::getignoredsend()->size(); + + if (static_cast(item) < s1) + { + dir = wxString("<"); + action = wxString("block"); + list = safeheaderlist::getblockedrecv(); + } + + else if (static_cast(item) < s2) + { + dir = wxString(">"); + action = wxString("block"); + list = safeheaderlist::getblockedsend(); + item -= s1; + } + + else if (static_cast(item) < s3) + { + dir = wxString("<"); + action = wxString("ignore"); + list = safeheaderlist::getignoredrecv(); + item -= s2; + } + + else if (static_cast(item) < s4) + { + dir = wxString(">"); + action = wxString("ignore"); + list = safeheaderlist::getignoredsend(); + item -= s3; + } + else + assert(false); // should never happen + + byte byte1 = list->at(item) & 0x00FF; + byte byte2 = (list->at(item) & 0xFF00) >> 8; + + switch (column) + { + case 0: + return wxString::Format("%02X %02X", byte1, byte2); + + case 1: + return dir; + + case 2: + return action; + + default: + assert(false); // should never happen + } + + return "wut"; + } + + void headerlist::refreshsize() + { + size_t s1 = safeheaderlist::getblockedrecv()->size(); + size_t s2 = safeheaderlist::getblockedsend()->size(); + size_t s3 = safeheaderlist::getignoredrecv()->size(); + size_t s4 = safeheaderlist::getignoredsend()->size(); + SetItemCount(s1 + s2 + s3 + s4); + Refresh(); + } + // headerlist end + // }}} + + const std::string headerdialog::tag = "wxPloiter::headerdialog"; + + headerdialog::headerdialog(wxWindow *parent) + : wxDialog(parent, wxID_ANY, "Header list", wxDefaultPosition, wxSize(300, 350)), + log(utils::logging::get()) + { + wxPanel *basepanel = new wxPanel(this); + wxBoxSizer *panelsizer = new wxBoxSizer(wxVERTICAL); + + wxStaticBoxSizer *headersbox = new wxStaticBoxSizer(wxVERTICAL, + basepanel, "Header List"); + { + wxStaticBox *box = headersbox->GetStaticBox(); + + // controls + headers = new headerlist(box); + wxButton *removeheader = new wxButton(box, wxID_ANY, "Remove"); + removeheader->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &headerdialog::OnRemoveClicked, this); + + // add controls to sizer + headersbox->Add(headers, 1, wxALL | wxEXPAND, 10); + headersbox->Add(removeheader, 0, wxBOTTOM | wxLEFT | wxRIGHT | wxEXPAND, 10); + } + + wxStaticBoxSizer *utilsbox = new wxStaticBoxSizer(wxVERTICAL, + basepanel, "Utilities"); + { + wxStaticBox *box = utilsbox->GetStaticBox(); + + headertext = new wxTextCtrl(box, wxID_ANY, "C8 00"); + headertext->SetFont(headers->GetFont()); + + wxBoxSizer *buttons = new wxBoxSizer(wxHORIZONTAL); + { + wxButton *addb = new wxButton(box, wxID_ANY, "Add"); + + wxArrayString choices; + choices.Add("Block Send"); + choices.Add("Block Recv"); + choices.Add("Ignore Send"); + choices.Add("Ignore Recv"); + combobox = new wxComboBox(box, wxID_ANY, "Block Send", wxDefaultPosition, + wxDefaultSize, choices, wxCB_READONLY); + + addb->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &headerdialog::OnAddClicked, this); + buttons->Add(addb, 0, wxTOP | wxALIGN_CENTER_VERTICAL | wxEXPAND, 5); + buttons->Add(combobox, 0, wxTOP | wxLEFT | wxALIGN_CENTER_VERTICAL, 5); + } + + utilsbox->Add(headertext, 0, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 10); + utilsbox->Add(buttons, 0, wxLEFT | wxRIGHT | wxBOTTOM, 10); + } + + panelsizer->Add(headersbox, 1, wxALL | wxEXPAND, 10); + panelsizer->Add(utilsbox, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10); + basepanel->SetSizer(panelsizer); + } + + void headerdialog::refresh() + { + headers->refreshsize(); + } + + void headerdialog::OnClose(wxCloseEvent &e) + { + e.Veto(); + Hide(); + } + + void headerdialog::OnAddClicked(wxCommandEvent &e) + { + if (headertext->GetValue().IsEmpty()) + wxLogError("Please enter a header."); + + maple::packet p; + safeheaderlist::ptr list; + + // TODO: do not compare string like a noob and use index + + if (combobox->GetValue().Cmp("Block Send") == 0) + list = safeheaderlist::getblockedsend(); + + else if (combobox->GetValue().Cmp("Block Recv") == 0) + list = safeheaderlist::getblockedrecv(); + + else if (combobox->GetValue().Cmp("Ignore Send") == 0) + list = safeheaderlist::getignoredsend(); + + else if (combobox->GetValue().Cmp("Ignore Recv") == 0) + list = safeheaderlist::getignoredrecv(); + + try + { + p.append_data(headertext->GetValue().ToStdString()); + log->i(tag, strfmt() << "OnAddClicked: " << combobox->GetValue().ToStdString() << " " << p.tostring()); + list->push_back(*reinterpret_cast(p.raw())); + headers->refreshsize(); + } + catch (std::exception &e) + { + wxLogError("Invalid header: %s.", wxString(e.what())); + } + + } + + void headerdialog::OnRemoveClicked(wxCommandEvent &e) + { + long sel = headers->GetFirstSelected(); + + if (sel == -1) + return; + + try + { + wxString dir = headers->GetItemText(sel, 1); + wxString action = headers->GetItemText(sel, 2); + safeheaderlist::ptr dalist; + + // TODO: avoid ghetto string checks + + if (dir.Cmp("<") == 0) + { + if (action.Cmp("block") == 0) + dalist = safeheaderlist::getblockedrecv(); + + else + dalist = safeheaderlist::getignoredrecv(); + } + + else + { + if (action.Cmp("block") == 0) + dalist = safeheaderlist::getblockedsend(); + + else + dalist = safeheaderlist::getignoredsend(); + } + + maple::packet p; + p.append_data(headers->GetItemText(sel).ToStdString()); + dalist->erase(*reinterpret_cast(p.raw())); + headers->refreshsize(); + + } + catch (std::exception &e) + { + wxLogError("Something went wrong: %s.", wxString(e.what())); + } + } +} diff --git a/wxPloiter/headerdialog.hpp b/wxPloiter/headerdialog.hpp new file mode 100644 index 0000000..46b24b8 --- /dev/null +++ b/wxPloiter/headerdialog.hpp @@ -0,0 +1,42 @@ +#pragma once +#include "safeheaderlist.hpp" +#include "logging.hpp" +#include +#include + +namespace wxPloiter +{ + // virtual listview of blocked/ignored headers + class headerlist : public wxListView + { + public: + headerlist(wxWindow *parent); + virtual ~headerlist(); + + // fires when the listview is being drawn, it's used by wxListView to obtain + // the text for the given item and column + wxString OnGetItemText(long item, long column) const; + + void refreshsize(); + }; + + class headerdialog : public wxDialog + { + public: + headerdialog(wxWindow *parent); + void refresh(); + + protected: + static const std::string tag; + + boost::shared_ptr log; + headerlist *headers; // header listview + wxTextCtrl *headertext; + wxComboBox *combobox; + + void OnClose(wxCloseEvent &e); + void OnAddClicked(wxCommandEvent &e); + void OnRemoveClicked(wxCommandEvent &e); + }; +} + diff --git a/wxPloiter/logging.cpp b/wxPloiter/logging.cpp new file mode 100644 index 0000000..e6e430d --- /dev/null +++ b/wxPloiter/logging.cpp @@ -0,0 +1,252 @@ +// logging.cpp +// author: Franc[e]sco +// contributors: + +#include "logging.hpp" + +#include "utils.hpp" +#include +#include + +// a complete logging class I copy-pasted from one of my other projects +namespace utils +{ + // static members + const char * const logging::tag = "utils::logging"; + const char * logging::filename = "lastsession.log"; + boost::shared_ptr logging::inst; + + logging::logging() + : verb(info) + { + // initialize empty logging file + std::ofstream f; + + if (openfile(f, std::fstream::out | std::fstream::trunc)) + f.close(); + } + + logging::~logging() + { + // empty + } + + void logging::setfilename(const char * const filename) + { + logging::filename = filename; + } + + boost::shared_ptr logging::get() + { + if (!inst.get()) + inst.reset(new logging); + + return inst; + } + + void logging::setverbosity(const logging::verbosity v) + { + log(logging::info, tag, strfmt() << "setverbosity: setting log verbosity to " << static_cast(v)); + this->verb = v; + } + + logging::verbosity logging::getverbosity() const + { + return verb; + } + + bool logging::wtf(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::assert)) + return log(logging::assert, tag, message); + + return true; // no errors, but it won't log anything because of verbosity + } + + bool logging::e(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::error)) + return log(logging::error, tag, message); + + return true; + } + + bool logging::w(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::warn)) + return log(logging::warn, tag, message); + + return true; + } + + bool logging::i(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::info)) + return log(logging::info, tag, message); + + return true; + } + + bool logging::d(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::debug)) + return log(logging::debug, tag, message); + + return true; + } + + bool logging::v(const std::string tag, const std::string message) const + { + if (static_cast(verb) >= static_cast(logging::verbose)) + return log(logging::verbose, tag, message); + + return true; + } + + bool logging::wtf(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::assert)) + return log(logging::assert, tag, format); + + return true; + } + + bool logging::e(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::error)) + return log(logging::error, tag, format); + + return true; + } + + bool logging::w(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::warn)) + return log(logging::warn, tag, format); + + return true; + } + + bool logging::i(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::info)) + return log(logging::info, tag, format); + + return true; + } + + bool logging::d(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::debug)) + return log(logging::debug, tag, format); + + return true; + } + + bool logging::v(const std::string tag, const std::basic_ostream &format) const + { + if (static_cast(verb) >= static_cast(logging::verbose)) + return log(logging::verbose, tag, format); + + return true; + } + + bool logging::openfile(std::ofstream &f, const std::fstream::openmode mode) const + { + f.open(filename, mode); + + if (!f.is_open()) + { + std::cout << "logging.openfile: failed to open log file." << std::endl; + return false; + } + + return true; + } + + // appends text to the log file + bool logging::puts(const char * const text) const + { + std::ofstream f; + + if (!openfile(f, std::fstream::out | std::fstream::app)) + return false; + + f << text; + + // define LOGCONSOLE to also send log messages to stdout + #ifdef LOGCONSOLE + std::cout << text; + #endif + + if (f.bad()) + { + std::cout << "logging.openfile: failed to write to log file." << std::endl; + return false; + } + + f.close(); + + return true; + } + + // logs something in the format [tag] message + bool logging::log(const logging::verbosity v, const std::string tag, const std::string message) const + { + const char * verbositytag; + std::ostringstream oss; + + // converts verbosity to text + switch (v) + { + case assert: + verbositytag = "assert"; + break; + + case error: + verbositytag = "error"; + break; + + case warn: + verbositytag = "warn"; + break; + + case info: + verbositytag = "info"; + break; + + case debug: + verbositytag = "debug"; + break; + + case verbose: + verbositytag = "verbose"; + break; + + default: + verbositytag = "invalid"; + wtf(this->tag, strfmt() << "log: invalid verbosity of " << static_cast(v) << " provided"); + break; + } + + oss << "<" << verbositytag << "> [" << tag << "] " << message << "\r\n"; + return puts(oss.str().c_str()); + } + + // this overload allows me to format messages on-the-fly like this: log(v, tag, strfmt() << "foo" << bar) + bool logging::log(const verbosity v, const std::string tag, const std::basic_ostream &format) const + { + // obtain the stream's streambuf and cast it back to stringbuf + std::basic_streambuf * const strbuf = format.rdbuf(); + + if (strbuf && typeid(*strbuf) == typeid(std::stringbuf)) + { + const std::string &str = dynamic_cast(*strbuf).str(); + return log(v, tag, str); + } + + wtf(this->tag, "log: invalid stringstream provided"); + + return false; + } +} diff --git a/wxPloiter/logging.hpp b/wxPloiter/logging.hpp new file mode 100644 index 0000000..c4c2d59 --- /dev/null +++ b/wxPloiter/logging.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include + +namespace utils +{ + // a complete logging class I copy-pasted from one of my other projects + class logging + { + public: + enum verbosity + { + assert = 0, + error = 1, + warn = 2, + info = 3, + debug = 4, + verbose = 5 + }; + + virtual ~logging(); + + // changes the log file name. THIS IS NOT THREAD SAFE + static void setfilename(const char * const filename); + + static boost::shared_ptr get(); // returns the singleton instance + + // sets the logging verbosity level. THIS IS NOT THREAD SAFE. + void setverbosity(const verbosity v); + + verbosity getverbosity() const; + + bool wtf(const std::string tag, const std::string message) const; + bool e(const std::string tag, const std::string message) const; + bool w(const std::string tag, const std::string message) const; + bool i(const std::string tag, const std::string message) const; + bool d(const std::string tag, const std::string message) const; + bool v(const std::string tag, const std::string message) const; + + bool wtf(const std::string tag, const std::basic_ostream &format) const; + bool e(const std::string tag, const std::basic_ostream &format) const; + bool w(const std::string tag, const std::basic_ostream &format) const; + bool i(const std::string tag, const std::basic_ostream &format) const; + bool d(const std::string tag, const std::basic_ostream &format) const; + bool v(const std::string tag, const std::basic_ostream &format) const; + + protected: + static const char * const tag; + static const char * filename; + static boost::shared_ptr inst; + + verbosity verb; + + logging(); // ensures there is no way to construct more than one singleton instance + bool openfile(std::ofstream &f, const std::fstream::openmode mode = std::fstream::out) const; + bool puts(const char * const text) const; + bool log(const verbosity v, const std::string tag, const std::string message) const; + bool log(const verbosity v, const std::string tag, const std::basic_ostream &format) const; + }; +} diff --git a/wxPloiter/mainform.cpp b/wxPloiter/mainform.cpp new file mode 100644 index 0000000..37c0cf4 --- /dev/null +++ b/wxPloiter/mainform.cpp @@ -0,0 +1,894 @@ +#include "mainform.hpp" + +#include "packethooks.hpp" +#include "resource.h" +#include "utils.hpp" +#include "safeheaderlist.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define menu_bind(functor, id) \ + Bind(wxEVT_COMMAND_MENU_SELECTED, functor, this, id); + +#define RECV_SYMBOL "<-" +#define SEND_SYMBOL "->" + +namespace wxPloiter +{ + // {{{ + // app begin + const std::string app::logfile = "wxPloiter.log"; + const std::string app::tag = "wxPloiter::app"; + const wxString app::appname = "wxPloiter"; + const wxString app::appver = "r1"; + + void app::rundll(HINSTANCE hInstance) + { + try + { + // placeholder cmd line args + int argc = 1; + char *argv[] = { "wxPloiter" }; + + // manually run the wxWidgets app + // (used to deploy as a dll) + wxPloiter::app *papp = new wxPloiter::app(hInstance); + wxApp::SetInstance(papp); + wxEntry(argc, argv); + } + catch (const std::exception &e) + { + utils::logging::get()->wtf(tag, strfmt() << "unexpected exception: " << e.what()); + fatal(); + } + + FreeLibraryAndExitThread(hInstance, 0); // unload injected dll + } + + app::app(HINSTANCE hInstance) + : wxApp(), + cryptoinit("thread_safe=true"), // init botan (only used in winsock mode) + hInstance(hInstance) + { + // empty + } + + app::~app() + { + // empty + } + + bool app::OnInit() + { + mainform *frame; + + // init logging + utils::logging::setfilename(logfile.c_str()); + log = utils::logging::get(); + + log->i(tag, strfmt() << appname << " " << appver << + " - initializing on " << utils::datetime::utc_date() << + "T" << utils::datetime::utc_time() << " UTC"); + + dbgcode(log->setverbosity(utils::logging::verbose)); + + // create main frame + mainform::init(hInstance, appname, wxDefaultPosition, wxSize(420 /* blaze it faggot */, 490)); + frame = mainform::get(); + + if (!frame) // out of memory? + { + log->wtf(tag, "OnInit: could not create top-level frame! Are we out of memory?"); + fatal(); + return false; + } + + // display top level window + SetTopWindow(frame); // optional (I think) + frame->Show(); // makes the main frame visible + + utils::random::init(); + + // init hooks + if (!packethooks::get()->isinitialized()) + wxLogWarning("Could not hook some or all of the packet functions. Logging / sending might not work."); + + return true; + } + + void app::fatal() + { + static const wxString msg = "A fatal error has occurred and the application " + "will now terminate.\nPlease check the log file for more information."; + + wxLogFatalError(msg); + } + // app end + // }}} + + // {{{ + // itemlist begin + itemlist::itemlist(wxWindow *parent, size_t columncount) + : wxListView(parent, wxID_ANY, wxDefaultPosition, + wxDefaultSize, wxLC_REPORT | wxLC_VIRTUAL), + autoscroll(true), + columncount(columncount) // used in push_back + { + SetItemCount(0); // initialize the listview as empty + + // default columns + AppendColumn("dir", wxLIST_FORMAT_LEFT, 50); + AppendColumn("ret / enc", wxLIST_FORMAT_LEFT, 75); + AppendColumn("size", wxLIST_FORMAT_LEFT, 50); + AppendColumn("data", wxLIST_FORMAT_LEFT, 238); + + SetFont(wxFont(8, wxFONTFAMILY_MODERN, wxFONTSTYLE_NORMAL, + wxFONTWEIGHT_NORMAL, false, "Consolas")); + + Bind(wxEVT_SIZE, &itemlist::OnSize, this); // adjust the column size on resize + + assert(GetColumnCount() == columncount); + } + + itemlist::~itemlist() + { + // empty + } + + size_t itemlist::getcolumncount() const + { + return columncount; + } + + void itemlist::push_back(size_t columns, ...) + { + va_list va; + va_start(va, columns); + boost::shared_array it(new wxString[columncount]); + + // append given columns + for (size_t i = 0; i < columns; i++) + { + it[i] = va_arg(va, wxString); + //utils::logging::get()->i("itemlist", strfmt() << "it[" << i << "] = " << it[i]); + } + + // missing columns will remain empty + + items.push_back(it); + va_end(va); + + SetItemCount(GetItemCount() + 1); // update item count + + if (autoscroll) + EnsureVisible(GetItemCount() - 1); + } + + void itemlist::clear() + { + items.clear(); + SetItemCount(0); + Refresh(); + } + + boost::shared_array itemlist::at(long index) + { + return items[index]; + } + + void itemlist::setautoscroll(bool autoscroll) + { + this->autoscroll = autoscroll; + } + + wxString itemlist::OnGetItemText(long item, long column) const + { + //utils::logging::get()->i("itemlist", strfmt() << "getting items[" << item << "][" << column << "]"); + return items[item][column]; + } + + void itemlist::OnSize(wxSizeEvent& e) + { + // make last column fill up available space + this->SetColumnWidth(columncount - 1, + e.GetSize().GetWidth() + - 50 * (columncount - 1) // size of the first columns (always 50 in my case) + - 25 // prevents horizontal scrollbar from popping up + - 25 // recently made the "enc" column larger + ); + } + // itemlist end + // }}} + + // {{{ + // wxPacketEvent begin + wxPacketEvent::wxPacketEvent(wxEventType commandType, int id) + : wxCommandEvent(commandType, id), + decrypted(false) + { + // empty + } + + wxPacketEvent::wxPacketEvent(const wxPacketEvent &event) + : wxCommandEvent(event), + p(event.GetPacket()), + decrypted(event.IsDecrypted()) // only used in winsock mode + { + // copy ctor + } + + wxPacketEvent::~wxPacketEvent() + { + // empty + } + + wxEvent *wxPacketEvent::Clone() const + { + // wrapper for copy ctor + return new wxPacketEvent(*this); + } + + boost::shared_ptr wxPacketEvent::GetPacket() const + { + return p; + } + + bool wxPacketEvent::IsDecrypted() const + { + return decrypted; + } + + void *wxPacketEvent::GetReturnAddress() const + { + return retaddy; + } + + void wxPacketEvent::SetPacket(boost::shared_ptr p) + { + this->p = p; + } + + void wxPacketEvent::SetDecrypted(bool decrypted) + { + this->decrypted = decrypted; + } + + void wxPacketEvent::SetReturnAddress(void *retaddy) + { + this->retaddy = retaddy; + } + // wxPacketEvent end + // }}} + + // {{{ + // mainform begin + const std::string mainform::tag = "wxPloiter::mainform"; + mainform *mainform::inst; + + void mainform::init(HINSTANCE hInstance, const wxString &title, + const wxPoint &pos, const wxSize &size) + { + if (inst) + return; + + inst = new mainform(hInstance, title, pos, size); + } + + mainform *mainform::get() + { + return inst; + } + + mainform::mainform(HINSTANCE hInstance, const wxString &title, + const wxPoint &pos, const wxSize &size) + : wxFrame(NULL, wxID_ANY, title, pos, size), + log(utils::logging::get()), + hInstance(hInstance), // used for LoadIcon + packets(NULL), // packet listview + logsend(false), // log send toggle + logrecv(false), // log recv toggle + loggingmenu(NULL), // logging menu + packetmenu(NULL), // packet menu + packettext(NULL), // inject packet textbox + spamdelay(NULL), // spam delay textbox + hdlg(NULL) // headers dialog + { + //SetMinSize(size); + + wxPanel *basepanel = new wxPanel(this); + wxBoxSizer *basesizer = new wxBoxSizer(wxVERTICAL); + + // we're on windows, so who cares about dealing with cross-platform icons + // this is the only way that seems to work to set icon in an injected dll + HWND hWnd = GetHWND(); + HICON hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1)); + + SendMessage(hWnd, WM_SETICON, ICON_BIG, reinterpret_cast(hIcon)); + SendMessage(hWnd, WM_SETICON, ICON_SMALL, reinterpret_cast(hIcon)); + + log->i(tag, "mainform: initializing controls"); + + // create menu bar + wxMenuBar *mbar = new wxMenuBar; + + // file menu + wxMenu *menu = new wxMenu; + menu->AppendCheckItem(wxID_FILE_HIDEMAPLE, "Hide MapleStory"); + menu->Append(wxID_FILE_EXIT, "Exit"); + mbar->Append(menu, "File"); // add menu to the menu + + // bind menu events + menu_bind(&mainform::OnFileHideMapleClicked, wxID_FILE_HIDEMAPLE); + menu_bind(&mainform::OnFileExitClicked, wxID_FILE_EXIT); + + // logging menu + menu = new wxMenu; + wxMenuItem *ascroll = menu->AppendCheckItem(wxID_LOGGING_AUTOSCROLL, "Autoscroll"); + ascroll->Check(true); + menu->Append(wxID_LOGGING_CLEAR, "Clear"); + menu->AppendCheckItem(wxID_LOGGING_SEND, "Log send"); + menu->AppendCheckItem(wxID_LOGGING_RECV, "Log recv"); + mbar->Append(menu, "Logging"); // add menu to the menu bar + loggingmenu = menu; + + // bind menu events + menu_bind(&mainform::OnLoggingAutoscrollClicked, wxID_LOGGING_AUTOSCROLL); + menu_bind(&mainform::OnLoggingClearClicked, wxID_LOGGING_CLEAR); + menu_bind(&mainform::OnLoggingSendClicked, wxID_LOGGING_SEND); + menu_bind(&mainform::OnLoggingRecvClicked, wxID_LOGGING_RECV); + + // packet menu + menu = new wxMenu; + menu->Append(wxID_PACKET_COPY, "Copy to clipboard"); + menu->Append(wxID_PACKET_COPYRET, "Copy return address to clipboard"); + menu->Append(wxID_PACKET_HEADERLIST, "Header list"); + menu->Append(wxID_PACKET_IGNORE, "Ignore header"); + menu->Append(wxID_PACKET_BLOCK, "Block header"); + //menu->AppendCheckItem(wxID_PACKET_ENABLESENDBLOCK, "Send blocking hook (requires bypass)"); + mbar->Append(menu, "Packet"); // add menu to the menu bar + packetmenu = menu; + + menu_bind(&mainform::OnPacketCopyClicked, wxID_PACKET_COPY); + menu_bind(&mainform::OnPacketCopyRetClicked, wxID_PACKET_COPYRET); + menu_bind(&mainform::OnPacketHeaderListClicked, wxID_PACKET_HEADERLIST); + menu_bind(&mainform::OnPacketIgnoreClicked, wxID_PACKET_IGNORE); + menu_bind(&mainform::OnPacketBlockClicked, wxID_PACKET_BLOCK); + //menu_bind(&mainform::OnPacketEnableSendBlockClicked, wxID_PACKET_ENABLESENDBLOCK); + + // help menu + menu = new wxMenu; + menu->Append(wxID_HELP_ABOUT, "About"); + mbar->Append(menu, "Help"); // add menu to the menu bar + + // bind menu events + menu_bind(&mainform::OnHelpAboutClicked, wxID_HELP_ABOUT); + + // add menu bar to frame + SetMenuBar(mbar); + + // status bar (the thing at the bottom of the window) + CreateStatusBar(); + + wxStaticBoxSizer *packetsbox = new wxStaticBoxSizer(wxVERTICAL, + basepanel, "Packet Log"); + { + wxStaticBox *box = packetsbox->GetStaticBox(); + + // controls + packets = new itemlist(box, 4); + packets->Bind(wxEVT_LIST_ITEM_SELECTED, &mainform::OnPacketSelected, this); + packetsbox->Add(packets, 1, wxALL | wxEXPAND, 10); + } + + wxStaticBoxSizer *injectbox = new wxStaticBoxSizer(wxVERTICAL, + basepanel, "Inject Packets (multiline)"); + { + wxStaticBox *box = injectbox->GetStaticBox(); + + // FUCK I spent like 2 hours trying to figure out what was wrong with the packet sender to discover + // that the text wrapping was causing the wrapped newlines to be treated as multiline packets + packettext = new wxTextCtrl(box, wxID_ANY, wxEmptyString, + wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE | wxTE_DONTWRAP); + packettext->SetFont(packets->GetFont()); + + // horizontal sizer for the two buttons + wxBoxSizer *buttons = new wxBoxSizer(wxHORIZONTAL); + { + choices.Add("Send"); + choices.Add("Recv"); + combobox = new wxComboBox(box, wxID_ANY, "Send", wxDefaultPosition, + wxDefaultSize, choices, wxCB_READONLY); + + sendpacket = new wxButton(box, wxID_ANY, "Inject"); + sendpacket->Bind(wxEVT_COMMAND_BUTTON_CLICKED, &mainform::OnInjectPacketClicked, this); + + wxCheckBox *spam = new wxCheckBox(box, wxID_ANY, "Spam"); + spamdelay = new wxTextCtrl(box, wxID_ANY, "20"); + spam->Bind(wxEVT_COMMAND_CHECKBOX_CLICKED, &mainform::OnSpamClicked, this); + + buttons->Add(sendpacket, 0, wxTOP | wxRIGHT, 5); + buttons->Add(combobox, 0, wxTOP | wxRIGHT | wxLEFT | wxALIGN_CENTER_VERTICAL, 5); + buttons->Add(spamdelay, 0, wxTOP | wxLEFT | wxALIGN_CENTER_VERTICAL, 5); + buttons->Add(spam, 0, wxTOP | wxLEFT | wxALIGN_CENTER_VERTICAL, 5); + } + + injectbox->Add(packettext, 2, wxLEFT | wxRIGHT | wxTOP | wxEXPAND, 10); + injectbox->Add(buttons, 1, wxLEFT | wxRIGHT | wxBOTTOM, 10); + } + + wxBoxSizer *begsizer = new wxBoxSizer(wxHORIZONTAL); + { + wxStaticText *begging0 = new wxStaticText(basepanel, wxID_ANY, "Like my releases?"); + wxHyperlinkCtrl *begging1 = new wxHyperlinkCtrl(basepanel, wxID_ANY, "donate", + "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=5E289LJ5UUG3Q"); + wxStaticText *begging2 = new wxStaticText(basepanel, wxID_ANY, "or"); + wxHyperlinkCtrl *begging3 = new wxHyperlinkCtrl(basepanel, wxID_ANY, "buy my cheap meso", + "https://ccplz.net/threads/s-meso-16%E2%82%AC-b-taxes-covered-paypal-btc-ltc-doge.60888/"); + + begsizer->Add(begging0, 0, wxRIGHT, 5); + begsizer->Add(begging1, 0, wxRIGHT, 5); + begsizer->Add(begging2, 0, wxRIGHT, 5); + begsizer->Add(begging3, 0, wxRIGHT, 0); + } + + basesizer->Add(packetsbox, 1, wxALL | wxEXPAND, 10); + basesizer->Add(injectbox, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10); + basesizer->Add(begsizer, 0, wxLEFT | wxRIGHT | wxBOTTOM | wxEXPAND, 10); + basepanel->SetAutoLayout(true); + basepanel->SetSizer(basesizer); + basepanel->Layout(); // fixes the layout snapping into place after the first resize + + // bind window events + Bind(wxEVT_CLOSE_WINDOW, &mainform::OnClose, this); + Bind(wxEVT_MENU_OPEN, &mainform::OnMenuOpened, this); // will keep the menu updated + + // bind custom events + Bind(wxEVT_PACKET_LOGGED, &mainform::OnPacketLogged, this, wxID_PACKET_SEND); + Bind(wxEVT_PACKET_LOGGED, &mainform::OnPacketLogged, this, wxID_PACKET_RECV); + + // create child dialogs + hdlg = new headerdialog(this); + + wxLogStatus("Idle."); + } + + mainform::~mainform() + { + // empty + } + + /* + void mainform::enablesendblockingtoggle(bool enabled) + { + loggingmenu->Enable(wxID_PACKET_ENABLESENDBLOCK, enabled); + } + */ + + void mainform::enablechoice(const wxString &choice, bool enabled) + { + bool found = choices.Index(choice) != wxNOT_FOUND; + + wxString current = combobox->GetValue(); + + if (enabled && !found) + choices.Add(choice); + + else if (!enabled && found) + choices.Remove(choice); + + combobox->Clear(); + combobox->Append(choices); + + if (!choices.size()) + sendpacket->Enable(false); + + else + sendpacket->Enable(true); + + if (choices.Index(current) != wxNOT_FOUND) + combobox->SetValue(current); + } + + void mainform::enablesend(bool enabled) + { + enablechoice("Send", enabled); + } + + void mainform::enablerecv(bool enabled) + { + enablechoice("Recv", enabled); + } + + void mainform::queuepacket(boost::shared_ptr p, int id, bool decrypted, void *retaddy) + { + // post custom packet log event to the gui + // this is thread safe + wxPacketEvent *event = new wxPacketEvent(wxEVT_PACKET_LOGGED, id); + event->SetEventObject(mainform::get()); + event->SetPacket(p); + event->SetDecrypted(decrypted); + event->SetReturnAddress(retaddy); + wxQueueEvent(mainform::get(), event); + } + + void mainform::OnPacketLogged(wxPacketEvent &e) + { + //log->i(tag, "processing packet event"); + + const char *direction = NULL; + word header = 0; + boost::shared_ptr p = e.GetPacket(); + + try + { + maple::packet::iterator it = p->begin(); + p->read(&header, it); + } + catch (const maple::readexception &) + { + log->w(tag, "OnPacketLogged: failed to read packet header!"); + return; + } + + switch (e.GetId()) + { + case wxID_PACKET_RECV: + if (!logrecv || safeheaderlist::getignoredrecv()->contains(header)) + return; + + direction = RECV_SYMBOL; + break; + + case wxID_PACKET_SEND: + if (!logsend || safeheaderlist::getignoredsend()->contains(header)) + return; + + direction = SEND_SYMBOL; + break; + + default: + assert(false); + break; + } + + packets->push_back(4, + wxString(direction), + + packethooks::get()->isusingwsock() ? + wxString(e.IsDecrypted() ? "no" : "yes") + : + wxString::Format("0x%08X", reinterpret_cast(e.GetReturnAddress())), + + wxString::Format("%lu", p->size()), + wxString(p->tostring()) + ); + } + + void mainform::OnInjectPacketClicked(wxCommandEvent &e) + { + if (packettext->GetValue().IsEmpty()) + { + wxLogError("Please enter a packet."); + return; + } + + if (combobox->GetValue().IsEmpty()) + { + wxLogError("Please select a direction."); + return; + } + + boost::shared_ptr ph = packethooks::get(); + bool recv = (combobox->GetValue().Cmp("Recv") == 0); + + try + { + for (int i = 0; i < packettext->GetNumberOfLines(); i++) + { + maple::packet p; + + if (recv) + { + // generate random recv header + dword dwHeader = utils::random::get()->getdword(); + p.append(dwHeader); + p.append_data(packettext->GetLineText(i).ToStdString()); + ph->recvpacket(p); + } + else + { + p.append_data(packettext->GetLineText(i).ToStdString()); + ph->sendpacket(p); + } + + log->i(tag, strfmt() << "OnInjectPacketClicked: injected " << + combobox->GetValue().ToStdString() << " " << p.tostring()); + } + } + catch (std::exception &e) + { + wxLogError("Invalid packet: %s.", wxString(e.what())); + } + } + + void mainform::packetspamthread(boost::shared_array lines, dword count, dword delay, bool recv) + { + namespace tt = boost::this_thread; + namespace pt = boost::posix_time; + + boost::shared_ptr ph = packethooks::get(); + + while (true) + { + for (dword i = 0; i < count; i++) + { + if (recv) + ph->recvpacket(lines[i]); + else + ph->sendpacket(lines[i]); // TODO: multisend delay between each line + } + + tt::sleep(pt::milliseconds(delay)); + } + } + + void mainform::OnSpamClicked(wxCommandEvent &e) + { + if (combobox->GetValue().IsEmpty()) + { + wxLogError("Please select a direction."); + return; + } + + bool recv = combobox->GetValue().Cmp("Recv") == 0; + + if (hpacketspam.get()) + { + hpacketspam->interrupt(); + hpacketspam.reset(); + } + + if (!e.IsChecked()) + return; + + dword datspamdelay; + + try + { + datspamdelay = boost::lexical_cast(spamdelay->GetValue().ToStdString()); + } + catch(boost::bad_lexical_cast &e) + { + wxLogError("Invalid spam delay: %s.", wxString(e.what())); + return; + } + + try + { + boost::shared_array lines(new maple::packet[packettext->GetNumberOfLines()]); + + for (int i = 0; i < packettext->GetNumberOfLines(); i++) + { + if (recv) + { + // generate random recv header + dword dwHeader = utils::random::get()->getdword(); + lines[i].append(dwHeader); + lines[i].append_data(packettext->GetLineText(i).ToStdString()); + } + else + lines[i].append_data(packettext->GetLineText(i).ToStdString()); + + log->i(tag, strfmt() << "OnSpamClicked: parsed " << combobox->GetValue().ToStdString() << " " << lines[i].tostring()); + } + + hpacketspam = boost::make_shared( + boost::bind(&mainform::packetspamthread, this, lines, packettext->GetNumberOfLines(), datspamdelay, recv) + ); + } + catch (std::exception &e) + { + wxLogError("Invalid packet: %s.", wxString(e.what())); + } + } + + void mainform::OnFileHideMapleClicked(wxCommandEvent &e) + { + HWND hMoopla = maple::getwnd(); + ShowWindow(hMoopla, e.IsChecked() ? SW_HIDE : SW_SHOW); + } + + void mainform::OnFileExitClicked(wxCommandEvent &e) + { + wxLogStatus("Terminating"); + Close(false); + } + + void mainform::OnLoggingAutoscrollClicked(wxCommandEvent &e) + { + packets->setautoscroll(e.IsChecked()); + } + + void mainform::OnLoggingClearClicked(wxCommandEvent &e) + { + packets->clear(); + } + + void mainform::OnLoggingSendClicked(wxCommandEvent &e) + { + logsend = e.IsChecked(); + } + + void mainform::OnLoggingRecvClicked(wxCommandEvent &e) + { + logrecv = e.IsChecked(); + } + + void mainform::OnPacketCopyClicked(wxCommandEvent &e) + { + long sel = packets->GetFirstSelected(); + assert(sel != -1); + + // store selected packet to the clipboard + utils::copytoclipboard( + new wxTextDataObject( + packets->at(sel)[packets->getcolumncount() - 1] + ) + ); + } + + void mainform::OnPacketCopyRetClicked(wxCommandEvent &e) + { + long sel = packets->GetFirstSelected(); + assert(sel != -1); + + // store selected packet to the clipboard + utils::copytoclipboard( + new wxTextDataObject( + packets->at(sel)[packets->getcolumncount() - 3] + ) + ); + } + + void mainform::OnPacketHeaderListClicked(wxCommandEvent &e) + { + hdlg->Show(); + } + + void mainform::OnPacketIgnoreClicked(wxCommandEvent &e) + { + // TODO: join with func below + + safeheaderlist::ptr plist; // send/recv ignore list + long sel = packets->GetFirstSelected(); + + if (sel == -1) + return; + + maple::packet p; + + try + { + boost::shared_array sitem = packets->at(sel); + bool recv = sitem[0].Cmp(RECV_SYMBOL) == 0; + + plist = recv ? safeheaderlist::getignoredrecv() : safeheaderlist::getignoredsend(); + p.append_data(sitem[packets->getcolumncount() - 1].ToStdString()); + + if (p.size() < 2) + throw std::invalid_argument("Invalid packet selected! Is this a bug?"); + + log->i(tag, strfmt() << "OnPacketIgnoreClicked: ignoring " << p.tostring()); + plist->push_back(*reinterpret_cast(p.raw())); + hdlg->refresh(); + } + catch (std::exception &e) + { + wxLogError("Invalid header: %s.", wxString(e.what())); + } + } + + void mainform::OnPacketBlockClicked(wxCommandEvent &e) + { + safeheaderlist::ptr plist; // send/recv block list + long sel = packets->GetFirstSelected(); + + if (sel == -1) + return; + + maple::packet p; + + try + { + boost::shared_array sitem = packets->at(sel); + bool recv = sitem[0].Cmp(RECV_SYMBOL) == 0; + + plist = recv ? safeheaderlist::getblockedrecv() : safeheaderlist::getblockedsend(); + p.append_data(sitem[packets->getcolumncount() - 1].ToStdString()); + + if (p.size() < 2) + throw std::invalid_argument("Invalid packet selected! Is this a bug?"); + + log->i(tag, strfmt() << "OnPacketBlockClicked: blocking " << p.tostring()); + plist->push_back(*reinterpret_cast(p.raw())); + hdlg->refresh(); + } + catch (std::exception &e) + { + wxLogError("Invalid header: %s.", wxString(e.what())); + } + } + + /* + void mainform::OnPacketEnableSendBlockClicked(wxCommandEvent &e) + { + packethooks::get()->enablesendblock(e.IsChecked()); + } + */ + + void mainform::OnHelpAboutClicked(wxCommandEvent &e) + { + wxString ver = wxString::Format("%s %s", app::appname, app::appver); + + wxMessageBox( + wxString::Format("%s\n\n" + "coded by Francesco \"Franc[e]sco\" Noferi\n" + "http://sabishiimedia.wordpress.com/\n" + "francesco1149@gmail.com", ver), + ver, wxICON_INFORMATION | wxOK, this + ); + } + + void mainform::OnClose(wxCloseEvent &e) + { + if (e.CanVeto()) // forced termination should not ask to kill maple + { + int res = wxMessageBox("This will also shut down MapleStory. Are you sure?", + app::appname, wxICON_INFORMATION | wxYES_NO, this); + + if (res == wxYES) + TerminateProcess(GetCurrentProcess(), EXIT_SUCCESS); + + e.Veto(); + return; + } + + e.Skip(); + } + + void mainform::OnMenuOpened(wxMenuEvent &e) + { + // toggle menu items that are only usable when a packet is selected + bool enable = packets->GetFirstSelected() != -1; + packetmenu->Enable(wxID_PACKET_COPY, enable); + packetmenu->Enable(wxID_PACKET_IGNORE, enable); + packetmenu->Enable(wxID_PACKET_BLOCK, enable); + e.Skip(); // not sure if this is really necessary + } + + void mainform::OnPacketSelected(wxListEvent &e) + { + long sel = packets->GetFirstSelected(); + assert(sel != -1); + + // store selected packet to the textbox + boost::shared_array sitem = packets->at(sel); + packettext->SetValue(sitem[packets->getcolumncount() - 1]); + + wxString direction = sitem[0].Cmp(RECV_SYMBOL) == 0 ? "Recv" : "Send"; + + if (choices.Index(direction) != wxNOT_FOUND) + combobox->SetValue(direction); + } + // mainform end + // }}} +} diff --git a/wxPloiter/mainform.hpp b/wxPloiter/mainform.hpp new file mode 100644 index 0000000..ba13d33 --- /dev/null +++ b/wxPloiter/mainform.hpp @@ -0,0 +1,193 @@ +#pragma once + +#include "logging.hpp" +#include "packet.hpp" +#include "packetstruct.h" +#include "headerdialog.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace wxPloiter +{ + // represents the application and all of its windows as a whole + // contains global information about the application and is + // responsible for initializing the program on startup + class app : public wxApp + { + public: + static const wxString appname; // application name + static const wxString appver; // application version + + app(HINSTANCE hInstance); + virtual ~app(); + static void rundll(HINSTANCE hInstance); // deploys the application as a dll + HINSTANCE getinstance(); + virtual bool OnInit(); // called on startup + static void fatal(); // displays a fatal error message and terminates the program + + protected: + static const std::string logfile; + static const std::string tag; + Botan::LibraryInitializer cryptoinit; + boost::shared_ptr log; + HINSTANCE hInstance; + }; + + // a custom virtual listview that retrieves values directly from an array + // this ensures excellent performance with no lag whatsoever when scrolling + // huge amounts of data that is updated in real time + class itemlist : public wxListView + { + public: + itemlist(wxWindow *parent, size_t columncount); + virtual ~itemlist(); + void push_back(size_t columns, ...); // appends an item with the given variable number of columns + void clear(); // clears the list + boost::shared_array at(long index); // returns the item at the given index + void setautoscroll(bool autoscroll); // toggles auto scrolling to the bottom + size_t getcolumncount() const; + + // fires when the listview is being drawn, it's used by wxListView to obtain + // the text for the given item and column + wxString OnGetItemText(long item, long column) const; + + // fires when the listview is resized + // used to make the last column fill up the remaining space + void OnSize(wxSizeEvent& e); + + protected: + std::vector< + boost::shared_array + > items; // internal item list + bool autoscroll; + size_t columncount; + }; + + // a custom event that transports a whole maple::packet + class wxPacketEvent: public wxCommandEvent + { + public: + wxPacketEvent(wxEventType commandType, int id = 0); + wxPacketEvent(const wxPacketEvent &event); // copy constructor. required + virtual ~wxPacketEvent(); + wxEvent *Clone() const; // clone the current instance and custom data. required + boost::shared_ptr GetPacket() const; + bool IsDecrypted() const; + void *GetReturnAddress() const; + void SetPacket(boost::shared_ptr p); + void SetDecrypted(bool decrypted); + void SetReturnAddress(void *retaddy); + + private: + boost::shared_ptr p; + bool decrypted; + void *retaddy; + }; + + // custom event types + wxDEFINE_EVENT(wxEVT_PACKET_LOGGED, wxPacketEvent); + + class mainform : public wxFrame + { + public: + // menu ids, used for event handling + enum + { + // file + wxID_FILE_EXIT = 1, + wxID_FILE_HIDEMAPLE, + + // logging + wxID_LOGGING_AUTOSCROLL, + wxID_LOGGING_CLEAR, + wxID_LOGGING_SEND, + wxID_LOGGING_RECV, + + // packet + //wxID_PACKET_ENABLESENDBLOCK, + wxID_PACKET_COPY, + wxID_PACKET_COPYRET, + wxID_PACKET_HEADERLIST, + wxID_PACKET_IGNORE, + wxID_PACKET_BLOCK, + + // help + wxID_HELP_ABOUT, + + // custom events + wxID_PACKET_SEND, + wxID_PACKET_RECV + }; + + static void init(HINSTANCE hInstance, const wxString &title, + const wxPoint &pos, const wxSize &size); // initializes the singleton + static mainform *get(); // returns the singleton instance + virtual ~mainform(); + //void enablesendblockingtoggle(bool enabled); + void enablesend(bool enabled); + void enablerecv(bool enabled); + void queuepacket(boost::shared_ptr p, int id, bool decrypted, void *retaddy); // queues a packet for logging + + protected: + static const std::string tag; + static mainform *inst; // singleton instance + + boost::shared_ptr log; + HINSTANCE hInstance; // dll HMODULE + itemlist *packets; // packet listview + bool logsend; // send logging toggle + bool logrecv; // recv logging toggle + wxMenu *loggingmenu; // ptr to the logging menu + wxMenu *packetmenu; // pointer to the packet menu + wxButton *sendpacket; // inject packet button + wxTextCtrl *packettext; // packet to be injected + wxTextCtrl *spamdelay; // spam delay textbox + wxComboBox *combobox; // send/recv combobox + headerdialog *hdlg; // dialog to manage blocked/ignored headers + boost::shared_ptr hpacketspam; // thread that spams packets + wxArrayString choices; // injection combobox choices + + mainform(HINSTANCE hInstance, const wxString &title, const wxPoint &pos, const wxSize &size); + void enablechoice(const wxString &choice, bool enabled); + + // custom events + void OnPacketLogged(wxPacketEvent &e); + + // buttons + void OnInjectPacketClicked(wxCommandEvent &e); + void mainform::packetspamthread(boost::shared_array lines, dword count, dword delay, bool recv); + void OnSpamClicked(wxCommandEvent &e); + + // menu events + void OnFileHideMapleClicked(wxCommandEvent &e); + void OnFileExitClicked(wxCommandEvent &e); + + void OnLoggingAutoscrollClicked(wxCommandEvent &e); + void OnLoggingClearClicked(wxCommandEvent &e); + void OnLoggingSendClicked(wxCommandEvent &e); + void OnLoggingRecvClicked(wxCommandEvent &e); + + void OnPacketCopyClicked(wxCommandEvent &e); + void OnPacketCopyRetClicked(wxCommandEvent &e); + void OnPacketHeaderListClicked(wxCommandEvent &e); + void OnPacketIgnoreClicked(wxCommandEvent &e); + void OnPacketBlockClicked(wxCommandEvent &e); + //void OnPacketEnableSendBlockClicked(wxCommandEvent &e); + + void OnHelpAboutClicked(wxCommandEvent &e); + + // window events + void OnClose(wxCloseEvent& e); + void OnMenuOpened(wxMenuEvent &e); + void OnPacketSelected(wxListEvent &e); + }; +} diff --git a/wxPloiter/mem.cpp b/wxPloiter/mem.cpp new file mode 100644 index 0000000..157740a --- /dev/null +++ b/wxPloiter/mem.cpp @@ -0,0 +1,89 @@ +#include "mem.h" + +#include +#include +#pragma comment(lib, "dbghelp") +#pragma comment(lib, "psapi") + +#define jmp(frm, to) (int)(((int)to - (int)frm) - 5) + +namespace utils { +namespace mem +{ + bool getmodulesize(HMODULE hModule, void **pbase, size_t *psize) + { + if (hModule == GetModuleHandle(NULL)) + { + PIMAGE_NT_HEADERS pImageNtHeaders = ImageNtHeader((PVOID)hModule); + + if (pImageNtHeaders == NULL) + return false; + + *pbase = reinterpret_cast(hModule); + *psize = pImageNtHeaders->OptionalHeader.SizeOfImage; + } + else + { + MODULEINFO ModuleInfo; + + if (!GetModuleInformation(GetCurrentProcess(), hModule, &ModuleInfo, sizeof(MODULEINFO))) + return FALSE; + + *pbase = ModuleInfo.lpBaseOfDll; + *psize = ModuleInfo.SizeOfImage; + } + + return true; + } + + byte *getopcodedestination(byte opcode, byte *address) + { + if (*address == opcode) + return (address + 5 + *reinterpret_cast(address + 1)); + + return NULL; + } + + byte *getcall(byte *address) + { + return getopcodedestination(0xE8, address); + } + + byte *getjump(byte *address) + { + return getopcodedestination(0xE9, address); + } + + dword makepagewritable(void *address, size_t cb, dword flprotect) + { + MEMORY_BASIC_INFORMATION mbi = {0}; + VirtualQuery(address, &mbi, cb); + + if (mbi.Protect != flprotect) + { + DWORD oldprotect; + VirtualProtect(address, cb, flprotect, &oldprotect); + return oldprotect; + } + + return flprotect; + } + + void writeopcodewithdistance(byte opcode, byte *address, void *destination, size_t nops) + { + makepagewritable(address, 5 + nops); + *address = 0xE9; + *reinterpret_cast(address + 1) = jmp(address, destination); + memset(address + 5, 0x90, nops); + } + + void writejmp(byte *address, void *hook, size_t nops) + { + writeopcodewithdistance(0xE9, address, hook, nops); + } + + void writecall(byte *address, void *hook, size_t nops) + { + writeopcodewithdistance(0xE8, address, hook, nops); + } +}} diff --git a/wxPloiter/mem.h b/wxPloiter/mem.h new file mode 100644 index 0000000..a6251c4 --- /dev/null +++ b/wxPloiter/mem.h @@ -0,0 +1,16 @@ +#pragma once + +#include "common.h" +#include + +namespace utils { +// utilities to read & write memory +namespace mem +{ + bool getmodulesize(HMODULE hModule, void **pbase, size_t *psize); + byte *getcall(byte *address); + byte *getjump(byte *address); + dword makepagewritable(void *address, size_t cb, dword flprotect = PAGE_EXECUTE_READWRITE); + void writejmp(byte *address, void *hook, size_t nops = 0); + void writecall(byte *address, void *hook, size_t nops = 0); +}} diff --git a/wxPloiter/packet.cpp b/wxPloiter/packet.cpp new file mode 100644 index 0000000..b4ff168 --- /dev/null +++ b/wxPloiter/packet.cpp @@ -0,0 +1,380 @@ +#include "packet.hpp" + +#include "utils.hpp" +#include +#include +#include + +namespace maple +{ + // readexception + readexception::readexception() + : out_of_range("failed to read expected packet data") + { + // empty + } + + // packet + packet::packet() + { + // empty + } + + packet::~packet() + { + // empty + } + + packet::packet(const packet &other) + { + // resize and reallocate the data array + // (reserve used in append() does not reallocate because each extra element + // is allocated by back_inserter as the data gets copied) + data.resize(other.data.size()); + + // copy new data over the data array + std::copy(other.data.begin(), other.data.end(), data.begin()); + } + + packet::packet(const byte *pdata, size_t cb) + { + data.resize(cb); + std::copy(pdata, pdata + cb, data.begin()); + } + + packet::packet(const std::vector &data) + { + this->data.resize(data.size()); + std::copy(data.begin(), data.end(), this->data.begin()); + } + + size_t packet::size() const + { + return data.size(); + } + + void packet::clear() + { + data.clear(); + } + + byte *packet::raw() + { + return &data[0]; + } + + const byte *packet::raw() const + { + return &data[0]; + } + + packet::iterator packet::begin() + { + return data.begin(); + } + + packet::iterator packet::end() + { + return data.end(); + } + + packet::const_iterator packet::begin() const + { + return data.begin(); + } + + packet::const_iterator packet::end() const + { + return data.end(); + } + + std::string packet::tostring() const + { + std::stringstream ss; + + // stringstream flags for formatting + // std::hex = hex number output + // std::setfill('0') = zero fill number formatting + // std::uppercase = uppercase hex letters + ss << std::hex << std::setfill('0') << std::uppercase; + + boost_foreach(const byte &b, data) + ss << std::setw(2) << static_cast(b) << " "; + // I have no idea why but the only way to make it + // format it properly is casting it to a 4-byte int + // setw(2) truncates it back to a 2-digit hex number + // setw is not "sticky" so it must be set each time + + return ss.str(); + } + + void packet::append_data(std::string hexstring) + { + boost::shared_ptr rnd = utils::random::get(); + boost::erase_all(hexstring, " "); + hexstring = boost::to_upper_copy(hexstring); + + // trucated nibble + if (hexstring.length() % 2) + throw std::invalid_argument("unexpected truncated nibble"); + + for (size_t i = 0; i < hexstring.length(); i += 2) + { + word b = 0x0000; + + // copy current byte string + std::string strbyte(hexstring.begin() + i, hexstring.begin() + i + 2); + + // random bytes + if (strbyte.find('*') != std::string::npos) + { + word mask = 0x0000; + + // random left nibble + if (strbyte[0] == '*') + mask |= 0x00F0; + + // random right nibble + if (strbyte[1] == '*') + mask |= 0x000F; + + b = rnd->getbyte(); + b &= mask; + } + else + { + // convert byte string to integer + std::istringstream iss(strbyte); + iss >> std::hex >> b; + + if (iss.fail()) + throw std::invalid_argument("not a valid hex string"); + } + + append(static_cast(b)); + } + } + + void packet::append_data(const byte *pdata, size_t cb) + { + data.reserve(data.size() + cb); + std::copy(pdata, pdata + cb, std::back_inserter(data)); + } + + void packet::append_data(const std::vector &data) + { + append_data(&data[0], data.size()); + } + + void packet::append_string(const char *pstr, word len) + { + append_buffer(reinterpret_cast(pstr), len); + } + + void packet::append_string(const std::string &str) + { + append_string(str.c_str(), static_cast(str.length())); + } + + bool packet::append_string(const std::basic_ostream &format) + { + // obtain the stream's streambuf and cast it back to stringbuf + std::basic_streambuf * const strbuf = format.rdbuf(); + + if (strbuf && typeid(*strbuf) == typeid(std::stringbuf)) + { + const std::string &str = dynamic_cast(*strbuf).str(); + append_string(str); + return true; + } + + return false; + } + + void packet::append_buffer(const byte *pdata, word cb) + { + append(cb); + append_data(pdata, cb); + } + + bool packet::append_buffer(const std::vector &data) + { + // we can't pack buffers with a size that is larger than 2 bytes + if (data.size() > 0xFFFF) + return false; + + append_buffer(&data[0], static_cast(data.size())); + return true; + } + + void packet::read_data(byte *pdata, size_t cb, const_iterator &it) const + { + // reached end of packet + if (it + cb - 1 >= data.end()) + throw readexception(); + + std::copy(it, it + cb, pdata); + + it += cb; + } + + void packet::read_data(std::vector &data, size_t cb, const_iterator &it) const + { + data.resize(cb); + + try + { + read_data(&data[0], cb, it); + } + catch (const readexception &/*e*/) + { + data.clear(); + throw; + } + } + + byte *packet::read_data_nullterminated(const_iterator &it, size_t *length) const + { + // TODO: refactor this to a shared array + byte *rawdata = NULL; + const_iterator findnull; + + // scan the packet for the next NULL byte + for (findnull = it; findnull != data.end(); findnull++) + { + if (*findnull == 0x00) + break; + } + + if (findnull == data.end()) + throw readexception(); + + // calculate buffer size and allocate it + size_t buffersize = findnull - it; + rawdata = new byte[buffersize]; + + try + { + read_data(rawdata, buffersize, it); + } + catch (const readexception &/*e*/) + { + delete [] rawdata; + rawdata = NULL; + throw; // cool! hands the exception over to the caller + } + + if (length) + *length = buffersize; + + return rawdata; + } + + void packet::read_data_nullterminated(std::vector &data, const_iterator &it) const + { + // reads the nullterminated buffer into rawdata, then copies it to the std vector + // and deletes our temporary rawdata array + size_t len; + byte *rawdata = read_data_nullterminated(it, &len); + + // this will never happen I think, read_data_nullterminated + // should throw as soon as there's an error but whatever + if (!rawdata) // read_data_nullterminated failed + throw readexception(); + + data.resize(len); + std::copy(rawdata, rawdata + len, &data[0]); + delete [] rawdata; + } + + char *packet::read_string(const_iterator &it, size_t *length) const + { + // TODO: refactor this to a shared array + char *str = NULL; + word len; + + // get string length (first 2 bytes) + read(&len, it); + + str = new char[len + 1]; // +1 for the zero termination + assert(str != NULL); + + // read string data with the size we obtained + try + { + read_data(reinterpret_cast(str), len, it); + } + catch (const readexception &/*e*/) + { + delete [] str; + str = NULL; + throw; + } + + // add null termination + str[len] = 0x00; + + if (length) + *length = len; + + return str; + } + + void packet::read_string(std::string &str, const_iterator &it) const + { + // uses read_string to normally read a string into a char array + // then copies it to the std::string and deletes the temp array + char *rawstr = read_string(it); + + if (!rawstr) + throw readexception(); + + str = rawstr; + delete [] rawstr; + } + + byte *packet::read_buffer(const_iterator &it, size_t *length) const + { + // TODO: refactor this to a shared array + byte *buffer = NULL; + word len; + + // get buffer length (first 2 bytes) + read(&len, it); + + buffer = new byte[len]; + assert(buffer != NULL); + + // read buffer data with the length we obtained + try + { + read_data(buffer, len, it); + } + catch (const readexception &/*e*/) + { + delete [] buffer; + buffer = NULL; + throw; + } + + if (length) + *length = len; + + return buffer; + } + + void packet::read_buffer(std::vector &data, const_iterator &it) const + { + // uses read_buffer normally to store the buffer to a temp byte array + // which is then copied to the std::vector and deleted + size_t len; + byte *rawbuffer = read_buffer(it, &len); + + if (!rawbuffer) + throw readexception(); + + data.resize(len); + std::copy(rawbuffer, rawbuffer + len, &data[0]); + delete [] rawbuffer; + } +} \ No newline at end of file diff --git a/wxPloiter/packet.hpp b/wxPloiter/packet.hpp new file mode 100644 index 0000000..0651b46 --- /dev/null +++ b/wxPloiter/packet.hpp @@ -0,0 +1,165 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +// this should fix the order of the bytes on little endian OSes +// when packing multibyte values as little endian into the packets +#ifdef BOOST_LITTLE_ENDIAN +#define endiansafe_copy reverse_copy +#else +#define endiansafe_copy copy +#endif + +namespace maple +{ + // thrown when a packet fails to read the expected data + class readexception + : public std::out_of_range + { + public: + explicit readexception(); + }; + + // represents a maplestory unecrypted packet + // contains utilities to pack and unpack values from the packet + // ensures correct endian-ness of the packed values + class packet + { + public: + typedef boost::shared_ptr shared_ptr; + typedef std::vector::iterator iterator; // for iteration and manipulation of the raw data + typedef std::vector::const_iterator const_iterator; // for iteration of the const raw data + + packet(); + virtual ~packet(); + packet(const packet &other); // copy constructor + packet(const byte *pdata, size_t cb); // initializes the packet with a copy of the given data + packet(const std::vector &data); // stl overload of packet(const byte *, size_t) + + size_t size() const; // retuns the size in bytes of the packet + void clear(); + + // returns a pointer to the raw data of the packet. the pointed data + // is only guaranteed to exist as long as the packet instance exists + byte *raw(); + const byte *raw() const; // const overload of byte *packet::data() + + iterator begin(); // returns an iterator to the first byte of the packet's raw data + iterator end(); // returns an iterator to the end of the packet's raw data + const_iterator begin() const; // const overload of begin() + const_iterator end() const; // const overload of end() + std::string tostring() const; // formats the packet raw data into a string + + // appends the raw data of any type and packs it as little endian + template + void append(T value) + { + byte *raw_value = reinterpret_cast(&value); // pointer to the raw bytes of the value + data.reserve(data.size() + sizeof(T)); // reserve space in the packet data array + std::endiansafe_copy(raw_value, raw_value + sizeof(T), std::back_inserter(data)); + // copy new data (raw_value to raw_value + size of the data) to the space we reserved at the + // end of the data array. back_inserter ensures that the data is appended at the end of the + // array and correctly allocated before writing to the new memory + } + + // packs a new byte into the packet + // this specializes append for byte since we don't need to loop + // and copy through the data for a single byte + template <> + void append(byte b) + { + data.push_back(b); + } + + void append_data(std::string hexstring); // converts a string to raw data to append + + // appends a copy of the given raw data to the packet + // generally used for null terminated buffers/strings + // note: this does not pack the size of the raw data into the packet. + // use append_buffer for data buffers with a nonstatic size + // or that are not null-terminated + void append_data(const byte *pdata, size_t cb); + + void append_data(const std::vector &data); // stl overload of append_data(const byte *, size_t) + void append_string(const char *pstr, word len); // pack a string into the packet + void append_string(const std::string &str); // stl overload of append_string(const char *, word) + + // this allows quick formatting of strings on the fly + bool append_string(const std::basic_ostream &format); + + // pack a copy of the byte array into the packet + // note: this also packs the size of the given buffer + // internally used by append_string + // I'm not sure maple even uses these kind of buffers, most of the time it uses null terminated buffers + // which you can append using append_data (making sure it's actually null terminated) + void append_buffer(const byte *pdata, word cb); + + bool append_buffer(const std::vector &data); // oload of append_buffer(const byte *, word) + + // reads a value of any given type at the iterator position and increases the iterator + template + void read(T *pvalue, const_iterator &it) const + { + // reached end of packet? + if (it + sizeof(T) - 1 >= data.end()) + throw readexception(); + + // see append to understand how the copy function works + std::endiansafe_copy(it, it + sizeof(T), reinterpret_cast(pvalue)); + + it += sizeof(T); // increase iterator by the byte count of our value type + } + + // reads given num of bytes and incs the it + // internally used by read_string + // throws readexception on failure + void read_data(byte *pdata, size_t cb, const_iterator &it) const; + + // stl overloadload of read_data(byte *, size_t, const_iterator &) + // note: this function automatically calls .resize on data + // throws readexception on failure + void read_data(std::vector &data, size_t cb, const_iterator &it) const; + + // allocates a new byte array and copies raw data from the packet until a null (zero) byte is reached + // note: the caller is responsible for deleting the returned array. + // returns NULL if any error occurs + // optionally stores resulting buffer length in the given pointer + // throws readexception on failure + byte *read_data_nullterminated(const_iterator &it, size_t *length = NULL) const; + + // stl overload of read_data_nullterminated(const_iterator &) + // note: this automatically calls .resize on data + // throws readexception on failure + void read_data_nullterminated(std::vector &data, const_iterator &it) const; + + // reads a string, allocates a new char array and copies the string to it. optionally stores + // the string length in the given ptr. also increases the iterator as usual. + // note: the caller is responsible for deleting the returned array. + // returns NULL if any error occurs + // throws readexception on failure + char *read_string(const_iterator &it, size_t *length = NULL) const; + + // reads a string, copies it to the given stl container and increases the iterator. + // throws readexception on failure + void read_string(std::string &str, const_iterator &it) const; + + // reads a buffer, allocates a new byte array and copies the buffer to it. optionally stores + // the buffer length in the given ptr. also increases the iterator as usual. + // note: the caller is responsible for deleting the returned array. + // returns NULL if any error occurs + // throws readexception on failure + byte *read_buffer(const_iterator &it, size_t *length = NULL) const; + + // reads a buffer, copies it to the given stl container and increases the iterator. + // note: this function automatically calls .resize on data + // throws readexception on failure + void read_buffer(std::vector &data, const_iterator &it) const; + + protected: + std::vector data; // raw packet data. should we make this a shared_ptr or shared_array? + }; +} diff --git a/wxPloiter/packethooks.cpp b/wxPloiter/packethooks.cpp new file mode 100644 index 0000000..9cb1e71 --- /dev/null +++ b/wxPloiter/packethooks.cpp @@ -0,0 +1,487 @@ +#include "packethooks.hpp" + +#include "mem.h" +#include "mainform.hpp" +#include "aobscan.hpp" +#include "utils.hpp" +#include "wsockhooks.hpp" + +#include +#include +#include + +#define FORCE_WINSOCK 0 +#define FORCE_NOSEND 0 +#define FORCE_NORECV 0 + +namespace wxPloiter +{ + namespace detours = utils::detours; + + const std::string packethooks::tag = "wxPloiter::packethooks"; + boost::shared_ptr packethooks::inst; + + // TODO: make these non-static and push them as params to injectpacket + void **packethooks::ppcclientsocket = NULL; // pointer to the CClientSocket instance + packethooks::pfnsendpacket packethooks::mssendpacket = NULL; // maplestory's internal send func + packethooks::pfnrecvpacket packethooks::msrecvpacket = NULL; // maplestory's internal recv func + void *packethooks::mssendhook = NULL; + dword packethooks::mssendhookret = 0; + dword *packethooks::recviat = 0; + dword packethooks::originalrecviat = 0; + dword packethooks::recviatret = 0; + void *packethooks::someretaddy = NULL; // for ret addy spoofing + dword packethooks::maplethreadid; + + boost::shared_ptr packethooks::get() + { + if (!inst.get()) + inst.reset(new packethooks); + + return inst; + } + + packethooks::packethooks() + : log(utils::logging::get()), + initialized(false), + wsocklogging(false) + { + void *pmaplebase = NULL; + size_t maplesize = 0; + + if (!utils::mem::getmodulesize(GetModuleHandle(NULL), &pmaplebase, &maplesize)) + { + log->e(tag, "packethooks: failed to retrieve maple module size & base"); + return; + } + + utils::mem::aobscan send("8B 0D ? ? ? ? E8 ? ? ? ? 8D 4C 24 ? E9", pmaplebase, maplesize); + + if (!send.result() || FORCE_NOSEND) + { + log->w(tag, "packethooks: failed to find send address. send injection will not work"); + mainform::get()->enablesend(false); + } + else + { + mssendpacket = reinterpret_cast(utils::mem::getcall(send.result() + 6)); + ppcclientsocket = reinterpret_cast(*reinterpret_cast(send.result() + 2)); + mainform::get()->enablesend(true); + } + + utils::mem::aobscan fakeret("90 C3", pmaplebase, maplesize, 1); + + if (!fakeret.result()) + { + wxLogWarning("Could not find the fake return address. Will fall-back to another return address. " + "Blocking send packets will crash, so don't even try."); + + someretaddy = reinterpret_cast(mssendpacket) - 0xA; + } + else + someretaddy = reinterpret_cast(fakeret.result()); + + utils::mem::aobscan recv("E8 ? ? ? ? 8D 4C 24 ? C7 44 24 ? ? ? ? ? E8 ? ? ? ? 83 7E " + "? ? 0F 85 ? ? ? ? 8B 4C 24 ? 64 89 0D ? ? ? ? 59 5F 5E 5D 5B", pmaplebase, maplesize); + + if (!recv.result() || FORCE_NORECV) + { + log->w(tag, "packethooks: failed to find recv address. recv injection will not work"); + mainform::get()->enablerecv(false); + } + else + { + mainform::get()->enablerecv(true); + + // TODO: fix recv injection + //mainform::get()->enablerecv(false); + + msrecvpacket = reinterpret_cast(utils::mem::getcall(recv.result())); + } + + // credits to AIRRIDE for the IAT hooking method and the send hooking method + utils::mem::aobscan findrecvhook("8B 7C 24 ? 8B CF C7 44 24 ? ? ? ? ? E8 ? ? ? ? 0F B7 D8", pmaplebase, maplesize); + + if (!findrecvhook.result()) + wxLogWarning("Could not find the IAT pointer for recv. Recv log will not work."); + else + { + recviat = *reinterpret_cast(findrecvhook.result() - 4); + recviatret = reinterpret_cast(findrecvhook.result()); + } + + /* + AIRRIDE's virtualized send hook + 55 8B EC 6A FF 68 ? ? ? ? 64 A1 00 00 00 00 50 83 EC ? 53 56 57 A1 ? ? ? ? 33 C5 50 ? ? ? 64 A3 00 00 00 00 ? ? ? 6A 00 E9 + + 1 - scroll down and follow the first jmp to 01XXXXXX (opcode at result + 0x2D) + 2 - scroll down and find the first jmp to 01XXXXXX (opcode at followed jmp + 0xE) + the address of the opcode is the sendhook address + the address the jump leads to is the return address + + DWORD SendHook_Addr = 0x018AF0E1; // 101.1 + DWORD SendHook_ret = 0x019BE599; + + memory regions from 101.1: + + 0057E250 - 55 - push ebp + 0057E251 - 8B EC - mov ebp,esp + 0057E253 - 6A FF - push -01 + 0057E255 - 68 0E941101 - push 0111940E : [0824548B] + 0057E25A - 64 A1 00000000 - mov eax,fs:[00000000] + 0057E260 - 50 - push eax + 0057E261 - 83 EC 6C - sub esp,6C + 0057E264 - 53 - push ebx + 0057E265 - 56 - push esi + 0057E266 - 57 - push edi + 0057E267 - A1 D0A47E01 - mov eax,[017EA4D0] : [(float)-0.0029] + 0057E26C - 33 C5 - xor eax,ebp + 0057E26E - 50 - push eax + 0057E26F - 8D 45 F4 - lea eax,[ebp-0C] + 0057E272 - 64 A3 00000000 - mov fs:[00000000],eax + 0057E278 - 89 4D 88 - mov [ebp-78],ecx + 0057E27B - 6A 00 - push 00 + -> 0057E27D - E9 510E3301 - jmp 018AF0D3 <- + 0057E282 - 0FBA ED 0E - bts ebp,0E + 0057E286 - C1 CE 11 - ror esi,11 + 0057E289 - E9 0F000000 - jmp 0057E29D + 0057E28E - 89 54 24 4C - mov [esp+4C],edx + 0057E292 - 66 0FCE - bswap si + 0057E295 - 0FB6 F1 - movzx esi,cl + 0057E298 - E9 C9000000 - jmp 0057E366 + 0057E29D - 66 F7 D5 - not bp + 0057E2A0 - 8D 6C 24 1C - lea ebp,[esp+1C] + 0057E2A4 - 9C - pushfd + 0057E2A5 - 66 89 24 24 - mov [esp],sp + 0057E2A9 - 60 - pushad + 0057E2AA - 8D 64 24 40 - lea esp,[esp+40] + + 018AF0D3 - 9F - lahf + 018AF0D4 - 10 C0 - adc al,al + 018AF0D6 - 66 0FAD E0 - shrd ax,sp,cl + 018AF0DA - 0F31 - rdtsc + 018AF0DC - 8D 64 24 04 - lea esp,[esp+04] + 018AF0E0 - F8 - clc + -> 018AF0E1 - E9 B3F41000 - jmp 019BE599 <- + 018AF0E6 - D2 FD - sar ch,cl + 018AF0E8 - B2 F6 - mov dl,-0A + 018AF0EA - 13 B9 3FA49D4A - adc edi,[ecx+4A9DA43F] + 018AF0F0 - 99 - cdq + 018AF0F1 - 88 22 - mov [edx],ah + 018AF0F3 - DCCB - fmul st(3),st(0) + 018AF0F5 - 14 2F - adc al,2F + 018AF0F7 - 54 - push esp + 018AF0F8 - 2E FC - cld + + -> 019BE599 - C7 45 E0 00004000 - mov [ebp-20],00400000 : [00905A4D] <- + 019BE5A0 - 0FA3 F9 - bt ecx,edi + 019BE5A3 - D2 C6 - rol dh,cl + 019BE5A5 - F9 - stc + 019BE5A6 - 66 0FA5 E0 - shld ax,cl + 019BE5AA - 8B 45 E0 - mov eax,[ebp-20] + 019BE5AD - 81 FA B9A00D51 - cmp edx,510DA0B9 + 019BE5B3 - 80 E5 F1 - and ch,-0F + 019BE5B6 - 8D 14 DD 1F3691AA - lea edx,[ebx*8-556EC9E1] + 019BE5BD - 0FC9 - bswap ecx + 019BE5BF - 8B 4D E0 - mov ecx,[ebp-20] + 019BE5C2 - F6 DE - neg dh + 019BE5C4 - 66 81 CA 5CE0 - or dx,E05C + 019BE5C9 - 03 48 3C - add ecx,[eax+3C] + 019BE5CC - 0FB6 D2 - movzx edx,dl + 019BE5CF - D2 FE - sar dh,cl + */ + + // this send hook method is bypassless because it hooks virtualized code + utils::mem::aobscan findsendhook("55 8B EC 6A FF 68 ? ? ? ? 64 A1 00 00 00 00 50 83 EC " + "? 53 56 57 A1 ? ? ? ? 33 C5 50 ? ? ? 64 A3 00 00 00 00 ? ? ? 6A 00 E9", pmaplebase, maplesize); + + if (!findsendhook.result() || FORCE_WINSOCK) + { + wxLogWarning("Could not find the virtualized send function. Falling back to decrypting " + "raw winsock packets (not very reliable but better than nothing)."); + + if (!wsockhooks::get()->ishooked()) + { + wxLogWarning("Could not hook winsock send/recv. Packet logging will not work."); + return; + } + else + { + wxLogWarning("Falling back to decrypting winsock packets."); + initialized = true; + wsocklogging = true; + return; + } + } + else + { + mssendhook = utils::mem::getjump(findsendhook.result() + 0x2D) + 0xE; + mssendhookret = reinterpret_cast(utils::mem::getjump(reinterpret_cast(mssendhook))); + } + + log->i(tag, + strfmt() << "packethooks: packet injection initialized - " + "maplebase = 0x" << pmaplebase << + " maplesize = " << maplesize << + " mssendpacket = 0x" << mssendpacket << + " msrecvpacket = 0x" << msrecvpacket << + " someretaddy = 0x" << someretaddy << + " ppcclientsocket = 0x" << ppcclientsocket << + " mssendhook = 0x" << mssendhook << + " mssendhookret = 0x" << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << mssendhookret << + " recviat = 0x" << recviat << + " recviatret = 0x" << std::hex << std::uppercase << std::setw(8) << std::setfill('0') << recviatret + ); + + maplethreadid = GetCurrentThreadId(); // will be changed to moopla thread id as soon as it's detected + boost::shared_ptr t = boost::make_shared( + &packethooks::getmaplethreadid, GetCurrentThreadId()); + + // hook everything + if (mssendhook) + { + log->i(tag, "packethooks: hooking virtualized send"); + utils::mem::writejmp(reinterpret_cast(mssendhook), sendhook); + } + + if (recviat) + { + log->i(tag, "packethooks: IAT hooking msrecvpacket"); + utils::mem::makepagewritable(recviat, 4); + originalrecviat = *recviat; + log->i(tag, strfmt() << "packethooks: original IAT value: " << + std::hex << std::uppercase << std::setw(8) << std::setfill('0') << originalrecviat); + *recviat = reinterpret_cast(recviathook); + } + + initialized = true; + } + + // NOTE: unused + void packethooks::enablesendblock(bool enabled) + { + if (!mssendpacket) + return; + + log->i(tag, "packethooks: hooking mssendpacket to block packets"); + + if (!detours::hook(enabled, reinterpret_cast(&mssendpacket), sendblockhook)) + wxLogWarning("Failed to hook/unhook mssendpacket. Send blocking will not work."); + } + + bool packethooks::isusingwsock() + { + return wsocklogging; + } + + packethooks::~packethooks() + { + // empty + } + + bool packethooks::isinitialized() + { + return initialized; + } + + void __declspec(naked) packethooks::injectpacket(maple::inpacket *ppacket) + { + __asm + { + // set class ptr + mov ecx,[ppcclientsocket] + mov ecx,[ecx] + + // push packet and fake return address + push [esp+0x4] // ppacket + push [someretaddy] + + // send packet + jmp [msrecvpacket] + } + } + + void __declspec(naked) packethooks::injectpacket(maple::outpacket *ppacket) + { + __asm + { + // set class ptr + mov ecx,[ppcclientsocket] + mov ecx,[ecx] + + // push packet and fake return address + push [esp+0x4] // ppacket + push [someretaddy] + + // send packet + jmp [mssendpacket] + } + } + + void packethooks::getmaplethreadid(dword current_thread) + { + namespace tt = boost::this_thread; + namespace pt = boost::posix_time; + + HWND hmoopla = NULL; + + while (!hmoopla) + { + hmoopla = maple::getwnd(); + tt::sleep(pt::milliseconds(500)); + } + + maplethreadid = GetWindowThreadProcessId(hmoopla, NULL); + utils::logging::get()->i(tag, strfmt() + << "getmaplethreadid: spoofing active - current thread: " << current_thread + << " spoofed to: " << maplethreadid); + } + + void packethooks::sendpacket(maple::packet &p) + { + maple::packet pt = p; // the raw data will be encrypted so we need to make a copy + + // construct packet object + maple::outpacket mspacket = {0}; + mspacket.cbData = pt.size(); + mspacket.pbData = pt.raw(); + + // spoof thread id + // credits to kma4 for hinting me the correct TIB thread id offset + __writefsdword(0x06B8, maplethreadid); + + // send packet + injectpacket(&mspacket); + } + + void packethooks::recvpacket(maple::packet &p) + { + // construct packet object + maple::inpacket mspacket = {0}; + mspacket.iState = 2; + mspacket.lpvData = p.raw(); + mspacket.dwTotalLength = p.size(); + mspacket.dwUnknown = 0; // 0x00CC; + mspacket.dwValidLength = mspacket.dwTotalLength - sizeof(DWORD); + mspacket.uOffset = 4; + + // spoof thread id + // credits to kma4 for hinting me the correct TIB thread id offset + __writefsdword(0x06B8, maplethreadid); + + // send packet + injectpacket(&mspacket); + } + + // non-bypassless hook of mssendpacket used to block packets + // NOTE: unused + void __fastcall packethooks::sendblockhook(void *instance, void *edx, maple::outpacket *ppacket) + { + if (safeheaderlist::getblockedsend()->contains(*ppacket->pwHeader)) + { + //get()->log->i(tag, strfmt() << "send: blocked header " << + //std::hex << std::uppercase << std::setw(4) << std::setfill('0') << *ppacket->pwHeader); + return; + } + + injectpacket(ppacket); + } + + dword _stdcall packethooks::handlepacket(dword isrecv, void *retaddy, int size, byte pdata[]) + { + word *pheader = reinterpret_cast(pdata); + + if (isrecv == 1) + { + if (safeheaderlist::getblockedrecv()->contains(*pheader)) + { + //get()->log->i(tag, strfmt() << "recv: blocked header " << + //std::hex << std::uppercase << std::setw(4) << std::setfill('0') << *pheader); + + *pheader = BLOCKED_HEADER; + return 0; + } + } + else + { + if (safeheaderlist::getblockedsend()->contains(*pheader)) + return 1; // send packets can't be blocked by invalidating the header + } + + boost::shared_ptr p(new maple::packet(pdata, size)); + mainform::get()->queuepacket(p, isrecv == 1 ? mainform::wxID_PACKET_RECV : mainform::wxID_PACKET_SEND, true, retaddy); + + // returns 1 if the send header must be blocked + return 0; + } + + void __declspec(naked) packethooks::sendhook() + { + // hook by AIRRIDE + __asm + { + pushad + + // TODO: save cpu when not logging by skipping the hook entirely here + + mov ecx, [ebp + 0x08] // pointer to packet struct + push [ecx + 0x04] // pdata + push [ecx + 0x08] // size + push [ebp + 0x04] // retaddy + push 0x00000000 // isrecv + call handlepacket + cmp eax, 0 + je dontblockplsicryeverytime + + leave // block the packet by skipping it completely + ret 0004 + + dontblockplsicryeverytime: + popad + jmp mssendhookret + } + } + + void __declspec(naked) packethooks::recviathook() + { + // hook by AIRRIDE + __asm + { + mov eax, [recviatret] + cmp dword ptr [esp], eax + jne deliciouslolis + mov eax, recvhook + mov dword ptr [esp], eax + + deliciouslolis: + jmp originalrecviat + } + } + + void __declspec(naked) packethooks::recvhook() + { + // hook by AIRRIDE + __asm + { + mov ecx,[esp + 0x28] // retaddy + mov edi,[esp + 0x2C] // pointer to packet struct (original code) + + pushad + + // TODO: save cpu when not logging by skipping the hook entirely here + + mov eax, [edi + 0x08] + add eax, 4 + push eax // pdata + mov edx, [edi + 0x0C] + sub edx, 4 + push edx // size + push ecx // retaddy + push 0x00000001 // isrecv + call handlepacket + + popad + jmp recviatret + } + } +} diff --git a/wxPloiter/packethooks.cpp.bak.cpp b/wxPloiter/packethooks.cpp.bak.cpp new file mode 100644 index 0000000..16334a5 --- /dev/null +++ b/wxPloiter/packethooks.cpp.bak.cpp @@ -0,0 +1,340 @@ +// I tried to keep the code nice and clean, but this file is unreadable as fuck. +// Sorry. + +#include "packethooks.hpp" + +#include "mem.h" +#include "mainform.hpp" +//#include "aobscan.hpp" +#include "utils.hpp" + +#include +#include +#include + +#include "lib/Asm.h" // temporary + +namespace wxPloiter +{ + const std::string packethooks::tag = "wxPloiter::packethooks"; + boost::shared_ptr packethooks::inst; + + /* + // TODO: make these non-static and push them as params to injectpacket + void **packethooks::ppcclientsocket = NULL; // pointer to the CClientSocket instance + packethooks::pfnsendpacket packethooks::mssendpacket = NULL; // maplestory's internal send func + packethooks::pfnrecvpacket packethooks::msrecvpacket = NULL; // maplestory's internal recv func + void *packethooks::someretaddy = NULL; // for ret addy spoofing + */ + + dword packethooks::maplethreadid; + void *packethooks::mssendpacketfunc = reinterpret_cast(0x00478020); + void *packethooks::mssendpacket = reinterpret_cast(0x018AF0E1); + void *packethooks::mssendpacketret = reinterpret_cast(0x019BE599); + void *packethooks::pmsrecvpacket = reinterpret_cast(0x0127F094); + void *packethooks::msrecvpacketret = reinterpret_cast(0x0057E931); + void *packethooks::msrecvptrmemory; + + boost::shared_ptr packethooks::get() + { + if (!inst.get()) + inst.reset(new packethooks); + + return inst; + } + + packethooks::packethooks() + : log(utils::logging::get()), + initialized(false) + { + // TODO: make my own memory writing class instead of using Asm.lib and check for write failure + Asm::Write_Hook("jmp", reinterpret_cast(mssendpacket), reinterpret_cast(sendhook)); + + msrecvptrmemory = reinterpret_cast( + Asm::Write_Pointer_Hook(reinterpret_cast(pmsrecvpacket), reinterpret_cast(recvptrhook)) + ); + + //8D 0C F5 00 00 00 00 66 0B D1 0F B7 CA 2nd result of starting function(0x007... jumped by the address(0x004... + + //0045E400 wryyy the magic jump + //Asm::Write_Hook("call", 0x01AA5F4F, reinterpret_cast(threadcheck), 0); + //Asm::Write_Hook("call", 0x01ABD739, reinterpret_cast(threadcheck), 0); + + //64 A1 18 00 00 00 + //Asm::Write_code(0x018D0F71, "B8 00 D0 FD 7E", 1);//6B8 + //Asm::Write_code(0x01A1BAEE, "B8 00 D0 FD 7E", 1);//24 + //Asm::Write_code(0x01B17033, "B8 00 D0 FD 7E", 1);//others + + /* + void *pmaplebase = NULL; + size_t maplesize = 0; + + if (!utils::mem::getmodulesize(GetModuleHandle(NULL), &pmaplebase, &maplesize)) + { + log->e(tag, "packethooks: failed to retrieve maple module size & base"); + return; + } + + utils::mem::aobscan send("8B 0D ? ? ? ? E8 ? ? ? ? 8D 4C 24 ? E9", pmaplebase, maplesize); + + if (!send.result()) + { + log->w(tag, "packethooks: failed to find send address. send injection will not work"); + mainform::get()->enablesend(false); + } + else + mainform::get()->enablesend(true); + + mssendpacket = reinterpret_cast(utils::mem::getcall(send.result() + 6)); + someretaddy = reinterpret_cast(mssendpacket) - 0xA; + ppcclientsocket = reinterpret_cast(*reinterpret_cast(send.result() + 2)); + + utils::mem::aobscan recv("E8 ? ? ? ? 8D 4C 24 ? C7 44 24 ? ? ? ? ? E8 ? ? ? ? 83 7E " + "? ? 0F 85 ? ? ? ? 8B 4C 24 ? 64 89 0D ? ? ? ? 59 5F 5E 5D 5B", pmaplebase, maplesize); + + if (!recv.result()) + { + log->w(tag, "packethooks: failed to find recv address. recv injection will not work"); + mainform::get()->enablerecv(false); + } + else + mainform::get()->enablerecv(true); + + if (!recv.result() && !send.result()) + return; + + msrecvpacket = reinterpret_cast(utils::mem::getcall(recv.result())); + + log->i(tag, + strfmt() << "packethooks: packet injection initialized - " + "maplebase = 0x" << pmaplebase << + " maplesize = " << maplesize << + " mssendpacket = 0x" << mssendpacket << + " msrecvpacket = 0x" << msrecvpacket << + " someretaddy = 0x" << someretaddy << + " ppcclientsocket = 0x" << ppcclientsocket + ); + */ + + maplethreadid = GetCurrentThreadId(); // will be changed to moopla thread id as soon as it's detected + boost::shared_ptr t = boost::make_shared( + &packethooks::getmaplethreadid, GetCurrentThreadId()); + + initialized = true; + } + + void _stdcall packethooks::handlepacket(int type, dword retaddy, int cb, const byte packet[]) + { + boost::shared_ptr p(new maple::packet(packet, cb)); + mainform::get()->queuepacket(p, type, true); + } + + // TODO: find a better workaround + const int sendtype = mainform::wxID_PACKET_SEND; + const int recvtype = mainform::wxID_PACKET_RECV; + + void _declspec(naked) packethooks::sendhook() + { + // credits to AIRRIDE + _asm + { + pushad + mov ecx,[ebp+0x08] // packet struct + push [ecx+0x04] // packet + push [ecx+0x08] // cb + push [ebp+0x04] // retaddy + push [sendtype] // type + call handlepacket + popad + //sub eax,ebx + //inc ax + + jmp mssendpacketret + } + } + + + void _declspec(naked) packethooks::recvhook() + { + // credits to AIRRIDE + _asm + { + mov ecx,[esp+0x28] // return Address + mov edi,[esp+0x2C] // RPacket Struct (original code) + + pushad + mov eax,[edi+0x08] + add eax,4 + push eax // packet + mov edx,[edi+0x0C] + sub edx,4 + push edx // size + push ecx // retaddy + push [recvtype] // type + call handlepacket + popad + + jmp msrecvpacketret + } + } + + + void _declspec(naked) packethooks::recvptrhook() + { + // credits to AIRRIDE + // this is an IAT hook for recv + _asm + { + mov eax,[msrecvpacketret] + cmp dword ptr [esp],eax + jne Ending_RP + mov eax, recvhook + mov dword ptr [esp],eax + + Ending_RP: + jmp msrecvptrmemory + } + } + + void _declspec(naked) packethooks::threadcheck() + { + // credits to AIRRIDE + _asm + { + mov eax, 0x7EFDD000 + mov eax, [eax+0x24] + mov ecx, eax //using ecx + ret + } + } + + packethooks::~packethooks() + { + // empty + } + + bool packethooks::isinitialized() + { + return initialized; + } + + void __declspec(naked) packethooks::injectpacket(maple::inpacket *ppacket) + { + // currently disabled + /* + __asm + { + // set class ptr + mov ecx,[ppcclientsocket] + mov ecx,[ecx] + + // push packet and fake return address + push [esp+0x4] // ppacket + push [someretaddy] + + // send packet + jmp [msrecvpacket] + } + */ + } + + void packethooks::injectpacket(maple::outpacket *ppacket) + { + // TODO: clean this up + + int cb = ppacket->cbData; + dword pdata = reinterpret_cast(ppacket->pbData); + + if (cb == -1 || cb < 2) + return; + + // spoof thread id + // credits to kma4 for hinting me the correct TIB thread id offset + //dword oldthread = GetCurrentThreadId(); + __writefsdword(0x06B8, maplethreadid); + + // credits to AIRRIDE + // created by airride^^ + _asm + { + mov eax,[pdata] + mov ebx,[cb] + push 0x00 + push ebx + push eax + push 0x00 + push esp + call mssendpacketfunc + } + + //__writefsdword(0x06B8, oldthread); + + /* + __asm + { + // set class ptr + mov ecx,[ppcclientsocket] + mov ecx,[ecx] + + // push packet and fake return address + push [esp+0x4] // ppacket + push [someretaddy] + + // send packet + jmp [mssendpacket] + } + */ + } + + void packethooks::getmaplethreadid(dword current_thread) + { + namespace tt = boost::this_thread; + namespace pt = boost::posix_time; + + HWND hmoopla = NULL; + + while (!hmoopla) + { + hmoopla = maple::getwnd(); + tt::sleep(pt::milliseconds(500)); + } + + maplethreadid = GetWindowThreadProcessId(hmoopla, NULL); + utils::logging::get()->i(tag, strfmt() + << "getmaplethreadid: spoofing active - current thread: " << current_thread + << " spoofed to: " << maplethreadid); + } + + void packethooks::sendpacket(maple::packet &p) + { + // construct packet object + maple::outpacket mspacket = {0}; + mspacket.cbData = p.size(); + mspacket.pbData = p.raw(); + + // send packet + injectpacket(&mspacket); + } + + void packethooks::recvpacket(maple::packet &p) + { + // construct packet object + maple::inpacket mspacket = {0}; + mspacket.iState = 2; + mspacket.lpvData = p.raw(); + mspacket.usLength = p.size(); + mspacket.usRawSeq = *reinterpret_cast(p.raw()) & 0xFFFF; + mspacket.usDataLen = mspacket.usLength - sizeof(DWORD); + mspacket.usUnknown = 0; // 0x00CC; + mspacket.uOffset = 4; + + /* + // spoof thread id + // credits to kma4 for hinting me the correct TIB thread id offset + __writefsdword(0x06B8, maplethreadid); + */ + + // send packet + injectpacket(&mspacket); + } +} diff --git a/wxPloiter/packethooks.hpp b/wxPloiter/packethooks.hpp new file mode 100644 index 0000000..f38c281 --- /dev/null +++ b/wxPloiter/packethooks.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "common.h" +#include "packetstruct.h" +#include "packet.hpp" +#include "logging.hpp" +#include "safeheaderlist.hpp" + +#include + +namespace wxPloiter +{ + class packethooks + { + public: + static boost::shared_ptr get(); + virtual ~packethooks(); + bool isinitialized(); // returns false if the class was unable to find the packet funcs + void sendpacket(maple::packet &p); // injects a send packet + void recvpacket(maple::packet &p); // injects a recv packet + bool isusingwsock(); + void enablesendblock(bool enabled); + + protected: + static const std::string tag; + static boost::shared_ptr inst; + + // function signatures of internal maplestory send/recv funcs + // since we can't use __thiscall directly, we have to use __fastcall and add a placeholder EDX param + // __thiscall passes the instance as a hidden first parameter in ecx + // __fastcall passes the first two parameters in ecx and edx, the other params are pushed normally + // so calling a __thiscall as a __fastcall requires ignoring the parameters on edx + // and making sure the real params are pushed + typedef void (__fastcall* pfnsendpacket)(void *instance, void *edx, maple::outpacket* ppacket); + typedef void (__fastcall* pfnrecvpacket)(void *instance, void *edx, maple::inpacket* ppacket); + + static void injectpacket(maple::inpacket *ppacket); + static void injectpacket(maple::outpacket *ppacket); + + boost::shared_ptr log; + bool initialized; + bool wsocklogging; + static dword maplethreadid; // thread that created the maplestory wnd + static void **ppcclientsocket; // pointer to the CClientSocket instance + static pfnsendpacket mssendpacket; // maplestory's internal send func + static void *mssendhook; // some virtualized code related to mssendpacket + static dword mssendhookret; + static pfnrecvpacket msrecvpacket; // maplestory's internal recv func + static dword *recviat; // pointer for recv iat hooking + static dword originalrecviat; + static dword recviatret; // return addy of the iat hook + static void *someretaddy; // for ret addy spoofing + + static dword _stdcall handlepacket(dword isrecv, void *retaddy, int size, byte pdata[]); + static void __fastcall sendblockhook(void *instance, void *edx, maple::outpacket* ppacket); + static void sendhook(); + static void recviathook(); + static void recvhook(); + + packethooks(); + static void getmaplethreadid(dword current_thread); // waits for the maplestory window and stores its thread id + }; +} diff --git a/wxPloiter/packethooks.hpp.bak.hpp b/wxPloiter/packethooks.hpp.bak.hpp new file mode 100644 index 0000000..8b646a0 --- /dev/null +++ b/wxPloiter/packethooks.hpp.bak.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "common.h" +#include "packetstruct.h" +#include "packet.hpp" +#include "logging.hpp" + +#include + +namespace wxPloiter +{ + class packethooks + { + public: + static boost::shared_ptr get(); + virtual ~packethooks(); + bool isinitialized(); // returns false if the class was unable to find the packet funcs + void sendpacket(maple::packet &p); // injects a send packet + void recvpacket(maple::packet &p); // injects a recv packet + + protected: + static const std::string tag; + static boost::shared_ptr inst; + + // function signatures of internal maplestory send/recv funcs + // since we can't use __thiscall directly, we have to use __fastcall and add a placeholder EDX param + // __thiscall passes the instance as a hidden first parameter in ecx + // __fastcall passes the first two parameters in ecx and edx, the other params are pushed normally + // so calling a __thiscall as a __fastcall requires ignoring the parameters on edx + // and making sure the real params are pushed + /* + typedef void (__fastcall* pfnsendpacket)(void *instance, void *edx, maple::outpacket* ppacket); + typedef void (__fastcall* pfnrecvpacket)(void *instance, void *edx, maple::inpacket* ppacket); + */ + + static void injectpacket(maple::inpacket *ppacket); + static void injectpacket(maple::outpacket *ppacket); + + boost::shared_ptr log; + bool initialized; + static dword maplethreadid; // thread that created the maplestory wnd + /* + static void **ppcclientsocket; // pointer to the CClientSocket instance + static pfnsendpacket mssendpacket; // maplestory's internal send func + static pfnrecvpacket msrecvpacket; // maplestory's internal recv func + */ + // NOTE: this is temporary TODO: make this look nice + static void *mssendpacketfunc; + static void *mssendpacket; // maplestory's internal send func + static void *mssendpacketret; + static void *pmsrecvpacket; // pointer to maplestory's internal recv func + static void *msrecvpacketret; + static void *msrecvptrmemory; + //static void *someretaddy; // for ret addy spoofing + + static void _stdcall handlepacket(int type, dword retAddr, int cb, const byte packet[]); + static void sendhook(); + static void recvhook(); + static void recvptrhook(); + static void threadcheck(); + + packethooks(); + static void getmaplethreadid(dword current_thread); // waits for the maplestory window and stores its thread id + }; +} diff --git a/wxPloiter/packetstruct.h b/wxPloiter/packetstruct.h new file mode 100644 index 0000000..2d18267 --- /dev/null +++ b/wxPloiter/packetstruct.h @@ -0,0 +1,49 @@ +#pragma once + +#include "common.h" + +namespace maple +{ + // internal maplestory packet structs + // credits to waffle or whoever made 21st century PE + + #pragma pack(push, 1) + struct outpacket + { + dword fLoopback; // win32 BOOL = int. that's fucking stupid. + union + { + byte *pbData; + void *pData; + word *pwHeader; + }; + dword cbData; + dword uOffset; + dword fEncryptedByShanda; + }; + + struct inpacket + { + dword fLoopback; // 0 + signed_dword iState; // 2 + union + { + void *lpvData; + struct + { + dword dw; + word wHeader; + } *pHeader; + struct + { + dword dw; + byte bData[0]; + } *pData; + }; + dword dwTotalLength; + dword dwUnknown; + dword dwValidLength; + dword uOffset; + }; + #pragma pack(pop) +} diff --git a/wxPloiter/resource.aps b/wxPloiter/resource.aps new file mode 100644 index 0000000000000000000000000000000000000000..b598d748e3e36dbbb0ef073b5c0eb5212d9ddcc5 GIT binary patch literal 122984 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 VW`|7UG$=Y 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 literal 0 HcmV?d00001 diff --git a/wxPloiter/resource.h b/wxPloiter/resource.h new file mode 100644 index 0000000..b4bae50 --- /dev/null +++ b/wxPloiter/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Resource.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif + +#define IDI_ICON1 101 diff --git a/wxPloiter/resource.rc b/wxPloiter/resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..c4cc7575d982d53a50a124cb5445058e835e2a8d GIT binary patch literal 388 zcmaKo%?iRm41~Y4;5(G|-qlACf1n5Jp?WS_TNJcPx1#v)>U4`gAe0ailG&Y^JRbpj zN-_>y*plPnQ>j``npE24gzPl!({mXyAtF_dSr9YSo341{x-h>xpF3;Lics1z>G*OR zjJT{c)vC32p5t%3t?*WFH*(WBx#zLq4}tSqIPVSeD(s4cseHw9l`?0f hE@Gxx!cv&Q=K`&YuHXM}+AtVtvEnk0FLc-Sc>!FmHk|+f literal 0 HcmV?d00001 diff --git a/wxPloiter/safeheaderlist.cpp b/wxPloiter/safeheaderlist.cpp new file mode 100644 index 0000000..8b7fa32 --- /dev/null +++ b/wxPloiter/safeheaderlist.cpp @@ -0,0 +1,148 @@ +#include "safeheaderlist.hpp" + +#include +#include +#include "utils.hpp" + +namespace wxPloiter +{ + typedef boost::mutex mutex; + + safeheaderlist::ptr safeheaderlist::blockedsend; + safeheaderlist::ptr safeheaderlist::blockedrecv; + safeheaderlist::ptr safeheaderlist::ignoredsend; + safeheaderlist::ptr safeheaderlist::ignoredrecv; + + safeheaderlist::ptr safeheaderlist::getblockedsend() + { + return getsingleton(blockedsend); + } + + safeheaderlist::ptr safeheaderlist::getblockedrecv() + { + return getsingleton(blockedrecv); + } + + safeheaderlist::ptr safeheaderlist::getignoredsend() + { + return getsingleton(ignoredsend); + } + + safeheaderlist::ptr safeheaderlist::getignoredrecv() + { + return getsingleton(ignoredrecv); + } + + std::string safeheaderlist::tostring() + { + bool first = true; + std::ostringstream oss; + + oss << "safeheaderlist(" << iv.size() << "): { "; + boost_foreach(const word &h, iv) + { + oss << (first ? "" : ", ") << std::hex << std::uppercase << std::setw(4) << std::setfill('0') << h; + first = false; + } + oss << " }"; + + return oss.str(); + } + + safeheaderlist::safeheaderlist() + { + // empty + } + + safeheaderlist::safeheaderlist(safeheaderlist &other) + { + other.copy(this); + } + + safeheaderlist::~safeheaderlist() + { + // empty + } + + void safeheaderlist::push_back(word header) + { + mutex::scoped_lock lock(mut); + + if (!v.empty() && v.find(header) != v.end()) + return; // already blocked + + v.insert(header); + iv.push_back(header); + } + + void safeheaderlist::erase(word header) + { + mutex::scoped_lock lock(mut); + + if (v.empty()) + return; + + std::set::iterator it = v.find(header); + if (it == v.end()) + return; // not blocked + + v.erase(it); + iv.erase(std::find(iv.begin(), iv.end(), header)); + } + + bool safeheaderlist::contains(word header) + { + mutex::scoped_lock lock(mut); + + if (v.empty()) + return false; + + return v.find(header) != v.end(); + } + + void safeheaderlist::copy(std::set &dst) + { + mutex::scoped_lock lock(mut); + boost_foreach(const word &h, v) + dst.insert(h); + } + + void safeheaderlist::copy(std::vector &dst) + { + mutex::scoped_lock lock(mut); + boost_foreach(const word &h, iv) + dst.push_back(h); + } + + void safeheaderlist::copy(safeheaderlist *dst) + { + copy(dst->v); + copy(dst->iv); + } + + void safeheaderlist::clear() + { + mutex::scoped_lock lock(mut); + v.clear(); + } + + size_t safeheaderlist::size() + { + mutex::scoped_lock lock(mut); + return v.size(); + } + + safeheaderlist::ptr safeheaderlist::getsingleton(ptr &pinstance) + { + if (!pinstance.get()) + pinstance.reset(new safeheaderlist); + + return pinstance; + } + + word &safeheaderlist::at(long index) + { + mutex::scoped_lock lock(mut); + return iv[index]; + } +} diff --git a/wxPloiter/safeheaderlist.hpp b/wxPloiter/safeheaderlist.hpp new file mode 100644 index 0000000..4ed98dc --- /dev/null +++ b/wxPloiter/safeheaderlist.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "common.h" + +#include +#include +#include + +namespace wxPloiter +{ + // a thread-safe header list + class safeheaderlist + { + public: + typedef boost::shared_ptr ptr; // shared ptr to a safeheaderlist instance + + // TODO: join all static lists into a single list to improve performance? + // or would it just deadlock because of even more mutex concurrency? + + // TODO: thread safety is not required for ignored headers. move them to simpler lists + // to improve performance + + // TODO: add const correctness to this class + + static ptr getblockedsend(); + static ptr getblockedrecv(); + static ptr getignoredsend(); + static ptr getignoredrecv(); + + safeheaderlist(); + safeheaderlist(safeheaderlist &other); + virtual ~safeheaderlist(); + std::string tostring(); + void push_back(word header); // thread safe push_back + void erase(word header); // thread safe erase + bool contains(word header); // checks if the list contains the given header. thread safe + void copy(std::set &dst); + void copy(std::vector &dst); + void copy(safeheaderlist *dst); + void clear(); + size_t size(); + word &at(long index); + + protected: + static ptr blockedsend; // blocked send headers list + static ptr blockedrecv; // blocked recv headers list + static ptr ignoredsend; // ignored send headers list + static ptr ignoredrecv; // ignored recv headers list + + std::set v; // internal set + std::vector iv; // internal vector for index-based stuff + boost::mutex mut; // mutex for thread safety + + static ptr getsingleton(ptr &pinstance); + }; +} diff --git a/wxPloiter/utils.cpp b/wxPloiter/utils.cpp new file mode 100644 index 0000000..47a02c0 --- /dev/null +++ b/wxPloiter/utils.cpp @@ -0,0 +1,191 @@ +#include "utils.hpp" + +#include "detours.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace maple +{ + HWND getwnd() + { + TCHAR buf[200]; + DWORD procid; + + for (HWND hwnd = GetTopWindow(NULL); hwnd != NULL; hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)) + { + GetWindowThreadProcessId(hwnd, &procid); + + if (procid != GetCurrentProcessId()) + continue; + + if (!GetClassName(hwnd, buf, 200)) + continue; + + if (_tcscmp(buf, _T("MapleStoryClass")) != 0) + continue; + + return hwnd; + } + + return NULL; + } +} + +namespace utils +{ + bool copytoclipboard(wxTextDataObject *source) + { + if (!wxTheClipboard->Open()) + { + wxLogError("Failed to open clipboard!"); + return false; + } + + wxTheClipboard->SetData(source); + wxTheClipboard->Close(); + return true; + } + + namespace detours + { + bool hook(bool enabled, __inout PVOID *ppvTarget, __in PVOID pvDetour) + { + if (DetourTransactionBegin() != NO_ERROR) + return false; + + do // cool trick to handle many errors with the same cleanup code + { + if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR) + break; + + if ((enabled ? DetourAttach : DetourDetach)(ppvTarget, pvDetour) != NO_ERROR) + break; + + if (DetourTransactionCommit() == NO_ERROR) + return true; + } + while (false); + + DetourTransactionAbort(); + return false; + } + } + + namespace datetime + { + std::string utc_date() + { + namespace bg = boost::gregorian; + + static const char * const fmt = "%Y-%m-%d"; + std::ostringstream ss; + // assumes std::cout's locale has been set appropriately for the entire app + ss.imbue(std::locale(std::cout.getloc(), new bg::date_facet(fmt))); + ss << bg::day_clock::universal_day(); + return ss.str(); + } + + std::string utc_time() + { + namespace pt = boost::posix_time; + + static const char * const fmt = "%H:%M:%S"; + std::ostringstream ss; + // assumes std::cout's locale has been set appropriately for the entire app + ss.imbue(std::locale(std::cout.getloc(), new pt::time_facet(fmt))); + ss << pt::second_clock::universal_time(); + return ss.str(); + } + } + + boost::shared_ptr random::instance; + + void random::init() + { + // thread safe singleton initialization + // must be called in the main thread + instance.reset(new random); + } + + boost::shared_ptr random::get() + { + return instance; // return a pointer to the singleton instance + } + + random::random() + { + // initialize random seed + gen.seed(static_cast(std::time(0))); + } + + random::~random() + { + // empty + } + + byte random::getbyte() + { + return getinteger(0, 0xFF); + } + + void random::getbytes(byte *bytes, size_t cb) + { + for (size_t i = 0; i < cb; i++) + bytes[i] = getbyte(); + } + + word random::getword() + { + return getinteger(0, 0xFFFF); + } + + dword random::getdword() + { + return getinteger(-0x7FFFFFFF, 0x7FFFFFFF); + } + + namespace asmop + { + byte ror(byte val, int num) + { + for (int i = 0; i < num; i++) + { + int lowbit; + + if(val & 1) + lowbit = 1; + else + lowbit = 0; + + val >>= 1; + val |= (lowbit << 7); + } + + return val; + } + + byte rol(byte val, int num) + { + int highbit; + + for (int i = 0; i < num; i++) + { + if(val & 0x80) + highbit = 1; + else + highbit = 0; + + val <<= 1; + val |= highbit; + } + + return val; + } + } +} diff --git a/wxPloiter/utils.hpp b/wxPloiter/utils.hpp new file mode 100644 index 0000000..03d3b0b --- /dev/null +++ b/wxPloiter/utils.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include "common.h" +#include +#include +#include +#include +#include + +#include +#include +#pragma comment(lib, "detours.lib") + +// macros +#define strfmt() std::ostringstream().flush() +#define boost_foreach BOOST_FOREACH + +#if defined(DEBUG) | defined(_DEBUG) +#ifndef dbgcode +#define dbgcode(...) do { __VA_ARGS__; } while (0) +#endif +#else +#ifndef dbgcode +#define dbgcode(x) +#endif +#endif + +// maplestory utility funcs +namespace maple +{ + HWND getwnd(); // returns a handle to the MapleStory in-game window +} + +namespace utils +{ + bool copytoclipboard(wxTextDataObject *source); + + // detours 2.1 utilities + // TODO: move this to utils::mem maybe? + namespace detours + { + bool hook(bool enabled, __inout PVOID *ppvTarget, __in PVOID pvDetour); + } + + // various datetime utilities + namespace datetime + { + std::string utc_date(); // formats the current utc date in a string + std::string utc_time(); // formats the current utc time in a string + } + + // multi-purpose random number generator + class random + { + public: + static void init(); + static boost::shared_ptr get(); + virtual ~random(); + + // generic integer random number generator + // T: the desired integer type + // the generated number will be >= min and <= max + // NOTE: this is for integer types ONLY! + template + T getinteger(T min, T max) + { + boost::mutex::scoped_lock lock(mut); + boost::random::uniform_int_distribution<> dist(min, max); // randomness distribution rule + return static_cast(dist(gen)); // generate a number between min and max + } + + // generates a number between 0x00U and 0xFFU + byte getbyte(); + + // fills a byte array with randomly generated bytes + // bytes: the destination byte array + // cb: the size of the destination array + void getbytes(byte *bytes, size_t cb); + + // generates a number netween 0x0000U and 0xFFFFU + word random::getword(); + + // generates a number netween 0x00000000U and 0xFFFFFFFFU + dword random::getdword(); + + protected: + static boost::shared_ptr instance; + boost::mutex mut; + boost::random::mt19937 gen; // seed + random(); // private singleton constructor + }; + + // various asm / low-level operators transposed to cpp + namespace asmop + { + // circular shift (rotate) right + // num is the number of bits to shift + // ror(0b101100, 1) = 0b010110 + byte ror(byte val, int num); + + // circular shift (rotate) left + // num is the number of bits to shift + // rol(0b010110, 1) = 0b101100 + byte rol(byte val, int num); + } +} diff --git a/wxPloiter/wsockhooks.cpp b/wxPloiter/wsockhooks.cpp new file mode 100644 index 0000000..8d04af1 --- /dev/null +++ b/wxPloiter/wsockhooks.cpp @@ -0,0 +1,309 @@ +#include "wsockhooks.hpp" + +#include "mainform.hpp" +#include "utils.hpp" + +#include +#include +#include + +namespace wxPloiter +{ + namespace detours = utils::detours; + + const std::string wsockhooks::tag = "maple::wsockhooks"; + const size_t wsockhooks::header_size = sizeof(dword); + boost::shared_ptr wsockhooks::inst; // singleton + + boost::shared_ptr wsockhooks::get() + { + if (!inst.get()) + inst.reset(new wsockhooks); + + return inst; + } + + wsockhooks::wsockhooks() + : pconnect(::connect), + psend(::send), + precv(::recv), + log(utils::logging::get()), + transition(false), + targetsocket(NULL), + hooked(false) + { + // get actual addresses of send, recv and connect. + // local ones might be messed up (local recv didn't work for example) + HMODULE hws2_32 = GetModuleHandle(_T("ws2_32.dll")); + pconnect = reinterpret_cast(GetProcAddress(hws2_32, "connect")); + psend = reinterpret_cast(GetProcAddress(hws2_32, "send")); + precv = reinterpret_cast(GetProcAddress(hws2_32, "recv")); + + if (!pconnect) + { + log->w(tag, "wsockhooks: failed to get the real address of connect(). " + "decryption might fail to grab the cypher keys."); + pconnect = ::connect; + } + + if (!psend) + { + log->w(tag, "wsockhooks: failed to get the real address of send(). " + "the send hook might not work."); + psend = ::send; + } + + if (!precv) + { + log->w(tag, "wsockhooks: failed to get the real address of recv(). " + "the recv hook might not work."); + precv = ::recv; + } + + if (!detours::hook(true, reinterpret_cast(&pconnect), connect_hook)) + { + log->e(tag, "wsockhooks: could not hook connect(). logging and decryption will not work."); + return; + } + + bool sendhooked = detours::hook(true, reinterpret_cast(&psend), send_hook); + bool recvhooked = detours::hook(true, reinterpret_cast(&precv), recv_hook); + + if (!sendhooked) + log->w(tag, "wsockhooks: could not hook send(). send logging will not work."); + + if (!recvhooked) + log->w(tag, "wsockhooks: could not hook recv(). recv logging will not work."); + + if (!sendhooked && !recvhooked) + { + log->e(tag, "wsockhooks: could not hook send() and recv(). logging will not work."); + return; + } + + hooked = true; + } + + wsockhooks::~wsockhooks() + { + // empty + } + + bool wsockhooks::ishooked() + { + return hooked; + } + + // hooks + int WINAPI wsockhooks::connect_hook(_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen) + { + return get()->connect(s, name, namelen); + } + + int WINAPI wsockhooks::send_hook(_In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags) + { + return get()->send(s, buf, len, flags); + } + + int WINAPI wsockhooks::recv_hook(_In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags) + { + return get()->recv(s, buf, len, flags); + } + + int wsockhooks::connect(_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen) + { + sockaddr_in sin; // we need to copy the sockaddr struct if we want to edit the ip because name is const + memcpy_s(&sin, sizeof(sockaddr), name, sizeof(sockaddr_in)); + + // store the real port + u_short port = ntohs(sin.sin_port); + + if (port == 8484 || port == 8585) + { + mutex::scoped_lock lock(sendmut); + mutex::scoped_lock lock2(recvmut); + + targetsocket = s; + transition = true; + + wxString serv = + port == 8484 ? "LoginServer" : + port == 8585 ? "ChannelServer" : wxString::Format("Unknown (%hu)", port); + + recvcrypt.reset(); + sendcrypt.reset(); + + wxLogStatus(wxString::Format("Server transition to %s in progress...", serv)); + } + + // convert ip to string by casting sockaddr struct to sockaddr_in and using inet_ntoa + std::string ip = inet_ntoa(sin.sin_addr); + log->i(tag, strfmt() << "connect@0x" << _ReturnAddress() << ": socket=" << s << + ", name=" << ip << ":" << port << ", namelen=" << namelen); + + return pconnect(s, name, namelen); // forward clean call + } + + int wsockhooks::send(_In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags) + { + mutex::scoped_lock lock(sendmut); + + if (s == targetsocket) + { + bool dec = false; + const byte *pcbbuf = reinterpret_cast(buf); + byte *pbbuf = const_cast(pcbbuf); + // TODO: fix const issues in the packet class -_- + + boost::shared_ptr p; + + if (!sendcrypt.get() || !sendcrypt->check(pbbuf)) + p.reset(new maple::packet(pcbbuf, len)); + else + { + p.reset(new maple::packet(pcbbuf + header_size, maple::crypt::length(pbbuf))); + sendcrypt->decrypt(p->raw(), p->size()); + sendcrypt->nextiv(); + + word *pheader = reinterpret_cast(p->raw()); + + if (safeheaderlist::getblockedsend()->contains(*pheader)) + { + // this is probabilly not gonna work. + // TODO: keep track of the client and the server's cyphers and skip the packet completely + + // block packet + *pheader = BLOCKED_HEADER; + + // re-encrypt modified packet and send it + size_t cbenc = p->size() + header_size; + boost::scoped_array bbuf(new byte[cbenc]); + sendcrypt->makeheader(bbuf.get(), p->size()); + std::copy(p->begin(), p->end(), bbuf.get() + header_size); + sendcrypt->encrypt(bbuf.get() + header_size, p->size()); + + return psend(s, reinterpret_cast(bbuf.get()), cbenc, 0); + } + + dec = true; + } + + mainform::get()->queuepacket(p, mainform::wxID_PACKET_SEND, dec, NULL); + + /* + log->i(tag, strfmt() << "send@0x" << _ReturnAddress() << + " to " << s << ", " << (dec ? "decrypted" : "encrypted") << ": " << p->tostring()); + */ + } + + return psend(s, buf, len, flags); + } + + int wsockhooks::recv(_In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags) + { + mutex::scoped_lock lock(recvmut); + + const byte *pcbbuf = reinterpret_cast(buf); + int res = precv(s, buf, len, flags); + + // recieved the hello packet + if (transition) + { + if (res <= 2) // hello packet header + return res; + + // hello packet + // 0E 00 vv vv 01 00 31 ss ss ss ss rr rr rr rr tt + // vv = maple version + // ss = send key + // rr = recv key + // tt = server type + try + { + mutex::scoped_lock lock2(sendmut); + + word maple_version = 0; + std::string unknown; + byte ivsend[4] = {0}; + byte ivrecv[4] = {0}; + byte server_type = 0; + + maple::packet p(pcbbuf, res); + maple::packet::iterator it = p.begin(); + p.read(&maple_version, it); + p.read_string(unknown, it); + p.read(reinterpret_cast(ivsend), it); + p.read(reinterpret_cast(ivrecv), it); + p.read(&server_type, it); + + log->i(tag, + strfmt() <<"encryption initialized - maple version: " << maple_version << + ", unknown string: " << unknown + << std::hex << std::setfill('0') << std::uppercase + << ", ivsend: 0x" << std::setw(8) << *reinterpret_cast(ivsend) << + ", ivrecv: 0x" << std::setw(8) << *reinterpret_cast(ivrecv) << + ", server type: " << std::dec << static_cast(server_type) + ); + + wxLogStatus(wxString::Format( + "Encryption initialized - MapleStory v%hu {%s, 0x%.8X, 0x%.8X, %hu}", + maple_version, wxString(unknown.c_str()), *reinterpret_cast(ivsend), + *reinterpret_cast(ivrecv), server_type)); + + sendcrypt.reset(new maple::crypt(maple_version, ivsend)); + recvcrypt.reset(new maple::crypt(0xFFFF - maple_version, ivrecv)); + } + catch (const maple::readexception &) + { + log->wtf(tag, "unexpected end of initialization packet"); + } + + transition = false; + } + + else if (res > 0 && s == targetsocket) + { + bool dec = false; + byte *pbbuf = reinterpret_cast(buf); + boost::shared_ptr p; + + if (!recvcrypt.get() || !recvcrypt->check(pbbuf)) + p.reset(new maple::packet(pcbbuf, res)); + else + { + p.reset(new maple::packet(pbbuf + header_size, maple::crypt::length(pbbuf))); + recvcrypt->decrypt(p->raw(), p->size()); + recvcrypt->nextiv(); + + word *pheader = reinterpret_cast(p->raw()); + + if (safeheaderlist::getblockedrecv()->contains(*pheader)) + { + // block packet + *pheader = BLOCKED_HEADER; + + // re-encrypt modified packet and send it + // TODO: skip the packet completely and compensate for the messed up recv cypher + size_t cbenc = p->size() + header_size; + recvcrypt->makeheader(reinterpret_cast(buf), p->size()); + std::copy(p->begin(), p->end(), buf + header_size); + recvcrypt->encrypt(reinterpret_cast(buf + header_size), p->size()); + + return res; + } + + dec = true; + } + + mainform::get()->queuepacket(p, mainform::wxID_PACKET_RECV, dec, NULL); + + /* + log->i(tag, strfmt() << "recv@0x" << _ReturnAddress() << + " from " << s << " [" << res << "/" << len << " bytes], " << + (dec ? "decrypted" : "encrypted") << ": " << p->tostring()); + */ + } + + return res; + } +} diff --git a/wxPloiter/wsockhooks.hpp b/wxPloiter/wsockhooks.hpp new file mode 100644 index 0000000..0500557 --- /dev/null +++ b/wxPloiter/wsockhooks.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "safeheaderlist.hpp" +#include "crypt.hpp" +#include "logging.hpp" + +#include +#include +#include + +namespace wxPloiter +{ + // hooks winsock connect/recv/send, grabs cypher keys + // and decrypts packets + class wsockhooks + { + public: + static boost::shared_ptr get(); + virtual ~wsockhooks(); + + // returns true if the hooks are correctly set + bool ishooked(); + + protected: + static const std::string tag; + static const size_t header_size; // size of encrypted headers + + static boost::shared_ptr inst; + + // these should prevent concurrent packets from messing up the keys + typedef boost::mutex mutex; + mutex sendmut; + mutex recvmut; + + // trampoline typedefs + typedef int (WINAPI *pfnconnect) + (_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen); + + typedef int (WINAPI *pfnsend) + (_In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags); + + typedef int (WINAPI *pfnrecv) + (_In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags); + + // trampolines + pfnconnect pconnect; + pfnsend psend; + pfnrecv precv; + + // hooks + static int WINAPI connect_hook(_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen); + static int WINAPI send_hook(_In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags); + static int WINAPI recv_hook(_In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags); + + boost::shared_ptr log; + bool transition; // true when moving from loginserver to channel server and stuff like that + SOCKET targetsocket; // currently active socket + bool hooked; + boost::shared_ptr sendcrypt; // send decoder + boost::shared_ptr recvcrypt; // recv decoder + + wsockhooks(); + int connect(_In_ SOCKET s, _In_ const struct sockaddr *name, _In_ int namelen); + int send(_In_ SOCKET s, _In_ const char *buf, _In_ int len, _In_ int flags); + int recv(_In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags); + }; +} diff --git a/wxPloiter/wxPloiter.vcxproj b/wxPloiter/wxPloiter.vcxproj new file mode 100644 index 0000000..1e970c9 --- /dev/null +++ b/wxPloiter/wxPloiter.vcxproj @@ -0,0 +1,131 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + + {2C8BAC1D-B028-4557-9FBB-AE5935FBB79A} + Win32Proj + wxPloiter + wxPloiterPublic + + + + DynamicLibrary + true + v110 + Unicode + + + DynamicLibrary + false + v110 + true + Unicode + + + + + + + + + + + + + true + + + false + + + + + + Level3 + Disabled + BOTAN_DLL=;WIN32;_WINDOWS;WINVER=0x0400;__WXMSW__;wxUSE_GUI=1;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;_DEBUG;_WINDOWS;_USRDLL;WXPLOITER_EXPORTS;%(PreprocessorDefinitions) + true + $(WXWIN)\include;$(WXWIN)\include\msvc;$(BOOST_include);$(BOTAN)\debugx86_static\include + + + Windows + true + $(WXWIN)\lib\vc_lib;$(BOOST_lib)\x86;$(BOTAN)\debugx86_static\lib + wxmsw29ud_core.lib;botan.lib;%(AdditionalDependencies) + + + $(WXWIN)\include + + + + + Level3 + + + MaxSpeed + true + true + BOTAN_DLL=;WIN32;_WINDOWS;WINVER=0x0400;__WXMSW__;wxUSE_GUI=1;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;NDEBUG;_WINDOWS;_USRDLL;WXPLOITER_EXPORTS;%(PreprocessorDefinitions) + true + $(WXWIN)\include;$(WXWIN)\include\msvc;$(BOOST_include);$(BOTAN)\releasex86_static\include + + + Windows + true + true + true + $(WXWIN)\lib\vc_lib;$(BOOST_lib)\x86;$(BOTAN)\releasex86_static\lib + wxmsw29u_core.lib;botan.lib;%(AdditionalDependencies) + + + $(WXWIN)\include + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/wxPloiter/wxPloiter.vcxproj.filters b/wxPloiter/wxPloiter.vcxproj.filters new file mode 100644 index 0000000..908de8f --- /dev/null +++ b/wxPloiter/wxPloiter.vcxproj.filters @@ -0,0 +1,110 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/wxPloiter/wxPloiter.vcxproj.user b/wxPloiter/wxPloiter.vcxproj.user new file mode 100644 index 0000000..a6d37dd --- /dev/null +++ b/wxPloiter/wxPloiter.vcxproj.user @@ -0,0 +1,6 @@ + + + + Auto + + \ No newline at end of file diff --git a/wxPloiter/zapp.ico b/wxPloiter/zapp.ico new file mode 100644 index 0000000000000000000000000000000000000000..34c3a620171562885af8c67f3814847f62166f23 GIT binary patch literal 23558 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