最近在研究.net的内存挂。
写了很久的c++,发现c#写出来的东西实在太香。
折腾c#外挂已经有很长时间了。都是用socket和c++配合。
这个模式其实蛮成功的,用rpc调用的方式加上c#的天生await 非常好写逻辑
类似这样
最近想换个口味。注入托管dll到非托管进程
这样做只是为了解决我目前遇见的一个问题。
在一个多线程的程序上逆向,我挂了很多钩子,导致我读写数据和储存我自己的数据
非常容易出现多线程冲突问题,换到.net里以后
lock 和Monitor 在.net里是同线程不互锁。这样能让我不容易出现互锁现象。
有一段时间正在烦恼那些卖驱动的,只有读写功能为什么还能实现很多功能。
在我的认知里,要调用游戏部分函数才能更方便自己做出更有用的功能
当然这里确实有些外挂是只读取角色顶点就能绘制的。
后来细想一下其实不需要调用功能通过写入代码的方式获取执行就可以了。
就是只要有读写就可以了。
游戏外挂无非就是 读写和调用。 调用是可以通过写来实现。
比如hook某个dx的函数。或者修改虚函数表的地址,然后jmp 到自己的函数里
达到获取执行权限。
好比挂钩了GetTickCount 这个API ,然后目标游戏不断的调用这个API
我们在这里插入自己的调用逻辑就可以了。
想明白了这个,于是我就干起了注入托管dll 到游戏进程里的勾当
当然这个托管dll实在是太大了(因为会用Costura.Fody把第三方库都打包在一起)
想法是这样,注入到游戏进程里以后申请内存空间,然后通过asm编译成bytes
然后写入到内存后,再去调用他就可以了
这里要安利一个asm的库
GitHub - icedland/iced: Blazing fast and correct x86/x64 disassembler, assembler, decoder, encoder for Rust, .NET, Java, Python, Lua
这个库很香
在c#里写asm长这样子,大概就是写好asm以后编译成bytes的过程
然后要介绍在c#内怎么完成thiscall
游戏大部分是thiscall 所以我在内存中申请一段asm
然后通过c#去调用这个段代码就可以了
asm 的作用就是把传入的参数 push到堆栈然后call
之后返回eax 这样就完整的跑通调用了。
用iced把asm生成好,然后通过c#的委托去调用这段asm代码即可
最后实现的效果类是这样
至于读写就更简单了。C#自带Marshal可以直接读写。
而且c#也支持不安全指针 直接 *(int*)(0x123456) = 100;
然后我们无限的包装自己的读写函数,比如byte float int string 之类的读写就可以
至于hook 也可以通过委托回调到自己的c#代码,hook在c#完成编译以后写入到目标地址
至此,完整的c#外挂需要的功能都实现了。
例子分3个工程
TestApp:测试工程模拟调用目标程序比如游戏
InjectionDLL:注入DLL,负责加载.net的dll,如果是远线程注入,就注入这个DLL即可,例子工程是主动loadlibrary
CShareLoadModule : c#的主要工作dll
附上源码一份
netInjection.rar
//以下是补充 2022 10 09
/
因为上面代码是用asm作为Thiscall 调用的,后来发现c#是自带调用约定的可以更优美的实现thiscall 调用
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_0(IntPtr ptr); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_1(IntPtr ptr, IntPtr p1); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_2(IntPtr ptr, IntPtr p1, IntPtr p2); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_3(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_4(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_5(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_6(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_7(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_8(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_9(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_10(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9, IntPtr p10); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_11(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9, IntPtr p10, IntPtr p11); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_12(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9, IntPtr p10, IntPtr p11, IntPtr p12); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_13(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9, IntPtr p10, IntPtr p11, IntPtr p12, IntPtr p13); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] public delegate IntPtr ThisCall_14(IntPtr ptr, IntPtr p1, IntPtr p2, IntPtr p3, IntPtr p4, IntPtr p5, IntPtr p6, IntPtr p7, IntPtr p8, IntPtr p9, IntPtr p10, IntPtr p11, IntPtr p12, IntPtr p13, IntPtr p14); unsafe public static IntPtr ThisCall(IntPtr address, IntPtr dwECX, params object[] args) { IntPtr p1 = IntPtr.Zero; IntPtr[] paramPtr = new IntPtr[args.Length*2]; int addParamCount = 0; for (int i = 0; i < args.Length; i++) { var paramtype = args[i].GetType(); if (paramtype == typeof(float)) { var v = (float)args[i]; paramPtr[addParamCount] = *(IntPtr*)&v; } else if (paramtype == typeof(double)) { var v = (double)args[i]; paramPtr[addParamCount] = *(IntPtr*)&v; addParamCount++; paramPtr[addParamCount] = *(IntPtr*)((&v) +4); } else if (paramtype == typeof(long)) { var v = (long)args[i]; paramPtr[addParamCount] = *(IntPtr*)&v; addParamCount++; paramPtr[addParamCount] = *(IntPtr*)((&v) + 4); } else if (paramtype == typeof(IntPtr)) { var v = (IntPtr)args[i]; paramPtr[addParamCount] = v; } else if (paramtype == typeof(PtrGameUIWindow)) { paramPtr[addParamCount] = (args[i] as PtrGameUIWindow).ptr; } else { paramPtr[addParamCount] = (IntPtr)Convert.ToInt32(args[i]); } addParamCount++; } Log.Console($"ThisCall:0x{address.ToString("X8")} ECX:0x{dwECX.ToString("X8")} {paramPtr.ArrayToString(0, addParamCount)}"); switch (args.Length) { case 0: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_0>(address).Invoke(dwECX); break; case 1: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_1>(address).Invoke(dwECX, paramPtr[0]); break; case 2: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_2>(address).Invoke(dwECX, paramPtr[0], paramPtr[1]); break; case 3: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_3>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2]); break; case 4: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_4>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3]); break; case 5: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_5>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4]); break; case 6: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_6>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5]); break; case 7: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_7>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6]); break; case 8: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_8>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7]); break; case 9: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_9>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8]); break; case 10: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_10>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8], paramPtr[9]); break; case 11: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_11>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8], paramPtr[9], paramPtr[10]); break; case 12: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_12>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8], paramPtr[9], paramPtr[10], paramPtr[11]); break; case 13: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_13>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8], paramPtr[9], paramPtr[10], paramPtr[11], paramPtr[12]); break; case 14: p1 = Marshal.GetDelegateForFunctionPointer<ThisCall_14>(address).Invoke(dwECX, paramPtr[0], paramPtr[1], paramPtr[2], paramPtr[3], paramPtr[4], paramPtr[5], paramPtr[6], paramPtr[7], paramPtr[8], paramPtr[9], paramPtr[10], paramPtr[11], paramPtr[12], paramPtr[13]); break; default: Log.Error(new Exception("ThisCall 参数个数未预测")); p1 = IntPtr.Zero; break; } return p1; }
因为c#会定期GC的问题,导致c#自身函数可能会被GC修改函数位置
所以需要固定住代码位置新增一下函数固定Delegate
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Ansi)] public delegate IntPtr Delegate_NewStringID2String(IntPtr dwESP); public uint LockDelegate(Delegate func) { GCHandleList.Add(GCHandle.Alloc(func)); GCHandleList.Add(GCHandle.Alloc(Marshal.GetFunctionPointerForDelegate(func), GCHandleType.Pinned)); GCHandleList.Add(GCHandle.Alloc(func.Method.MethodHandle.GetFunctionPointer())); return (uint)Marshal.GetFunctionPointerForDelegate(func); } //使用方法 public static IntPtr MyStringID2String(IntPtr dwESP) { var ret = IntPtr.IntPtr.Zero; return ret; } public override void Start() { var asm = new Assembler(32); asm.pushad(); asm.mov(eax, esp); asm.add(eax, 0x20); asm.push(eax); asm.mov(eax, LockDelegate(new Delegate_NewStringID2String(MyStringID2String))); asm.call(eax); asm.mov(__[esp + 0x1c], eax); asm.popad(); asm.ret(); var newJmpCode = PatchSelfHelper.AllocMem(0x30); PatchSelfHelper.WriteAssembler(newJmpCode, asm); }