Stronghold Kingdomsのボット作成の歴史
長い間、私はこのゲームのボットを書く問題に取り組みましたが、十分な経験がなく、時には怠け者で、時には間違った側から入ろうとしました。
その結果、C#でコードの作成とリバース開発の経験を積んで、目標を達成することにしました。
はい、ご覧のとおり、C#はカジュアルではありません。ゲームは.net 2.0を使用して書かれています。
![](https://habrastorage.org/getpro/habr/post_images/20f/ad3/1e9/20fad31e952cfe8b26369ea8676bbace.png)
当初、ネットワークプロトコル(暗号化されていない)のみをエミュレートするソケットボットを書くことを考えていましたが、「ソースコード」(il-codeの逆コンパイルの結果)がサードパーティアプリケーションで簡単に復元されます。
しかし、それが退屈で退屈だと思えたのは、まさに「ソースコード」があるのに、なぜ自転車を悩ませるからです。
リフレクターを装備して、私はゲームのエントリーポイントに対処し始めました(そのコードは3年以上もまったく難読化されていないため、開発者は疑問に思っています)-特別なことは何もありません。
分析と部分的に間違った決定
明らかに、ゲームプロジェクトはもともとコンソールアプリケーションとして作成されました。
プライベートスタティックボイドMain(string [] args)エントリポイントとそのクラスプログラムヒントとして、クラスはちなみにプライベートです。
まず最初に、Reflexilを追加したReflectorの部隊が、何を期待すべきかを知らずに、クラスとメソッドを急いで公開しました。
しかし、突然、変更されたファイルをダウンロードしているランチャーに出会いました。
同じリフレクターを使って彼と長い間戦わなかった後、検死の後、そこからゲームの実行可能ファイルに渡される引数を設定するためのコードを引き出しました。
if (DDText.getText(0x17) == "XX") parameters = new string[] { "-InstallerVersion", "", "", "st" }; // st == steam else parameters = new string[] { "-InstallerVersion", "", "" }; parameters[1] = SelfUpdater.CurrentBuildVersion.ToString(); parameters[2] = DDText.getText(0); // , , “ru”, “de”, “en” .. local.txt . UpdateManager.SetCommandLineParameters(parameters); // System.Diagnostics.Process UpdateManager.StartApplication();
解析:
if (DDText.getText(0x17) == "XX")
-ランチャーの隣のlocal.txtファイルの行。
スチーム/非スチームバージョンでのこれらの奇妙なテスト:X-スチームではなく、XX-スチーム。 :\
parameters[1] = SelfUpdater.CurrentBuildVersion
後で見つけたように、クライアントでのチェックは奇妙ですが、現在の番号よりもはるかに大きい「予約済み」の番号を簡単に指定できますが、ランチャーのバージョン。 このチェックは陳腐化のみを目的としているため、数字を「より少ない」と比較してバージョンを言ってください。
parameters[2] = DDText.getText(0)
-バージョンを取得すると、これがゲームの言語であり、「ru」、「de」、「en」などの形式であることが
parameters[2] = DDText.getText(0)
ました。
また、local.txtファイルからロードされます。
ところで、ランチャーバージョンは次のようになります。
static SelfUpdater() { currentBuildVersion = 0x75; // 117, .. 1.17 . }
そして、彼は魔法のバッチファイルを作成しました。
StrongholdKingdoms.exe -InstallerVersion 117 ru
これはできますが:
StrongholdKingdoms.exe -InstallerVersion 100500 ru
私が少し高く言った。
だから、私たちが持っているもの:少し変更されたクライアントとランチャーバイパスシステム(私がそれを呼ぶかもしれないなら)。
すべてを実行しようとしたが、ゲームは機能しており、私のパッチはそれを害しなかったことがわかります(なぜそこに害を及ぼすのでしょうか)。
その後、ゲームの実行可能ファイルをクラスライブラリとしてプロジェクトに接続し、ゲームの名前空間-Kingdomsを接続しようとしました。
それから、たくさんのコードを入れました。Mainを呼び出してProgrammクラスをエミュレートしようとしましたが、何らかの理由で、フレームワークを超えて実行時にクラッシュしてゲームがクラッシュしました。
ゲームが多くの非C#ライブラリと多くの安全でないコードを使用するという事実に言及しました。 本当の理由は見つかりませんでした。
決定は正しい
長い苦痛と解決策を見つけられなかった後、私はすでに吐いた。 しかし、何らかの理由でTerrariaサーバーのフォークを覚えていました-TShock(そう、フォーク、デコンパイラーをどうやって楽しんでいたのでしょうか)とDLLからモジュール(mod /プラグイン)をロードしました。
このアイデアは私にとって興味深いものでした。 グーグルは方法とコードの両方を見つけました。
少し掘り下げて自分のプロジェクトで確認したところ、(突然のように)動作していることに気がつきました。
実際には、コード:
System.Reflection.Assembly A = System.Reflection.Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll"); Type ClassType = A.GetType("BotDLL.Main", true); object Obj = Activator.CreateInstance(ClassType); System.Reflection.MethodInfo MI = ClassType.GetMethod("Inject"); MI.Invoke(Obj, null);
コードを分析しましょう:
System.Reflection.Assembly
これは、ファイルをプロジェクトに接続するときに、コードからのみファイルへのリンクを作成する役割を果たします。 また、プロジェクトのバージョンと著作権(はい、すべてのプロジェクトのAssemblyInfo.csそのもの)に関する情報も保存します。
Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll")
-ライブラリをダウンロードします。
次に、このクラスInject()内の関数を呼び出します。これは、本質的にボットの始まりです。 =)
サードパーティのアプリケーションでスケッチしたコードを試しました-インジェクションは機能しました。
クライアントパッチ
次に、最も興味深いことに注目します-チャレンジコードをゲームに導入します。
Reflexilを使用したコード置換により、Mainの厚かましいものに入れようとしましたが、逆コンパイルの結果としてパッチが適用されなかったパッチの送信に成功しました。 まあ、または私は怠けていた、それは問題ではありません。
私はこのMain自体でサードパーティ関数(ifなどのメインブランチ以外)の保証された呼び出しを探しに行きましたが、すぐにMySettings.load()関数の呼び出しを見つけました。
しかし、タンバリンなしではコンパイルしたくないコードが山ほどありました。
しかし、まぐれによって、その隣にはブール関数hasLoggedIn()があります。これは、ゲームが開始されたときにブール値のみを返します。
return (this.HasLoggedIn || (this.Username.Length > 0));
それはすぐに私を喜ばせ、すぐにこの関数は次のように変換されました:
if (!IsStarted) { System.Reflection.Assembly A = System.Reflection.Assembly.LoadFrom(System.Windows.Forms.Application.StartupPath + @"\BotDLL.dll"); Type ClassType = A.GetType("BotDLL.Main", true); object Obj = Activator.CreateInstance(ClassType); System.Reflection.MethodInfo MI = ClassType.GetMethod("Inject"); MI.Invoke(Obj, null); IsStarted = true; } return (this.HasLoggedIn || (this.Username.Length > 0));
彼に対処します。
if(!IsStarted)-このチェックを追加する必要があり、これを行うには、MySettingsクラスに追加のフィールドを入力します。関数が複数回呼び出され、実際にいくつかのボットスレッドが必要ないためです。 これはすべて同じReflexil'omで行われます。
さて、私たちはすでにメインコードをもう少し高く分析しました。
そして最後に、ここにあるはずだったものを返します。 =)
だから-ボット自体
注入機能:
public void Inject() { AllocConsole(); Console.Title = "SHKBot"; Console.WriteLine("DLL !"); Thread Th = new Thread(SHK); Th.Start(); BotForm FBot = new BotForm(); FBot.Show(); } … [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool AllocConsole();
まず、関数を呼び出してコンソールウィンドウを開きます。デバッグが簡単になります。
メインボットサイクルでスレッドを開始した後-SHK();
同時に、便宜上ボット制御フォームを開きます。
それが中小企業です-必要な機能を実装するために。
ここに私のコードの残りがあります-ここに自動取引システムを実装しました。
それが機能するためには、まず各セッションで村を「キャッシュ」する必要があります-取引する予定の各村を開きます。
このコードは疑いなく役立ちますが、村を自動的にロードする他の方法には触れていません。
InterfaceMgr.Instance.selectVillage(VillageID); GameEngine.Instance.downloadCurrentVillage();
SHK機能コードは次のとおりです。
非表示のテキスト
public void SHK() { Console.WriteLine(" !"); while (!GameEngine.Instance.World.isDownloadComplete()) { Console.WriteLine(" !"); Thread.Sleep(5000); // 5 sec Console.Clear(); } Console.WriteLine(" ! ."); Console.WriteLine("\n======| DEBUG INFO |======"); Console.WriteLine(RemoteServices.Instance.UserID); Console.WriteLine(RemoteServices.Instance.UserName); List<int> VillageIDs = GameEngine.Instance.World.getListOfUserVillages(); foreach (int VillageID in VillageIDs) { WorldMap.VillageData Village = GameEngine.Instance.World.getVillageData(VillageID); Console.WriteLine("[] " + Village.m_villageName + " - " + VillageID); InterfaceMgr.Instance.selectVillage(VillageID); GameEngine.Instance.downloadCurrentVillage(); } Console.WriteLine("======| ========== |======\n"); while (true) { try { // - } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Console.WriteLine(ex); Console.WriteLine("======| ======= |======\n"); } } }
コントロールフォームコード:
非表示のテキスト
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Threading; using Kingdoms; using System.CodeDom.Compiler; using Microsoft.CSharp; using System.Reflection; namespace BotDLL { public partial class BotForm : Form { Thread TradeThread; bool IsTrading = false; public void Log(string Text) { Console.WriteLine(Text); richTextBox_Log.Text = Text + "\r\n" + richTextBox_Log.Text; } public BotForm() { CheckForIllegalCrossThreadCalls = false; InitializeComponent(); this.Show(); Log(" ."); listBox_ResList.SelectedIndex = 0; Log(" ..."); TradeThread = new Thread(Trade); TradeThread.Start(); } private void button_Trade_Click(object sender, EventArgs e) { // if (GameEngine.Instance.World.isDownloadComplete() && textBox_TradeTargetID.Text.Length > 0) { try { if (!IsTrading) // { button_Trade.Text = ""; IsTrading = true; // } else // { button_Trade.Text = ""; IsTrading = false; } } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Log(ex.ToString()); Console.WriteLine("======| ======= |======\n"); } } } public void Trade() { Log(" !"); int Sleep = 0; while (true) // { Sleep = 60 + new Random().Next(-5, 60); if (IsTrading) { Log("[" + DateTime.Now + "] \"" + listBox_ResList.SelectedItem.ToString() + "\""); // ID int ResID = int.Parse(listBox_ResList.SelectedItem.ToString().Replace(" ", "").Split('-')[0]); int TargetID = int.Parse(textBox_TradeTargetID.Text); // ID - List<int> VillageIDs = GameEngine.Instance.World.getListOfUserVillages(); // foreach (int VillageID in VillageIDs) // { // ( ) if (GameEngine.Instance.getVillage(VillageID) != null) { // WorldMap.VillageData Village = GameEngine.Instance.World.getVillageData(VillageID); VillageMap Map = GameEngine.Instance.getVillage(VillageID); // int ResAmount = (int)Map.getResourceLevel(ResID); // - int MerchantsCount = Map.calcTotalTradersAtHome(); // - Log(" " + VillageID + " " + MerchantsCount + " "); // int SendWithOne = int.Parse(textBox_ResCount.Text); // - int MaxAmount = MerchantsCount * SendWithOne; // - if (ResAmount < MaxAmount) // MerchantsCount = (int)(ResAmount / SendWithOne); // if (MerchantsCount > 0) // { TargetID = GameEngine.Instance.World.getRegionCapitalVillage(Village.regionID); // , textBox_TradeTargetID.Text = TargetID.ToString(); // GameEngine.Instance.getVillage(VillageID).stockExchangeTrade(TargetID, ResID, MerchantsCount * SendWithOne, false); AllVillagesPanel.travellersChanged(); // ( ) GUI- } } } Log(" " + Sleep + " " + DateTime.Now.AddSeconds(Sleep).ToString("HH:mm:ss")); Console.WriteLine(); } Thread.Sleep(Sleep * 1000); // , . . } } private void BotForm_FormClosing(object sender, FormClosingEventArgs e) { try { TradeThread.Abort(); } catch {} } private void button_MapEditing_Click(object sender, EventArgs e) { button_MapEditing.Text = (!GameEngine.Instance.World.MapEditing).ToString(); GameEngine.Instance.World.MapEditing = !GameEngine.Instance.World.MapEditing; } private void button_Exec_Click(object sender, EventArgs e) { if (richTextBox_In.Text.Length == 0 || !GameEngine.Instance.World.isDownloadComplete()) return; richTextBox_Out.Text = ""; // *** Example form input has code in a text box string lcCode = richTextBox_In.Text; ICodeCompiler loCompiler = new CSharpCodeProvider().CreateCompiler(); CompilerParameters loParameters = new CompilerParameters(); // *** Start by adding any referenced assemblies loParameters.ReferencedAssemblies.Add("System.dll"); loParameters.ReferencedAssemblies.Add("System.Data.dll"); loParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll"); loParameters.ReferencedAssemblies.Add("StrongholdKingdoms.exe"); // *** Must create a fully functional assembly as a string lcCode = @"using System; using System.IO; using System.Windows.Forms; using System.Collections.Generic; using System.Text; using Kingdoms; namespace NSpace { public class NClass { public object DynamicCode(params object[] Parameters) { " + lcCode + @" return null; } } }"; // *** Load the resulting assembly into memory loParameters.GenerateInMemory = false; // *** Now compile the whole thing CompilerResults loCompiled = loCompiler.CompileAssemblyFromSource(loParameters, lcCode); if (loCompiled.Errors.HasErrors) { string lcErrorMsg = ""; lcErrorMsg = loCompiled.Errors.Count.ToString() + " Errors:"; for (int x = 0; x < loCompiled.Errors.Count; x++) lcErrorMsg = lcErrorMsg + "\r\nLine: " + loCompiled.Errors[x].Line.ToString() + " - " + loCompiled.Errors[x].ErrorText; richTextBox_Out.Text = lcErrorMsg + "\r\n\r\n" + lcCode; return; } Assembly loAssembly = loCompiled.CompiledAssembly; // *** Retrieve an obj ref – generic type only object loObject = loAssembly.CreateInstance("NSpace.NClass"); if (loObject == null) { richTextBox_Out.Text = "Couldn't load class."; return; } object[] loCodeParms = new object[1]; loCodeParms[0] = "SHKBot"; try { object loResult = loObject.GetType().InvokeMember( "DynamicCode", BindingFlags.InvokeMethod, null, loObject, loCodeParms); //DateTime ltNow = (DateTime)loResult; if (loResult != null) richTextBox_Out.Text = "Method Call Result:\r\n\r\n" + loResult.ToString(); } catch (Exception ex) { Console.WriteLine("\n======| EX INFO |======"); Console.WriteLine(ex); Console.WriteLine("======| ======= |======\n"); } } } }
当初、NLuaボット(C#用のLuaライブラリ)に固執したかったのですが、3.5 +フレームワークのみをサポートしているため、何らかの理由で古いバージョンを使用したくありませんでした。
便宜上、シャープ自体でリアルタイムにコードの実行を導入しました。何度も何度も再コンパイルした後、ゲームを再起動するのにうんざりしました。
このチュートリアルを使用しました 。
まとめ
このソリューションの利点:
- ソースコードがあるかのように、すべてのゲームコードにアクセスします。
- 建物の列、制限のない研究の研究、およびさらに多くを備えたプレミアムカードの独自のシステムを作成できます。
- 周囲の地域間で商品を再販するためのアルゴリズム。
- 例として、既存のものから取られた「レイアウトによる」村の自動構造。
- さまざまなユニットの自動時間。
- あなたがいない間、城の自動ロック。
- 時間内に保証されたカードの自動収集。
- そしてもちろん、動的なコード実行。
- 検出に対する面白い保護。 さて、疑わしいダミーリクエストを送信しないための条件はさらに2つあります。
短所:
- ハンドルの各バージョンでクライアントにパッチを適用する必要があります。 または、Mono.Cecilまたはフレームワークのアナログを使用してパッチャーを作成できます。
- プレミアムカードとは異なり、クライアントを常にオンおよびオンラインにしておく必要があります。
- ゲームはかなり大きいので、「API」を学ぶのに間違いなく1時間ではありません。 欲望と道具は何年も理解していますが、欲望があるでしょう。 いずれにしても、パッケージをいじるよりはましです。
これは、この全体の様子です。
![](https://habrastorage.org/getpro/habr/post_images/891/718/f32/891718f328c638f31b8ced5c36a62c86.png)
興味のある方は、ゲームの以下のクラスをご覧になることをお勧めします。
クラスリスト
- ゲームエンジン
- GameEngine.Instance
- GameEngine.Instance.World
- ワールドマップ
- WorldMap.VillageData
- RemoteServices
- RemoteServices.Instance
- AllVillagesPanel
- 村の地図
執筆時点では、ゲームバージョンは2.0.18.6でした。
このバージョンをゲームの実行可能ファイルとボットとともにここからダウンロードします 。
心配しないで、私は個人データを盗みません。 =)ゲームにうんざりしているので、コミュニティと経験を共有しています。
ソースコードはこちらで利用できます 。
ソースコードを使用する場合は、クリーンな実行可能ファイル(パッチを適用していない)をクラスライブラリとして使用し、このリンクを宛先ディレクトリへのコピーを無効にして、パッチを誤って置き換えないようにします。
私は記事の執筆スタイルをおaびします-初めて執筆しています。 トピックからトピックにジャンプしたり、技術的な側面をいくつか説明したりするでしょう。
たくさんの水があるように思えるかもしれませんが、この記事はもともと小さな物語として考えられていました-それがおそらく理由です。