📜 ⬆️ ⬇️

Dump memory and write maphack

image


One evening of school summer, I had a need for a maphack for DayZ Mod (Arma 2 OA). Looking for information on the topic, I realized that contacting the Battleye anti-cheat is not worth it, because there is neither the knowledge nor the experience to bypass the defensive kernel driver, which politely placed a bunch of hooks to access the game process.


DayZ is one of the few games where the location of important objects for the gameplay does not change often and persists after a restart (the bases do not move, most of the equipment also remains in place for a long time). This fact opens up the possibility of an attack through a RAM dump.


All links at the end.


Chapter 1. Dampy


To get a snapshot of memory in runtime without bypassing anti-cheat will hardly work. Therefore, we are looking for other tactics. The first that we find it habrasatya from which it becomes clear where to dig.


Attempt 1


I did not manage to get the memory image through the hot crack described in the article, booting from Ubuntu CyberPack (IRF) and getting the image via fmem, for unexplained reasons, fmem hung.
A little googling we find an alternative tool, with the same LiME ~ Linux Memory Extractor functionality. Now it had to be assembled and run on livecd.
The choice of the distribution fell on TinyCore (TinyCorePure64, if you need more than 3 GB of sdampit). Having downloaded from it we swing and install packages.


tce-load -iw linux-kernel-sources-env.tcz cliorx linux-kernel-sources-env.sh 

Next, we mount the flash drive with the sorts, where we will also dump the dump, collect it via make and get the image


 insmod ./lime.ko "path=/path/mem-image.lime format=lime" 

Now this file needs to be fed to someone in order to get the memory of the process we need at the output. For this we had to approach the Volatility Framework with the plugin memdump.


 vol.py -f F:\mem-image.lime format=lime pslist vol.py -f F:\mem-image.lime format=lime memdump –dump-dir ./output –p 868 

Or Rekall Framework, which is its fork and is actively developing, in contrast to the volatility itself


 rekal -f F:\mem-image.lime pslist rekal -f F:\mem-image.lime memdump dump_dir="./output", pids=868 

However, what I would not do, he did not want to wind up, and I continued to dig.


When working with Rekall on windows 10, when you first search for something on the dump, you may receive a message like this:


 WARNING:rekall.1:Profile nt/GUID/F6F4895554894B24B4DF942361F0730D1 fetched and built. Please consider reporting this profile to the Rekall team so we may add it to the public profile repository. 

And next time, it may fall with such an error:


 CRITICAL:rekall.1:A DTB value was found but failed to verify. See logging messages for more information. 

If this happens, at startup you need to specify the --profile parameter with the profile value that you displayed for the first time.


Attempt 2


To get the most complete snapshot of memory, you can use the hiberfil.sys file, in which all the memory is saved when the Windows go into hibernation.


It's still easier here, go to hibernation mode, boot from any livecd (in my case, all the same TinyCore), mount the system disk (Read-Only) and the USB flash drive, copy the desired file.


For TinyCore, do not forget to install the package for ntfs support.


 tce-load -iw ntfs-3g 

Through fdisk -l, we find the logical partitions we need and mount them


 sudo ntfs-3g -o ro /dev/sda2 /tmp/a1 //   Read-Only sudo ntfs-3g /dev/sdc1 /tmp/a2 

We copy


 cp /tmp/a1/hiberfil.sys /tmp/a2 

Then this file could be fed volatility (supports hibernation file with win7 or earlier).


 vol.py imagecopy -f hiberfil.sys -O win7.img 

Since I have win10, this option did not suit me.
I tried to give the file to the Hibr2bin program, which used to be the Sandman Framework.


 HIBR2BIN /PLATFORM X64 /MAJOR 10 /MINOR 0 /INPUT hiberfil.sys /OUTPUT uncompressed.bin 

But she gave an incomprehensible output, which frameworks for analysis refused to work with.
Hibernation Recon came to the rescue with the Free version, which gave an exhaust readable for the frameworks without any problems.
At the output from memdump we get a file with the process memory itself and a file with the ratio of virtual addresses to addresses in the file.


 File Address Length Virtual Addr -------------- -------------- -------------- 0x000000000000 0x000000001000 0x000000010000 0x000000001000 0x000000001000 0x000000020000 0x000000002000 0x000000001000 0x000000021000 0x000000003000 0x000000001000 0x00000002f000 0x000000004000 0x000000001000 0x000000040000 0x000000005000 0x000000001000 0x000000050000 0x000000006000 0x000000001000 0x000000051000 

