めいくりぷとのブログ

技術的なことやゲームのことやら・・・

AVA 更新

gyazo.com

何やら変なクラスが追加されましたの...
まあ avaGame/avaRules/Engine/AVANET 辺りしか触れないからどうでもいいんですけど...

それと今まではsplash画像はファイルチェックの対象外でフォルダごと消すだけで良かったけど、アプデ入ってから毎回全ファイルチェックされる糞仕様に変わったのでってことで。
ファイルチェックはPmangDownloaderに対してReadFile辺りフックしときゃなんとかなる。(知らないけど
無駄に3つくらい起動するから、proxy dllとかで全部にフックかければok

スプラッシュウィンドウ非表示

void Detour_CreateWindowExW()
{
	static decltype(&CreateWindowExW) _CreateWindowExW = CreateWindowExW;

	decltype(&CreateWindowExW) CreateWindowExW__Hook = [](
		_In_     DWORD     dwExStyle,
		_In_opt_ LPCWSTR   lpClassName,
		_In_opt_ LPCWSTR   lpWindowName,
		_In_     DWORD     dwStyle,
		_In_     int       x,
		_In_     int       y,
		_In_     int       nWidth,
		_In_     int       nHeight,
		_In_opt_ HWND      hWndParent,
		_In_opt_ HMENU     hMenu,
		_In_opt_ HINSTANCE hInstance,
		_In_opt_ LPVOID    lpParam) -> HWND
	{
		HMODULE hModule;

		if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast<TCHAR*>(_ReturnAddress()), &hModule))
		{
			if (hModule == GetModuleHandle(L"ava.exe"))
			{
				if (wcsstr(lpClassName, L"SPLASH"))
					return NULL;
			}
		}

		return _CreateWindowExW(dwExStyle, lpClassName, lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, hInstance, lpParam);
	};

	DetourFunction(TRUE, reinterpret_cast<LPVOID*>(&_CreateWindowExW), CreateWindowExW__Hook);
}

誤クリックの地味にウザいアレ

VOID Detour_ShellExecuteExW()
{
	static decltype(&ShellExecuteExW) _ShellExecuteExW = ShellExecuteExW;

	decltype(&ShellExecuteExW) ShellExecuteExW__Hook = [](
		_Inout_ SHELLEXECUTEINFO *pExecInfo) -> BOOL
	{
		if (pExecInfo && wcsstr(pExecInfo->lpFile, L"IEXPLORE.EXE"))
			return FALSE;

		return _ShellExecuteExW(pExecInfo);
	};

	DetourFunction(TRUE, reinterpret_cast<LPVOID*>(&_ShellExecuteExW), ShellExecuteExW__Hook);
}

ウィンドウフレーム以外からでもウィンドウを移動出来るようにする。

前回同じような記事を書いたのですが、継承するクラスによっては移動出来ない物もあったみたいですので、今回は nativeEvent関数をオーバーライドして実装してみました。

bool CMainWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
	MSG *msg = reinterpret_cast<MSG*>(message);

	switch (msg->message)
	{
		case WM_LBUTTONDOWN:
		{
			if (!this->m_bClicked)
			{
				this->m_bClicked = true;
				this->m_originPos.setX(GET_X_LPARAM(msg->lParam));
				this->m_originPos.setY(GET_Y_LPARAM(msg->lParam));
			}
			break;
		}
		case WM_LBUTTONUP:
		{
			if (this->m_bClicked)
				this->m_bClicked = false;
			break;
		}
		case WM_MOUSEMOVE:
		{
			if (this->m_bClicked)
			{
				this->move(
					this->pos().x() + GET_X_LPARAM(msg->lParam) - this->m_originPos.x(),
					this->pos().y() + GET_Y_LPARAM(msg->lParam) - this->m_originPos.y());
			}
			break;
		}
		default:
			return QWidget::nativeEvent(eventType, message, result);
	}

	return false;
}

別プロセスのAPIのアドレスを取得する。

https://pastebin.com/QR1TUW7U

// GetProcessModuleInfo
ReadProcessMemoryで対象プロセスのPEBを読み取る -> モジュールの一覧取得 -> モジュールのベースアドレスの取得

// GetProcAddressEx
取得したモジュールのPEヘッダーからエクスポート関数のアドレス、名前、序数を列挙 -> 該当のアドレスを返す


プロセスとモジュールのメモリ共有型のアンチチート作ってみようと思って、モジュールのAPI/関数チェックに書いてみたけど、なんか微妙な感じに...また次触るときに書き直します。