Chapter 2. We write maphak.


For the GUI, I chose Qt.


To begin with, we write a convenient wrapper for accessing virtual memory in a file through a table.


 class MemoryAPI { public: MemoryAPI(){} MemoryAPI(QString pathDump, QString pathIDX); //      quint32 readPtr (const quint32 offset); qint32 readInt (const quint32 offset); float readFloat (const quint32 offset); QString readStringAscii(const quint32 offset, const quint32 size); QString readArmaString(quint32 offset); //  void loadIDX (QString path); void loadDump (QString path); private: //       QVector <MemoryRange> memoryRelations; quint32 convertVirtToPhys(const quint32 virt) const; QByteArray readVirtMem(const quint32 baseAddr, const quint32 size); QFile dumpFile; }; 

We present each line of the idx file as a simple structure.


 class MemoryRange { private: quint32 baseVirtualAddress; quint32 basePhysicalAddress; quint32 size; }; 

All functions of reading data by virtual addresses are reduced to calling this function with the necessary parameters.


 QByteArray MemoryAPI::readVirtMem(const quint32 baseAddr, const quint32 size) { QByteArray result; //  quint32 addr = convertVirtToPhys(baseAddr); dumpFile.seek(addr); result = dumpFile.read(size); return result; } 

The address conversion is performed by simply searching for the desired offset in the array (a binary search could be used, but not).


 quint32 MemoryAPI::convertVirtToPhys(const quint32 virt) const { for(auto it = memoryRelations.begin(); it != memoryRelations.end(); ++it) { if((*it).inRange(virt)) { const quint32& phBase = (*it).getPhysicalAddress(), vrBase = (*it).getVirtualAddress(); //   if(phBase>vrBase) return virt + (phBase - vrBase); else return virt - (vrBase - phBase); } } //       throw 1; } 

Now we will make a structure in which we will store the data of each object in the game.


 class EntityData { public: friend class WorldState; //      enum class type {airplane, car, motorcycle, ship, helicopter, parachute, tank, tent, stash, fence, ammoBox, campFire, crashSite, animals, players, zombies, stuff, hedgehog, invalid}; type entityType; EntityData(); EntityData(QString n, QPointF c, type t = type::stuff); QString shortDescription()const; QString fullDescription()const; QPointF getCoords() const {return coords;} private: //  QString name; //  QPointF coords; //    ( ) QMap<QString, QString> additionalFields; }; 

Next, we write a class in which we will store the state of the world (all objects).


 class WorldState { public: //    ,     WorldState(const QString& dumpFile, const QString& idxFile); //  xml-    WorldState(const QString& stateFile); // xml-,      void saveState(const QString& stateFile); // ,          ( ) QMap <EntityData::type, EntityRange> entityRanges; QString worldName; private: //    QVector <EntityData> entityArray; //     QVector<quint32> masterOffsets; QVector<quint32> tableOffsets; quint32 objTableAddress; void handleEntity (quint32 entityAddress, MemoryAPI& mem); // void initRanges(); void initOffsets(); QDomElement makeElement(QDomDocument& domDoc, const QString& name, const QString& strData = QString()); }; 

Here all the work with the memory dump and loading of information about all objects takes place.


 WorldState::WorldState(const QString& dumpFile, const QString& idxFile) { //  initOffsets(); //      QProgressDialog progress; progress.setCancelButton(nullptr); progress.setLabelText("Loading dump..."); progress.setModal(true); progress.setMinimum(0); progress.setMaximum(masterOffsets.length()+2); progress.show(); MemoryAPI mem(dumpFile,idxFile); progress.setValue(1); for(auto mO = masterOffsets.begin(); mO != masterOffsets.end(); ++mO) { quint32 entityTableBasePtr = mem.readPtr(objTableAddress) + (*mO); for(auto tO = tableOffsets.begin(); tO != tableOffsets.end(); ++tO) { qint32 size = mem.readInt(entityTableBasePtr + 0x4 +(*tO)); for(qint32 i = 0; i!=size; ++i) { quint32 fPtr = mem.readPtr(entityTableBasePtr + (*tO)); quint32 entityAddress = mem.readPtr(fPtr + 4 * i); //  handleEntity(entityAddress, mem); //   ,       QCoreApplication::processEvents(); } } progress.setValue(progress.value()+1); } initRanges(); worldName = "chernarus"; progress.setValue(progress.value()+1); } 

Initialize Offsets


 void WorldState::initOffsets() { masterOffsets.append(0x880); masterOffsets.append(0xb24); masterOffsets.append(0xdc8); tableOffsets.append(0x8); tableOffsets.append(0xb0); tableOffsets.append(0x158); tableOffsets.append(0x200); objTableAddress = 0xDAD8C0; } 

We will stop here in more detail. All information about the game world is stored in approximately such a structure (Based on a dump found on the forum).


World structure
 class World { public: char _0x0000[8]; InGameUI* inGameUI; //0x0008 char _0x000C[1520]; EntityTablePointer* entityTablePointer; //0x05FC VariableTableInfo* variableTableInfo; //0x0600 char _0x0604[428]; __int32 gameMode; //0x07B0 char _0x07B4[4]; float speedMultiplier; //0x07B8 char _0x07BC[196]; EntitiesDistributed table1; //0x0880 char _0x0B00[36]; EntitiesDistributed table2; //0x0B24 char _0x0DA4[36]; EntitiesDistributed table3; //0x0DC8 char _0x1048[849]; BYTE artilleryEnabled; //0x1399 BYTE enableItemsDropping; //0x139A char _0x139B[13]; UnitInfo* cameraOn; //0x13A8 char _0x13AC[4]; UnitInfo* cplayerOn; //0x13B0 UnitInfo* realPlayer; //0x13B4 char _0x13B8[48]; float actualOvercast; //0x13E8 float wantedOvercast; //0x13EC __int32 nextWeatherChange; //0x13F0 float currentFogLevel; //0x13F4 float fogTarget; //0x13F8 char _0x13FC[32]; __int32 weatherTime; //0x141C char _0x1420[8]; BYTE playerManual; //0x1428 BYTE playerSuspended; //0x1429 char _0x142A[30]; __int32 N0D09AD19; //0x1448 char _0x144C[92]; ArmaString* currentCampaign; //0x14A8 char _0x14AC[4]; __int32 N0D09B79F; //0x14B0 char _0x14B4[52]; float viewDistanceHard; //0x14E8 float viewDistanceMin; //0x14EC float grass; //0x14F0 char _0x14F4[36]; __int32 initTableCount; //0x1518 __int32 initTableMaxCount; //0x151C char _0x1520[4]; };//Size=0x1524 

You can get access to this structure by a pointer, which lies in a static offset for each version of the game (the offset can be zoomed or you can find it yourself through reverse, but this is a completely different story). This offset is stored in the variable objTableAddress. In masterOffsets, we store offsets by 3 tables, relative to this structure.


Table with tables
 class EntitiesDistributed { public: char _0x0000[8]; Entity* table1; //0x0008 __int32 table1Size; //0x000C char _0x0010[160]; Entity* table2; //0x00B0 __int32 table2Size; //0x00B4 char _0x00B8[160]; Entity* table3; //0x0158 __int32 table3Size; //0x015C char _0x0160[160]; Entity* table4; //0x0200 __int32 table4Size; //0x0204 char _0x0208[120]; };//Size=0x0280 

In turn, each table stores 4 more tables with a length (we store offsets for these tables in tableOffsets).
Now we can iterate over all objects in the game. Let us analyze the function that processes each entity.


 void WorldState::handleEntity(quint32 entityAddress, MemoryAPI &mem) { QString objType; QString objName; float coordX; float coordY; try{ quint32 obj1 = entityAddress; quint32 pCfgVehicle = mem.readPtr(obj1 + 0x3C); quint32 obj3 = mem.readPtr(pCfgVehicle + 0x30); quint32 pObjType = mem.readPtr(pCfgVehicle + 0x6C); objType = mem.readArmaString(pObjType); objName = mem.readStringAscii(obj3 + 0x8, 25); quint32 pEntityVisualState = mem.readPtr(obj1 + 0x18); coordX = mem.readFloat(pEntityVisualState + 0x28); coordY = mem.readFloat(pEntityVisualState + 0x30); }catch(int a) { qDebug() << "    ."; return; } //   EntityData ed(objName, QPointF(coordX, coordY)); //    if(objType == "car") ed.entityType = EntityData::type::car; else if(objType == "motorcycle") ed.entityType = EntityData::type::motorcycle; else if(objType == "airplane") ed.entityType = EntityData::type::airplane; else if(objType == "helicopter") ed.entityType = EntityData::type::helicopter; else if(objType == "ship") ed.entityType = EntityData::type::ship; else if(objType == "tank") ed.entityType = EntityData::type::tank; else if(objType == "parachute") ed.entityType = EntityData::type::parachute; else if(objName.indexOf("TentStorage")!=-1) ed.entityType = EntityData::type::tent; else if(objName.indexOf("Stash")!=-1) ed.entityType = EntityData::type::stash; else if(objName.indexOf("WoodenGate")!=-1 || objName.indexOf("WoodenFence")!=-1) ed.entityType = EntityData::type::fence; else if(objName.indexOf("DZ_MedBox")!=-1 || objName.indexOf("DZ_AmmoBox")!=-1) ed.entityType = EntityData::type::ammoBox; else if(objName.indexOf("Hedgehog_DZ")!=-1) ed.entityType = EntityData::type::hedgehog; else if(objName.indexOf("Land_Camp_Fire_DZ")!= -1) ed.entityType = EntityData::type::campFire; else if(objName.indexOf("CrashSite")!= -1) ed.entityType = EntityData::type::crashSite; else if(objName.indexOf("WildBoar")== 0 || objName.indexOf("Rabbit")== 0 || objName.indexOf("Cow")== 0 || objName.indexOf("Sheep")== 0 || objName.indexOf("Goat")== 0 || objName.indexOf("Hen")== 0) ed.entityType = EntityData::type::animals; else if(objName.indexOf("Survivor2_DZ")!= -1 || objName.indexOf("Sniper1_DZ")!=-1 || objName.indexOf("Camo1_DZ")!=-1 || objName.indexOf("Survivor3_DZ")!=-1 || objName.indexOf("Bandit1_DZ")!= -1 || objName.indexOf("Soldier1_DZ")!= -1) ed.entityType = EntityData::type::players; else ed.entityType = EntityData::type::stuff; entityArray.append(ed); } 

Each entity represents something like this.


Entity structure
 class Entity { public: char _0x0000[24]; EntityVisualState* entityVisualState; //0x0018 char _0x001C[32]; CfgVehicle* cfgVehicle; //0x003C char _0x0040[476]; EntityInventory* entityInventory; //0x021C };//Size=0x0220 

Here we are interested in all three pointers.



From CfgVehicle we read the name and type.


 ArmaString* entityName; //0x0030 ArmaString* objectType; //0x006C 

EntityVisualState
 class EntityVisualState { public: char _0x0000[4]; D3DXVECTOR3 dimension; //0x0004 D3DXVECTOR3 rotation1; //0x0010 D3DXVECTOR3 direction; //0x001C D3DXVECTOR3 coordinates; //0x0028 char _0x0034[20]; D3DXVECTOR3 velocity; //0x0048 float angularVelocity; //0x0054 float zVelocity2; //0x0058 float Speed; //0x005C D3DXVECTOR3 acceleration; //0x0060 char _0x006C[16]; D3DXVECTOR3 direction2; //0x007C D3DXVECTOR3 rotation2; //0x0088 D3DXVECTOR3 direction3; //0x0094 char _0x00A0[12]; float fuelLevel; //0x00AC char _0x00B0[92]; D3DXVECTOR3 headCoordinates; //0x010C D3DXVECTOR3 torsoCoordinates; //0x0118 char _0x0124[244]; float N047F1D6C; //0x0218 char _0x021C[200]; };//Size=0x02E4 

From EntityVisualState we read the vector of coordinates, which is a structure of three variables.


 D3DXVECTOR3 coordinates; 

 struct D3DXVECTOR3 { FLOAT x; FLOAT y; FLOAT z; }; 

Here we need only x and y (in fact, z), so we read them like this:


 coordX = mem.readFloat(pEntityVisualState + 0x28); coordY = mem.readFloat(pEntityVisualState + 0x30); 

By the way, in the additionalFields map, which is in EntityData, at this stage you can record any additional information. For example, the contents of the inventory or the speed of movement.


Now we have received and classified information about all the entities in the game world, now we need to somehow display it, for this I used QPainter.


Create a widget class for drawing.


 class InteractiveMap : public QWidget { Q_OBJECT public: InteractiveMap(QWidget* pwgt = nullptr); virtual ~InteractiveMap(); protected: virtual void paintEvent(QPaintEvent* pe); private: // (  ) const float minScale = 1.0f; const float maxScale = 8.0f; const float scaleStep= 2.0f; void updateScale(const qreal value, const QPointF& dpos); void updateTranslate(const QPointF& value); bool getFilterValue(EntityData::type t); bool getFilterValue(QString t); void mousePressEvent (QMouseEvent* pe); void mouseMoveEvent (QMouseEvent* pe); void wheelEvent (QWheelEvent *pe); void findCloseObjects(QPointF coords); QVector<CloseObjects>* input; QPainter* painter; QPixmap* image; WorldState* worldState; qreal scale; QPointF translate; QPoint startMove; //  QPixmap cache; QMutex renderMutex; //  ,    QFutureWatcher<QString> closeObjWatcher; QFuture<QString> closeObjFuture; public slots: //  void loadState(QString stateFile); void loadDump(QString dumpFile, QString idxFile); void closeState(); void saveState(QString stateFile); void updateCache(); void sendCloseObjects(); signals: void showCloseObjects(QString str); void saveStateChanged(bool state); }; 

I draw tags with the technique over the picture with the map. Labels on the map for the current scale I cache in QPixmap (it is expensive to re-draw a few hundred or thousands of objects with each camera shift).


 void InteractiveMap::paintEvent(QPaintEvent *pe) { renderMutex.lock(); painter->begin(this); ////////////////////////////////////////////////// QTransform mat; painter->setTransform(mat); painter->scale(scale, scale); painter->translate(translate); painter->drawPixmap(0,0, *image); if(cache.isNull()) { //  DPR,        cache = QPixmap(image->size()*4); cache.setDevicePixelRatio(4); cache.fill(Qt::transparent); QPainter cachePaint(&cache); //     for(QMap<EntityData::type, EntityRange>::const_iterator it = worldState->entityRanges.cbegin(); it!=worldState->entityRanges.cend();++it) { //      if(getFilterValue(it.key())) { for(QVector<EntityData>::const_iterator i = it.value().start; i!= it.value().end; ++i) { float x = i->getCoords().x(); float y = i->getCoords().y(); //     x = (((x) / (15360.0f / 975.0f))); y = (((15360.0f - y) / (15360.0f / 970.0f)) - 4.0f); //  QFont font("Arial"); QPen pen; pen.setWidthF(4.0f/scale); pen.setStyle(Qt::SolidLine); font.setPointSizeF(qMax(float(8.0f*1.0f/scale),2.0f)); cachePaint.setFont(font); cachePaint.setPen(pen); cachePaint.drawPoint(x,y); //  ,   if(getFilterValue(QString("name"))) cachePaint.drawText(x,y,i->shortDescription()); } } } } painter->drawPixmap(0,0,cache); ////////////////////////////////////////////////// painter->end(); renderMutex.unlock(); } 

To select the types of entities that need to be displayed and other settings, I use QCheckBoxes on the sidebar (you can look at their implementation on the github). To associate the draw with the settings, I first used bare QSettings, but it turned out that it does not cache the settings in memory, but works directly with the registry, so I had to write a wrapper singleton with a cache, which also sends a redraw signal when updating parameters.


 class SettingsManager : public QObject { Q_OBJECT public: SettingsManager(); ~SettingsManager(); static SettingsManager& instance(); QVariant value(const QString &key, const QVariant &defaultValue = QVariant()); void setValue(const QString &key, const QVariant &value); SettingsManager(SettingsManager const&) = delete; SettingsManager& operator= (SettingsManager const&) = delete; private: QMap<QString, QVariant> data; QSettings settings; signals: void updateMap(); }; 

For easy viewing of the map, I implemented the scaling on the cursor (on the mouse wheel) and the shift (with the latch pressed). Another important feature is the viewing of the full characteristics and game coordinates of the entities (when you click SCM to the area of ​​the desired objects).


 //   void InteractiveMap::updateScale(qreal value, const QPointF& dpos) { qreal newScale = scale * value; if(newScale >= minScale && newScale <= maxScale) { scale = newScale; //       translate += dpos/scale; updateCache(); } } //   void InteractiveMap::updateTranslate(const QPointF& value) { QPointF newV = translate + (value * 1/scale); translate = newV; update(); } //    void InteractiveMap::mousePressEvent(QMouseEvent *pe) { //    if(pe->buttons() & Qt::LeftButton) startMove = pe->pos(); //       else if(pe->buttons() & Qt::MidButton) { if(worldState) { //   ,    QPointF pos = pe->pos()/scale - translate; if(pos.x() >= 0.0f && pos.x() <= image->width() && pos.y() >= 0.0f && pos.y() <= image->height()) { //     pos.rx() = pos.x() * (15360.0f / 975.0f); pos.ry() = -((15360.0f/970.0f)*(pos.y()+4.0f)-15360.0f); //   findCloseObjects(pos); } } } } void InteractiveMap::mouseMoveEvent(QMouseEvent *pe) { //    if(pe->buttons() & Qt::LeftButton) { updateTranslate(pe->pos() - startMove); startMove = pe->pos(); } } void InteractiveMap::wheelEvent(QWheelEvent *pe) { //  float dScale = (pe->angleDelta().y() < 0) ? 1/scaleStep : scaleStep; QPointF nPos = pe->pos() * (dScale); QPointF dPos = pe->pos() - nPos; updateScale(dScale,dPos); } 

Characterization is implemented using the QtConcurrent framework, through the MapReduce model.


 //Reduce  void addToAnswer(QString& result, const QString& interm) { if(!interm.isEmpty()) result += interm; } void InteractiveMap::findCloseObjects(QPointF coords) { if(!closeObjWatcher.isRunning()) { //   input = new QVector<CloseObjects>; for(QMap<EntityData::type, EntityRange>::iterator it = worldState->entityRanges.begin(); it!=worldState->entityRanges.end();++it) { if(getFilterValue(it.key())) { //   CloseObjects obj(&it.value(), coords); input->append(obj); } } closeObjFuture = QtConcurrent::mappedReduced(*input, &CloseObjects::findCloseObjects, addToAnswer); //     connect(&closeObjWatcher, &QFutureWatcher<QString>::finished, this, &InteractiveMap::sendCloseObjects); //  closeObjWatcher.setFuture(closeObjFuture); } } void InteractiveMap::sendCloseObjects() { //    emit showCloseObjects(closeObjWatcher.result()); //     delete input; input = nullptr; } 

The input class consists of a pointer to the category of entities and points to search.


 class CloseObjects { public: CloseObjects() {} CloseObjects(EntityRange *r, QPointF p): range(r), coords(p) {} QString findCloseObjects() const; private: EntityRange* range; QPointF coords; }; 

In the Map function, we go through all the objects in the category, if the entity is in a constant radius from the cursor position, then we return a full description of the object (name + additional fields) and game coordinates.


 QString CloseObjects::findCloseObjects() const { QString result; QTextStream stream(&result); //     2    stream.setRealNumberNotation(QTextStream::FixedNotation); stream.setRealNumberPrecision(2); for(QVector<EntityData>::const_iterator it = range->start; it != range->end; ++it) { float len = qSqrt(qPow((it->getCoords().x() - coords.x()),2) + qPow((it->getCoords().y() - coords.y()),2)); if(len <= 350) { stream << it->fullDescription() << "\n" << QVariant(it->getCoords().x()/100).toFloat() << " " << QVariant((15360 - it->getCoords().y())/100).toFloat() << "\n"; } } return result; } 

On this I finish my story. Who is not difficult, look at the code, point out possible jambs.
Interested parties can add additional characteristics of the objects and throw a pull-request.


References:



')

Source: https://habr.com/ru/post/338734/


All Articles