全プロセスから xul.XRE_GetBootstrap (firefox), KERNELBASE.CreateFileW を列挙した画像です。
gyazo.com

QXmlStreamReader/Writerでxmlにウィジェットのステータスを保存する

サンプルコード

CConfigManager.hpp

#pragma once

#include <qwidget.h>
#include <map>

class CConfigManager
{
public:
	CConfigManager();
	~CConfigManager();

public:
	// 
	void WriteXmlElement(__in QXmlStreamWriter &writer);
	void ReadFromXml(__in QXmlStreamReader &reader);

	// for qwidget
	QCheckBox *CreateCheckBox(__in const QString &name, __in QWidget *parent = 0);
	QSpinBox *CreateSpinBox(__in const QString &name, __in QWidget *parent = 0);
	QLineEdit *CreateLineEdit(__in const QString &name, __in QWidget *parent = 0);
	QComboBox *CreateComboBox(__in const QString &name, __in QWidget *parent = 0);
	QPushButton *CreatePushButton(__in const QString &name, __in bool checkable, __in QWidget *parent = 0);
	QRadioButton *CreateRudioButton(__in const QString &name, __in QWidget *parent = 0);
	QSlider *CreateSlider(__in const QString &name, __in QWidget *parent = 0);

	static CConfigManager *GetInstance()
	{
		static CConfigManager instance;
		return &instance;
	}

private:
	void WriteXmlSettingsElement(__in QXmlStreamWriter &writer);
	void ReadXmlSettings(__in QXmlStreamReader &reader);

	void ReadXmlCheckBox(__in QXmlStreamReader &reader);
	void ReadXmlSpinBox(__in QXmlStreamReader &reader);
	void ReadXmlLineEdit(__in QXmlStreamReader &reader);
	void ReadXmlComboBox(__in QXmlStreamReader &reader);
	void ReadXmlPushButton(__in QXmlStreamReader &reader);
	void ReadXmlRadioButton(__in QXmlStreamReader &reader);
	void ReadXmlSlider(__in QXmlStreamReader &reader);

	// for qwidget
	std::map<QString, QCheckBox*> m_mCheckBox;
	std::map<QString, QSpinBox*> m_mSpinBox;
	std::map<QString, QLineEdit*> m_mLineEdit;
	std::map<QString, QComboBox*> m_mComboBox;
	std::map<QString, QPushButton*> m_mPushButton;
	std::map<QString, QRadioButton*> m_mRadioButton;
	std::map<QString, QSlider*> m_mSlider;
};

CConfigManager.cpp

#include "stdafx.h"

#include "CConfigManager.hpp"

CConfigManager::CConfigManager()
{
}

CConfigManager::~CConfigManager()
{
}

void CConfigManager::WriteXmlElement(__in QXmlStreamWriter &writer)
{
	// settings
	this->WriteXmlSettingsElement(writer);
}

void CConfigManager::WriteXmlSettingsElement(__in QXmlStreamWriter &writer)
{
	writer.writeStartElement("settings");

	foreach(auto it, this->m_mCheckBox)
	{
		if (it.second && it.first.size() > 1)
		{
			writer.writeStartElement("checkbox");
			writer.writeTextElement("name", it.first);
			writer.writeTextElement("state", QString::number(it.second->isChecked()));
			writer.writeEndElement();
		}
	}

	writer.writeEndElement();
}

void CConfigManager::ReadFromXml(__in QXmlStreamReader &reader)
{
	while (!reader.atEnd() && !reader.hasError())
	{
		QXmlStreamReader::TokenType token = reader.readNext();
		if (token == QXmlStreamReader::StartElement)
		{
			if (reader.name() == "settings")
			{
				// settings
				this->ReadXmlSettings(reader);
			}
		}
	}
}

void CConfigManager::ReadXmlSettings(__in QXmlStreamReader &reader)
{
	while (!reader.atEnd() && !reader.hasError())
	{
		QXmlStreamReader::TokenType token = reader.readNext();
		if (reader.tokenType() == QXmlStreamReader::EndElement && reader.name() == "settings")
		{
			// element item end
			break;
		}
		if (token == QXmlStreamReader::StartElement)
		{
			if (reader.name() == "checkbox")
				this->ReadXmlCheckBox(reader);
		}
	}
}

void CConfigManager::ReadXmlCheckBox(__in QXmlStreamReader &reader)
{
	QString name = QString();
	while (!reader.atEnd() && !reader.hasError())
	{
		QXmlStreamReader::TokenType token = reader.readNext();
		if (reader.tokenType() == QXmlStreamReader::EndElement && reader.name() == "checkbox")
		{
			// element item end
			break;
		}
		if (token == QXmlStreamReader::StartElement)
		{
			if (reader.name() == "name")
				name = reader.readElementText();
			else if (reader.name() == "state" && name.length() > 1)
				this->m_mCheckBox[name]->setChecked(reader.readElementText().toInt());
		}
	}
}

// thanks to mahorori
QCheckBox *CConfigManager::CreateCheckBox(__in const QString &name, __in QWidget *parent)
{
	QCheckBox *checkBox = new QCheckBox(name, parent);
	this->m_mCheckBox[name] = checkBox;
	return checkBox;
}
//

...

使い方

CMainWindow::CMainWindow(__in QWidget *parent) : QMainWindow(parent)
{
        CConfigManager *pConfig = CConfigManager::GetInstance();
        QCheckBox *chkTestBox = pConfig->CreateCheckBox("Test Box");

        connect(chkTestBox, &QCheckBox::toggled, [=](bool toggled)
        {
                //
        }
        ...
}

void CMainWindow::OnLoad()
{
	QSettings settings;
	QString sFileName = QFileDialog::getOpenFileName(this, QString(), settings.value("filename").toString(), tr("Xml Files (*.xml)"));
	if (sFileName.isEmpty())
	{
		QMessageBox::warning(this, QString(), tr("Unknown error occurred."), QMessageBox::Ok);
		return;
	}

	QFile file(sFileName);
	if (!file.open(QIODevice::ReadOnly))
	{
		QMessageBox::warning(this, QString(), file.errorString(), QMessageBox::Ok);
		return;
	}

	CConfigManager::GetInstance()->ReadFromXml(QXmlStreamReader(&file));

	this->m_sXmlFileName = sFileName;
}

void CMainWindow::OnSave()
{
	if (this->m_sXmlFileName.isEmpty())
	{
		// no file exists, try to save as...
		this->OnSaveAs();
		return;
	}

	QFile file(this->m_sXmlFileName);
	if (!file.open(QIODevice::WriteOnly))
	{
		QMessageBox::warning(this, QString(), file.errorString(), QMessageBox::Ok);
		return;
	}

	QXmlStreamWriter writer(&file);
	writer.setAutoFormatting(true);
	writer.setAutoFormattingIndent(4);
	writer.writeStartDocument();

	// write elements
	writer.writeStartElement("XmlTester");
	CConfigManager::GetInstance()->WriteXmlElement(writer);
	writer.writeEndElement();

	file.close();

	QMessageBox::information(this, QString(), tr("Done."), QMessageBox::Ok);
}

void CMainWindow::OnSaveAs()
{
	//
	QString sFileName = QFileDialog::getSaveFileName(this, QString(), QString(), tr("Xml Files (*.xml)"));
	if (sFileName.isEmpty())
	{
		// QMessageBox::warning(this, QString(), tr("Unknown error occurred."), QMessageBox::Ok);
		return;
	}

	this->m_sXmlFileName = sFileName;
	this->OnSave();
}

プロテクターに検出されないD3D・OpenGLフックの考察

FPS等でWallhackやD3Dフックをベースにしたチートを行うにも殆どのゲームでXignCodeやHackShield, NGS(BlackChiper)などのプロテクターが導入されているため、チート検出されてしまいます。

ですが、海外フォーラム等に落ちている物などで、検出されずに使えるものも存在します。
検出される物とされない物の違いをプロテクター別で考察していきます。

チート検出される物

[XIGNCODE, nProtect, NGS(BlackCipher), HackShield]

・IDirect3DDevice9インターフェースなどのメンバ関数を示すアドレスを書き換えてフックする方法 (関数テーブルの書き換え)
IDirect3DDevice9の場合だと、インターフェースが示すアドレスがD3D9.DLLの範囲に収まるかどうかをチェックしていますので、これに当たらないアドレスがある場合はチート検出されます。

・D3Dxx.DLL/OpenGL32.DLLの特定部分にJMPコードを仕込ませてフックする方法
基本的に1Byteでも書き換えるとチート検出されます。

HackShield(v5.6.34.449, v5.7.6.502)は D3Dxx.DLL, OpenGL32.DLLにCRCを実装しているため、
適当なアドレスをCEのRead Accessで調べてみると...(何故か仮想化されていないので、検出関数がすぐ見つかります。)

チート検出されない物

何度か記事で上げましたが、グラフィックドライバにつけ込んだ物がまだ生き残っているようです。(XignCodeで確認済み)
D3Dxx.DLLはWDDM, OpenGLはICDを...(省略

唯一NGSだけはグラフィックドライバもチェックしています。
・関数テーブル上の関数が示すアドレスの配置範囲のチェック
・関数テーブル上の関数が示すアドレス分のメモリのチェック
がありますが、Mid function? (関数の途中にJMPコードを仕込ませてフックする方法)でどうにかなります。

まあこれくらいです..

WDDM, OpenGL ICDの説明は省きましたが、過去記事にそれっぽいのがいくつかありますので、そちらを見てください。(参考にはなりませんが())


以上...と説明していた夢の内容でした。

WDDM Hook (2)

Direct3D DDIは、OpenGL ICDとは違って、グラフィックドライバ自体にテーブルは無くd3dx.dllにドライバのテーブルがあるみたいですね...(ICD同様にグラフィックドライバ自体にテーブルが用意されているものだと勘違いしておりました。)


まずDDIの関数テーブル自体が、グラフィックドライバのテーブル関数(?) CreateDevice関数 によって初期化されますし、同じ所にあるものだと普通思いますよね...


一見どうでも良いようにも思えますが...
少し前のプロテクターにはD3D関数テーブルの整合性チェックがあっても、ドライバの関数テーブルのチェックは殆ど無かったため問題はありませんでした。ですが、最近のものはd3dx.dll自体にメモリチェックがあったりするので、どうでも良いとは言い切れません。


どうにかグラフィックドライバ自体に存在するドライバ関数?だけで完結したいですよね..


一応、DDIの各ドライバ関数の第一引数 HANDLE hDeviceにDisplay Device(関数テーブル?)のポインタが格納されており、それに対応した関数が呼ばれているので、何とかなるような気もしますけどね。


また何か進展があれば記事にしたいと思います。以上


追記:
よくよく考えれば、D3D DDIの各ドライバ関数をフックしなくても、各ドライバ関数へのテーブル自体をすり替えてしまえば、ポインタチェック?とかはされない限りは、CRCがあっても問題ありません。(.exeは別みたいですが。)


関数テーブル上の関数は、asm上では call dword ptr [eax+04] とかで呼ばれため、
テーブル自体の書き換えは、直接メモリを書き換える訳でもないので検出が難しいみたいですね。
出来るとすれば、関数ポインタ先が、メモリ内or外、またはBase Relocation (.dll+XXXX) などが挙げられるようです。


他にもポインタ自体のチェックがありますが、プロテクタがそのポインタを読み込む前に書き換えてしまえば難なく突破出来てしまいます。
良く海外フォーラム等にWallhackなどが落ちていますよね。モノによってはプロテクタの有無に関わらず、そのまま使えたりするものもありますよね。そうです、そういうことです。


少し話が逸れますが、関数テーブルの書き換えによって行うチートの検出は難しいと思いました。


更に追記:
冒頭にグラフィックドライバ自体に関数テーブルが用意されてないだとか書きましたが、OpenAdapterはグラフィックドライバの関数テーブルの他に、D3Dの何らかの関数テーブルの初期化時にも呼ばれることが分かりました。
今までは決め打ちでやっていたため、D3Dの何らかの関数テーブルの方を取得してたみたいです。

ウィンドウフレームを消して、ウィンドウを移動出来るようにする。

eventFilter(QObject* object, QEvent* event); をオーバーライドし、
event->type() から各マウスの状態から制御します。以下ソースコードです。

CMainWindow::CMainWindow(__in QWidget *parent) : QMainWindow(parent)
{
    ....
    this->installEventFilter(this);                 // eventFilterを有効にする
    this->setWindowFlags(Qt::FramelessWindowHint);  // ウィンドウの枠?を消す
    ....
}
 
bool CMainWindow::eventFilter(QObject* object, QEvent* event)
{
    if (this->m_mousePressed)
    {
        if (event->type() == QEvent::MouseMove)
        {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
            this->m_currentPos = mouseEvent->pos();
            this->move(
                this->pos().x() + this->m_currentPos.x() - this->m_originPos.x(),
                this->pos().y() + this->m_currentPos.y() - this->m_originPos.y());
        }
 
        if (event->type() == QEvent::MouseButtonRelease)
        {
            this->m_mousePressed = false;
        }
    }
    else
    {
        if (event->type() == QEvent::MouseButtonPress)
        {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
            this->m_originPos = mouseEvent->pos();
            this->m_mousePressed = true;
        }
    }
 
    return false;
}