From 66751337750332b85bbf246997073aae4fa16db9 Mon Sep 17 00:00:00 2001 From: ryudacatt Date: Mon, 20 Jul 2026 23:33:31 +0530 Subject: [PATCH 1/5] Add MCP control server, branded icon, and web dashboard - Add built-in MCP server (McpServer, ToolRegistry, session/job/auth managers, installer) so an agent can drive the engine over MCP. - Add web dashboard and configuration pages served on loopback. - Wire the branded favicon.ico as the window icon (class + WM_SETICON) and serve it from the dashboard pages. - Rename the generic CustomSetting primitive to McpControls and label the MCP settings entry cleanly. - Add process/window icon cache for the process selector. - Document the MCP server in the README. --- .gitignore | 11 +- CheatStengine/CMakeLists.txt | 12 + CheatStengine/Resources/favicon.ico | Bin 0 -> 270622 bytes .../src/CheatStengine/Core/ThreadPool.h | 7 +- CheatStengine/src/CheatStengine/MainLayer.cpp | 148 ++++ CheatStengine/src/CheatStengine/MainLayer.h | 21 + .../CheatStengine/Process/KernelProcess.cpp | 3 + .../src/CheatStengine/Server/AuthManager.cpp | 84 ++ .../src/CheatStengine/Server/AuthManager.h | 26 + .../src/CheatStengine/Server/CommandQueue.h | 90 +++ .../src/CheatStengine/Server/JobManager.cpp | 111 +++ .../src/CheatStengine/Server/JobManager.h | 54 ++ .../src/CheatStengine/Server/McpInstaller.cpp | 378 +++++++++ .../src/CheatStengine/Server/McpInstaller.h | 57 ++ .../src/CheatStengine/Server/McpServer.cpp | 396 ++++++++++ .../src/CheatStengine/Server/McpServer.h | 74 ++ .../CheatStengine/Server/SessionManager.cpp | 117 +++ .../src/CheatStengine/Server/SessionManager.h | 45 ++ .../src/CheatStengine/Server/ToolRegistry.cpp | 745 ++++++++++++++++++ .../src/CheatStengine/Server/ToolRegistry.h | 58 ++ .../src/CheatStengine/Server/WebUI.h | 367 +++++++++ .../src/CheatStengine/Settings/McpControls.h | 33 + .../src/CheatStengine/Tools/MemoryScanner.h | 5 +- .../src/CheatStengine/UI/ImGui/IconCache.cpp | 218 +++++ .../src/CheatStengine/UI/ImGui/IconCache.h | 66 ++ CheatStengine/vendor/httplib.cmake | 14 + CheatStengine/vendor/json.cmake | 14 + Engine/src/Platform/Win32/Win32Window.cpp | 24 +- Engine/src/Platform/Win32/Win32Window.h | 2 + Images/128x128.png | Bin 0 -> 7364 bytes Images/64x64.png | Bin 0 -> 3816 bytes Images/favicon.ico | Bin 0 -> 270622 bytes README.md | 97 ++- 33 files changed, 3244 insertions(+), 33 deletions(-) create mode 100644 CheatStengine/Resources/favicon.ico create mode 100644 CheatStengine/src/CheatStengine/Server/AuthManager.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/AuthManager.h create mode 100644 CheatStengine/src/CheatStengine/Server/CommandQueue.h create mode 100644 CheatStengine/src/CheatStengine/Server/JobManager.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/JobManager.h create mode 100644 CheatStengine/src/CheatStengine/Server/McpInstaller.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/McpInstaller.h create mode 100644 CheatStengine/src/CheatStengine/Server/McpServer.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/McpServer.h create mode 100644 CheatStengine/src/CheatStengine/Server/SessionManager.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/SessionManager.h create mode 100644 CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp create mode 100644 CheatStengine/src/CheatStengine/Server/ToolRegistry.h create mode 100644 CheatStengine/src/CheatStengine/Server/WebUI.h create mode 100644 CheatStengine/src/CheatStengine/Settings/McpControls.h create mode 100644 CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp create mode 100644 CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h create mode 100644 CheatStengine/vendor/httplib.cmake create mode 100644 CheatStengine/vendor/json.cmake create mode 100644 Images/128x128.png create mode 100644 Images/64x64.png create mode 100644 Images/favicon.ico diff --git a/.gitignore b/.gitignore index 42e0eb5..3897d3f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,13 @@ /.idea/ # Dependency directories -*/vendor/*/ \ No newline at end of file +*/vendor/*/ + +# Local scratch / persona files (not part of the tool) +nyx_core.txt +nyx_style.txt +nyx_injector.bat +logs/ + +# Local editor / agent settings +.claude/settings.local.json \ No newline at end of file diff --git a/CheatStengine/CMakeLists.txt b/CheatStengine/CMakeLists.txt index 666f7f2..4be2c0a 100644 --- a/CheatStengine/CMakeLists.txt +++ b/CheatStengine/CMakeLists.txt @@ -2,6 +2,8 @@ project(CheatStengine) include(vendor/zasm.cmake) include(vendor/icon_font_cpp_headers.cmake) +include(vendor/json.cmake) +include(vendor/httplib.cmake) #include(vendor/imgui_hex_editor.cmake) file(GLOB_RECURSE SOURCE_FILES src/*.cpp) @@ -9,6 +11,12 @@ add_executable(${PROJECT_NAME} ${SOURCE_FILES}) if (WIN32) add_definitions(-DNOMINMAX) + # cpp-httplib pulls in winsock2.h; define this so doesn't drag in + # the legacy winsock.h first and cause sockaddr/type redefinition errors. + add_definitions(-DWIN32_LEAN_AND_MEAN) + # Needed so the DX11 context header (guarded by PLATFORM_WINDOWS) is visible + # here; the icon cache uploads process icons through the live D3D11 device. + add_definitions(-DPLATFORM_WINDOWS) endif () target_include_directories(${PROJECT_NAME} PUBLIC @@ -18,7 +26,11 @@ target_include_directories(${PROJECT_NAME} PUBLIC target_link_libraries(${PROJECT_NAME} PUBLIC Engine zasm + nlohmann_json::nlohmann_json + httplib::httplib Dbghelp + Shell32 + d3d11 ) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD diff --git a/CheatStengine/Resources/favicon.ico b/CheatStengine/Resources/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d1e1dd1577c0ee15c0be0a6f803f367219762f4a GIT binary patch literal 270622 zcmeF42b`4E)&CbTioL`)YSh1pnrPHmqOoFc^s*}pQ6ouIumOS{u*MkQHzsPNv%nTW z2_U_?iXFj{W&w5)6$F8$ihzy#|9$T>cjlRA>h@W7^mCcnnP=|3=bYc~o^yLnP80pl ze@$|JlGFUByXE{y?~nIB$jNEyea@dbIlFl;zJJZw&POO+X2#|}rS!fT)8#8AQ_E?) z@?UC-az~nimL1LPeutTgvfYg_IX0~;%P}+WYifpFd5S5_?QcrDysYQfrctNWW=z+) zrm$llGxU;U&Gb8S%-SVho-Xf9e61*JW@h)_#}u@>KstFsa=xduB@KCN+fFr=KeD`M zWkW_}MdnPjg}+W<2MmEFFa@^8bP>i<@?)*VEZ7CZU>Quiyumt{2m6J21*W9y66Luq zjeM(XkFPAp$L2rdalZ56q1T)A`9G9CQ5|;FK|Q`!87qZ{+0wy%W={X}&Bl)p2GI!tep4Ft+Ms9NF@>F;Hp4Ev%*?p^K(lV?ZsB~L{xj{hqHJ$* z;zaTP2I=HE$xyCT7kH>Hp3?n2vXoodkP%stIn(SAtPj`$LtqI^fvquJz*s7|H(AVr zT`&xm!L-XCtb=*5Uz9gX{9ma&;Y6x@tNZ;9%ki=Klf-**-unH4`fQTQ26Oo3opn$T z%J^D%_`-~6xya1B{}J=m=f{Ngvu^4CsD12cMz$QRXFkxgUn+f3h8|0k*LA4>-K@_iceRu>O-^?PL5W@ST0WJTsovqP{x zU)q);(_Smej#piWsLsShb%BG*>9ex>PxL&pAtSOPbEerLSRb$hhQJb- zN+egJ&%rJj2FqZY{J}U_2lJ!bd~e`DqTkov=igb*NBaA95%b5&IkvMg<8qA|`MVd) zU;3Y6#!vP1W823E9+BSvCB0Xb|MO(Kp2FFY4FG4Ss;<){FELT9o@x&Vbz2s)AtSO@ zl%3iD>~f^VP(iDCRpLL`1;g`%WiSo*!8lk4^I#ti)Q^4Pd|iz7C6VtcV@ldrnBmtJ zn*Zx}k{Lfu{NIsdwyr!v@&B!6*yXd;?@Fh?6C_5wL?E-ed5Lg0JmE|ee1;bz&OoMGO4%Wdu*dHSfR5ssT;`y#dW2)QC z&JRJLv}M+_quYOAo@vq3Jm2dW zvu1JgjK^6;+3DiMOO1&CFUkL(p7C}8J75SbfvL*l@MDW%uneY&|G_v|2lHSb4pb`N zO7?}*=1tsu855{5CHbG4;;R_H)tp^kS+!DeuJY-$*c5hPOn;J@*!>ps_Q+GrH|zHa z<6+~9W@f>N{mjH3%u8wvu>Cvw&4(&KaoNTCuDIg|diE{-=5J;~kF(61!<(5+D#b)$~n7COQ?g)>vW7JI%;T|6oR(d#QeNtbV(% z`D$a1`To0x!vE4F{NErul*$gzm{D!-*St|5r8_-+pp*Je)J1ioZq$*w#+xUL@V~S{ z@E`1eA+Q9dz!n$-YhW%Ghj)9n5LUq~*agF28B9C=gLN@T!*|vAWw|Mi z!c{m6cfB^C67jPy4)FZ`U(9pe+L~#1{mRUF=vXuR{^QN80l%nRx?j&L%^q-qnWMhX zth3bYk_*^nAvw7s(zH0$E^ELP=1j#TlJaK z?|3uk&L5dsw|T$ew=SQZJ0tJiXPNZs`TxZHFUemidk&Eux|->C{MyWY;21M!z;W`U z(nkg)>O!5U8+D|v)HxV?HYEQ0G2?Iuw!j!z19N^1@*aF!j0&q@7VJjhFo18c4+r1^ zoPZnT0at9QB+uX`9EGcJmNvkP1FFV<;t1Lj@+*J&V6$cQZf5=R=4M^_UR6pJ`W@>& z)-Bz`tXXM58#d)8|w>~a@U1-YZA8NL&YGyXb4ptxNq@ELXp-$9|I#O5a+>rV2w^xnfFo18c z4+r1^oPZng@SQH-;3gb}t8mtf16bEnHU86=Am*n{Az!~vYVy6mt#$dv`^zJZ(*O2_bvu90UC|(}%K67PO_Z6 z+%-i0XPS=FsSj}goQ1oLQ&x-r8tbFKPYjYy+1Y2}b3B<|(&B%sKdP^PmSV*HGS&{% zjXD;nu37$99tX)VQkn1IDx77UKFk05vqQQz%kn?-ZIrGpGvT=`{~H#bvixsY_R0i~ zC#yqa=6~hJRlK>z%KfJ1v6|(7&FeH7KlmR2`oF3B`lhOz|NF+wfAqk-3TwC4 zF5bhg|F0-JR{du7oFA`rxIWuQ-+9mQtY3Ha@jlD{`msR-Cz@jxCcuV=5wLQs#SRz( zOPPR^bg|hO`5$S|bIdla{dsG6j+xr)DDB_qsxhg@m5P-HD|Odrr|P?A_8Fcvrhemq zEdT3{{}G&MjcFJs%`7IshKCWb^0>tg7y?TkPP}?X@~FFdr^|hwYmEGN`e9AglDYev zNj-jL2DiA`48MGg#-ra?+Mx8A(lmW`kG?xs&+MmXqt_t1eCqNw%m2FPk%yBUivh3z zCcuV=5wP;U#SRz(OJK^sR<$niQQ5K@6aW2fv0YC&Do6V&PSc#{Q`%SYU+t?{t+}YL zl)h79e|?2Mdr#jD(K8+N?A~UPe&gA;e&T>E|7)NB5u7x!w#Mez9t?m5Fk!J_F#=YE z8DR$ufh8~%!AWMv0gZ|Ou5GmK`5f&T?0%{la$!I1tNcj&AEMWv`PV^=?OLv9hU(c% z^_#uSk~yvosC_KDI=j~_|LacY9!}u?Ue@+t04(^i;eQW?z!I1OTT6s7&sWr4ogGfn zeQgZXR5K?3No%i?&Z*8e*=|ZYe`MQ$G5PH*9wO~~-Npe~{?{ItA~^B; z{k?>fcGmV_z&rOcv9AD2v;km?HXwo%uilX|YEO=IxzBTrf&We)mG%4nww*IJ$97$6 zaWLeKhKxcyHZ}2W0tQbNqMMjP?6NtZkF^_rV4j zDbC}}%jEa*WO5t;S2;%r?pA02ALkK{cdB_f*(Qw4RhlN8tW1HE z%pDJat8f0-(fP=?+;Gx_l1SRjvIuH*MyP2O4sZCaDDc! zaFYDqpG=Gc;40?{!d*BV&3D)Dh4XNq?>|=xoY%~{hYla=Y|^WAKH=_mzl_syUA#ux z^Q!xOZ{5bdX7a7SGM~QnKT0R){cYm(4B2*DiuS$k;(#pwtIq!jPO9$r3p;KXCguqn zeT9*eln&PWll9r1!inz3OByd`UL3&QHaH7+;c)Ez0^jM*(a-I@!`#8mJ-kJ^tJLoa z-A8oQcaod4!zbDEi8-T`a|-K%_rWr%=J(xt4R77%J4N39E%tD|Q*@kS`d(Vg@rso( z{W##_T3a8mVfm>#H|qtRn-%~5AI{R@Or2fg{2$4PtjHX+b#?xZ_WKvhw(0x*G5N2{ zws#5}CuyHw6T8RwNpS#Ng|nP3ZtoX>%i=V5jlps5 z9Od3&?jGk3a_%7>(Q<+ABmB{SIN`6tiP<fe|;6tYmfE&gA}hXmfuT!e>yQA{e9>67mfO* z_!8lM1Sf8NS;L6~{2Yb5+%p81;WXUl-eK+@pVR+*%l(S7-Oa|24=`V?Jlft%%-y8i zPskl*-kn^LdwHB*YU4G!-cIl7ujj99{6!m3wK!mqaML>NIKVHL)7!QUq4NoPA1oW3 zFFrh``x>~nNqx!u*x1tJ|D%fk&(=Ilrl0?Lw)!fMD#lzM6E7nhG9oK7Bl}e8!0xY? zPMm(+YdqaWJ`4K&*0z1Fir zKQVWdy1O{}+}_E<-35AI+o?AEegrd|`{c#!$WgU8fIUN%ivv7=zngsB{<=ru$EK|G zBs1xz3(TMve=x%?ds05}HSvNwzGAsgEU>HaA6bwQS&B!DTqj-Q#dP8Hf3vdy4(f)F%Jq z>$&#)zF#ia22?%{_`tRS;W%KVJ0B*}w)o5O{Jrin?A<~>?-nz??{G8jnwRX@Y*DAr zG$yl2em^k(;u~u!%Kbz>RsOR7xoWfbkFt%wocexG-KZmVrOv^AjQbl|kr~+;pGFtx z1l^#cw}$J^yg2cIC!cq2&m!rZu^4PXdtB04_w(BMtoUuH7aw}#{I0*B%9sd* zJm4Oc`9Z?=IxJ^>?H35}A6ejZJnpY6(|*A+&dEF2Z20&n8yj#}IrkNFXKD1kh0*Oo zP)F7IySDv)ux|dgzUpznK#d1nr1N3+l>gGbV_)QGTwDG6GWBcA_R;&p^x4t+?mYRp zYvk)5m(QCm-}kwVgA@6ChadM^)M>fSqPb7!QJo|^aaU^X*`YdlQ#a~JU8%FHf3jCE z9$;MHbE_M4gs#vTx;{MG0nkpTkwzs{RJK%0J?*Yv-^}E$@XF2y4b9brJ ziC-7)v#P%O^NPe*b?5hk^{(1@0CB)T`7e#%_1!~b__`l*fbJ^ozlYLkdVjq>ds^Sk z)ieK;ulq#2V14N~ar^o4cw+IR-wjrrJhj(BT89xkP8F1;zW!djxS8sBpz8X2D{H## zn;jRBPS6cHLRaVv-JwHtiB8ciI!4#%9NpKp-w*0C5(lt8AoJn?Kd;~}9EQtq8g9e! z+5I|N&d(6{hh2G!8GdE1<^l$qk*!`;?DkJH?DE-W;Q0^PyUV%1m^(~s>pso8_WMD5 zRzD8-P`-Si{MTjD;ZaJbN{7;Cs{wjHMQORd^Txf><%4Snym9LF@}o25?=H6EWQ>&u zWyqAjsT*T?lX_gFI0PCArFBX> z^j-XMeLwDl@2%hO_@#WzO!aeb(>jcw+B0bDY8^&UFX{YE-8d(A$=st<=bKdbscHkZ zr0gdLW%uF%#yxk0bcim|DY_kEbzLHz*Jgh|mAsi82f$T0JN3rnEr;PUoQB&T#}&7V z^KieU%Y3ExrOR)Wd}AkShl|?3ZHAwFg}uw1d$m*Pq%z;vx8D!ewR&*?dRrYzJ{|h? zndE&q{$BKt4>ZD>%A`uf{nr|p~eRGm*y-EWi( z&stfKsVG;n8fGskil4#%$l{nm0G z?vLrRR%x?R?0%S{P9K_rOK&if{}Ed4>X_#aHbJLz8gc;W`rU zu2yXOveIykpWUuLbUVe-r^uHcA?`Eg*R)#t56V}&e>b)5)sne~ONNuJEXagx1uf?* zeqUXw@lU5K?=`uey|}6Bzi#fcOndd$F>=2~6Dt$4&3)hw$vVQyjO^&Z>O$vjSEZli z$8T-={h*%X`!g2@e5i4c)j7QWxmR<$USQ6E ztEH1aNjH#J#fMdE-{RHyoh12TdCPOlBk@4I@tSHJIwjsxbaeP647-gn;ks$y2p-|OzE zP8;RJSjY0VeA-0y#rkPo>t*uu2PawA+A#gR(^I;ye!md>jTaXjBwbxBo!ukdjh7A= zN|(!}(+!FTG;YoQDEbHJTe@eB!I=D4#r-?wd!zU9`E3_0tET_<%T?cTKz;H*z8)H` z-w)!%ivyV3>Y?_1g2mt1->Cnoc|{QYC{W%=^=XKG)I)>D@skk-DIV0n${ z@92F}M@V;PNQZgS ze&4wM-k(>noW}aM-)}e_c=5nq(&d@bX;0~PlyqDg()U8?zC<>-LAE$v-1o0jkG~Jg z7IMJSs$@y zc#bsF^j9Bo0P}xv7Vg5~Styg`({Gjx4~zy7gtz(=*ZK7jQu;aL8| zbvQ5Xcb%j3PrVPUUDNnVQTqjE#JQK4S+{BaHo^Me_;T*Le&1n{S2w@CM!F$Y=Nv14 zzS;NDZGqE=n>UEkx7HZA?=$%|Iu5u`<3`Evr-!puT^|5f;Vj&R!|~StTF#66g&q57 zZ@|-9C-jceTBQvdYyCofEUjPA9Ol#;kF|RVlI{PA?EBbX{jt+DCeyE0<;w&+``f)> zeUg0_d^Y=cdi+hM(`4V(w|@O~i^Ks{^^ZEwcJBTu_kl(B{~T*M442`w<+kPcdzSNX zf9NI0YHal~GvdmJwO(+d(p>HTEiuou=w&AKI7@p7_f5I~H?r^ZSBfK^689IY?b@dI ziPv#?^R~6K{;|Goo-IE;PB%t9MfZ=>jRUq>PQfk4Jm_b_xkUPl?El@@au_bdX}Ar? zN3{H-p5@MTVP`-Dla?h<>uVXn4BtV>*5u@MUU$DQ>|r zxCZCq;lN)G+=at%8BW7(I6mvX3oPg1{@SIi&E8Gt0qw6be$HAtQhy&})+}!B^=H%Q zU-A6Rd%9{ZxAwqyVt-~TzyEK$Hp-pP@8^CznX;cXJiKRZmfU91aRBEC{VS#4gj;Y7 zuEDufIu4IdiOW0>x8eA@vI8vV*Dgt3N0-09O>v+3Un|OvRlD+M#TM_YkFh-=|Lr}z zx{H(h__&jcJ68O6bEGQAUuO1wdbZ2zDYNwC$2i!B1Cwt(&2oZ!YT-yC{|UF?7+iyM z$!zN{C)sl~@m)pPiKZY;7+{EWSaj){oHdF0goZQDp`&1LZcNb{XRT*W5I@5$I0o0?T&BoeQFgrQG)Q&(O#DxLj5{d#y^Ax;|157a z1;h1K50C#Rh!efMd&lJ?6Z8LD%Q3hH=jyA@!E!6g4j2Est8O#I|HR`MUi`0n$gX`^ zF;7o>7bo}ex%~WVYusnq*K8ZHD`CKoaj?&Qv~U4V6knzDQj^C2Us{gAH8__kdRe!0 zAB{sOh)Ip1Y!uV$K?I7jn;+`C++HX!jh0B1tm`^9w!_r&hKwSVIm z8MA+*GIH1FvvoPXlS7SPEq=m(ix$|dp_ekp3$@RH~Ov3gS^O7 z^F}IzvM7_XsYBLQ>FXfY?{7=#_u&$pf?IGbvi_(3_Br}(&KQPw?mKs~?JZ7Us`?I< zTuW2L0o=o^@eX@_!|mBPKzj=OdgXmG-^zZNMI)M;RqD;!B4#7IR zzmwJ7J+rI7ruO@>`|cv+fB1c!1urhaDY&&{uE(`Z89R^IICvj(J6rGc;sDNTo@M(2 z**GAUPj=tS3hgn%&o4bt=VcwK^Rj+!%3eIjOujkK47s3(8Ggl`8XJ08_q#lyG)#Lx z$0$wJXQkQ)zEH34D}Aa|p`%c{>d)mzu(-OW1W!b>U<2dodp z0evLr2~m9ke;Zn#vejnU$!z(ReC&duS|`%;Ff;S+i*>H{ z#)ir?_wup!dw;5=`$coVP~!d}t;xvSqSvpKzE;xtWVzg}6EE={&*)kE8=bkOJiNTf z^HY8Iu`P=-DVsV_7wSaas3Ucy&WZGuOx8rdX=tDOF&n?XpRV7(#BzVh9LN2{daX|# zMQoZZ4oJKw$eR}+4wx-@Z*P?20mO8hKG<t;<|jrgJer`H}fjXN4{;Jykln)C@fD zMl<}fzsd)fY7fUMGqQ^J3P$3~#Ot(EJ%hbC?~FQ77wSaas3Ucy&eRw_HC7vyzNz+o8L2p)>ifujCc3+qI;n2#HKnf9nYtqb zvLF+(`S*JGWe?(~q5kccscL?IiDKp<@`DY-?+5LaIu77IqRPYp+j%;aBlqj8q)1>F-@!K%J>OG9U{wAsaGg@_AiB zJXiJas`>p7ZGX8T`~9H4V&eeT2UI2w=&SL76QkMye;XStFVp{yw2zT~9CIfNM;>n` zbnmXSTAmXI{w;r6X8YZ&r%d-AjGE?hRrB-Wg6}1R>aKAJWI-lmLq=pp=AfuZ~L`276@dsGs&8Q%EL7AqsNA~Uk119Y*gj0;rV?+;PG zx2^iU4P%@?$oEu#*GwF6QiF{HBJH8q2VSVY@U*^%>0Ij@&9pwc=i=&ri~AKC`$@JA zEWvo4xIc#b2XqhCsJ35fJZ^>3QZu5}Kb79l*c|7+Oi>!6G(zb~&F?*`^gt--J)ha{ zc!p=^+TYqd$V+*WH)T*(?3r>2>gH=F+`3O>MP@6zbbv0R#|2_zgH-scukW*dKe??U zxUX&;Fh^};-%uQ|TTJX&U-qv|x%jwkI_GG~3%i?1x9Gg23$8Q6FP&yawb__{|32*J zcgQENmrq`)v)q=Lk?nMb>u+9EoAR8--G*rW&fmk-na0;wbcQ-Kqu%19c7~Jf_3s^u4?`L#TNTRF1*;r z{Fy$#AC$kcebs22{2__3?bgvfVTfcBuNmzcr&f%vZaY z_^gDm@3j72{qH>W+jXaON!z#0Kf3A+l|E0KxewlI=G@;(d&iG46K-l|UVA3TY^|t0 z{*X6iP!?rUHg%vb)QP%LN9tOVue~4iOH#CH=m1@y6Lf=)>cciwEwk!=f3W)dZ7la2 z!n(ey>L=0f;>Q8(8A{y$Es>9j-1AHvP=EJ5CXy@ox!+H0S#^lk)8C?XGPC9TzDUUp z%N=p3=qlaA)>i%Yi*7U}r(I!Q`0XiX`P`qI&1;U3Z~mdy-8MI0e3)Z4epb8dZduby z8I(ntluaF|3w5Gy)RDSU=XCgwjOYTLpc{1Lbm-Te`>eiS{dKCE-)Aq+AdT}k4!<9? zSK>IJwbe^WXYMOWXMKQ+12TW#AiQCp40nG|?=#ViZlimx^Ah_;FFw||dnb*{w|~t% zdyVd4=-b}RyZbOR|Hd5i;gdP$n{|o(d_>2Ip9$I})!)hTq%PEnx=}~!N}Z`YG8E@+ zP1?Uf7w81tpd)mZ>H7z`19Hh6?t$$oF3%8lQ|;qpUEc>5<74t$H(I|R#98&?0PcOh zO>qGCKkn)C8#RafV129d_okIUFbhT;Y9`+Nd%I@7u9%rs(pdqWR8r~g*s6;>PTIwGj&IXk!}BJWl9(iNEhgY z{RHR;U7@q8=(@Ik<8S}C`_a4qwTJA;J@Cxiq*~A4xcz?6E)jhO=LP(6z*q4(BHnoO z$zQ4MovJqAdV3EfYf3V4{CM56ADSt*w=#pyAF96Y8e8{x+%M^@y*C{`(_CPw#@hd+ zc=$3itJhxUUG1mY9%nyIM2Gd)7R0Lqb*0YK9T|`XnUD<`_Mk0eO%u>G{e z?$p`VT{0jGG9eo>B5S-h0NtP?bcN2s_VMX5{8<{9s_ADV_gtN>`WK7)YlXqY<5gf6 z3>S7B*l7FvscaDOAE7va`LP~*%3rin%s5E>M%D_`2E>W4grl*Va~j=#p}h~9dttd7 zGF6#%^*wR%w`+3DC$mr182JRn!QNWlAoff0zm=>y7xn7N;z>*M_K4lgcKMlLJtMk` z{Vw=yef=G;j>v*c$cBvGIhV0>AFOjmN9YQjp}S1#=YTCR#(nRj+rMqG7r##fR=>8` z1;bz&OfL$rbIRQP`}KtzS1vJpS!s@$-gghpD_^Fuqp{K@ca+Du7a`35ckG?;na2NZ z%X3Wm<2mM){4>nN{Fh8&{`aONPM@DM-$%BcrZsW5$X6a6m-|jX4fT~a0NIccS>v5^ zk8aQrxMDy}FbtN#bi6$enIc<#*CXOLn&>>49IZFY(b|%u zw7yL1q7(4HB!8!T$u}A|Dm9aCI@5P9T4rEo<9~9@>%Yk{U>gHj*M|6@I@>B&Qei@Jr8Icv4gK-eLK}YBcouNB)Se0^X<2T{> z|E6< zWJ5+|9W9w-#sO#>S>GsKp)+)c4l7$uZ9N~E$Ni-+S1jzUk&ln<$9R2J&QY3aF$|W$ zbZymZSIf7etclj2@1Z>?xzgdgv3?@d-WGM*r17E`w2!2x#>$Qi>(}pd>Qgs%y>CG? zt!-_u`yJkuzwq|xIlt%D!H&E3RWs?f3(bO24QKspRrHEJkP%stIk@iG>O(q0SLp05 z=`K^&1N$)r)@JwXBkaxBc<$!J_!M@*FjxlDRnb{v{7psKKGNA~(%k^@U+c(X;;Yd7 zUxyWDP>V;+)ZS;>HSC$DE9)=l_v(zy>35XMUubUMx9*4cquNx+mz3z4lWqU7w%5J* zaU9fFvhRY=R^{)AK2EkWBYTWKq$6~N&g^_{2A(g@eu=5Q&lUC_*Zzeh^BKY}7zWE= zx+*$rjKA5i{4mA;*DH3KBL2tThvxZ-@6-odsD6K*`u%%py-3rH(yyOOTUH${fAN?Z zcFDiY=yq$gKK@I|wb_hnr+)v%GtBJ!Zq>S($bC-!-`0lx;o;_$I%jY(X9{~UM_sl% z$coI!jt=Y@1=0z+K}YBcojKWSTOY2RbYH>RTf_Gi_VR_nw-wjL-@gTR!7x|`)9L2l z7@zaoc#GzK=RWYXboYs{5j+1++&HT3M){EO>i7RT3dQ2er2jo`zt$6!6&-`>3eoG0AGOzQ2$fmn07MM zlpbAvyUJ@sujfgxbEVhsqvAuS-=a=m3M&ibC+<s&>!#0|ngxTJ>)xi5ba&hJ ziU%K3-TUdh>@H?XucOTygPNI*S}#*mc0`YiRj*mxT>EsiKdAf&?dScee9&>i;fZQ9 z&(hibm+P$FcINNrw>1-PyUZ+o@l5mO>J#@LvX&fN--do$h6NbTZS?PHh@26@~PgTil z)5_fxR~@Lh>KesW-kR~?{0d{ng&kI@@AahmU>C_x?4OZ1@yq}1cJWboUb}d2aZ3B} z%a8Urw)7Bv*4%QRXZ*iO_k9G9k?|#aU%>EYW^(VJm>Ku9)STPRO8s??$KU0Po)sQT zwLfQ(*4wH-qBQQ>Me1|Epzp@&j-MxOyUH0LGw!*>OzC;Jc}r_~x2=!VZ)a#8%Y3iT zs?Y`?D{TO>uagdFZ_o+4K}YDSs`^M*Z#D5;Mfm~3;HARi-z_GC^BLjsOko*JgKaQg z6J_s;dBEkVz0Z?A|5y545i>vJ>63jT8Xva$JSgM!8R2JyKZ$HtnP>fZd@ByF5kEiI z`1+umGj*2N0-fQtTxqK}4jDR67|fRFJ!6vvS8h?f zsxhmUuPGj%qTl|_3_7oe&L+IbEP3gtX8UK_W3@J#Zwz&`cs^3IjZee z`LD^s_}Ps*p7QH$;||TK&CM}`pVa*NA5Sy0Zttr#r_;@-4mx9|(+Z6@uGcsUcK~r_ znAS{b-ocMk+pg-fqFm1G<*d(5`i;&2>iosi?lDVqE;5^crkH))9%j=v?YC4NoE;m}tN$DjusHXD$A!(0)ON<6 zT>+NCG}taHJ*iRqt#}xTtgF00anwtSqc~48PCJ^rMmm00^C?$roa#r76n7E6abD)a z5jp0$9zQn)-TIiJI)iI;p3Vr%+mQHtfjDa^5^>*sUZTBMg`L-%v2EWs)6So4W?kOf zOz!q`yOxJKIGZ+Bf3{({&UG4at*|*>{9ha6x4d=gJ1n**-E@J*0le{9r?a@%Zzx+Op6Q889qP@eoYi#=4sqy_Cny*_rXKyp%hLg?U z^KUoBZReY3JL?RZWc=ow4S&*E-X$d8XNk@w8LMZCwKjL;@3xwuU0&20-rH^2i|6iT zwr$SLa~J9g7hrMbJtqsB{nTe#7Q=t=>x}~}u^0#IV7{*Em+9qf{#@&Q`<<^o(o@pk z$|U-G-wbZiS#w0qHBKBE@6L4jYPx<~G`IKeFtwRC9&N_vvj*+I8pGS5_pvdPYdbB^ zHO|KxWcvTX`+xb&u20tYJnMeve=X5iVe!?91-fj|`u+cCFXNxhgj#NdJ|6fu(9k+4~>c7K3dt4%Wdu*oOmk!8MQ5lm*8RSAz4+wC;kn z_9rTF=fhB?X-cmsy`=PFD3$89=F^-3%sJ1MK3+G^bz3UEZIIq#_fr+;eJ#DM(D=y+ zjh|%RKEG7>sEO~R#+z2}uXDB9n-Q0dOc*a&{>qQz=Xnpur`*QcdLvsJ^TNZ%y!fb> zc+Y2i$1^<3Z}@GZSb*=DTNrxzaIGz9qkYSfbr_9p93V1&eWEa0Agr#GAB$bL>Yekl z!D1cEgMBzq8{G2pbo_ZZQ_ZwHp4L4)AF0iZohNbn<7}j7<&a?HHco_J1g*hs!ViDZ=?T2OG0ssCYlzXzsvEyyr8%b8P{?;kV@B@(bF< z>z93{y1b@3<*RN7XkXE;e^*Lga zpKyc6-}(RRaeL87W8NOEG)hbO^nOu+xF1Tqx8LhKp5Z<10l%dUATRQ?eF2r>$AdSn zy;gO4Ms>SPX{*eED?zyyZANcf1ez zeisAK7DPRx?}-QK7mycul6SoEfTB)2RHqMAx5qRNa7>v0{@A!NwAo-2jDpp|4*!X5 zpEHVMa31bQx96GkH(29tg)v>m>HO4_8|}D1ed@2Y&Tr`pzgAy6_8bvxX2DFwcX$Pw=;m5r`9%2YHbvdD}Dk0xozsm;TD$RnXYR z0chXC^8mssnDy=j$y|PA64!%uv3|efH##?cy6#%IL3b|v*gFsAyT;}JEv~dz?#)5E zck}uSH8-~~HVzjb*~7!!-Q-)1nfIVEZ#)x<_3`%!IG*Zr;sN>#g}wWO^Zy%z&-cq1xgX@m7PDZtP#6ZwL3}rizZnx?PLBBu&f;L)V0kSk zmD8zQ@e}s|&X68&mJd2X_j^Uo$7pPErFZW1ZaU+fm zm@dkNL$%GHk1bc>EZnW$xSrOOb!9!n5XE=wZ+J-Q5zQO?u@=&!%9He%#!v25AL=^A z{1?mb|0Hy`U(-hCPn>UAw{&;y#s9JPjL0^r;kj*Wjf#Ft{<6-rI~eqpVMXCyf(F%1-oDvEQ4vV?O`08*8+#& zrZ@^$;Vj(s+JI=h1o4yT?~?o#%I6uaSLmv>As1?H^K|)uGqrButXfECD^Jq7c76ZU zUI*F!zW>ey{~DOOv14ZVu!{11&FuS6HwA4T)V|!0WBE_~uQuTMTjZxTUiWUXI9r82 zf1>^Xd66f1QwC+(@rFPcK-QV<(S-1;_6Y)FM=f!!;)Rx?@{C=(Viic{wqV9ZOzK?9XUoEEn?cScXi+8i*DVcbm zXI-~F%=^t5(9(=-Jy5Y-v^}H7{ciW-^mhttp8w=YA7HA=NHh*ul)FNH?eD_&6*_P1 zphn4mKQ_QFV}f9rJwg>_``03G_OslCqi_|@!d*C=isy;GFUno2_PmeUlw-vG`dioG z?_2rx+gRRLl<7R={@0n2TBBL4JI;Pzqk1Rg80vvPKbA*xF3H!vAfpr_cP}8ztaB-^ql_J%C|lj8~@AyvPXyYd36^5t1QY4jsem) zp^ns*I>(bG`&sSdG0Xqj*Ku9qxT5?Z#dDWy{O@Uv|3{Di*>yOYdmF2Gd`fTi(KSY2 zYvX^)n=&Yib%6dgK*X5TjXF|S>RgvP%9a(ue3t(aUF-_DSXaJ}nKj^4o#Q<~9E%;# z#ShaLXKwD*zlr}vwJ`p#Jjpxh{GaMZ9jPmI-W7D;5M{{ne;4C_*$>Q&`wlh3uDrn% z=6)12o&)#Y`k$8`)42ce(Aa-X%mKJOlqY#7T>m5gPu-{^bzP@AH$?923U$u%f7jyw z+9g^;(x<5zd|?NzC)fI4VsGCVF8-f2xr_nKzR&a1Zw*xpptbweS_8=1KjlGQ_-gf^F^ZqK+wL!N1QJtt8b)>G;xgm0A zSEzHA|GO6U?73fBOIr4#)^_%tqjg=LzjyqzuOU7%F3%Z$FaJf{)w*4mD%A$Sd*5%# zi#)?;f%&*k+1~zNogb*WQAgIjHpczGnbLM-`M>M%-@lG*%c^6v{^rj*L;6GQwTV6V z&y52-*Ugw&iq&8Ft9PCk;{n!}!)amxms}hW`a9|w{f4u^$b-DdGw!}0#{E?n>O|eB zqvL_U?(VbfYwcq*%m3Qfaed&J`(~%yez6&J{!_Y_Z%xcN05Lgj0DhV?e5d#I#_ZrR z{+xNi@cCb%wjj)T{mpl7?vURw-bWtfW#{{qw?FR3_tWpEF4T#-QOEkwO}5Mk=Ck~d z=wesG#U`DtxnRV;X5!7c>TAEOb2z@#I9%*G^R68jFP}ZVk1?f>X#7ro{0(s$UN0Ep z?df;t|MJRbe8)39%WwECdC;Z=c@Os~n>tVz>O|eB3ttJquTsaeec-&Cf#@dzM1hl*6VY&|J(<}>qot@f!DQ9=*@v%;yrzQzT+9510i@*4B#_!p`JM$iIJfE}vNsR6D8Q<}Y_4)cuqW-?ipR%a~b)im9R)1aHXW7@f z!(^8Kb*Jn4!1cP1bIhc>_cVXMROf+pdM$PwK)%^>l<|O&@23AgPX3%bJ($DiTz~rY zHt9W|@g2{&aen*T(C;1R-K*{Qt8D5(U8qxi=ptKYtlqQykJZVpgp-Y{a?Be=Ic9vX z|ED{7AJUnCi?l9ijpFfd!+dur7@49cQR z%BBv~g*xp@I&R3aWcgoxEY?R1n{Ju*oLSwu{m~1}&G_!enWxXa(G0zGiq-|Jv;4Mw zb{7NqUugqUq==|I5 z@@t>wm^B~mq4S##(Vo`hlulDRvzm$fl}}clKhfDx2bfJOc2ikR((<2vEOX!98@dO4 zQm-S;%zoGFZh$emr{_KS=e6?V-0>1wqgS2x=Hpf|8VKZ!p4dc~eSN<%^|C+Hm_<8MV*4_Tt%(%OY_OSxw`*vw42lMb z*bLiY!+3Rw`Ya7BL_J?m=X#}Ehb;e7a2tDn?>)0Ki7nSI+22gR>pbD<5i_dYzr~B~ z;)ORaRuiwI+pRalE_p}mW*?SM{b@>^ro*%Cn|3!#=N@4u-*mPadj4vf*jU0u4PzTG=E@t)84 zj%Rq5-|$=VATRQCJo1<2KJ&j)SL#gNk-^D>Y{-bL$ULg;w>B23b{{1$&VjA48MecQ z)D4?rTWpN2u{pLkV8G#F7kSO{Ke0`t{L?Z$*HvR!C*_z0H|}L7^xwyfyIFnyAvxx| zZ*$DoUmj{+c;tRlaOJz=|5w%TO9yaj<8IBljlb>yer|E6a4 zy}O%feYE#?WRQP}f9jjBUEEA#Vf%`Mx{s&q2%X`4wAu2-@w%h)R5P{5xn|gz z7uv*oKI1!{;aPseZ^?tbys6utNS$aas55m(24q1dWJ5+|Mdp}&%mMvWwOzng*bLiY zLu`pnu`M>n*4P}|8!!;!eH1nta9fb&f5fKFw*J>|73*!%xNyaDyPGXPJIs{j{Mt-B zJch3IkpD_U;#|T zler;&mgRqPTW(Tpx?r^Y(~XCl*;jNo(_4%)pch3IkpD_U;#{|lC`0H zpXGmYTUIPWpZ@bfi@=VadnpY{JudqSP48+D|v)Y&hq$5rH%jL0fE{ojY*OGoG`9i5{4 z_%Xx|Ycp(z4Y4IQ#kSZOTVr!<4+g*jnDEQgkl$zdAIpC~wzsT4K;v7jb)W7aGb&H9 z{cpZACCr5h8*aZ_ah~SE^45!sjQhd;=zX@}8T?JSUi)Tx>5Pnfbw9TD&dt$Y8t%#F zb#{O6?2IY;9y}+G1HM-N@PWI%xN|+aoha%2mFD0-GGp@}H&bu?iFsG+ZnnsOX9`BC z3w5Gy)RDSU=b#*(Y{-bL$Q&)F-M1{BqAPU9xg+S%uTyl4uF*NV#|E)+0Jhp;ZHEo9 zC3(Abk=PoWV|y?F7QlqlryncsvxfAV<$ru`;HOxxR#tkvw|};+V$-}hI1J)4__wfB zqIL4@k7d8yoPOFr`+qto?-}ooJnqTl9==y~UjEBZco-%wppOuX)c^;AW$`y{lYCZD zhu6*U^V*nMy_%WzO|eBqn%fz&hcbP@)^28N9YRPqdRnnE}c%%F}gBp&ZwRmR9`g3tRhj=G&J=YjHzO|79A3XObk5d3_3E$uK9agn zC+bEW)8$|AImdtYB%(8P7d)#39djNjI!E`k!L-GR_ALa<@c(Vg69&Knm;f7KBpJ6F z!gpEz$G2fc`JRd!PcVbdyU!H1|1=yA`Z1BtdvHT6Z{yUS+C%@acXp0FJ8P78))r@J zfmQI!SvvnvteR>+eX6pHuiC7&Eu}i2qp!}{JgVN$*|hPL`u)^}I#D<3m`aXB-@{{c zgs#vTx_P#=y|MyCc2xH92pOb2r}{?0t_UZ4Q&ZDRf)P*`xH|m&-Z^^z3^Iy8MZK`x<&o1@p^zG4}t}XxZ z{~9YuJg$;nn*bKT1lRy0j$3~0xX&8GYnJ~J{yTeZ$4`}>rM}*?-ags9#5lG79gZu- z#Vg-A{~w3{?o7=}@jq0aD?3p_B zT8z%plr0(molYyuf2Yrcwp8D$SXf~1G}r(mV5P2fSR3VJ`JZF_{nH&AchmW||6?Zh zxK3wPz9T=C|XBa#kA>Cp-Y{+;5Ws@&9rrcnAK|R3+7y&C_2JC>L zOy+vLx@GzA*=&RMe7rm^$BgOwQ&W8P6PlYUm+fQc`@kR=;!Hfwzn%ZT^63v5tzY-< zy})OM{fJ@$-WPWA=Bap|xhck~?K!%do8tG}o57gCTLqf0(tCSvuERKS1^K8<#st6~ z*qnK{_gl{0cfO79EJwr_=esy}yZEYY;?b+-`5W75t^Mw1^R^}toPIC)eSD3FkxG3= zUgQ~SVTzorKy0bcN2)9Xdpp{MOE~ zYAz66qjPl6Tp_lwzYpbOZKb@P>*m=ITk?CJ$Huly)z9hEy@Cxe0#?8b*a1UM?uPPO zqx^4qr`EF^r+p#K8=M~2F4q}(+I#ZXD_fb7xv$u@U(VL<)o}}-#F~=VG~dkF9cN>| z_$T?8JH4~8!5L#sHfhdjY$#S>jY{bsz45BK13Y}s&~I3yGEcaBNw}N$u=szg=c~eb zC@*{$`A!zzc^-Vz&*i?&c@KK?+xRPs?EqfkPhrQ;&ESh3Gt;{1+?;Xyczap2mc7<+ zO5WNpLs^tb+0=o$IDPVJ+f#9ox-&@PCDdybV}QS zjv4zy=jh(HB`Sw`K$j2s+B47f8*GRzu_?C2#^LxQpi}D3`arM(X21>@sx^Iv+S8iO zAFjgL0&%w}w=BvwemkXlUzEF4dGt}cd5p$Io7I~hD%1b+Kf8JL-e%?i?L)cZe))*# z^{7Fd5+Ab84Buk=?xC@Jd^)ivSmS(bd>Ql3yyB-O%9q(QbpH^-b2rcL*fVzZ8L=lA z1&`0G4DQhbgZ!TOb>0KwgxUz!xDe-p)3;PM{%v+YFRw)N21Q-An&J66Gv~5?+RJjd zx1UXWT6dZzHEt`LC;Mf)YVD!QqD;!B4nZ8@$Eh>^7a5QRnUD<`krkPd9UY(xs}t!4 z9ic09hVIZUx+E_W+;;j#_t?O16KsU7u$jH%V6ZobfK9P2HpbS8+Eru$D_{offFZ|E zKepUwiC!!7Y@$4>`Z-*Mvv3y<2kn#Y-^F>$)VI4|Hu|+GpMR*@faZz=_R-$Z{c16B zCfwfIce>b9m5k=d1btb zaeUUH;dk&sQ+s=O1*h~eixhvlSeDq*#-DoSv!afnGK5`l2miTK3tr%xz~rJ4URkbO z`nV)wR`4BO;~wx=#aHh%qq=P~)6bu-{cIQ5J*{?MdoB4D@@C&GWl<(&Q->%#xjJ_B z>W&Of9{7)pR!+%>?C1bppc8b1j>bz@whfW)!g^IaVRh^DDxITyY(U$AO|T6%!dBQU zJRTb=7u&k~46!+71jhl81+0LX>B0_c8^BVk{m+ST2!_Ejn64<>zZQA3pXDYTg{yED z?!w_H`$)cY|1Qa2AuK541P)Y{CmY9XDiJW1zj&(=w@=?~>Iym2O{ z+jQq+qvxf^o02@8g_mSrniv&-fse9SRh-HlnQqRBSN;Y&#G$M^#}|d;NZ|-95%)%= z(C1(btm1R*nqhJJg@-)<}oZf2cmAol~vM96oH@g2zJP+du`6V-Yq0ZDD8N&CFJJ~|IMRt5Nx}cpy zH|Pjm*|EG-dKH!_3tM0lY=e#B+0Nd1s`9Zhw#Me+dIvcntbmzu*N(6BItxEW86yS5 zQ+pj`F%7mojDz!<=Mdb4qi_|@!d*C=P|l#-F@JAR-;On1+5_MAam5o4DLtaIRsL8D z=~3lLddv*J{9ZG-MK?34$0gcJ{!{IF-AkBmnl`T2ENNyY-*JHUc3q=+>Ho&`PtgZF zb>ma`9@dn;TkK)g`76goUKxv?|G4L)Ebqa(d=mMjqhH%D2+za@Hr^07UU?!CZ#aC~ zGUc!EU5t6-AE(_ej9=&dHdx+Rl|TBb3Nxs~7&GPJe6x1hA!+NWvh4zSQwC*GCT06& zr*6ncU8ys5M+RiU_lNZ={u2XOe=Hr)PM{Mz&H?8G`TEQ7zqdM7d9)SSg!lm)VJpAw zT)DJE*ce-5bH)q6K)mw!9PEH0umq;k*H=?vc28mVC&KW>!t!+%(_q`fI5@8b4#7=v z6t2QqxI3nce4p@MwZvXD)^x2@9Px=_G0u`)Udu`4XbpK`hYwWtEX8*{s;PrD}mFTN950S>W&_f#m|4@8)Rl203DzUbYf+zqE7iO zw!uc&igt%K#mgi$a{V%(U%CF4%SrueBu+{Y&d{bEqhmM6*s{Jr?7 zhgIvZR2F!}e}PeAS=PbZc}V$G>mQYWFhBap1+BI#rktq$<;h8Lnhe9Kz9VnSpe)J^ z*2BqV{eK^??#O^F)+dK_imW!ak?iOokzD@#lj>DEArEYWjj)xQ$Fc1WIvwuW78_%0 zY@VoJ<;n#+UMq@jO=SHj+{6TH>3>!H~1*qUld*# zm$JWuf224u6&M{S9Fh_}!|%d8>E_{(v2gmrl(&%fMt*VbLmJak3=KbAoSG<4kN@&% z{Y!<@(^KjvmHrZaPu`Tl8R+r&@ATvPRn#3BkOi5L&FWOLA~PJM@1BzXKD{PvTYYxH zR;)L`c9e%Lu_?AC-oVz_-1g&yfq1s$Gq3}Oz!I2Bq!;%&*rdNdsmIC6Q|GUDtngxs zI9N$kX5D|z`4&mewyWp5+c==Zf7FL+uW^(n8cWSdX@`oX+UIjm7gKQg81*IACbTbn zqK$D0Owcc6Zkcf_{1)R=cAbg7W1WeO6@xm-*Lvc-)#?-f+YGy=RB5gm-Fcxt(_Ywo z_QOSwkvskqdotg}oHl;R^@nHcZ|1+J^dARDih{z7xs=7zV}RNhjojan;E@+VurQoB|C_N$?V|#hF*yu z8H2`u(0>Ip_>tN7dANzkLu_1{oXuuQw#I(O{bZ`xtE&a*7un)c?(p2 z;QXLayMzB>jvJr!@?X?%l~2T9;uGP8tvAn7#{?Ml+Q%Jc@cFYf2i8OFA}cZ@J35Hvp}3FEq&swoF3~BvMaSqGod?Tz z?G7BrX4uZI6Ot{lDYkWU3pSQYqF3q-cEAu=0#k|f;ywqPV08H9ns*mw6WOf(KWFb7 zoabC?t&7clPKoget$)n*V!OJ0jmIm$j*4;*7!cr&o|Az)ic)QtHRywX(0|O&OF$nUqZ(lIm1)A`3Fnk3mLcMP|FlL2YiZU*nzM z`M%X1Iz*S~6y2g@bdAo7uiCEiBlfiXS6SE!n_)X_h%K=*wzX|dg0|F?gFJPYvM>X7 zzz|phQ>pX?Ho+)Z1+#u^`QN*CsxCPeDGQD>jt}RbP+#ssr5lv~p|nzIr&3Mbag`#U zmik_4R9g=t!S>wgtSEPro_kd>-JLq4KGF~KkGcA7{ybg<{#aVBr z^1wLlA(#jIHq{m1EVtqKBqcb1fyQ(8Q94oS2Bo1&)0AFOdP(WUP}1258uzK`G-tpI z(wEj*-SN8cpgm;v{D|0oh<8rgO4+Y!B^I+3DM-@>PZI(Rg@wjp1CNv7CXj z?MF(n^HxDTz&+Yaa6{af=$tw3nHX{92a@4Y^X1yuxckesd+NK>^^DHNQkxqr$NkOR z7kpscoHCPhAHLx8whQ|U#DV+Fth-J%E8c2qc77utW%vEmMjN0!(XYy&EXt&8>X3^6 z$caqIhK!c`lG({0d_`C24Ber_U>k+5(K))0x|b!yal6J!Z49=fOzZdcU9g?|Q;RqD;!B4$0zwr`s_9)y^Snczh$q2c!E>tj^FKI&}JU zujm?`qkC-N$_V>^*$SIsyD6f^2!8RBN>tG)2!-2Zsn#XC%g5!rP!TDw}x^+6P%h%1HH_;sH`ML}CDfJWKXsoS> zo4AX1aEpA6vF@pHuEZFRw8w{=X?L{Kxpe;*@>7h(e;+r0TV}|A@Sa$e`Qhp6KeE1@ zedIy@7w3H|d#;f^pEb+opRcnxbbsG+FY&(o<)gKK@OLVoJ46EKW8jZM+-J_(?kA#8 z9BOB58<5gxsZY^$mMLm~uNi*cS!QO>z0JChn%0_4$(u4Li!v#DOqV$Q8o&L}X-Kcu zFH1&bMdolD5RemHp)+)c4ug6{*XSJGV}meWLgip9Y=-UJ96!$l?HB%CxV~Uxv&9OS z0Xt=-T6dBp{)Z=EacZyT!e(cS(S-e2VH=EtbueF-9LwagD#|sk)BA^JP>buOza=r_ zy3S5Tot9~AwV%e2PPY3g63MiAMN_kI=-%e}?k%-XS8IT-`a~Fn`;2?Wwk;MLU`PEj z_Um9jdE*y$!xec` z24zv^m@XAcN#ZGVDjDqfpJYNdWJFeEPSoE)SLh7gp+j_uPSGtoM%U;Z-P?9DWK-T# z7I73d!*){8W^y`Pi1@)n~d^anm6Q^|$t;z0I_qCz-+L z^*5v2E1r<;W9=OJ%<&6fOg~({gV=H21KwN}^Hr=BC1!<}LF`~x?_AzFW>m`oy5~-F zzgJ$Wv6bu0$o9|ZyARc#1Y(X5FImF~<8tY}57qk0|LSq50ZAw^wVZSK%Ta~v^ z{keXM>3)(>FV)2rc~b^uQD(9_IX$^H&hF2VY{UprcyP9t4r64IUBM0*0!v^jk^VL;|B*0xwXpfD z^b&~kl(*M^-C?mk>81-3_g@uDBr0Q9{yY+|pX80}w*6EwShT;yK1I3Pq~nh?Zt}S1 z0gejm%D?9dJ*``MsQlD5@>81Q=!B1oV_!alharCeE?5`FoDrB|Zz^k4-xw(TJmg_2 z(fS|nTwdKXle^56=nEaElseKV~?uoYZM8Y(47GVGx-P>xvd*5l&<2ecS$oV`aIQ*4FJ?4AnQ5L?ILh;2-SXpJ5(u*oVWq zaj?VMb-Q0o&w}k#>y?7`_y3IyHVJR=!=5`3Dl>crfZ8AY7VB2+n)SQAK5ZgDS(N*| zDQNwrxcRL1snwqUCvVE2EXwrD#e2^_q5ik(Ox=+IS>Rt-KDDXr2jY8lz`49mFTp(1 z`P-JO7zx{8Pi%$FY#S~cx;=n)U)5mG*4Uh9!9cwJ2jzktFa(ysl*5;M1$%D|*Inl~ zU88*7Rb29YjBoJz{VOb{!8RCozt6r#_jf#f9vIT6Vm-xY*w&8$PoJx#&%dj!xK#Q) zAV#0-mhP*5^ciY%A5uK1@fi68e;#h0$nl@`XmJ1C68R^5g}9IJ;FZ7GoAc@uUf+N| ztIbRG3f3*E4B;p0Z{!Fi#=#v@az26tAABr_73^C-+6V4*FFX7YX7FKP`p9nd`WP}{uKCO z_oxY{lwsQ(wL6ag_)XTYvyRQKKTOabj_$C=3_5?HnbP|T%|#s)qmP=x6M0hxWl?6l z@i+R^cKw~|Ox=+ohX0b)wyTN>&;hzYC+NoM$#FWaKJ^@*+xCV&yxwCIY=e!Qz5F`0 zwxnGx@@$RGu|2qmX;bwXu>{xwLtqI^1@#E_X7{^CSbS6Mq3#$C)yuttT`&xm%Sz8n z*k29i+Zg}$_l356akMb;w8m37FClh*sW|U@>2{?V9Umh8bh*cSgsaMgcY!A^Md2< zy^r(YfiWj|!JHTUPq<BN}Z`YGT43;dJO5+@shRy9oRNbIzcz+$d1uVXRa-X(W%Nn z=jfhiDT{T**ajQX*KqM$yfU#hHplisonkAn0%pJt7y?UR%IPuUr&xzPURc~9KNUM3 z33kCSSO(MX_kOIq&$6!({cc!(nBueRrMoHOf9&|Zhx_k@!M7Dx<;#ceE8I5?>-T%D zQDxoMN2Q0E3HM%ShU88(g`L;OoXg?mBiwcKu*%s{V3IK^{1b^jB=I8O5v$@i*t>#H zW?q>&C2&ex&o~^fHt9Wo+kLXaDd*YpyGa_Gn%vX#Rm@A-J+Ueij=Wvyl?QeiBjfxm z#;Ywx!Mw2PPj>xLo3+B=IJJXk#k7Mpjj0IkxkqiB*8i@Rta0W6Rafdv-R&7*lEwO8 zbc&3U6+I(6I>2wE6Lf=)&=r0e-C3Rb^y+ks&e1(Ku(r_eunjiCR@e;NIXhxg%H+Fb z=MHeb09XMtUO^bFCGP-j1|59j4&#H!g>|vuvky-&hnm28z7$r{w67Qb>~&n_a!mD zOn;QP*6tzD^Opa5p3m?<PVBAtSOPGqR%tbYb-* z-Jm0@C+V8c&>^}E>-Bn1*XZ1GUpBCL@T~Gd*Vqc1VLNPyEwL$m4Q!09vANZ0zE7{x zDHyTmsR}b-2Mh&u1=gnaK2z9xH10UHvlG|_!(bUq2lL7P9lsr@DARi2{(DGwxx&%A z(YP1B!usvBS$(hhYV-SO9N@@U{VvjZOuyv3>I=>E7BkK0Zdz9+ySv}pSD*cvH{yN@ z=bKoohF@ac8d#>kXp`~)KVXw{0yx8qv21)DeObnqn1AM+DTj41%~=>Wk&k}Qm>2ni z^J#s=iAOws5EFAAIr;Nk3{Is3+P~55mzzQ7-Yx%kobVNk)7rupWl<(&QwQ4z^4Z1q zhtvj8cdH}Gf=qDVoo9y3$c_$p&gwO!A9Ur~M0AKQ(J8t`$5z+UIl3o*Y!S{wx+Pz1 zhV8JSi?Oh+oyT=`_1PXQfDJGLR=^C{F~U$#k6>+fzn;S03yM)<_p3O3fn6{Rmceu| zpX}eUx%Iz| zchDuydK~Akx+#7Xrb^{Iz!q~;V9Umw^cD3SiE%$-)8}};zc-$QkFo1_gmcCc-x%oq z&3F@iVEmGQU2iy_TrW>BOB~Dj_46Lo^NOA6M+e&|{w~R9ZQPf_{2a9%7pt$Jd%#!3 z*JW*BjIt<`vZ(`gp-v9>{yHNAvLF+(AtSOPe_Xw)|Aem48M?FUg``V#N*lu8=$gOL zJvP7=b}ZI!AL$j_VMA<*O|h+gKBV)g`bw{01B`$b|9vWMPj#gK!#=-J>i4fw|L2=% zxoz75R=>8`1;bz&OxK1jc9lFMVCBeD?-U~S2zX7 z#G0HlWBZ$mF&#ebD_B*+hvBEfalFe%ujFONvcwa{z9^f-y0{=d-QUG|Tg}Ke@0zK7 z{$!Sx9v5i$Y9D8g2b4`6s0(#+^$WftOIR++Xxp`r>_NSvBXot%{5o{)D{(x!M(5}r z8!+DD^k`!br`N8Y4Y4KVV_V9O*H^dkl&}FtzzUcFI|dBNCy^rS)Xx;=*yqRogIK@h zVRe%VgWJ3Hc8vS{ZPR^x^uu(uWQ6KbT*r{UOhkWlQFwjw{UYO9rXmf2I2U2ZZri7r645LD|%S zx=^R!IDpk_h>PeG*^n_F|9IBL1n3H#!E-xq7}BR5&y$YPwT&gDdu%|SwrvjTHCVRl z5brr}eg1Zf4KM;$z)X;fU<-^rb9M`1?j5fklRd)t373zrA~qOnF$|W=glVuH%(Joo z9l=u*o$;R|-f({Eo{B45iKl~vl_k<;{P;h@|F@I!-`-cPyvs^Y(Votsn%51S8xyvh zhYxTV_DlZn+axE+#X38A zpyKooL)?$o#^Nu&*K@4zEtY;ySG?%OXnwmz+84!Ot93qG|7)~QQ+t9t#km*2@jKOP z9R7n<>;LgpA*?$8|H30_@;^nnB|7uASm$0o`y1_x?S7JZSAFm;X`D;v*GEmhr)=s# zU8oavqmGWxm3@ukzjPMm{~iBFN7_{8UsCcvg-)p>SO61X1B^Hw`LV-i;rPGDuOwrM zuqSyt$BX|RUca^&2FqX?Y=d#H4T$A{E4MMcMsyqU`=dPWkI8Q>y~^)*{y;j89jkMg zvh#l(w@R;*q}Og~-4C+9{0C;n?MIrS=ij1v8{LZ_U0-@;k9$>M(}SF%_3ql|9#yqVei_h#7(hnekBXvv)SO61X1B@i%U*vv|BZRr@ zg}tf5>Q`~clOkn(Ww8vV!8UEcB4Irn2Y7gm_4|XQPtNHJj3@hSOPkBx3S;u;OP@WY z&!0)3&BJz!v@Ow`;0)JAIzx8y&1Y+TZHi*EP4ZXC+V2EyKq!`F?uxl*=A{^qW{e8H zyX3z=fcW2zDd@KmY^495wXvh}J}^(W?PDhY@#kjan$+?p!f{P}?$FKi{K!Y(Tq_tY49+E?&QXlf?p<02_(U`Sr`o-F}Nk9WU(N zrgC2u7IjBn(m3FIi)q?`#vTVm^q9==CqARo^ZVPR#~IRpPsLLwi2F^$+)svWH*Q^C zs&l#e-mCR%8Vk(xogbGjZbSTM&YE?nGw$~Ml{>?qy@WPB?#)-T24;LWVOudnRs0ld z(w^=7of+D7j_G%e?vs2n$E>vH{6)8K$z-q1@2CrPqHffYx+c7*Cy9CnC`zf7u()BZy z^@+;dV(Vbr3D-Z-UDU;G7np)>{mj#k9d0Jg)jY>~@Yy6RyOXo-Uz^yvP$%j}9jWUW z?p#Nwk^$Y?@&7#534E&0xsy8fd>1=TC|{1Q&>6b(>k{3fV|0zq(LFYZZBwz8e28qv zS>l`eba8jAZ@^%D(m%Z9N^=()0caV z=rri}+emMN)iyHLR~f&5QWW=HIqAG^DA)SJ-g}y%XI-edt7kMvqqE0_JNyXevX8iO zjnWjoFBDE*RJ*=Rb&C@x2;1NfyfP2{T(>w$pYwgp8acS(Qde=}H&3dpKg%C2Qqukt z>Z82*T~YhD%#ibMH_!Dr!TkG`=4RuL9P{nYqvBG>6NRc zSI@@S8k=K#FaQ?7L{;=b8?a=~DV2-^KD3x0lix6+jt2RQZoA#x26Q@Urm%4M#-gD#Q zYzV9P9@-6J`$Q>Kd|J}^znVYqqBFJ6R@{5PnSJMEGpWn#W=!h^W>m*Hrts7297){38* zt?TwR<@0~7HsERbip7c@6UQbtrUbJI65ofh9q@Jkzr8bo(xSTZIEqJ&3(6d`7>%40 z$plx_7`LD%f)P{@8d)ZjIZ2F)alws6Kpm6JWIUrWIwH-|DBVDeaRr42wnkR{NX#tnqMA#yt(D%V@%7qub7t~*~4sM{YZlBzSC#tQFWt^)Rj6@ zcVs}8^}p|HUVG*Mv+zRg>o{qU`hb_>`Hzlbbm?@QO{cWg?PtDf+wL!qAFa0BL**-G zW$UrFjcIGzoVh+Q02aVRryN=Q*tlzo_X8Rw>zUiUAJEnApJM%=`J-9eQ$4#++Xee0 z*Or^#>Sq3UqgOU>@Nuwd{k|H9zDoY%(|Ui5pNov;c9`p-&tRFDQFnglw`Re4$D0@C zbTc1s)$ecaW?s9Yn|boPniIXen|bWcZsz^J`}Oi=@Xt9}YIO|OiMmo}>W&P^f=tMU zj0?`w{EK9cikDTaE`#^cRA5kRiBM7bu(|S&{1p0ZNE^2U5b;rlIN)_b+&bv49J2^{3bFY>w{O^ zX=O(T<<={0MVl>A+ihI)*-oCvwR1Ujk@^8$`^co&{kS&v`>C$==K7A6ibF85uW1@^ zrI|DMDfI!$d#`b1*71~9w(5S(Jq`YwnciRd-zHyS-dle_Cr*p0Yur86KA%mWt}e)m z%*c)o&;{{COVcTBLfgD!+ln@$?Px>yyWwL#bq(JW_c`?gJpTAP4cq;}@^hxw?EB{$K00k=lwjqwQ$JTIi>E zKVZ7X0|wXqctEDzZ`6EH_22hPY5UH3g0b4?HsPBZFTO&1&dHy8f2iE2&)JH*{sCP_ z4%T0)++-e__CLt9y|ZnUNhGpbJkY=ms4*ePz=r-?{7D zak|$=YOBR+vtq{eOVvqv{D4NSDXOpWfZTTfRlVoMkD)W?J7;KcU0?C@`=z#_yWh?e zXkMs&`A+g9dn$&@0V?EAm0wwOAqVTBD*vpQ()%i2?{1d+rH((lI@iMyS?L3i9UY(x zPbcW66y4G`Y9qB3ZASlHYrPid2fS*1<$Cl3GVT6!*?sM`sq#2~sXXxev&?aUQlA}n ze=UA9t}c~2Uke#)^P9duM;}o5I>D8;4@kNn;OjQ-eIL6Yv3Bm$@$w|v{Yqg!eQx#o z(xmCSa6dq6(nr*XAK>qcWDoD|dUxq>`>uMl`$@x1TA%vvGfE#1aDG64%dHjKXF(hx zIF}4;>=1~H&_lNVEOCE^?YmODpOiLA>yZquu%W3e)ty-x6#3L0$X`*5&{V9+4ed6}G{rj~a=dXs2(%(o9UX#!kUHyQivWMEU7`GqF zj+JSRl(+@Q;2NAuf*jPLqisLU#h$PA7Y}LuRc0PUx4v`kNbSKsyIpa$pHS?UOBBoL zt7gl4`R(6NimYipQXZFrfuJ7%_MdA$R9v`HocNu%(V;zL@$nPk794|XaITbkl=I$> zwmz!UZK~Vr;(vVoTZVUN5BP(Mk#VW{>(d7(XbdC8bUE>mJ}ZAbZT)x$ap5p=;tFwN zvG$o}=DT$E6K=sVxCZBvqUVmbLsX|0)or8rAHSc^yIiPwyj6{LtFQ?EK$qIR@9@T($J!&+$k1YaMO9#sB|O-Ij^}@pJv2|G!SYPLq6| zqvZQ&KiGTzGjsh;wd75I$8VEV7v|7Yg8^6meJmSb=Y z&Sh(x;O}I+Zo^8gZ(X^QX>Z$0&txs;m-PAb=B?$s+IhWv<{7zu(Ztajo0z9DiuD@* zi{I1ft@n9R<9}CaKJ!S$PU@jKk@D&|o4o0FsjicN0dJk`p5nk^;=(wM|2>~${7?HE z#Vt4n*Wg?(Ip8ii3*a)>!)-X;-nO^p{08kUe{ftk#bxTLe6#&z!`X{*iJm{o+&A_> z^T(Op&1PcGeCnsh^(h;c@1i-0UYh^=k>)5~$|=X(5r3DwS{FF-X4?n&@zA~Biu)Z` zkM!ubNvezb&_xi_a39RW{dpsA6c?6hz09WEc2nGfV{i@5MYVnKci=7@hRbjoZo~1# zlTNanhx^k{>8(7*0~GJ_$MP){CvnhHGvm~`=GJ~=&D~>;H7l3yX=6HlqFfJAzsI?* zc~NebJj*3d{9YgY1J4h5Rej%N>EVdf4@k!XV(}G+yLvqD+5J5}wfhq+_p{IY5nu8H z%c*&zZdRVFqXO%HqB#jyiNgkW;V@i=({LM(&l>ns%XzpzXXxc-&ahkb-mciJHcqm} z@P@sq{ARBxhWky5)zsTOKO=jdDf|r9B(7Mxm$m0`8vCAGuFdwkc_V*e`+$1#1A_f* z7>C)u3tyXlj>d3UFWGdO?Yr=Es{J`ww`jW`pEo(vPU?MG9Lu%-*XCstXW=d!hRble zS^1+pj{nJW9`4UmzJ__DmAf&VUz@m;L!L6TPB_IZzF1=f%6%H8yJ-22kYDo)%k8-% zvd7J1lMoS z50i70oX`~hCx=W>AN(KeBg5aNpELCpr!#b$p>MT;(_ye}+__pEgKb{A-7oC@7_P!u zxC@8zBf1v24(H*1_&fh|OT%9jPidTDs(e-BhP!p@IZCI|@@~6+06E}REZyI}kKKCQ zNj4|^y%!v#{LzOfuC&HkSM`vs+A;M9N*LGh?EQ|?NFtmQf1?jNqPu8H!AmsR||7_P!uxC@7~aXj01&BIrjrqjop)~gj) zWmR_mhjqNI`2j)OLe4QS|KqvR?OdA+{*EEfDi_@&ifenjVq0Ef7F>FaY}Ib5KaeN? zgZgz?^6dSd(&Y)#>E+UGi*)?B^!=R5v(kNw{LJsGEy(}s$N7tTmV0KUkI{DUaJ>)z zT-<+IV|j(V?-yqGoosLw&canzzH1)dW@eo+!7RS!VAGx_|D)yKcKiTu-^O9W z-9_rp?^m0?ZRY=r<}*~t|8x798#K21u(_?@Ph_ix%OCi+F(jR#C zUa{6z?QM0|_Q)~P;YjK7XVU4T((T(;$4=jKhPJB>?p9lj(fkp4VEw%$jtjLqX4-w_ zMb5gvhjISm-uJ^Y!c{m6cj0h0j%WL>82*RrQty61(3Ze`FOJid>d(ph+^#nJ*c)F_ z+uD2{!<9R9*hZ~`SR-5Yzp`0($#zYY4f~e-fq#nh2b|7=eWyFOtsS$;=-K;yq^kka z*<|VN9_jE|>GCz{^c}5@|Jdr-j%T57>3+`84QdPWz!#Pew%mFR$_3`V`Td~XuiiL# z68ImjkNWik{Qa6&$mXzLb4%9#oW9=k2mUVG^#|FopUam0tNelE%%(ScDKEw6v<7Q; zt>f_Gr)*F225uZ5+0ntqHJ`USX?x^&>FD32tDB{>r)BTo_w0QZyXnmzB`0odE$zPR z(~`h{R~L7zH$NcL?jI@}gWY#|p8avsI3LRM2b9-h*n6^JugjJ_FPnD1@>2b$S#tGv zv?l9Nt>xM^cHY40F4&j4b4fpDtFs%&U!eH>izXf}U3^zMxn8?2TTDoc;MjrG} zJbRyIj2hjSJ3nk8x(&)%YrF62T|WLZ{x?Bm3HiVOyE?dIw0`Qx58$10#gfCteP{PS z%xd@J`U2k{*rMEY%J(_&POas>O6xh#md!iPn>Tp#OR|Og$R>Wq#&@yn3F>5i+qHM_ z=+;LNs|J~nZIfhVj)ZaiY5hh@2a~0XyHsA0y(d0Ij$G+6eL=1LfF#&`r+fGhXW?#1 z<9`j}{~(S3PbRK)8NYWkt_^&@V7+YM<0>;{6EBl(9IW~6!?nhM z`EB}Me|{^h+vI)T(bk!|BLlJ^6S6f8d_s19ebIK_({a3>tL+E)cE7L2g{~4#syEIb z*Y2$UfwOQI4rgKyuEY5aX6~rRRhDI9S7R>Cnq!@L!ne($OXAo6#?>L$`K0*)LA&M6 z?;m1){&^#Rr#5}tzGG(UF*pXmzUS9HAjtt0xOjfprcV*}Qk=J{Dw7zodxsrZBS2^7{e_xFY z-6Wj6BAirjoIkE@S^o=X;VvAGTK@~@;r^V4DJnD7HZN(d(7QUmue||pYkkFP?H!n= zJp_Gn?*ECaN7VUf{Uyf_@a_KP>d#yAu=^hf%gcpn#Xe|gR9U3*yb6A8yULcL{94bB zZBiW;h({O559pP1-$#_bt9j1f2iD8hnYzyzx~bst68}AE%cnX-m*^DTHd^Ni1GI;*XU_e|39FzM&EgJfw}Mzws&(!5GTD|IDbff^f&Ez0AqsL^jXXAs2l6^sVjB% z)cXLRJvn;NS-t3z~&PSLG&jIPl+y02DVxoEqUEW7X8-rxV*({dOt!)dq; z$7c_mW;qY{7fsqxWq*wqXl&-{3FhHzf2#j4Qf}6x%~P{_m=6MLs)9Ojzg6rp_$>MW zZ@onA`~c7Hv##&5{O$e-^`k?lYAkDz#cEN zm*^DTqGNQ8&e8o`wLvxgfF#>}+9Sv-xC@8jGMt9naJ+T$P|Nv;#QpX*z1z3#qWPJ9 z%*NHd%;t3mn)lagy=vQ@%F)_g>s70hW2D-CKQxi9OxZ`idzxAnVT>snf6zt$Br{?$BejmFO=+4TafmkIMODW9nuYZ|C4 zb&j&{;d8#dUuSiLj=XhJ->|wvhv*WWqFZ!~uF*NVrw!(hCdXvFpHa$qK#)(qol2(N z590vt!eO`!r{OjnZ(sRYkMrVw_?~w@RBq-iUC)2AReiuGUC)K@EB14=eyisP6mR!$ z(>ucH@)ve9&&>AXpBNL}S9|sO?_kG4 z&^fxN4dx8Jt>S(_((QhiwiH(_X9IgJ;56Lsa$Vqi&h!0U3se>=&ca9jwUy+5xIUHk z12XOYLDuKb|9v03f17O2w=DivEFq_xudA>;eD?RXu#feNvR{YF#+{^fuII?+{X+f# zxgR#h`peA?Ys9Jl5VwvK|9fO@!>He{);a1%9jPmIc6<*WJ$t{=%8cyj09~LHbi-cX zeD+CsIPv-b+JZJIryme)m+HgrW2=Mwhs$spZioATj<%i6JFE9Lo7aBXy!hl1=7uRp zoBy7CsQL4w`s0^Ws#eBu9Xh5zd5VcNmL5cDEdc%WxWQ!*SjN;5^)KoxHDE zGUY_`@HPK#9=P%WGyD6$F*CnA$J{mMeDmz=zOrrp`=+n^?Dj6cKKb9*Rqy4AC+O-0 z_n)~PyML+tNA~gMn%`%ff4Sv;A$H&WUij$tzjUnXX5L%3gIV>s#t*Ojy=0Egio!y_NzTyn)^ZRj1kXk4+!R> z1T*BOAU1X5$=Ln+iDwH}ZNDh)!eO`!r{OjnpEd9q%Xzrp+%Q|ERpkw9d*+W2{?2A! zHF?PHHTP+6$}>%}Q#G1*F3k_{@(Yk-1)WgHISTj5J&ufw4TtTm{~5c#!s4kMc0Y_^ z_j%)*BQ&@5o9%}G$coHP{&E~M{eWq|1;Ng1b9p0qxSE{U235`E%9R_2wjVGOL7GAcq-H#UcdzCov_wQa`R=gi} zmCZFqzCm)1-ha`>_T4XkyWgn3;!I&d<64F+&32F2Y?MQLO#CxFFH~*; z#mb)YzkKhOUHyPl)Ykd?0bt{y@ncnfU@S$S;WDx8J8?7N8K zw#WJSaZ>JS(Hi#I8XH)mvBQowe;lw_oELMS-@g~{2eh2MPPtVzwn={Fvp3r|i`EH! zpnKo1yxn*DNcmqn|9zYk?+1X5*=HK%wz}^Q~*4*A?XlD5q-6@OBlA^N-BueZNt@U*+w-lRf2s>HK%PbjM6T zpfUG&fUp5Zz)H3}yl^CmcHhbF#s4M#$HX$k{~WJaq6g(2|JT)_+(*BEFU}7L_G{7q ze<)v%wLR1IzCXC4@B3lhr2H=*|9zZ<{eZ!?ZE53qqdqL!CW4)6?c)pMB;U_o{I7!) z|5x!pN{;`V4E~2@FUAju<~=?TWjApScU z-LW)3ptg43$&m8D{QP&ib;nZufF!@~I~m<^d*Hv{zZdHV)ZXs9_DcC*IsW@NDc%pL z7rXE9v_0_O$?A@U`T_GtEB3B^-|t`X`F&T%xMRxy%JDy}^R9kCe~SUI5ckesPj=tQ zx;^pV@85%dz>#Xto3-}oC9Qv0ul0_bRX$evNToyP$T6Y${ZVI$`@2_cU7u@@T*s9E zmFK^Ylb|24yTt%l023A)79(Iqm=Sis5LmkB{38Q6@#^b8v-0)Ng`wd0+Y|qTa`V6K z-Bw5Uu|IbY|F zP@KB%-oA62U!(ASe8sj+)O{)ctH&!JCte&OVh?}?Fab6^jDVHvEp`@+?G?a@S5N;L z)vI?DZsN{ukNgknf<0X9Dc`W1xQ3tC+U4FVhp6;X*-z*8NK}5Ul>fEik&hGdlM!QZ zzfUm&R(f0PfFT>7K$r?*EXn_G&-^c}Z?3J~cNj|fUwv-*IPoyx+9KQ0zo+{3%~r4Q zciS`n!@8)4&nf?t&8K?6P_T|E|LY4+DgW!My@Fgz;{PfC>kCgQ|Ld#0lF0dBoyyAp z)5n?CYx|hD*X&w1;~zm?)WiRI@qeIVHeP*<;${((IhlN+Zq%_!b&ZPuZR7uyIsQ*l zby3TD!BsdrOWZAK|6haR|D1k>S#n)(#b)2BVz`SMSBW}Ts7}lAj6#32nCo-%f3-Mu zo#JMGO>r}m+YeJW>PTIi8&+%GOxO4l^~nEnU?}PtQRj-mPSibc70$xlW^p*&4*7m= zIa~3622U{y#~)=@J+Y7N1L#}uFN!I|B_K8l@rgDp-@(QdWe%oO2IVaX<{2W7A-PP+ zXG~7>jyByN@!Q^W^_{NaTJGWA&gUl7Ub_0_;(yEV*AzGVdd1B?S#h(!pgy3x{D56O zeV~&X7wSTts2g>puGBe^LdiB9JE)lWG7GAcCjZqq0Q$@~_ zVBV4~TATCy3@?{?>-aAySAqA8#n&9H=lP{o^;t5xk6AMAb7t|y-aXvw_*Kci*8N5) z$6WlMJ94esp;7HHTKxLDZ0v#Zv%jh}D$+*+3U#4Q)Qvh)SLz(bSv~PTh#7}Num#4z z8kh@WkpIEA#i+2l=#mH=hVTvciPH-g-~=(dcm`atr9hs+O*jho;Vj&R!{K(w|8s|8 zB|K!GLtlbF!8--}ii;^tr%zB!va1xA@FK+~9d6?jd-(;3E$HVNB5xtNOUXUH;JiL& z<_X_6P5sZ+n#}QvX*5B<-L*8GK3>lqXJ+?5$ILpRzwYU+d-pV()dzI(FB0kp9WVrzz!ca5V_*%; zW#Vvx*A~Jmm<79F7%X?qquX6;iFdcTHHgs-7vKck@bFz6zbrS!QP1w*V7WVQ6mk5! zzK7-inV8^;k#GHgRrXzi@q|(e@d%rTDVNS!FY4L%YG1{LHb;q5Bu7Jb(;^krT9wC=gd+&$)4^T(Op?b@wUY;!g4_4xm&uoE5s z+l$SP8(s?uX*Z3xhuYAC<3Lp7`IDY#-MBfg!L2roa{$ z18cDy;5)D?%z|Ao43@#Pt1DP1FAvza@%hCCIAOU_BENXw$LII#KHSf2hy3|pY`>Am zedHHV?w#q%!E@J`^K8CBmvfYS#pEoXdg68Fjv*RH(zSWlS=Zh%9k(qI7tC=$_(~r#cJM7z>aq3*{ z-xy`ra66tQ_1MvNwBF;N+>ZGFr1)@jr#(z+yRdfz41pyul?!*_@4+q@2FqX?Y=d#I zPCh=cKTjM8-&ftwoAvu!YElRC5s{OOyoBT?b@_?OPx|n+KM@~Zkk43ntk&j+O}h7C zaq1G`>`RHqSw|at&)9oPe1clzp>n_P+5eBMtjL^H-n)8%9WVrzz*M2U1iN4uEZdyB zA9^{s!8(`+`!?U6vxikZ!g=Mw7`e>MAH^79_1_c7MRv!TTWwBK@)En;z$}r{@RJWJ5+|WiO`VSyGQ+ z2Mje0y00kygIzETmccY~f^o1;9^O07*s3v;TC}Nh0gCgM`y+4B@z>%%>jdChHSou8 zY>`giuz82cJ-&JEmpk#$zH%S=OhYuLJXO~ae}%p`pZ*fh{3eV%80BTZC^}$yMP@q1eU-Q*vcP= zn{>}h7Qje>3y61~aE&rkOj6xq^39a`lyr6+EXggImmu0RzlK z6M9PTm0P#y+OyOn>pohi93pO~C0n1bmY)>h_kvvjjgs?E zDuusqR=(ZxY1^o4kcIeqH(D8y6`7OP4&nNM9WVrzz!cb;H!7cTzzr6&U>6L7WiaiY z57xmv*e3_KpNBU}cjY|y>HGf3sUGJ??B6Z@OEr*cAk{#sfm8#j22u^A8b~#eY9Q4> ys)1AksRmLFq#8&ykZK^+K&pXM1E~g54Wt@KHIQl`)j+C&R0F97QVk?r1OE>+w}K!5 literal 0 HcmV?d00001 diff --git a/CheatStengine/src/CheatStengine/Core/ThreadPool.h b/CheatStengine/src/CheatStengine/Core/ThreadPool.h index 876bdc1..78b222d 100644 --- a/CheatStengine/src/CheatStengine/Core/ThreadPool.h +++ b/CheatStengine/src/CheatStengine/Core/ThreadPool.h @@ -1,3 +1,5 @@ +#pragma once + #include #include #include @@ -6,6 +8,7 @@ #include #include #include +#include #include class ThreadPool { @@ -51,9 +54,9 @@ class ThreadPool { } template - std::future> Enqueue(F&& f, Args&&... args) + std::future> Enqueue(F&& f, Args&&... args) { - using return_type = std::result_of_t; + using return_type = std::invoke_result_t; auto task = std::make_shared>( std::bind(std::forward(f), std::forward(args)...)); diff --git a/CheatStengine/src/CheatStengine/MainLayer.cpp b/CheatStengine/src/CheatStengine/MainLayer.cpp index da608a9..7e3b3fb 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.cpp +++ b/CheatStengine/src/CheatStengine/MainLayer.cpp @@ -11,15 +11,21 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include +#include + +#include + #include DEFINE_ENUM_SETTING(ProcessMode, @@ -35,6 +41,18 @@ MainLayer::MainLayer(Window& window) SettingsCategory& general = m_SettingsManager.AddCategory("General"); m_ProcessModeSetting = general.AddSetting>("Process Mode", "What should be used to interact with processes?", ProcessMode::WinAPI); + // MCP control server settings. Auth is off by default so an agent connects + // with just the URL; flip that on to require the bearer token. The toggle + // values are synced to the live server each frame in OnUpdate. The rest of + // the tab (enable/disable, Open Web, Install, live URL/token) is drawn by a + // McpControls so it can act immediately instead of on Apply. + SettingsCategory& server = m_SettingsManager.AddCategory("MCP"); + m_ServerEnabledSetting = server.AddSetting("Enable MCP Server", + "Master switch. When off, the control server is stopped and no agent can connect.", true); + m_RequireAuthSetting = server.AddSetting("Require Auth Token", + "When on, MCP requests must carry the bearer token. When off, the loopback bind is the only gate.", false); + server.AddSetting("MCP Server", [this]() { DrawMcpControls(); }); + // Register modals m_ModalManager.RegisterModal("Settings", BIND_FN(MainLayer::SettingsModal)); m_ModalManager.RegisterModal("Open Process", BIND_FN(MainLayer::OpenProcessModal)); @@ -84,10 +102,45 @@ MainLayer::MainLayer(Window& window) void MainLayer::OnAttach() { + // Bind the icon cache to the live D3D11 device so the process selector can + // show executable icons. GetGraphicsContext() is the concrete DX11 backend. + auto& context = static_cast(m_Window.GetGraphicsContext()); + m_IconCache.Initialize(context.GetDevice()); + + // Bring up the MCP control server so an external agent can drive the engine. + // Binds to loopback with a per-run bearer token (both logged on startup). + m_McpServer.Start(m_State); +} + +void MainLayer::OnDetach() +{ + m_McpServer.Stop(); + + // Release icon textures while the device is still alive. + m_IconCache.Shutdown(); } void MainLayer::OnUpdate(float deltaTime) { + // Run any tool work the server queued from its listener thread. Must happen + // on this (main) thread before we touch State below. + m_McpServer.DrainCommands(); + + // Keep the server's auth requirement in step with the settings toggle. + if (m_RequireAuthSetting) { + m_McpServer.SetAuthRequired(m_RequireAuthSetting->GetValue()); + } + + // Start or stop the control server to match the master enable toggle. + if (m_ServerEnabledSetting) { + bool wantRunning = m_ServerEnabledSetting->GetValue(); + if (wantRunning && !m_McpServer.IsRunning()) { + m_McpServer.Start(m_State); + } else if (!wantRunning && m_McpServer.IsRunning()) { + m_McpServer.Stop(); + } + } + if (m_State.Process->IsValid()) { m_State.Modules = m_State.Process->GetModuleEntries(); } @@ -139,6 +192,29 @@ void MainLayer::OpenProcess(uint32_t pid) } } +void MainLayer::DrawRowIcon(uint64_t texture) +{ + // Keep the glyph aligned with the row text and vertically centred against a + // one-line Selectable. A 0 texture still consumes the same width so labels + // line up whether or not an icon was available. + constexpr float iconSize = 16.0f; + float offset = (ImGui::GetTextLineHeight() - iconSize) * 0.5f; + if (offset > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offset); + } + + if (texture) { + ImGui::Image(static_cast(texture), ImVec2 { iconSize, iconSize }); + } else { + ImGui::Dummy(ImVec2 { iconSize, iconSize }); + } + + if (offset > 0.0f) { + ImGui::SetCursorPosY(ImGui::GetCursorPosY() - offset); + } + ImGui::SameLine(0.0f, 6.0f); +} + void MainLayer::OpenProcessModal(const std::string& name, const std::any& payload) { ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2 { 0.5f, 0.5f }); @@ -197,6 +273,14 @@ void MainLayer::DrawOpenProcessList() ImGui::PushID(static_cast(i)); + // Row icon: the process's own executable icon, falling back to the + // generic app glyph. Drawn inline so the Selectable still owns the row. + uint64_t icon = m_IconCache.GetProcessIcon(proc.th32ProcessID); + if (!icon) { + icon = m_IconCache.GetFallbackIcon(); + } + DrawRowIcon(icon); + if (ImGui::Selectable(proc.szExeFile)) { OpenProcess(proc.th32ProcessID); ImGui::CloseCurrentPopup(); @@ -233,6 +317,13 @@ void MainLayer::DrawOpenWindowList() ImGui::PushID(static_cast(i)); + // Row icon: the owning process's icon, resolved via its window handle. + uint64_t icon = m_IconCache.GetWindowIcon(window.Handle); + if (!icon) { + icon = m_IconCache.GetFallbackIcon(); + } + DrawRowIcon(icon); + if (ImGui::Selectable(window.Title.c_str())) { OpenProcess(window.Pid); ImGui::CloseCurrentPopup(); @@ -249,3 +340,60 @@ void MainLayer::SettingsModal(const std::string& name, const std::any& payload) { m_SettingsManager.DrawSettingsPopup(name); } + +void MainLayer::DrawMcpControls() +{ + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (!m_McpServer.IsRunning()) { + ImGui::TextDisabled("Server stopped. Enable it above to accept connections."); + return; + } + + const std::string& url = m_McpServer.GetUrl(); + const std::string& token = m_McpServer.GetToken(); + + ImGui::TextColored(ImVec4 { 0.5f, 0.73f, 1.0f, 1.0f }, "%s", url.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##url")) { + ImGui::SetClipboardText(url.c_str()); + } + + ImGui::TextDisabled("Endpoint"); + ImGui::SameLine(); + std::string endpoint = url + "/mcp"; + ImGui::TextUnformatted(endpoint.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##endpoint")) { + ImGui::SetClipboardText(endpoint.c_str()); + } + + ImGui::TextDisabled("Token"); + ImGui::SameLine(); + ImGui::TextUnformatted(token.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton("Copy##token")) { + ImGui::SetClipboardText(token.c_str()); + } + + ImGui::Spacing(); + + if (ImGui::Button("Open Web UI")) { + ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + ImGui::SameLine(); + if (ImGui::Button("Open Configuration")) { + std::string configUrl = url + "/config.html"; + ShellExecuteA(nullptr, "open", configUrl.c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + ImGui::SameLine(); + if (ImGui::Button("Reinstall Client Configs")) { + m_McpServer.ReinstallClients(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rewrite the MCP entries for Claude Code, Cursor, Claude Desktop, Codex and the skill."); + } +} + diff --git a/CheatStengine/src/CheatStengine/MainLayer.h b/CheatStengine/src/CheatStengine/MainLayer.h index a4ec26c..7513b41 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.h +++ b/CheatStengine/src/CheatStengine/MainLayer.h @@ -3,8 +3,11 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -15,6 +18,7 @@ class MainLayer final : public Layer { ~MainLayer() override = default; void OnAttach() override; + void OnDetach() override; void OnUpdate(float deltaTime) override; void OnImGuiRender() override; void OnImGuiRenderDock() override; @@ -41,8 +45,15 @@ class MainLayer final : public Layer { void DrawOpenProcessList(); void DrawOpenWindowList(); + // Draws a 16px row icon (or a blank same-size spacer when texture is 0) and + // advances the cursor so the following Selectable sits beside it. + static void DrawRowIcon(uint64_t texture); + void SettingsModal(const std::string& name, const std::any& payload); + // Draws the extra MCP tab controls (live URL/token, Open Web, Install). + void DrawMcpControls(); + private: Window& m_Window; @@ -57,11 +68,21 @@ class MainLayer final : public Layer { MenuBar m_MenuBar; TitleBar m_TitleBar; + // Agent-facing MCP control server. Owns its own listener thread; we only + // pump its command queue from OnUpdate so engine access stays on this thread. + Server::McpServer m_McpServer; + + // Small cache of process/window shell icons for the selector. Bound to the + // D3D11 device in OnAttach and released in OnDetach. + IconCache m_IconCache; + std::vector m_ProcessEntries; std::vector m_WindowEntries; // Settings EnumSetting* m_ProcessModeSetting; + ToggleSetting* m_ServerEnabledSetting = nullptr; + ToggleSetting* m_RequireAuthSetting = nullptr; friend class MenuBar; friend class DisassemblyPane; diff --git a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp index 39f4a79..78dda9c 100644 --- a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp +++ b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp @@ -1,6 +1,9 @@ #include "KernelProcess.h" #include + +#include + #include #define IOCTL_CS_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, 0x6969, METHOD_BUFFERED, FILE_ANY_ACCESS) diff --git a/CheatStengine/src/CheatStengine/Server/AuthManager.cpp b/CheatStengine/src/CheatStengine/Server/AuthManager.cpp new file mode 100644 index 0000000..1dd2e35 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/AuthManager.cpp @@ -0,0 +1,84 @@ +#include "AuthManager.h" + +#include + +#include + +#include + +#include +#include +#include + +#pragma comment(lib, "bcrypt.lib") + +namespace Server { + + AuthManager::AuthManager() + : m_Token(GenerateToken()) + { + } + + bool AuthManager::IsAuthorized(const httplib::Request& request) const + { + if (!m_Required) { + return true; + } + + auto it = request.headers.find("Authorization"); + if (it == request.headers.end()) { + return false; + } + + std::string value = it->second; + + constexpr std::string_view scheme = "bearer "; + if (value.size() >= scheme.size()) { + std::string prefix = value.substr(0, scheme.size()); + std::ranges::transform(prefix, prefix.begin(), ::tolower); + if (prefix == scheme) { + value = value.substr(scheme.size()); + } + } + + return ConstantTimeEquals(value, m_Token); + } + + std::string AuthManager::GenerateToken(size_t byteCount) + { + std::vector bytes(byteCount); + + NTSTATUS status = BCryptGenRandom(nullptr, bytes.data(), static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != 0) { + WARN("BCryptGenRandom failed (0x{:X}); falling back to a weaker token source", static_cast(status)); + uint64_t ticks = GetTickCount64(); + for (size_t i = 0; i < bytes.size(); ++i) { + bytes[i] = static_cast(ticks >> (i % 8 * 8)) ^ static_cast(i * 131 + 7); + } + } + + static constexpr std::array hex { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + std::string token; + token.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + token.push_back(hex[byte >> 4]); + token.push_back(hex[byte & 0x0F]); + } + return token; + } + + bool AuthManager::ConstantTimeEquals(const std::string& a, const std::string& b) + { + size_t diff = a.size() ^ b.size(); + size_t length = b.size(); + for (size_t i = 0; i < length; ++i) { + uint8_t lhs = i < a.size() ? static_cast(a[i]) : 0; + diff |= static_cast(lhs ^ static_cast(b[i])); + } + return diff == 0; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/AuthManager.h b/CheatStengine/src/CheatStengine/Server/AuthManager.h new file mode 100644 index 0000000..67dc08e --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/AuthManager.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include + +namespace Server { + + class AuthManager { + public: + AuthManager(); + [[nodiscard]] const std::string& GetToken() const { return m_Token; } + [[nodiscard]] bool IsAuthorized(const httplib::Request& request) const; + + void SetRequired(bool required) { m_Required = required; } + [[nodiscard]] bool IsRequired() const { return m_Required; } + + private: + [[nodiscard]] static std::string GenerateToken(size_t byteCount = 32); + [[nodiscard]] static bool ConstantTimeEquals(const std::string& a, const std::string& b); + + std::string m_Token; + bool m_Required = false; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/CommandQueue.h b/CheatStengine/src/CheatStengine/Server/CommandQueue.h new file mode 100644 index 0000000..3e04983 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/CommandQueue.h @@ -0,0 +1,90 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +class CommandQueue { +public: + using Task = std::function; + + [[nodiscard]] std::future Push(Task task) + { + auto packaged = std::make_shared>(std::move(task)); + std::future future = packaged->get_future(); + + { + std::unique_lock lock(m_Mutex); + if (m_Stopping) { + lock.unlock(); + (*packaged) = std::packaged_task([] { + return nlohmann::json { { "error", "server shutting down" }, { "code", 503 } }; + }); + (*packaged)(); + return future; + } + + m_Tasks.emplace([packaged] { (*packaged)(); }); + } + + return future; + } + + void Drain(size_t maxTasks = 256) + { + for (size_t processed = 0; processed < maxTasks; ++processed) { + std::function task; + { + std::unique_lock lock(m_Mutex); + if (m_Tasks.empty()) { + return; + } + task = std::move(m_Tasks.front()); + m_Tasks.pop(); + } + task(); + } + } + + void Shutdown() + { + { + std::unique_lock lock(m_Mutex); + m_Stopping = true; + } + + while (true) { + std::function task; + { + std::unique_lock lock(m_Mutex); + if (m_Tasks.empty()) { + return; + } + task = std::move(m_Tasks.front()); + m_Tasks.pop(); + } + task(); + } + } + + void Reset() + { + std::unique_lock lock(m_Mutex); + m_Stopping = false; + } + + [[nodiscard]] size_t GetPendingCount() const + { + std::unique_lock lock(m_Mutex); + return m_Tasks.size(); + } + +private: + std::queue> m_Tasks; + mutable std::mutex m_Mutex; + std::atomic m_Stopping = false; +}; diff --git a/CheatStengine/src/CheatStengine/Server/JobManager.cpp b/CheatStengine/src/CheatStengine/Server/JobManager.cpp new file mode 100644 index 0000000..caa7796 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/JobManager.cpp @@ -0,0 +1,111 @@ +#include "JobManager.h" + +#include + +#include +#include +#include + +namespace Server { + + uint64_t JobManager::Submit(const std::string& label, std::function work) + { + uint64_t id; + { + std::lock_guard lock(m_Mutex); + id = m_NextId++; + m_Jobs.emplace(id, Job { id, label }); + } + + ++m_ActiveCount; + m_Pool.Enqueue([this, id, work = std::move(work)] { + nlohmann::json result; + std::string error; + bool failed = false; + + try { + result = work(); + } catch (const std::exception& e) { + failed = true; + error = e.what(); + ERR("Job {} failed: {}", id, error); + } + + { + std::lock_guard lock(m_Mutex); + auto it = m_Jobs.find(id); + if (it != m_Jobs.end()) { + if (failed) { + it->second.Status = Status::Failed; + it->second.Error = std::move(error); + } else { + it->second.Status = Status::Completed; + it->second.Result = std::move(result); + } + } + } + + --m_ActiveCount; + }); + + return id; + } + + nlohmann::json JobManager::Describe(uint64_t id) const + { + std::lock_guard lock(m_Mutex); + auto it = m_Jobs.find(id); + if (it == m_Jobs.end()) { + return nlohmann::json { { "error", "unknown job id" }, { "code", 404 } }; + } + + const Job& job = it->second; + nlohmann::json out { + { "id", job.Id }, + { "label", job.Label }, + { "status", StatusToString(job.Status) }, + }; + + if (job.Status == Status::Completed) { + out["result"] = job.Result; + } else if (job.Status == Status::Failed) { + out["error"] = job.Error; + } + + return out; + } + + nlohmann::json JobManager::DescribeAll() const + { + std::lock_guard lock(m_Mutex); + + std::vector ids; + ids.reserve(m_Jobs.size()); + for (const auto& [id, job] : m_Jobs) { + ids.push_back(id); + } + std::sort(ids.begin(), ids.end(), std::greater {}); + + nlohmann::json jobs = nlohmann::json::array(); + for (uint64_t id : ids) { + const Job& job = m_Jobs.at(id); + jobs.push_back({ + { "id", job.Id }, + { "label", job.Label }, + { "status", StatusToString(job.Status) }, + }); + } + return jobs; + } + + const char* JobManager::StatusToString(Status status) + { + switch (status) { + case Status::Running: return "running"; + case Status::Completed: return "completed"; + case Status::Failed: return "failed"; + default: return "unknown"; + } + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/JobManager.h b/CheatStengine/src/CheatStengine/Server/JobManager.h new file mode 100644 index 0000000..1a1f2f2 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/JobManager.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Server { + + class JobManager { + public: + enum class Status { + Running, + Completed, + Failed, + }; + + explicit JobManager(size_t numThreads = 2) + : m_Pool(numThreads) + { + } + + [[nodiscard]] uint64_t Submit(const std::string& label, std::function work); + [[nodiscard]] nlohmann::json Describe(uint64_t id) const; + [[nodiscard]] nlohmann::json DescribeAll() const; + [[nodiscard]] size_t GetActiveCount() const { return m_ActiveCount; } + [[nodiscard]] bool HasActiveJobs() const { return m_ActiveCount > 0; } + + private: + struct Job { + uint64_t Id; + std::string Label; + Status Status = Status::Running; + nlohmann::json Result; + std::string Error; + }; + + [[nodiscard]] static const char* StatusToString(Status status); + + ThreadPool m_Pool; + + mutable std::mutex m_Mutex; + std::unordered_map m_Jobs; + uint64_t m_NextId = 1; + std::atomic m_ActiveCount = 0; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp b/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp new file mode 100644 index 0000000..5ca357e --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpInstaller.cpp @@ -0,0 +1,378 @@ +#include "McpInstaller.h" + +#include + +#include + +#include + +#include + +#include +#include +#include + +#pragma comment(lib, "Shell32.lib") + +namespace Server { + + namespace { + + std::string ReadFile(const std::string& path) + { + std::ifstream stream(path, std::ios::binary); + if (!stream) { + return {}; + } + std::ostringstream buffer; + buffer << stream.rdbuf(); + return buffer.str(); + } + + bool WriteFile(const std::string& path, const std::string& content) + { + try { + std::filesystem::path fsPath(path); + if (fsPath.has_parent_path()) { + std::filesystem::create_directories(fsPath.parent_path()); + } + std::ofstream stream(path, std::ios::binary | std::ios::trunc); + if (!stream) { + WARN("MCP install: could not open '{}' for writing", path); + return false; + } + stream << content; + return true; + } catch (const std::exception& e) { + WARN("MCP install: write to '{}' failed: {}", path, e.what()); + return false; + } + } + + } + + void McpInstaller::InstallAll() const + { + WritePortableConfig(); + WriteClaudeCodeConfig(); + WriteCursorConfig(); + WriteClaudeDesktopConfig(); + WriteCodexConfig(); + WriteSkill(); + INFO("MCP client configs and skill written; agents can connect to {}/mcp", m_Url); + } + + nlohmann::json McpInstaller::HttpServerEntry() const + { + return { + { "type", "http" }, + { "url", m_Url + "/mcp" }, + { "headers", { { "Authorization", "Bearer " + m_Token } } }, + }; + } + + nlohmann::json McpInstaller::BridgeServerEntry() const + { + return { + { "command", "npx" }, + { "args", { "-y", "mcp-remote", m_Url + "/mcp", "--header", "Authorization: Bearer " + m_Token } }, + }; + } + + std::vector McpInstaller::ResolveTargets() const + { + std::vector targets; + + std::string exeDir = GetExecutableDir(); + targets.push_back({ "portable", "Portable (.mcp.json next to the exe)", + exeDir + "\\.mcp.json", !exeDir.empty(), false }); + + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + cwd.clear(); + } + targets.push_back({ "claude-code", "Claude Code (project .mcp.json)", + cwd.empty() ? std::string() : cwd + "\\.mcp.json", !cwd.empty(), false }); + targets.push_back({ "cursor", "Cursor (.cursor/mcp.json)", + cwd.empty() ? std::string() : cwd + "\\.cursor\\mcp.json", !cwd.empty(), false }); + + std::string appData = GetAppDataDir(); + targets.push_back({ "claude-desktop", "Claude Desktop (via mcp-remote)", + appData.empty() ? std::string() : appData + "\\Claude\\claude_desktop_config.json", + !appData.empty(), false }); + + std::string home = GetHomeDir(); + targets.push_back({ "codex", "Codex CLI (~/.codex/config.toml)", + home.empty() ? std::string() : home + "\\.codex\\config.toml", !home.empty(), false }); + targets.push_back({ "skill", "Claude Code skill (~/.claude/skills)", + home.empty() ? std::string() : home + "\\.claude\\skills\\cheatstengine\\SKILL.md", + !home.empty(), false }); + + return targets; + } + + bool McpInstaller::IsInstalled(const std::string& key, const std::string& path) const + { + if (path.empty()) { + return false; + } + std::string existing = ReadFile(path); + if (existing.empty()) { + return false; + } + + if (key == "codex") { + return existing.find("[mcp_servers.cheatstengine]") != std::string::npos; + } + if (key == "skill") { + return true; + } + + try { + nlohmann::json root = nlohmann::json::parse(existing); + return root.contains("mcpServers") && root["mcpServers"].is_object() + && root["mcpServers"].contains("cheatstengine"); + } catch (const std::exception&) { + return false; + } + } + + std::vector McpInstaller::Describe() const + { + std::vector targets = ResolveTargets(); + for (Target& t : targets) { + t.Installed = t.Available && IsInstalled(t.Key, t.Path); + } + return targets; + } + + bool McpInstaller::InstallOne(const std::string& key) const + { + if (key == "portable") { + WritePortableConfig(); + return true; + } + if (key == "claude-code") { + WriteClaudeCodeConfig(); + return true; + } + if (key == "cursor") { + WriteCursorConfig(); + return true; + } + if (key == "claude-desktop") { + WriteClaudeDesktopConfig(); + return true; + } + if (key == "codex") { + WriteCodexConfig(); + return true; + } + if (key == "skill") { + WriteSkill(); + return true; + } + WARN("MCP install: unknown target '{}'", key); + return false; + } + + void McpInstaller::MergeJsonServer(const std::string& path, const std::string& topKey, + const std::string& serverKey, const std::string& serverJson) const + { + nlohmann::json root = nlohmann::json::object(); + + std::string existing = ReadFile(path); + if (!existing.empty()) { + try { + root = nlohmann::json::parse(existing); + } catch (const std::exception&) { + WARN("MCP install: '{}' wasn't valid JSON; rewriting it", path); + root = nlohmann::json::object(); + } + } + + if (!root.contains(topKey) || !root[topKey].is_object()) { + root[topKey] = nlohmann::json::object(); + } + + try { + root[topKey][serverKey] = nlohmann::json::parse(serverJson); + } catch (const std::exception& e) { + WARN("MCP install: internal server json invalid: {}", e.what()); + return; + } + + WriteFile(path, root.dump(2)); + } + + void McpInstaller::WritePortableConfig() const + { + nlohmann::json root { { "mcpServers", { { "cheatstengine", HttpServerEntry() } } } }; + + std::string dir = GetExecutableDir(); + std::string payload = root.dump(2); + WriteFile(dir + "\\.mcp.json", payload); + WriteFile(dir + "\\mcp.json", payload); + } + + void McpInstaller::WriteClaudeCodeConfig() const + { + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + return; + } + if (cwd == GetExecutableDir()) { + return; + } + + MergeJsonServer(cwd + "\\.mcp.json", "mcpServers", "cheatstengine", HttpServerEntry().dump()); + } + + void McpInstaller::WriteCursorConfig() const + { + std::string cwd; + try { + cwd = std::filesystem::current_path().string(); + } catch (const std::exception&) { + return; + } + + MergeJsonServer(cwd + "\\.cursor\\mcp.json", "mcpServers", "cheatstengine", HttpServerEntry().dump()); + } + + void McpInstaller::WriteClaudeDesktopConfig() const + { + std::string appData = GetAppDataDir(); + if (appData.empty()) { + return; + } + + MergeJsonServer(appData + "\\Claude\\claude_desktop_config.json", "mcpServers", "cheatstengine", + BridgeServerEntry().dump()); + } + + void McpInstaller::WriteCodexConfig() const + { + std::string home = GetHomeDir(); + if (home.empty()) { + return; + } + + std::string path = home + "\\.codex\\config.toml"; + std::string existing = ReadFile(path); + if (existing.find("[mcp_servers.cheatstengine]") != std::string::npos) { + return; // already registered + } + + std::ostringstream block; + block << "\n[mcp_servers.cheatstengine]\n"; + block << "command = \"npx\"\n"; + block << "args = [\"-y\", \"mcp-remote\", \"" << m_Url << "/mcp\", \"--header\", \"Authorization: Bearer " + << m_Token << "\"]\n"; + + WriteFile(path, existing + block.str()); + } + + std::string McpInstaller::SkillContent() const + { + std::ostringstream skill; + skill << "---\n"; + skill << "name: cheatstengine\n"; + skill << "description: Drive Cheat Stengine (a Windows reverse-engineering tool) over MCP: " + "list/attach processes, read/write and scan memory, disassemble, assemble, dissect " + "structs, and generate byte signatures. Use when inspecting or modifying a running " + "process's memory on this machine.\n"; + skill << "---\n\n"; + skill << "# Cheat Stengine MCP\n\n"; + skill << "This tool exposes a live reverse-engineering engine over MCP at `" << m_Url << "/mcp`.\n"; + skill << "Auth is off by default, so you can connect with just the URL. If the operator turns " + "auth on, send `Authorization: Bearer " << m_Token << "`.\n\n"; + skill << "## Workflow\n\n"; + skill << "1. `list_processes` - find the target's pid.\n"; + skill << "2. `open_process` { pid } - attach. Everything below needs an attached process.\n"; + skill << "3. Inspect: `list_modules`, `read_memory`, `query_memory`, `disassemble`, `dissect_struct`.\n"; + skill << "4. Resolve addresses symbolically with `resolve_address` { expression: \"module.dll+0x1234\" }; " + "most tools also accept an expression string directly in place of a numeric address.\n"; + skill << "5. Modify: `write_memory` { address, hex }, or `assemble` { source, address, write: true }.\n\n"; + skill << "## Async tools (poll, don't block)\n\n"; + skill << "- `pattern_scan` { pattern } returns a `jobId`. Poll `job_status` { id } until status is " + "`completed`, then read `result.matches`.\n"; + skill << "- `scan_value` { valueType, scanType, value } starts a value scan. Poll `scan_results` " + "until `scanning` is false. Refine with `scan_value` { ..., next: true }.\n\n"; + skill << "## Notes\n\n"; + skill << "- Addresses in results are hex strings like `0x7FF6...`.\n"; + skill << "- Value types: int8/16/32/64, uint8/16/32/64, float, double.\n"; + skill << "- Scan types: exact, bigger, smaller, between, unknown.\n"; + skill << "- `pattern` is space-separated hex with `??` wildcards, e.g. `48 8B ?? C3`.\n"; + + return skill.str(); + } + + void McpInstaller::WriteSkill() const + { + std::string content = SkillContent(); + + std::string home = GetHomeDir(); + if (!home.empty()) { + WriteFile(home + "\\.claude\\skills\\cheatstengine\\SKILL.md", content); + } + WriteFile(GetExecutableDir() + "\\SKILL.md", content); + } + + std::string McpInstaller::GetExecutableDir() + { + wchar_t buffer[MAX_PATH]; + DWORD len = GetModuleFileNameW(nullptr, buffer, MAX_PATH); + if (len == 0) { + return "."; + } + try { + return std::filesystem::path(std::wstring(buffer, len)).parent_path().string(); + } catch (const std::exception&) { + return "."; + } + } + + std::string McpInstaller::GetHomeDir() + { + if (const char* profile = std::getenv("USERPROFILE")) { + return profile; + } + + PWSTR path = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Profile, 0, nullptr, &path))) { + std::string result; + try { + result = std::filesystem::path(path).string(); + } catch (const std::exception&) { + } + CoTaskMemFree(path); + return result; + } + return {}; + } + + std::string McpInstaller::GetAppDataDir() + { + if (const char* appData = std::getenv("APPDATA")) { + return appData; + } + + PWSTR path = nullptr; + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &path))) { + std::string result; + try { + result = std::filesystem::path(path).string(); + } catch (const std::exception&) { + } + CoTaskMemFree(path); + return result; + } + return {}; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpInstaller.h b/CheatStengine/src/CheatStengine/Server/McpInstaller.h new file mode 100644 index 0000000..e38e0b6 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpInstaller.h @@ -0,0 +1,57 @@ +#pragma once + +#include + +#include +#include + +namespace Server { + + class McpInstaller { + public: + McpInstaller(std::string url, std::string token) + : m_Url(std::move(url)) + , m_Token(std::move(token)) + { + } + + struct Target { + std::string Key; + std::string Label; + std::string Path; + bool Available = true; + bool Installed = false; + }; + + void InstallAll() const; + bool InstallOne(const std::string& key) const; + [[nodiscard]] std::vector Describe() const; + + private: + [[nodiscard]] std::vector ResolveTargets() const; + + [[nodiscard]] bool IsInstalled(const std::string& key, const std::string& path) const; + + void WritePortableConfig() const; + void WriteClaudeCodeConfig() const; + void WriteCursorConfig() const; + void WriteClaudeDesktopConfig() const; + void WriteCodexConfig() const; + void WriteSkill() const; + + [[nodiscard]] nlohmann::json HttpServerEntry() const; + [[nodiscard]] nlohmann::json BridgeServerEntry() const; + [[nodiscard]] std::string SkillContent() const; + + void MergeJsonServer(const std::string& path, const std::string& topKey, + const std::string& serverKey, const std::string& serverJson) const; + + [[nodiscard]] static std::string GetExecutableDir(); + [[nodiscard]] static std::string GetHomeDir(); + [[nodiscard]] static std::string GetAppDataDir(); + + std::string m_Url; + std::string m_Token; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpServer.cpp b/CheatStengine/src/CheatStengine/Server/McpServer.cpp new file mode 100644 index 0000000..2c7dd1e --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpServer.cpp @@ -0,0 +1,396 @@ +#include "McpServer.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Server { + + static constexpr const char* kProtocolVersion = "0.1.45"; + + McpServer::McpServer() = default; + + McpServer::~McpServer() + { + Stop(); + } + + bool McpServer::Start(State& state, const std::string& host, uint16_t port, uint16_t portAttempts) + { + if (m_Running) { + return true; + } + + m_State = &state; + m_Host = host; + + m_CommandQueue.Reset(); + + if (!m_RoutesRegistered) { + RegisterRoutes(); + m_RoutesRegistered = true; + } + + uint16_t bound = 0; + for (uint16_t offset = 0; offset < portAttempts; ++offset) { + int candidate = static_cast(port) + offset; + if (candidate > 65535) { + break; // don't wrap past the valid port range + } + if (m_Http.bind_to_port(m_Host.c_str(), static_cast(candidate))) { + bound = static_cast(candidate); + break; + } + } + + if (bound == 0) { + ERR("MCP server could not bind any port in [{}, {}]", port, port + portAttempts - 1); + return false; + } + + m_Port = bound; + m_Url = std::format("http://{}:{}", m_Host, m_Port); + + m_Running = true; + m_ListenThread = std::thread([this] { + m_Http.listen_after_bind(); + }); + + INFO("MCP control server listening on {}", m_Url); + INFO("MCP endpoint: {}/mcp (auth {})", m_Url, m_Auth.IsRequired() ? "required" : "off"); + INFO("MCP bearer token: {}", m_Auth.GetToken()); + + McpInstaller(m_Url, m_Auth.GetToken()).InstallAll(); + return true; + } + + void McpServer::ReinstallClients() const + { + McpInstaller(m_Url, m_Auth.GetToken()).InstallAll(); + } + + void McpServer::Stop() + { + if (!m_Running) { + return; + } + + m_Running = false; + m_Http.stop(); + + m_CommandQueue.Shutdown(); + + if (m_ListenThread.joinable()) { + m_ListenThread.join(); + } + + if (m_Scanner) { + for (int waited = 0; m_Scanner->IsScanning() && waited < 5000; waited += 10) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + INFO("MCP control server stopped"); + } + + void McpServer::RegisterRoutes() + { + m_Http.Get("/", [](const httplib::Request&, httplib::Response& res) { + res.set_content(kDashboardHtml, "text/html"); + }); + + m_Http.Get("/config.html", [](const httplib::Request&, httplib::Response& res) { + res.set_content(kConfigHtml, "text/html"); + }); + + // Serve the branded icon straight off disk (Resources sits next to the + // executable). Browsers request /favicon.ico automatically, and the + // dashboard pages point their here too. + m_Http.Get("/favicon.ico", [](const httplib::Request&, httplib::Response& res) { + std::ifstream file("Resources/favicon.ico", std::ios::binary); + if (!file) { + res.status = 404; + return; + } + std::string bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + res.set_content(bytes, "image/x-icon"); + }); + + auto authorized = [this](const httplib::Request& req, httplib::Response& res) -> bool { + if (!m_Auth.IsAuthorized(req)) { + res.status = 401; + res.set_content(R"({"error":"unauthorized"})", "application/json"); + return false; + } + return true; + }; + + m_Http.Get("/status", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + nlohmann::json process = m_CommandQueue.Push([this]() -> nlohmann::json { + bool attached = m_State && m_State->Process && m_State->Process->IsValid(); + nlohmann::json out { { "attached", attached } }; + if (attached) { + out["processName"] = m_State->Process->GetName(); + out["pid"] = m_State->Process->GetPid(); + } + return out; + }) + .get(); + + process["sessions"] = m_Sessions.DescribeAll(); + process["jobs"] = m_Jobs.DescribeAll(); + res.set_content(process.dump(), "application/json"); + }); + + m_Http.Get("/config", [this](const httplib::Request&, httplib::Response& res) { + McpInstaller installer(m_Url, m_Auth.GetToken()); + nlohmann::json targets = nlohmann::json::array(); + for (const auto& t : installer.Describe()) { + targets.push_back({ + { "key", t.Key }, + { "label", t.Label }, + { "path", t.Path }, + { "available", t.Available }, + { "installed", t.Installed }, + }); + } + + nlohmann::json out { + { "url", m_Url }, + { "endpoint", m_Url + "/mcp" }, + { "token", m_Auth.GetToken() }, + { "authRequired", m_Auth.IsRequired() }, + { "targets", targets }, + }; + res.set_content(out.dump(), "application/json"); + }); + + m_Http.Post("/install", [this](const httplib::Request& req, httplib::Response& res) { + std::string target; + try { + nlohmann::json body = nlohmann::json::parse(req.body); + target = body.value("target", ""); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + + McpInstaller installer(m_Url, m_Auth.GetToken()); + bool ok = installer.InstallOne(target); + res.status = ok ? 200 : 400; + res.set_content(nlohmann::json { { "ok", ok }, { "target", target } }.dump(), "application/json"); + }); + + m_Http.Post("/auth", [this](const httplib::Request& req, httplib::Response& res) { + try { + nlohmann::json body = nlohmann::json::parse(req.body); + m_Auth.SetRequired(body.value("required", m_Auth.IsRequired())); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + res.set_content(nlohmann::json { { "ok", true }, { "authRequired", m_Auth.IsRequired() } }.dump(), + "application/json"); + }); + + // The tool catalogue with each tool's enabled flag, for the dashboard's + // control panel. Authenticated like /status. + m_Http.Get("/tools", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + res.set_content(nlohmann::json { { "tools", m_Tools.DescribeAll() } }.dump(), "application/json"); + }); + + // Enable or disable a single tool. Body: {"name":"read_memory","enabled":false}. + m_Http.Post("/tools/toggle", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + std::string name; + bool enabled = true; + try { + nlohmann::json body = nlohmann::json::parse(req.body); + name = body.value("name", ""); + enabled = body.value("enabled", true); + } catch (const std::exception&) { + res.status = 400; + res.set_content(R"({"ok":false,"error":"invalid json"})", "application/json"); + return; + } + bool ok = m_Tools.SetEnabled(name, enabled); + res.status = ok ? 200 : 404; + res.set_content(nlohmann::json { { "ok", ok }, { "name", name }, { "enabled", enabled } }.dump(), + "application/json"); + }); + + m_Http.Post("/mcp", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + // Keep the client's session alive on activity. MCP echoes the id from + // initialize back in this header; touching it stops an actively-used + // session from being reaped at its TTL and keeps /status honest. + if (auto it = req.headers.find("Mcp-Session-Id"); it != req.headers.end()) { + (void)m_Sessions.Touch(it->second); + } + + nlohmann::json request; + try { + request = nlohmann::json::parse(req.body); + } catch (const std::exception&) { + res.set_content(MakeError(nullptr, -32700, "parse error").dump(), "application/json"); + return; + } + + nlohmann::json response = HandleRpc(request); + if (response.is_null()) { + res.status = 202; + return; + } + res.set_content(response.dump(), "application/json"); + }); + + m_Http.Get("/sse", [this, authorized](const httplib::Request& req, httplib::Response& res) { + if (!authorized(req, res)) { + return; + } + + res.set_header("Content-Type", "text/event-stream"); + res.set_header("Cache-Control", "no-cache"); + res.set_chunked_content_provider("text/event-stream", + [this](size_t, httplib::DataSink& sink) -> bool { + std::string event = "event: endpoint\ndata: /mcp\n\n"; + if (!sink.write(event.data(), event.size())) { + return false; + } + std::this_thread::sleep_for(std::chrono::seconds(15)); + return m_Running.load(); + }); + }); + } + + nlohmann::json McpServer::HandleRpc(const nlohmann::json& request) + { + if (!request.is_object() || !request.contains("method")) { + return MakeError(request.value("id", nlohmann::json(nullptr)), -32600, "invalid request"); + } + + std::string method = request.value("method", ""); + nlohmann::json id = request.value("id", nlohmann::json(nullptr)); + nlohmann::json params = request.value("params", nlohmann::json::object()); + + bool isNotification = !request.contains("id"); + + if (method == "initialize") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", HandleInitialize(params) } }; + } + if (method == "notifications/initialized" || method == "notifications/cancelled") { + return nullptr; + } + if (method == "ping") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", nlohmann::json::object() } }; + } + if (method == "tools/list") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", { { "tools", m_Tools.ListTools() } } } }; + } + if (method == "tools/call") { + return { { "jsonrpc", "2.0" }, { "id", id }, { "result", HandleToolsCall(params) } }; + } + + if (isNotification) { + return nullptr; + } + return MakeError(id, -32601, "method not found: " + method); + } + + nlohmann::json McpServer::HandleInitialize(const nlohmann::json& params) + { + std::string clientName = "unknown"; + std::string clientVersion = "?"; + if (params.contains("clientInfo")) { + clientName = params["clientInfo"].value("name", clientName); + clientVersion = params["clientInfo"].value("version", clientVersion); + } + + std::string sessionId = m_Sessions.Create(clientName, clientVersion); + INFO("MCP client initialized: {} {} (session {})", clientName, clientVersion, sessionId.substr(0, 8)); + + return { + { "protocolVersion", kProtocolVersion }, + { "capabilities", { { "tools", nlohmann::json::object() } } }, + { "serverInfo", { { "name", "CheatStengine" }, { "version", "0.1.0" } } }, + { "sessionId", sessionId }, + }; + } + + nlohmann::json McpServer::HandleToolsCall(const nlohmann::json& params) + { + std::string name = params.value("name", ""); + nlohmann::json arguments = params.value("arguments", nlohmann::json::object()); + if (name.empty()) { + return { + { "isError", true }, + { "content", { { { "type", "text" }, { "text", "tools/call requires a tool name" } } } }, + }; + } + return DispatchToolCall(name, arguments); + } + + nlohmann::json McpServer::DispatchToolCall(const std::string& name, const nlohmann::json& arguments) + { + std::future future = m_CommandQueue.Push([this, name, arguments]() -> nlohmann::json { + try { + ToolContext context { *m_State, m_Jobs, m_Scanner }; + nlohmann::json result = m_Tools.CallTool(context, name, arguments); + return { { "ok", true }, { "data", result } }; + } catch (const std::exception& e) { + return { { "ok", false }, { "message", e.what() } }; + } + }); + + nlohmann::json outcome = future.get(); + + if (!outcome.value("ok", false)) { + // Normal handler failures carry "message"; a queue that's shutting + // down resolves the future with {"error","code":503} instead, so fall + // back to "error" before the generic text. + std::string message = outcome.contains("message") + ? outcome.value("message", "tool failed") + : outcome.value("error", "tool failed"); + return { + { "isError", true }, + { "content", { { { "type", "text" }, { "text", message } } } }, + }; + } + + nlohmann::json data = outcome["data"]; + return { + { "content", { { { "type", "text" }, { "text", data.dump(2) } } } }, + { "structuredContent", data }, + }; + } + + nlohmann::json McpServer::MakeError(const nlohmann::json& id, int code, const std::string& message) + { + return { + { "jsonrpc", "2.0" }, + { "id", id }, + { "error", { { "code", code }, { "message", message } } }, + }; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/McpServer.h b/CheatStengine/src/CheatStengine/Server/McpServer.h new file mode 100644 index 0000000..8b2ee9f --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/McpServer.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace Server { + + class McpServer { + public: + McpServer(); + ~McpServer(); + + McpServer(const McpServer&) = delete; + McpServer& operator=(const McpServer&) = delete; + + bool Start(State& state, const std::string& host = "127.0.0.1", uint16_t port = 13777, uint16_t portAttempts = 32); + + void Stop(); + + [[nodiscard]] bool IsRunning() const { return m_Running; } + + void DrainCommands() { m_CommandQueue.Drain(); } + + [[nodiscard]] const std::string& GetToken() const { return m_Auth.GetToken(); } + [[nodiscard]] uint16_t GetPort() const { return m_Port; } + [[nodiscard]] const std::string& GetUrl() const { return m_Url; } + void SetAuthRequired(bool required) { m_Auth.SetRequired(required); } + [[nodiscard]] bool IsAuthRequired() const { return m_Auth.IsRequired(); } + + [[nodiscard]] nlohmann::json DescribeTools() const { return m_Tools.DescribeAll(); } + bool SetToolEnabled(const std::string& name, bool enabled) { return m_Tools.SetEnabled(name, enabled); } + + void ReinstallClients() const; + + private: + void RegisterRoutes(); + [[nodiscard]] nlohmann::json HandleRpc(const nlohmann::json& request); + [[nodiscard]] nlohmann::json HandleInitialize(const nlohmann::json& params); + [[nodiscard]] nlohmann::json HandleToolsCall(const nlohmann::json& params); + [[nodiscard]] nlohmann::json DispatchToolCall(const std::string& name, const nlohmann::json& arguments); + [[nodiscard]] static nlohmann::json MakeError(const nlohmann::json& id, int code, const std::string& message); + + State* m_State = nullptr; + + AuthManager m_Auth; + SessionManager m_Sessions; + ToolRegistry m_Tools; + CommandQueue m_CommandQueue; + JobManager m_Jobs; + + std::unique_ptr m_Scanner; + + httplib::Server m_Http; + std::thread m_ListenThread; + std::string m_Host; + std::string m_Url; + uint16_t m_Port = 0; + std::atomic m_Running = false; + bool m_RoutesRegistered = false; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/SessionManager.cpp b/CheatStengine/src/CheatStengine/Server/SessionManager.cpp new file mode 100644 index 0000000..5f3a47c --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/SessionManager.cpp @@ -0,0 +1,117 @@ +#include "SessionManager.h" + +#include + +#include + +#include + +#pragma comment(lib, "bcrypt.lib") + +namespace Server { + + std::string SessionManager::Create(const std::string& clientName, const std::string& clientVersion) + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto now = std::chrono::steady_clock::now(); + std::string id = GenerateId(); + + m_Sessions.emplace(id, Session { + .Id = id, + .ClientName = clientName, + .ClientVersion = clientVersion, + .CreatedAt = now, + .LastSeenAt = now, + }); + + return id; + } + + bool SessionManager::Touch(const std::string& id) + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto it = m_Sessions.find(id); + if (it == m_Sessions.end()) { + return false; + } + + it->second.LastSeenAt = std::chrono::steady_clock::now(); + return true; + } + + void SessionManager::Remove(const std::string& id) + { + std::lock_guard lock(m_Mutex); + m_Sessions.erase(id); + } + + nlohmann::json SessionManager::DescribeAll() + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + + auto now = std::chrono::steady_clock::now(); + nlohmann::json sessions = nlohmann::json::array(); + for (const auto& [id, session] : m_Sessions) { + auto ageSeconds = std::chrono::duration_cast(now - session.CreatedAt).count(); + + sessions.push_back({ + { "id", id.substr(0, 8) }, + { "client", session.ClientName }, + { "version", session.ClientVersion }, + { "ageSeconds", ageSeconds }, + }); + } + return sessions; + } + + size_t SessionManager::GetActiveCount() + { + std::lock_guard lock(m_Mutex); + ReapExpired(); + return m_Sessions.size(); + } + + void SessionManager::ReapExpired() + { + auto now = std::chrono::steady_clock::now(); + for (auto it = m_Sessions.begin(); it != m_Sessions.end();) { + if (now - it->second.LastSeenAt > m_Ttl) { + it = m_Sessions.erase(it); + } else { + ++it; + } + } + } + + std::string SessionManager::GenerateId() + { + std::array bytes {}; + + NTSTATUS status = BCryptGenRandom(nullptr, bytes.data(), static_cast(bytes.size()), + BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (status != 0) { + static uint64_t counter = 0; + uint64_t seed = GetTickCount64() ^ (++counter << 24); + for (size_t i = 0; i < bytes.size(); ++i) { + bytes[i] = static_cast(seed >> ((i % 8) * 8)); + } + } + + static constexpr std::array hex { '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + std::string id; + id.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + id.push_back(hex[byte >> 4]); + id.push_back(hex[byte & 0x0F]); + } + return id; + } + +} diff --git a/CheatStengine/src/CheatStengine/Server/SessionManager.h b/CheatStengine/src/CheatStengine/Server/SessionManager.h new file mode 100644 index 0000000..77987b5 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/SessionManager.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace Server { + + class SessionManager { + public: + struct Session { + std::string Id; + std::string ClientName; + std::string ClientVersion; + std::chrono::steady_clock::time_point CreatedAt; + std::chrono::steady_clock::time_point LastSeenAt; + }; + + explicit SessionManager(std::chrono::seconds ttl = std::chrono::minutes(30)) + : m_Ttl(ttl) + { + } + + [[nodiscard]] std::string Create(const std::string& clientName, const std::string& clientVersion); + [[nodiscard]] bool Touch(const std::string& id); + void Remove(const std::string& id); + [[nodiscard]] nlohmann::json DescribeAll(); + [[nodiscard]] size_t GetActiveCount(); + + private: + void ReapExpired(); + + [[nodiscard]] static std::string GenerateId(); + + std::chrono::seconds m_Ttl; + + std::mutex m_Mutex; + std::unordered_map m_Sessions; + uint64_t m_NextId = 1; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp new file mode 100644 index 0000000..f71884d --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp @@ -0,0 +1,745 @@ +#include "ToolRegistry.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Server { + + namespace { + + [[noreturn]] void Fail(const std::string& message) + { + throw std::runtime_error(message); + } + + Process& RequireProcess(ToolContext& context) + { + if (!context.AppState.Process || !context.AppState.Process->IsValid()) { + Fail("no process is attached; call open_process first"); + } + return *context.AppState.Process; + } + + uintptr_t ResolveAddress(ToolContext& context, const nlohmann::json& value) + { + if (value.is_number_unsigned()) { + return value.get(); + } + + if (value.is_string()) { + Process& process = RequireProcess(context); + AddressEvaluator::Result result = AddressEvaluator::Evaluate(value.get(), process); + if (result.IsError()) { + Fail(std::format("could not resolve address expression: {}", value.get())); + } + return result.Value; + } + + Fail("address must be a number or an expression string"); + } + + const nlohmann::json& Require(const nlohmann::json& args, const std::string& key) + { + auto it = args.find(key); + if (it == args.end()) { + Fail(std::format("missing required argument '{}'", key)); + } + return *it; + } + + ValueType ParseValueType(const std::string& name) + { + static const std::unordered_map map { + { "int8", ValueType::Int8 }, { "int16", ValueType::Int16 }, + { "int32", ValueType::Int32 }, { "int64", ValueType::Int64 }, + { "uint8", ValueType::UInt8 }, { "uint16", ValueType::UInt16 }, + { "uint32", ValueType::UInt32 }, { "uint64", ValueType::UInt64 }, + { "float", ValueType::Float }, { "double", ValueType::Double } + }; + auto it = map.find(name); + if (it == map.end()) { + Fail(std::format("unknown value type '{}'", name)); + } + return it->second; + } + + ScanType ParseScanType(const std::string& name) + { + static const std::unordered_map map { + { "exact", ScanType::ExactValue }, + { "bigger", ScanType::BiggerThan }, + { "smaller", ScanType::SmallerThan }, + { "between", ScanType::ValueBetween }, + { "unknown", ScanType::UnknownInitialValue } + }; + auto it = map.find(name); + if (it == map.end()) { + Fail(std::format("unknown scan type '{}'", name)); + } + return it->second; + } + + } + + ToolRegistry::ToolRegistry() + { + RegisterCoreTools(); + RegisterMemoryTools(); + RegisterAnalysisTools(); + RegisterScanTools(); + } + + void ToolRegistry::Register(Tool tool) + { + m_Tools.push_back(std::move(tool)); + } + + nlohmann::json ToolRegistry::ListTools() const + { + std::lock_guard lock(m_Mutex); + nlohmann::json tools = nlohmann::json::array(); + for (const Tool& tool : m_Tools) { + if (auto it = m_Disabled.find(tool.Name); it != m_Disabled.end() && it->second) { + continue; // user switched this one off + } + tools.push_back({ + { "name", tool.Name }, + { "description", tool.Description }, + { "inputSchema", tool.InputSchema }, + }); + } + return tools; + } + + nlohmann::json ToolRegistry::DescribeAll() const + { + std::lock_guard lock(m_Mutex); + nlohmann::json tools = nlohmann::json::array(); + for (const Tool& tool : m_Tools) { + bool disabled = false; + if (auto it = m_Disabled.find(tool.Name); it != m_Disabled.end()) { + disabled = it->second; + } + tools.push_back({ + { "name", tool.Name }, + { "description", tool.Description }, + { "enabled", !disabled }, + }); + } + return tools; + } + + bool ToolRegistry::SetEnabled(const std::string& name, bool enabled) + { + std::lock_guard lock(m_Mutex); + auto known = std::ranges::find_if(m_Tools, [&name](const Tool& t) { return t.Name == name; }); + if (known == m_Tools.end()) { + return false; + } + m_Disabled[name] = !enabled; + return true; + } + + bool ToolRegistry::IsEnabled(const std::string& name) const + { + std::lock_guard lock(m_Mutex); + auto it = m_Disabled.find(name); + return it == m_Disabled.end() || !it->second; + } + + nlohmann::json ToolRegistry::CallTool(ToolContext& context, const std::string& name, const nlohmann::json& arguments) const + { + for (const Tool& tool : m_Tools) { + if (tool.Name != name) { + continue; + } + + { + std::lock_guard lock(m_Mutex); + if (auto it = m_Disabled.find(name); it != m_Disabled.end() && it->second) { + Fail(std::format("tool '{}' is disabled by the operator", name)); + } + } + + const nlohmann::json args = arguments.is_object() ? arguments : nlohmann::json::object(); + return tool.Invoke(context, args); + } + + Fail(std::format("unknown tool '{}'", name)); + } + + void ToolRegistry::RegisterCoreTools() + { + Register(Tool { + .Name = "list_processes", + .Description = "List running processes as { pid, name }. Use this to find a target to attach to.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext&, const nlohmann::json&) { + nlohmann::json list = nlohmann::json::array(); + for (const PROCESSENTRY32& entry : Process::EnumerateProcesses()) { + list.push_back({ { "pid", entry.th32ProcessID }, { "name", entry.szExeFile } }); + } + return nlohmann::json { { "processes", list }, { "count", list.size() } }; + }, + }); + + Register(Tool { + .Name = "open_process", + .Description = "Attach to a process by pid. Replaces any currently attached process.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "pid", { { "type", "integer" }, { "description", "Target process id" } } } } }, + { "required", { "pid" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + uint32_t pid = Require(args, "pid").get(); + + if (context.Jobs.HasActiveJobs()) { + Fail("a scan job is still running; wait for it to finish (poll job_status) before attaching"); + } + if (context.Scanner && context.Scanner->IsScanning()) { + Fail("a value scan is still running; wait for scan_results to report scanning=false before attaching"); + } + + context.Scanner.reset(); + + context.AppState.Process = Process::Create(pid, ProcessMode::WinAPI); + if (!context.AppState.Process || !context.AppState.Process->IsValid()) { + Fail(std::format("failed to attach to pid {}", pid)); + } + + context.AppState.Modules = context.AppState.Process->GetModuleEntries(true); + return nlohmann::json { + { "attached", true }, + { "pid", pid }, + { "name", context.AppState.Process->GetName() }, + { "moduleCount", context.AppState.Modules.size() }, + }; + }, + }); + + Register(Tool { + .Name = "get_status", + .Description = "Report whether a process is attached and its pid/name/module count.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext& context, const nlohmann::json&) { + bool attached = context.AppState.Process && context.AppState.Process->IsValid(); + nlohmann::json out { { "attached", attached } }; + if (attached) { + out["pid"] = context.AppState.Process->GetPid(); + out["name"] = context.AppState.Process->GetName(); + out["moduleCount"] = context.AppState.Modules.size(); + } + return out; + }, + }); + + Register(Tool { + .Name = "list_modules", + .Description = "List loaded modules of the attached process as { name, base, size }.", + .InputSchema = { { "type", "object" }, { "properties", nlohmann::json::object() } }, + .Invoke = [](ToolContext& context, const nlohmann::json&) { + Process& process = RequireProcess(context); + std::vector modules = process.GetModuleEntries(true); + context.AppState.Modules = modules; + + nlohmann::json list = nlohmann::json::array(); + for (const MODULEENTRY32& mod : modules) { + list.push_back({ + { "name", mod.szModule }, + { "base", std::format("0x{:X}", reinterpret_cast(mod.modBaseAddr)) }, + { "size", mod.modBaseSize }, + }); + } + return nlohmann::json { { "modules", list }, { "count", list.size() } }; + }, + }); + + Register(Tool { + .Name = "job_status", + .Description = "Query a long-running job (e.g. pattern_scan) by id, or list all jobs when id is omitted.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "id", { { "type", "integer" }, { "description", "Job id from a scan tool" } } } } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + auto it = args.find("id"); + if (it == args.end()) { + return nlohmann::json { { "jobs", context.Jobs.DescribeAll() } }; + } + return context.Jobs.Describe(it->get()); + }, + }); + } + + void ToolRegistry::RegisterMemoryTools() + { + Register(Tool { + .Name = "resolve_address", + .Description = "Evaluate an address expression like \"module.dll+0x1234\" or a pointer chain into a numeric address.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "expression", { { "type", "string" } } } } }, + { "required", { "expression" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + std::string expr = Require(args, "expression").get(); + AddressEvaluator::Result result = AddressEvaluator::Evaluate(expr, process); + if (result.IsError()) { + Fail(std::format("could not resolve '{}'", expr)); + } + return nlohmann::json { + { "expression", expr }, + { "address", std::format("0x{:X}", result.Value) }, + { "value", result.Value }, + }; + }, + }); + + Register(Tool { + .Name = "read_memory", + .Description = "Read up to 4096 bytes at an address (number or expression). Returns hex plus common integer interpretations.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "size", { { "type", "integer" }, { "description", "Byte count (1-4096, default 8)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t size = 8; + if (auto it = args.find("size"); it != args.end()) { + size = it->get(); + } + if (size == 0 || size > 4096) { + Fail("size must be between 1 and 4096"); + } + + std::vector bytes = process.ReadBytes(address, size); + if (bytes.empty()) { + Fail(std::format("read failed at 0x{:X} (unreadable region?)", address)); + } + + std::string hex; + hex.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + hex += std::format("{:02X}", byte); + } + + nlohmann::json out { + { "address", std::format("0x{:X}", address) }, + { "size", bytes.size() }, + { "hex", hex }, + }; + + auto decode = [&bytes](auto sample) { + std::memcpy(&sample, bytes.data(), sizeof(sample)); + return sample; + }; + + if (bytes.size() >= 1) out["u8"] = bytes[0]; + if (bytes.size() >= 2) out["u16"] = decode(uint16_t {}); + if (bytes.size() >= 4) { + out["u32"] = decode(uint32_t {}); + out["f32"] = decode(float {}); + } + if (bytes.size() >= 8) { + out["u64"] = decode(uint64_t {}); + out["f64"] = decode(double {}); + } + return out; + }, + }); + + Register(Tool { + .Name = "write_memory", + .Description = "Write raw bytes (hex string) at an address (number or expression). Length is taken from the hex.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "hex", { { "type", "string" }, { "description", "Bytes as hex, e.g. \"90 90\" or \"9090\"" } } } } }, + { "required", { "address", "hex" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + std::string hex = Require(args, "hex").get(); + + std::vector bytes; + std::string nibble; + for (char c : hex) { + if (std::isspace(static_cast(c))) continue; + if (!std::isxdigit(static_cast(c))) { + Fail(std::format("invalid hex digit '{}' in byte string", c)); + } + nibble += c; + if (nibble.size() == 2) { + bytes.push_back(static_cast(std::stoul(nibble, nullptr, 16))); + nibble.clear(); + } + } + if (!nibble.empty()) { + Fail("hex string has an odd number of digits"); + } + if (bytes.empty()) { + Fail("no bytes to write"); + } + + if (!process.WriteBuffer(address, bytes.data(), bytes.size())) { + Fail(std::format("write failed at 0x{:X} (protection?)", address)); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "written", bytes.size() }, + }; + }, + }); + + Register(Tool { + .Name = "query_memory", + .Description = "Query the memory region containing an address: base, size, state, protection, type.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "address", { { "description", "Numeric address or expression string" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + std::optional mbi = process.Query(address); + if (!mbi) { + Fail(std::format("query failed at 0x{:X}", address)); + } + return nlohmann::json { + { "baseAddress", std::format("0x{:X}", reinterpret_cast(mbi->BaseAddress)) }, + { "regionSize", mbi->RegionSize }, + { "state", std::format("0x{:X}", mbi->State) }, + { "protect", std::format("0x{:X}", mbi->Protect) }, + { "type", std::format("0x{:X}", mbi->Type) }, + }; + }, + }); + } + + void ToolRegistry::RegisterAnalysisTools() + { + Register(Tool { + .Name = "disassemble", + .Description = "Disassemble up to 64 instructions at an address (number or expression). Returns address, bytes and text per line.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "count", { { "type", "integer" }, { "description", "Instruction count (1-64, default 10)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t count = 10; + if (auto it = args.find("count"); it != args.end()) { + count = it->get(); + } + count = std::clamp(count, 1, 64); + + std::vector code = process.ReadBytes(address, count * 16); + if (code.empty()) { + Fail(std::format("read failed at 0x{:X}", address)); + } + + zasm::Decoder decoder(zasm::MachineMode::AMD64); + nlohmann::json lines = nlohmann::json::array(); + + size_t offset = 0; + for (size_t i = 0; i < count && offset < code.size(); ++i) { + uintptr_t lineAddress = address + offset; + zasm::Decoder::Result res = decoder.decode(code.data() + offset, code.size() - offset, lineAddress); + if (!res) { + break; + } + + const zasm::InstructionDetail& detail = res.value(); + FormattedInstruction formatted = Formatter::Format(detail.getInstruction()); + + std::string bytes; + for (size_t b = 0; b < detail.getLength(); ++b) { + bytes += std::format("{:02X}", code[offset + b]); + } + + lines.push_back({ + { "address", std::format("0x{:X}", lineAddress) }, + { "bytes", bytes }, + { "text", formatted.Text }, + }); + offset += detail.getLength(); + } + return nlohmann::json { { "instructions", lines }, { "count", lines.size() } }; + }, + }); + + Register(Tool { + .Name = "assemble", + .Description = "Assemble x86-64 source into machine code bytes. If write=true and a process is attached, patches them at address.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "source", { { "type", "string" }, { "description", "Assembly, newline separated" } } }, + { "address", { { "description", "Base address for encoding / patch target" } } }, + { "write", { { "type", "boolean" }, { "description", "Write bytes to the process (default false)" } } } } }, + { "required", { "source" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + std::string source = Require(args, "source").get(); + + uintptr_t base = 0; + bool write = args.value("write", false); + if (auto it = args.find("address"); it != args.end()) { + base = ResolveAddress(context, *it); + } + + Assembly::Assembler assembler; + assembler.Assemble(source); + + zasm::Program program(zasm::MachineMode::AMD64); + zasm::x86::Assembler a(program); + for (const zasm::Instruction& instruction : assembler.GetInstructions()) { + a.emit(instruction); + } + + zasm::Serializer serializer; + if (serializer.serialize(program, base).getCode() != zasm::ErrorCode::None) { + Fail("assembler failed to serialize the given source"); + } + + const uint8_t* code = serializer.getCode(); + size_t codeSize = serializer.getCodeSize(); + if (codeSize == 0) { + Fail("assembled to zero bytes; check the source"); + } + + std::string hex; + for (size_t i = 0; i < codeSize; ++i) { + hex += std::format("{:02X}", code[i]); + } + + nlohmann::json out { { "hex", hex }, { "size", codeSize } }; + + if (write) { + Process& process = RequireProcess(context); + if (!process.WriteBuffer(base, code, codeSize)) { + Fail(std::format("assembled ok but write failed at 0x{:X}", base)); + } + out["written"] = true; + out["address"] = std::format("0x{:X}", base); + } + return out; + }, + }); + + Register(Tool { + .Name = "dissect_struct", + .Description = "Interpret memory at an address as a struct: for each 8-byte slot report the raw qword and, if it points into readable memory, that it looks like a pointer.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "address", { { "description", "Numeric address or expression string" } } }, + { "size", { { "type", "integer" }, { "description", "Struct size in bytes (8-1024, default 64)" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + Process& process = RequireProcess(context); + uintptr_t address = ResolveAddress(context, Require(args, "address")); + + size_t size = 64; + if (auto it = args.find("size"); it != args.end()) { + size = it->get(); + } + size = std::clamp(size, 8, 1024); + + nlohmann::json fields = nlohmann::json::array(); + for (size_t offset = 0; offset + 8 <= size; offset += 8) { + uintptr_t slot = address + offset; + uintptr_t qword = process.Read(slot); + + nlohmann::json field { + { "offset", std::format("0x{:X}", offset) }, + { "value", std::format("0x{:X}", qword) }, + }; + if (qword != 0 && process.IsAddressReadable(qword)) { + field["isPointer"] = true; + } + fields.push_back(field); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "size", size }, + { "fields", fields }, + }; + }, + }); + + Register(Tool { + .Name = "generate_pattern", + .Description = "Generate a unique byte signature (IDA-style) for the instruction(s) at an address, wildcarding volatile operands.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "address", { { "description", "Numeric address or expression string" } } } } }, + { "required", { "address" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + uintptr_t address = ResolveAddress(context, Require(args, "address")); + RequireProcess(context); + + PatternGenerator generator; + std::optional result = generator.Generate( + context.AppState.Process, address, 0x0, 0x7FFFFFFFFFFF); + if (!result) { + Fail(std::format("could not generate a pattern at 0x{:X}", address)); + } + return nlohmann::json { + { "address", std::format("0x{:X}", address) }, + { "pattern", result->pattern }, + { "byteCount", result->byteCount }, + { "instrCount", result->instrCount }, + { "wildcards", result->wildcards }, + }; + }, + }); + } + + void ToolRegistry::RegisterScanTools() + { + Register(Tool { + .Name = "pattern_scan", + .Description = "Scan the attached process for a byte pattern (\"48 8B ?? C3\"). Long-running: returns a jobId, then poll job_status.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "pattern", { { "type", "string" }, { "description", "Space-separated hex with ?? wildcards" } } }, + { "module", { { "type", "string" }, { "description", "Optional module to limit the scan to" } } } } }, + { "required", { "pattern" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + RequireProcess(context); + std::string pattern = Require(args, "pattern").get(); + std::string module = args.value("module", std::string {}); + + std::unique_ptr* processSlot = &context.AppState.Process; + std::string label = std::format("pattern_scan {}", pattern); + + uint64_t jobId = context.Jobs.Submit(label, [processSlot, pattern, module]() -> nlohmann::json { + PatternScanner scanner(*processSlot); + std::vector hits = module.empty() + ? scanner.PatternScan(pattern) + : scanner.PatternScan(pattern, module); + + nlohmann::json addresses = nlohmann::json::array(); + for (uintptr_t hit : hits) { + addresses.push_back(std::format("0x{:X}", hit)); + } + return nlohmann::json { { "matches", addresses }, { "count", addresses.size() } }; + }); + + return nlohmann::json { + { "jobId", jobId }, + { "status", "running" }, + { "hint", "poll job_status with this id" }, + }; + }, + }); + + Register(Tool { + .Name = "scan_value", + .Description = "Start a value scan over the attached process. Fire-and-poll: returns immediately, then use scan_results.", + .InputSchema = { + { "type", "object" }, + { "properties", { + { "valueType", { { "type", "string" }, { "description", "int8/16/32/64, uint8/16/32/64, float, double" } } }, + { "scanType", { { "type", "string" }, { "description", "exact, bigger, smaller, between, unknown" } } }, + { "value", { { "type", "string" }, { "description", "Target value for exact/bigger/smaller" } } }, + { "upper", { { "type", "string" }, { "description", "Upper bound for 'between'" } } }, + { "next", { { "type", "boolean" }, { "description", "Refine the previous scan instead of starting fresh" } } } } }, + { "required", { "valueType", "scanType" } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + RequireProcess(context); + + ValueType valueType = ParseValueType(Require(args, "valueType").get()); + ScanType scanType = ParseScanType(Require(args, "scanType").get()); + std::string value = args.value("value", std::string {}); + std::string upper = args.value("upper", std::string {}); + bool next = args.value("next", false); + + if (next) { + if (!context.Scanner) { + Fail("no active scan to refine; run scan_value without next=true first"); + } + if (context.Scanner->IsScanning()) { + Fail("a scan is already in progress"); + } + context.Scanner->NextScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper); + } else { + if (context.Scanner && context.Scanner->IsScanning()) { + Fail("a scan is already in progress; poll scan_results until scanning=false"); + } + context.Scanner = std::make_unique(context.AppState.Process); + if (!context.Scanner->FirstScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper)) { + context.Scanner.reset(); + Fail("failed to start scan (invalid value for the chosen type?)"); + } + } + + return nlohmann::json { + { "status", "scanning" }, + { "hint", "poll scan_results until scanning is false" }, + }; + }, + }); + + Register(Tool { + .Name = "scan_results", + .Description = "Report the current value-scan state: whether it's still scanning, the match count, and up to 100 result addresses.", + .InputSchema = { + { "type", "object" }, + { "properties", { { "limit", { { "type", "integer" }, { "description", "Max addresses to return (1-1000, default 100)" } } } } } }, + .Invoke = [](ToolContext& context, const nlohmann::json& args) { + if (!context.Scanner) { + return nlohmann::json { { "scanning", false }, { "count", 0 }, { "results", nlohmann::json::array() } }; + } + + size_t limit = 100; + if (auto it = args.find("limit"); it != args.end()) { + limit = std::clamp(it->get(), 1, 1000); + } + + std::lock_guard lock(context.Scanner->GetMutex()); + const std::vector& results = context.Scanner->GetResults(); + + nlohmann::json addresses = nlohmann::json::array(); + for (size_t i = 0; i < results.size() && i < limit; ++i) { + addresses.push_back(std::format("0x{:X}", results[i].Address)); + } + return nlohmann::json { + { "scanning", context.Scanner->IsScanning() }, + { "count", results.size() }, + { "results", addresses }, + }; + }, + }); + } + +} + + + + diff --git a/CheatStengine/src/CheatStengine/Server/ToolRegistry.h b/CheatStengine/src/CheatStengine/Server/ToolRegistry.h new file mode 100644 index 0000000..ca40fea --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/ToolRegistry.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace Server { + + struct ToolContext { + State& AppState; + JobManager& Jobs; + std::unique_ptr& Scanner; + }; + + struct Tool { + using Handler = std::function; + + std::string Name; + std::string Description; + nlohmann::json InputSchema; + Handler Invoke; + }; + + class ToolRegistry { + public: + ToolRegistry(); + + [[nodiscard]] nlohmann::json ListTools() const; + [[nodiscard]] nlohmann::json CallTool(ToolContext& context, const std::string& name, const nlohmann::json& arguments) const; + [[nodiscard]] nlohmann::json DescribeAll() const; + + bool SetEnabled(const std::string& name, bool enabled); + [[nodiscard]] bool IsEnabled(const std::string& name) const; + + private: + void Register(Tool tool); + + void RegisterCoreTools(); // process list / attach / modules / job status + void RegisterMemoryTools(); // read / write / query / resolve + void RegisterAnalysisTools(); // disassemble / assemble / dissect / pattern gen + void RegisterScanTools(); // pattern scan / value scan + + std::vector m_Tools; + + mutable std::mutex m_Mutex; + std::unordered_map m_Disabled; + }; + +} diff --git a/CheatStengine/src/CheatStengine/Server/WebUI.h b/CheatStengine/src/CheatStengine/Server/WebUI.h new file mode 100644 index 0000000..1ff4ce8 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Server/WebUI.h @@ -0,0 +1,367 @@ +#pragma once + +namespace Server { + + inline constexpr const char* kDashboardHtml = R"HTML( + + + + +Cheat Stengine — MCP + + + + +
+ +

Cheat Stengine — MCP

+ Configuration → +
+
+
+
Server
+
Attached process
+
Sessions
+
Active jobs
+
+ +
+

Sessions

+ +
IDClientVersionAge (s)
none
+
+ +
+

Jobs

+ +
IDLabelStatus
none
+
+ +
+

Tools

+
+ + +
+

loading…

+
+
+ + +)HTML"; + + inline constexpr const char* kConfigHtml = R"HTML( + + + + +Cheat Stengine — MCP Config + + + + +
+

Cheat Stengine — MCP Configuration

+ ← Dashboard +
+
+
+

Connection

+
+
Endpoint
+
Bearer token
+
+ Require auth + + +
+
+
+ +
+

Install targets

+

loading…

+
+
+
+ + +)HTML"; + +} diff --git a/CheatStengine/src/CheatStengine/Settings/McpControls.h b/CheatStengine/src/CheatStengine/Settings/McpControls.h new file mode 100644 index 0000000..1b86267 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/McpControls.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Setting.h" + +#include +#include + +class McpControls final : public Setting { +public: + explicit McpControls(std::string name, std::function draw) + : m_Name(std::move(name)) + , m_Draw(std::move(draw)) + { + } + + void Draw() override + { + if (m_Draw) { + m_Draw(); + } + } + + void Restore() override { } + void Apply() override { } + + [[nodiscard]] std::string GetName() const override { return m_Name; } + [[nodiscard]] std::string GetDescription() const override { return {}; } + [[nodiscard]] bool HasValueChanged() const override { return false; } + +private: + std::string m_Name; + std::function m_Draw; +}; diff --git a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h index 3c0cf80..e366699 100644 --- a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h +++ b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -108,5 +109,7 @@ class MemoryScanner { std::vector m_CurrentResults; std::mutex m_Mutex; - bool m_Scanning = false; + // Written on the detached scan thread, read from the main/HTTP thread; atomic + // so those accesses aren't a data race. + std::atomic m_Scanning = false; }; diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp new file mode 100644 index 0000000..218dc3e --- /dev/null +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp @@ -0,0 +1,218 @@ +#include "IconCache.h" + +#include + +#include + +#include +#include + +#include +#include + +#pragma comment(lib, "Shell32.lib") + +IconCache::~IconCache() +{ + Shutdown(); +} + +void IconCache::Initialize(ID3D11Device* device) +{ + m_Device = device; +} + +void IconCache::Shutdown() +{ + for (auto& [pid, texture] : m_ProcessIcons) { + if (texture) { + reinterpret_cast(texture)->Release(); + } + } + m_ProcessIcons.clear(); + + if (m_FallbackIcon) { + reinterpret_cast(m_FallbackIcon)->Release(); + m_FallbackIcon = 0; + } +} + +uint64_t IconCache::GetProcessIcon(uint32_t pid) +{ + if (!m_Device) { + return 0; + } + + if (auto it = m_ProcessIcons.find(pid); it != m_ProcessIcons.end()) { + return it->second; + } + + std::wstring path = GetProcessPath(pid); + uint64_t texture = path.empty() ? 0 : CreateTextureFromPath(path); + + m_ProcessIcons.emplace(pid, texture); + return texture; +} + +uint64_t IconCache::GetWindowIcon(void* hwnd) +{ + DWORD pid = 0; + GetWindowThreadProcessId(static_cast(hwnd), &pid); + if (pid == 0) { + return 0; + } + return GetProcessIcon(pid); +} + +uint64_t IconCache::GetFallbackIcon() +{ + if (!m_Device || m_FallbackIcon) { + return m_FallbackIcon; + } + + SHFILEINFOW info {}; + if (SHGetFileInfoW(L"app.exe", FILE_ATTRIBUTE_NORMAL, &info, sizeof(info), + SHGFI_ICON | SHGFI_SMALLICON | SHGFI_USEFILEATTRIBUTES) + && info.hIcon) { + m_FallbackIcon = CreateTextureFromIcon(info.hIcon); + DestroyIcon(info.hIcon); + } + return m_FallbackIcon; +} + +uint64_t IconCache::CreateTextureFromPath(const std::wstring& path) +{ + SHFILEINFOW info {}; + if (!SHGetFileInfoW(path.c_str(), 0, &info, sizeof(info), SHGFI_ICON | SHGFI_SMALLICON) + || !info.hIcon) { + return 0; + } + + uint64_t texture = CreateTextureFromIcon(info.hIcon); + DestroyIcon(info.hIcon); + return texture; +} + +uint64_t IconCache::CreateTextureFromIcon(HICON icon) +{ + ICONINFO iconInfo {}; + if (!GetIconInfo(icon, &iconInfo)) { + return 0; + } + + BITMAP bmp {}; + if (GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bmp) == 0) { + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + return 0; + } + + int width = bmp.bmWidth; + int height = bmp.bmHeight; + + BITMAPINFO bmi {}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + std::vector pixels(static_cast(width) * height); + + HDC screenDc = GetDC(nullptr); + int scanlines = GetDIBits(screenDc, iconInfo.hbmColor, 0, height, pixels.data(), &bmi, DIB_RGB_COLORS); + + if (scanlines == 0) { + ReleaseDC(nullptr, screenDc); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + return 0; + } + + // Swap B and R (GDI gives BGRA, D3D wants RGBA) while checking whether the + // colour bitmap carried any real alpha. + bool hasAlpha = false; + for (uint32_t& pixel : pixels) { + if (pixel & 0xFF000000) { + hasAlpha = true; + } + uint32_t b = (pixel & 0x000000FF); + uint32_t r = (pixel & 0x00FF0000) >> 16; + pixel = (pixel & 0xFF00FF00) | (b << 16) | r; + } + + // Legacy icons store no per-pixel alpha; transparency lives in the AND mask + // (a set bit means transparent). Without this, such icons decode to alpha 0 + // everywhere and render fully invisible. Rebuild alpha from the mask. + if (!hasAlpha) { + std::vector mask(static_cast(width) * height); + if (GetDIBits(screenDc, iconInfo.hbmMask, 0, height, mask.data(), &bmi, DIB_RGB_COLORS) != 0) { + for (size_t i = 0; i < pixels.size(); ++i) { + bool transparent = (mask[i] & 0x00FFFFFF) != 0; + pixels[i] = transparent ? (pixels[i] & 0x00FFFFFF) : (pixels[i] | 0xFF000000); + } + } else { + // Mask unreadable: make the icon opaque rather than invisible. + for (uint32_t& pixel : pixels) { + pixel |= 0xFF000000; + } + } + } + + ReleaseDC(nullptr, screenDc); + DeleteObject(iconInfo.hbmColor); + DeleteObject(iconInfo.hbmMask); + + D3D11_TEXTURE2D_DESC desc {}; + desc.Width = width; + desc.Height = height; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_IMMUTABLE; + desc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + + D3D11_SUBRESOURCE_DATA data {}; + data.pSysMem = pixels.data(); + data.SysMemPitch = width * 4; + + ID3D11Texture2D* tex = nullptr; + if (FAILED(m_Device->CreateTexture2D(&desc, &data, &tex)) || !tex) { + return 0; + } + + D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc {}; + srvDesc.Format = desc.Format; + srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + srvDesc.Texture2D.MipLevels = 1; + + ID3D11ShaderResourceView* srv = nullptr; + HRESULT hr = m_Device->CreateShaderResourceView(tex, &srvDesc, &srv); + tex->Release(); + + if (FAILED(hr) || !srv) { + return 0; + } + + return reinterpret_cast(srv); +} + +std::wstring IconCache::GetProcessPath(uint32_t pid) +{ + HANDLE handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid); + if (!handle) { + return {}; + } + + wchar_t buffer[MAX_PATH]; + DWORD size = MAX_PATH; + std::wstring path; + if (QueryFullProcessImageNameW(handle, 0, buffer, &size)) { + path.assign(buffer, size); + } + + CloseHandle(handle); + return path; +} diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h new file mode 100644 index 0000000..d7acc58 --- /dev/null +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include + +struct ID3D11Device; +struct ID3D11ShaderResourceView; +struct HICON__; +using HICON = HICON__*; + +// Turns Windows shell icons into ImGui-renderable textures and caches them. +// +// The process/window selector wants a small icon in front of every row. Pulling +// and uploading an icon is comparatively expensive (shell query + a GPU texture +// per icon), so results are cached: process icons by pid, file icons by path. +// Textures live for the lifetime of the cache and are released together in +// Shutdown(), which the owner calls before the D3D11 device goes away. +// +// Everything here is best-effort: if an icon can't be extracted the lookup +// returns 0 and the caller simply draws nothing (or a shared fallback glyph), +// never an error. +class IconCache { +public: + IconCache() = default; + ~IconCache(); + + // Binds the cache to the live D3D11 device. Must be called once, on the main + // thread, before any Get* call. Passing nullptr disables texture creation so + // the selector still works headless (every lookup just returns 0). + void Initialize(ID3D11Device* device); + + // Releases every cached texture. Safe to call more than once; the destructor + // calls it too. + void Shutdown(); + + // Icon for a running process, resolved from its executable path and keyed by + // pid. Returns an ImTextureID-compatible handle (the SRV pointer as a + // uint64_t) or 0 when unavailable. + [[nodiscard]] uint64_t GetProcessIcon(uint32_t pid); + + // Icon for the window's owning process, keyed by pid so it shares the process + // cache. hwnd is only used to find that pid. + [[nodiscard]] uint64_t GetWindowIcon(void* hwnd); + + // A neutral fallback (the generic application icon) for rows whose real icon + // couldn't be extracted. Cached once. + [[nodiscard]] uint64_t GetFallbackIcon(); + +private: + // Extracts the shell icon for a file path and uploads it as a texture. + // Returns 0 on any failure. + [[nodiscard]] uint64_t CreateTextureFromPath(const std::wstring& path); + + // Rasterizes an HICON into BGRA pixels and uploads a texture. Takes ownership + // of nothing; the caller still destroys the HICON. + [[nodiscard]] uint64_t CreateTextureFromIcon(HICON icon); + + // Resolves a pid to its executable path, or an empty string on failure. + [[nodiscard]] static std::wstring GetProcessPath(uint32_t pid); + + ID3D11Device* m_Device = nullptr; + + std::unordered_map m_ProcessIcons; + uint64_t m_FallbackIcon = 0; +}; diff --git a/CheatStengine/vendor/httplib.cmake b/CheatStengine/vendor/httplib.cmake new file mode 100644 index 0000000..103fa77 --- /dev/null +++ b/CheatStengine/vendor/httplib.cmake @@ -0,0 +1,14 @@ +set(CPP_HTTPLIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/cpp-httplib) + +if (NOT EXISTS ${CPP_HTTPLIB_DIR}) + include(FetchContent) + FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.18.3 + SOURCE_DIR ${CPP_HTTPLIB_DIR} + ) + FetchContent_MakeAvailable(httplib) +else () + add_subdirectory(${CPP_HTTPLIB_DIR}) +endif () diff --git a/CheatStengine/vendor/json.cmake b/CheatStengine/vendor/json.cmake new file mode 100644 index 0000000..32ec554 --- /dev/null +++ b/CheatStengine/vendor/json.cmake @@ -0,0 +1,14 @@ +set(NLOHMANN_JSON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendor/json) + +if (NOT EXISTS ${NLOHMANN_JSON_DIR}) + include(FetchContent) + FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + SOURCE_DIR ${NLOHMANN_JSON_DIR} + ) + FetchContent_MakeAvailable(nlohmann_json) +else () + add_subdirectory(${NLOHMANN_JSON_DIR}) +endif () diff --git a/Engine/src/Platform/Win32/Win32Window.cpp b/Engine/src/Platform/Win32/Win32Window.cpp index 58f85c8..190ffdd 100644 --- a/Engine/src/Platform/Win32/Win32Window.cpp +++ b/Engine/src/Platform/Win32/Win32Window.cpp @@ -364,6 +364,19 @@ void Win32Window::Init(const WindowProps& props) SetVSync(false); } +HICON Win32Window::LoadAppIcon(int size) +{ + // The branded icon ships next to the executable under Resources/. Fall back + // to the stock application icon if it can't be loaded (e.g. running from a + // directory where Resources wasn't copied). + HICON icon = static_cast(::LoadImage( + nullptr, "Resources/favicon.ico", IMAGE_ICON, size, size, LR_LOADFROMFILE)); + if (!icon) { + icon = ::LoadIcon(nullptr, IDI_APPLICATION); + } + return icon; +} + void Win32Window::RegisterWindowClass() const { WNDCLASSEX wc = {}; @@ -371,10 +384,10 @@ void Win32Window::RegisterWindowClass() const wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wc.lpfnWndProc = WindowProc; wc.hInstance = m_Instance; - wc.hIcon = ::LoadIcon(nullptr, IDI_APPLICATION); + wc.hIcon = LoadAppIcon(::GetSystemMetrics(SM_CXICON)); wc.hCursor = ::LoadCursor(nullptr, IDC_ARROW); wc.lpszClassName = "CECLASS"; - wc.hIconSm = ::LoadIcon(nullptr, IDI_APPLICATION); + wc.hIconSm = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON)); if (!::RegisterClassEx(&wc)) { throw std::runtime_error("Failed to register window class"); @@ -399,6 +412,13 @@ void Win32Window::CreateWindow(const WindowProps& props) throw std::runtime_error("Failed to create window"); } + if (HICON large = LoadAppIcon(::GetSystemMetrics(SM_CXICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(large)); + } + if (HICON small = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(small)); + } + ::ShowWindow(m_Hwnd, SW_SHOWNA); } diff --git a/Engine/src/Platform/Win32/Win32Window.h b/Engine/src/Platform/Win32/Win32Window.h index 823db46..14553b9 100644 --- a/Engine/src/Platform/Win32/Win32Window.h +++ b/Engine/src/Platform/Win32/Win32Window.h @@ -62,6 +62,8 @@ class Win32Window final : public Window { void CreateWindow(const WindowProps& props); void ProcessEvents(); + static HICON LoadAppIcon(int size); + private: HWND m_Hwnd = nullptr; HINSTANCE m_Instance = nullptr; diff --git a/Images/128x128.png b/Images/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..7f80d25aeb91510dc1c94c1181939d1ae9b763f2 GIT binary patch literal 7364 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index e96e3b8..91a94c2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,22 @@ -# Cheat Stengine +

+ Cheat Stengine Logo + Cheat Stengine +

-Cheat Stengine is a reverse engineering tool for Windows processes. +Cheat Stengine is a reverse engineering tool for Windows The goal is to create a tool that has a better user experience than Cheat Engine, while still providing powerful -features for reverse engineering and game hacking. +features for reverse engineering, debugging, memory analysis, and game hacking. > [!WARNING] > Cheat Stengine is still in the very early stages of development. Many features are not yet implemented, and the tool -> may be unstable. +> may be unstable. Expect bugs, crashes, and breaking changes between versions. + +> [!NOTE] +> If you're on an AMD chipset, you might face some issues. If you do, please copy the generated dump file and create an issue +> with any relevant information that could help us reproduce the problem. +> +> We are not responsible for individual data loss, system instability, or other issues caused by using Cheat Stengine. +> Use it at your own risk. ## Preview @@ -16,32 +26,65 @@ features for reverse engineering and game hacking. ## Compiling from Source -```console +```bash cmake -S . -B build cmake --build build --config Release ``` +## MCP Server + +Cheat Stengine ships with a built-in MCP server so an AI agent can drive the +engine: list and attach processes, read/write and scan memory, disassemble, +assemble, dissect structs, and generate byte signatures. + +The server starts automatically on launch, binds to loopback, and prints its +URL and bearer token to the log. Open the **MCP** tab in Settings +(`Ctrl+,`) to: + +* toggle the server on or off, +* require the bearer token (off by default, since the loopback bind is the + gate), +* copy the endpoint and token, +* open the web dashboard and configuration pages, and +* reinstall the client configs for Claude Code, Cursor, Claude Desktop, Codex + and the skill. + +The dashboard (served at the printed URL) shows live sessions, jobs and the +tool catalogue, and lets you enable or disable individual tools. The +configuration page at `/config.html` manages the connection and install +targets. + ## Features -- [x] Pattern Scanner -- [x] Disassembler -- [x] Assembler -- [x] Struct Dissector -- [x] Memory Scanner -- [x] Address Watcher -- [x] Module List -- [x] PE Viewer -- [x] Kernel Mode -- [x] Pattern Generator -- [ ] Process Dumper -- [ ] String Scanner/Viewer -- [ ] Code cave scanner -- [ ] Code injection -- [ ] Syscall Tracer -- [ ] Lua scripting -- [ ] Pointer Scanner -- [ ] Memory Viewer -- [ ] Debugger -- [ ] Thread Explorer -- [ ] Handle Viewer -- [ ] Plugin System +* [x] Pattern Scanner +* [x] MCP Support +* [x] Disassembler +* [x] Assembler +* [x] Struct Dissector +* [x] Memory Scanner +* [x] Address Watcher +* [x] Module List +* [x] PE Viewer +* [x] Kernel Mode +* [x] Pattern Generator +* [ ] Process Dumper +* [ ] String Scanner/Viewer +* [ ] Code Cave Scanner +* [ ] Code Injection +* [ ] Syscall Tracer +* [ ] Lua Scripting +* [ ] Pointer Scanner +* [ ] Memory Viewer +* [ ] Debugger +* [ ] Thread Explorer +* [ ] Handle Viewer +* [ ] Plugin System +* [ ] DBVM + +## Development + +Cheat Stengine is under active development. Features marked as incomplete are planned or currently being worked on. +The project is still experimental, so functionality and APIs may change as development progresses. + +Bug reports and contributions are welcome. If you encounter a crash, please include relevant logs or dump files when +creating an issue. \ No newline at end of file From 441e39bec686b69525f07202c4b5e7987f2dece3 Mon Sep 17 00:00:00 2001 From: ryudacatt Date: Mon, 20 Jul 2026 23:40:30 +0530 Subject: [PATCH 2/5] fixed the build issue --- Engine/src/Platform/Win32/Win32Window.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Engine/src/Platform/Win32/Win32Window.cpp b/Engine/src/Platform/Win32/Win32Window.cpp index 190ffdd..0f4d256 100644 --- a/Engine/src/Platform/Win32/Win32Window.cpp +++ b/Engine/src/Platform/Win32/Win32Window.cpp @@ -412,11 +412,11 @@ void Win32Window::CreateWindow(const WindowProps& props) throw std::runtime_error("Failed to create window"); } - if (HICON large = LoadAppIcon(::GetSystemMetrics(SM_CXICON))) { - ::SendMessage(m_Hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(large)); + if (HICON largeIcon = LoadAppIcon(::GetSystemMetrics(SM_CXICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); } - if (HICON small = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON))) { - ::SendMessage(m_Hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(small)); + if (HICON smallIcon = LoadAppIcon(::GetSystemMetrics(SM_CXSMICON))) { + ::SendMessage(m_Hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); } ::ShowWindow(m_Hwnd, SW_SHOWNA); From f3f4008031e0fffd0c7d8b0cee6b016ccd38c339 Mon Sep 17 00:00:00 2001 From: Ryu Date: Mon, 20 Jul 2026 23:44:35 +0530 Subject: [PATCH 3/5] Update .gitignore --- .gitignore | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.gitignore b/.gitignore index 3897d3f..4c45839 100644 --- a/.gitignore +++ b/.gitignore @@ -3,15 +3,3 @@ /build/ /out/ /.idea/ - -# Dependency directories -*/vendor/*/ - -# Local scratch / persona files (not part of the tool) -nyx_core.txt -nyx_style.txt -nyx_injector.bat -logs/ - -# Local editor / agent settings -.claude/settings.local.json \ No newline at end of file From 869229e4688abcdacf3ffb7c4fe6e9d62045cc66 Mon Sep 17 00:00:00 2001 From: ryudacatt Date: Mon, 20 Jul 2026 23:48:07 +0530 Subject: [PATCH 4/5] fixed protocol versin not working in claude code --- .../src/CheatStengine/Server/McpServer.cpp | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CheatStengine/src/CheatStengine/Server/McpServer.cpp b/CheatStengine/src/CheatStengine/Server/McpServer.cpp index 2c7dd1e..abf1b41 100644 --- a/CheatStengine/src/CheatStengine/Server/McpServer.cpp +++ b/CheatStengine/src/CheatStengine/Server/McpServer.cpp @@ -12,7 +12,7 @@ namespace Server { - static constexpr const char* kProtocolVersion = "0.1.45"; + static constexpr const char* kProtocolVersion = "2025-06-18"; McpServer::McpServer() = default; @@ -327,10 +327,25 @@ namespace Server { } std::string sessionId = m_Sessions.Create(clientName, clientVersion); - INFO("MCP client initialized: {} {} (session {})", clientName, clientVersion, sessionId.substr(0, 8)); + + // MCP spec: echo the client's requested protocol if we support it, + // otherwise return our latest and let the client decide. + static constexpr const char* kSupported[] = { + "2025-06-18", "2025-03-26", "2024-11-05" + }; + std::string negotiated = kProtocolVersion; // our latest as default + if (params.contains("protocolVersion")) { + std::string requested = params["protocolVersion"].get(); + for (const char* v : kSupported) { + if (requested == v) { negotiated = requested; break; } + } + } + + INFO("MCP client initialized: {} {} (session {}), protocol {}", + clientName, clientVersion, sessionId.substr(0, 8), negotiated); return { - { "protocolVersion", kProtocolVersion }, + { "protocolVersion", negotiated }, { "capabilities", { { "tools", nlohmann::json::object() } } }, { "serverInfo", { { "name", "CheatStengine" }, { "version", "0.1.0" } } }, { "sessionId", sessionId }, From 5ccc47d24ee11999676e527af4b265eb39315e83 Mon Sep 17 00:00:00 2001 From: ryudacatt Date: Tue, 21 Jul 2026 18:26:35 +0530 Subject: [PATCH 5/5] rewrote the driver , some UI changes , some improvements in the memory scanner --- .gitignore | 9 +- CMakeLists.txt | 4 +- CheatStengine/CMakeLists.txt | 93 +- CheatStengine/build/CMakeCache.txt | 56 ++ .../build/CMakeFiles/cmake.check_cache | 1 + .../out/build/x64-Release/CMakeCache.txt | 59 ++ .../x64-Release/CMakeFiles/cmake.check_cache | 1 + CheatStengine/src/CheatStengine/MainLayer.cpp | 12 +- CheatStengine/src/CheatStengine/MainLayer.h | 4 +- .../CheatStengine/Panes/MemoryScannerPane.cpp | 97 +- .../CheatStengine/Panes/MemoryScannerPane.h | 1 + .../src/CheatStengine/Process/Device.cpp | 828 +++++++++++++++++ .../src/CheatStengine/Process/Device.h | 77 ++ .../CheatStengine/Process/KernelProcess.cpp | 257 ++---- .../src/CheatStengine/Process/KernelProcess.h | 10 +- .../src/CheatStengine/Process/Monitor.cpp | 125 +++ .../src/CheatStengine/Process/Monitor.h | 58 ++ .../src/CheatStengine/Process/Process.h | 8 +- .../src/CheatStengine/Process/WinAPIProcess.h | 4 +- .../src/CheatStengine/Server/ToolRegistry.cpp | 10 +- .../src/CheatStengine/Settings/EnumSetting.h | 3 +- .../CheatStengine/Settings/KernelSettings.cpp | 317 +++++++ .../CheatStengine/Settings/KernelSettings.h | 69 ++ .../src/CheatStengine/Tools/MemoryScanner.cpp | 829 ++++++++++++------ .../src/CheatStengine/Tools/MemoryScanner.h | 21 +- .../src/CheatStengine/UI/ImGui/IconCache.cpp | 7 - .../src/CheatStengine/UI/ImGui/IconCache.h | 33 - CheatStengine/vendor/IconFontCppHeaders | 1 + CheatStengine/vendor/cpp-httplib | 1 + CheatStengine/vendor/json | 1 + CheatStengine/vendor/zasm | 1 + CheatStengineDriver/CMakeLists.txt | 61 +- CheatStengineDriver/Driver.inf | 63 ++ .../src/CheatStengineDriver/Control.cpp | 598 +++++++++++++ .../src/CheatStengineDriver/Control.h | 10 + .../src/CheatStengineDriver/Device.cpp | 478 ++++++++++ .../src/CheatStengineDriver/Device.h | 82 ++ .../src/CheatStengineDriver/Driver.cpp | 222 +---- .../src/CheatStengineDriver/Driver.h | 65 +- .../src/CheatStengineDriver/Mapper.cpp | 116 +++ .../src/CheatStengineDriver/Mapper.h | 8 + .../src/CheatStengineDriver/Memory.cpp | 259 ++---- .../src/CheatStengineDriver/Memory.h | 23 +- .../src/CheatStengineDriver/Pnp.cpp | 270 ++++++ .../src/CheatStengineDriver/Pnp.h | 15 + .../src/CheatStengineDriver/Process.cpp | 301 +++---- .../src/CheatStengineDriver/Process.h | 49 +- CheatStengineDriver/vendor/FindWDK | 1 + Engine/CMakeLists.txt | 21 +- Engine/vendor/imgui | 1 + Engine/vendor/spdlog | 1 + README.md | 38 +- SKILL.md | 29 + SampleApp/CMakeLists.txt | 7 +- Shared/Protocol.h | 255 ++++++ 55 files changed, 4763 insertions(+), 1207 deletions(-) create mode 100644 CheatStengine/build/CMakeCache.txt create mode 100644 CheatStengine/build/CMakeFiles/cmake.check_cache create mode 100644 CheatStengine/out/build/x64-Release/CMakeCache.txt create mode 100644 CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache create mode 100644 CheatStengine/src/CheatStengine/Process/Device.cpp create mode 100644 CheatStengine/src/CheatStengine/Process/Device.h create mode 100644 CheatStengine/src/CheatStengine/Process/Monitor.cpp create mode 100644 CheatStengine/src/CheatStengine/Process/Monitor.h create mode 100644 CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp create mode 100644 CheatStengine/src/CheatStengine/Settings/KernelSettings.h create mode 160000 CheatStengine/vendor/IconFontCppHeaders create mode 160000 CheatStengine/vendor/cpp-httplib create mode 160000 CheatStengine/vendor/json create mode 160000 CheatStengine/vendor/zasm create mode 100644 CheatStengineDriver/Driver.inf create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Control.cpp create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Control.h create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Device.cpp create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Device.h create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Mapper.h create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp create mode 100644 CheatStengineDriver/src/CheatStengineDriver/Pnp.h create mode 160000 CheatStengineDriver/vendor/FindWDK create mode 160000 Engine/vendor/imgui create mode 160000 Engine/vendor/spdlog create mode 100644 SKILL.md create mode 100644 Shared/Protocol.h diff --git a/.gitignore b/.gitignore index 4c45839..b5c9e97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ -# Build directories /cmake-build-*/ /build/ /out/ /.idea/ +.gitignore +.vscode/ +nyx_core.txt +nyx_injector.bat +nyx_style.txt +logs +*.logs +.* \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 93697ba..180e685 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -add_definitions(-DENGINE_IMGUI -DIMGUI_USE_WCHAR32) - add_subdirectory(Engine) #add_subdirectory(SampleApp) add_subdirectory(CheatStengine) -add_subdirectory(CheatStengineDriver) +add_subdirectory(CheatStengineDriver) \ No newline at end of file diff --git a/CheatStengine/CMakeLists.txt b/CheatStengine/CMakeLists.txt index 4be2c0a..596baee 100644 --- a/CheatStengine/CMakeLists.txt +++ b/CheatStengine/CMakeLists.txt @@ -1,3 +1,18 @@ +# Matches the version required by the top-level CMakeLists.txt. Present here too so +# this file is self-consistent and never emits a "No cmake_minimum_required" warning. +cmake_minimum_required(VERSION 3.30) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + get_filename_component(_cs_repo_root "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY) + message(FATAL_ERROR + "CheatStengine is a subproject and cannot be configured on its own.\n" + "It links the 'Engine' target and requires C++20, both set up by the " + "top-level CMakeLists.txt. Configure from the repository root instead:\n" + " cmake -S \"${_cs_repo_root}\" -B \"${_cs_repo_root}/out/build/x64-Release\" -G Ninja -DCMAKE_BUILD_TYPE=Release\n" + " cmake --build \"${_cs_repo_root}/out/build/x64-Release\"\n" + "See the 'Compiling from Source' section of the README.") +endif () + project(CheatStengine) include(vendor/zasm.cmake) @@ -6,30 +21,84 @@ include(vendor/json.cmake) include(vendor/httplib.cmake) #include(vendor/imgui_hex_editor.cmake) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +add_executable(${PROJECT_NAME} + src/CheatStengine/AddressEvaluator/Ast.cpp + src/CheatStengine/AddressEvaluator/Evaluator.cpp + src/CheatStengine/AddressEvaluator/Lexer.cpp + src/CheatStengine/AddressEvaluator/Parser.cpp + src/CheatStengine/Assembly/Assembler.cpp + src/CheatStengine/Assembly/Formatter.cpp + src/CheatStengine/main.cpp + src/CheatStengine/MainLayer.cpp + src/CheatStengine/Panes/DebugPane.cpp + src/CheatStengine/Panes/DisassemblyPane.cpp + src/CheatStengine/Panes/MemoryScannerPane.cpp + src/CheatStengine/Panes/MemoryViewerPane.cpp + src/CheatStengine/Panes/ModulesPane.cpp + src/CheatStengine/Panes/PatternScannerPane.cpp + src/CheatStengine/Panes/PEViewer.cpp + src/CheatStengine/Panes/StructDissectPane.cpp + src/CheatStengine/Panes/WatchPane.cpp + src/CheatStengine/Process/Device.cpp + src/CheatStengine/Process/KernelProcess.cpp + src/CheatStengine/Process/Monitor.cpp + src/CheatStengine/Process/Process.cpp + src/CheatStengine/Process/WinAPIProcess.cpp + src/CheatStengine/Server/AuthManager.cpp + src/CheatStengine/Server/JobManager.cpp + src/CheatStengine/Server/McpInstaller.cpp + src/CheatStengine/Server/McpServer.cpp + src/CheatStengine/Server/SessionManager.cpp + src/CheatStengine/Server/ToolRegistry.cpp + src/CheatStengine/Settings/ComboSettings.cpp + src/CheatStengine/Settings/KeybindSetting.cpp + src/CheatStengine/Settings/KernelSettings.cpp + src/CheatStengine/Settings/SettingsCategory.cpp + src/CheatStengine/Settings/SettingsManager.cpp + src/CheatStengine/Settings/SliderSetting.cpp + src/CheatStengine/Settings/ToggleSetting.cpp + src/CheatStengine/Tools/MemoryScanner.cpp + src/CheatStengine/Tools/PatternGenerator.cpp + src/CheatStengine/Tools/PatternScanner.cpp + src/CheatStengine/Tools/RTTI.cpp + src/CheatStengine/Tools/StructDissect.cpp + src/CheatStengine/UI/ImGui/EditableLabel.cpp + src/CheatStengine/UI/ImGui/Fonts.cpp + src/CheatStengine/UI/ImGui/IconCache.cpp + src/CheatStengine/UI/ImGui/ImGuiHex.cpp + src/CheatStengine/UI/ImGui/Menu.cpp + src/CheatStengine/UI/MenuBar.cpp + src/CheatStengine/UI/TitleBar.cpp + src/CheatStengine/Utils.cpp +) if (WIN32) - add_definitions(-DNOMINMAX) - # cpp-httplib pulls in winsock2.h; define this so doesn't drag in - # the legacy winsock.h first and cause sockaddr/type redefinition errors. - add_definitions(-DWIN32_LEAN_AND_MEAN) - # Needed so the DX11 context header (guarded by PLATFORM_WINDOWS) is visible - # here; the icon cache uploads process icons through the live D3D11 device. - add_definitions(-DPLATFORM_WINDOWS) + target_compile_definitions(${PROJECT_NAME} PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + PLATFORM_WINDOWS + WINVER=0x0A00 + _WIN32_WINNT=0x0A00 + NTDDI_VERSION=0x0A000008 + ) endif () -target_include_directories(${PROJECT_NAME} PUBLIC +target_include_directories(${PROJECT_NAME} PRIVATE src + ${CMAKE_CURRENT_SOURCE_DIR}/../Shared ${ICON_FONT_CPP_HEADERS} ) -target_link_libraries(${PROJECT_NAME} PUBLIC +target_compile_options(${PROJECT_NAME} PRIVATE + $<$:/W4> +) +target_link_libraries(${PROJECT_NAME} PRIVATE Engine zasm nlohmann_json::nlohmann_json httplib::httplib Dbghelp Shell32 + Cfgmgr32 d3d11 ) @@ -37,4 +106,4 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${PROJECT_SOURCE_DIR}/Resources $/Resources -) \ No newline at end of file +) diff --git a/CheatStengine/build/CMakeCache.txt b/CheatStengine/build/CMakeCache.txt new file mode 100644 index 0000000..5e74abf --- /dev/null +++ b/CheatStengine/build/CMakeCache.txt @@ -0,0 +1,56 @@ +# This is the CMakeCache file. +# For build in directory: f:/Downloads/CheatStengine-master/CheatStengine/build +# It was generated by CMake: C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=F:/Downloads/CheatStengine-master/CheatStengine/build/CMakeFiles/pkgRedirects + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=f:/Downloads/CheatStengine-master/CheatStengine/build +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cpack.exe +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/ctest.exe +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022 +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=F:/Downloads/CheatStengine-master/CheatStengine +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/share/cmake-3.31 + diff --git a/CheatStengine/build/CMakeFiles/cmake.check_cache b/CheatStengine/build/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/CheatStengine/build/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/CheatStengine/out/build/x64-Release/CMakeCache.txt b/CheatStengine/out/build/x64-Release/CMakeCache.txt new file mode 100644 index 0000000..1e80079 --- /dev/null +++ b/CheatStengine/out/build/x64-Release/CMakeCache.txt @@ -0,0 +1,59 @@ +# This is the CMakeCache file. +# For build in directory: f:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release +# It was generated by CMake: C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +# You can edit this file to change values found and used by cmake. +# If you do not want to change any of the values, simply exit the editor. +# If you do want to change a value, simply edit, save, and exit the editor. +# The syntax for the file is as follows: +# KEY:TYPE=VALUE +# KEY is the name of a variable in the cache. +# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!. +# VALUE is the current value for the KEY. + +######################## +# EXTERNAL cache entries +######################## + +//No help, variable specified on the command line. +CMAKE_BUILD_TYPE:UNINITIALIZED=Release + +//Value Computed by CMake. +CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=F:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release/CMakeFiles/pkgRedirects + + +######################## +# INTERNAL cache entries +######################## + +//This is the directory where this CMakeCache.txt was created +CMAKE_CACHEFILE_DIR:INTERNAL=f:/Downloads/CheatStengine-master/CheatStengine/out/build/x64-Release +//Major version of cmake used to create the current loaded cache +CMAKE_CACHE_MAJOR_VERSION:INTERNAL=3 +//Minor version of cmake used to create the current loaded cache +CMAKE_CACHE_MINOR_VERSION:INTERNAL=31 +//Patch version of cmake used to create the current loaded cache +CMAKE_CACHE_PATCH_VERSION:INTERNAL=6 +//Path to CMake executable. +CMAKE_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe +//Path to cpack program executable. +CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cpack.exe +//Path to ctest program executable. +CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/ctest.exe +//Name of external makefile project generator. +CMAKE_EXTRA_GENERATOR:INTERNAL= +//Name of generator. +CMAKE_GENERATOR:INTERNAL=Ninja +//Generator instance identifier. +CMAKE_GENERATOR_INSTANCE:INTERNAL= +//Name of generator platform. +CMAKE_GENERATOR_PLATFORM:INTERNAL= +//Name of generator toolset. +CMAKE_GENERATOR_TOOLSET:INTERNAL= +//Source directory with the top level CMakeLists.txt file for this +// project +CMAKE_HOME_DIRECTORY:INTERNAL=F:/Downloads/CheatStengine-master/CheatStengine +//number of local generators +CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1 +//Path to CMake installation. +CMAKE_ROOT:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/share/cmake-3.31 + diff --git a/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache b/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache new file mode 100644 index 0000000..3dccd73 --- /dev/null +++ b/CheatStengine/out/build/x64-Release/CMakeFiles/cmake.check_cache @@ -0,0 +1 @@ +# This file is generated by cmake for dependency checking of the CMakeCache.txt file diff --git a/CheatStengine/src/CheatStengine/MainLayer.cpp b/CheatStengine/src/CheatStengine/MainLayer.cpp index 7e3b3fb..828c457 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.cpp +++ b/CheatStengine/src/CheatStengine/MainLayer.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,9 @@ MainLayer::MainLayer(Window& window) { SettingsCategory& general = m_SettingsManager.AddCategory("General"); m_ProcessModeSetting = general.AddSetting>("Process Mode", "What should be used to interact with processes?", ProcessMode::WinAPI); + general.AddSetting("Kernel", m_Monitor, [this] { + return m_ProcessModeSetting->GetPendingValue() == ProcessMode::Kernel; + }); // MCP control server settings. Auth is off by default so an agent connects // with just the URL; flip that on to require the bearer token. The toggle @@ -114,6 +118,7 @@ void MainLayer::OnAttach() void MainLayer::OnDetach() { + m_Monitor.Stop(); m_McpServer.Stop(); // Release icon textures while the device is still alive. @@ -122,6 +127,12 @@ void MainLayer::OnDetach() void MainLayer::OnUpdate(float deltaTime) { + if (m_ProcessModeSetting->GetValue() == ProcessMode::Kernel) { + m_Monitor.Start(); + } else { + m_Monitor.Stop(); + } + // Run any tool work the server queued from its listener thread. Must happen // on this (main) thread before we touch State below. m_McpServer.DrainCommands(); @@ -396,4 +407,3 @@ void MainLayer::DrawMcpControls() ImGui::SetTooltip("Rewrite the MCP entries for Claude Code, Cursor, Claude Desktop, Codex and the skill."); } } - diff --git a/CheatStengine/src/CheatStengine/MainLayer.h b/CheatStengine/src/CheatStengine/MainLayer.h index 7513b41..87a62ce 100644 --- a/CheatStengine/src/CheatStengine/MainLayer.h +++ b/CheatStengine/src/CheatStengine/MainLayer.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,7 @@ class MainLayer final : public Layer { // Managers ModalManager m_ModalManager; KeybindManager m_KeybindManager; + Monitor m_Monitor; SettingsManager m_SettingsManager; MenuBar m_MenuBar; @@ -104,4 +106,4 @@ template T, typename... Args> T& MainLayer::AddPane(Args&&... args) { return static_cast(*m_Panes.emplace_back(std::make_unique(std::forward(args)...))); -} \ No newline at end of file +} diff --git a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp index 60e4f31..6a7f9ba 100644 --- a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp +++ b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.cpp @@ -10,7 +10,7 @@ #include #include -#include +#include static const char* s_ScanTypeNames[] = { "Exact Value", @@ -35,6 +35,35 @@ static const char* s_ValueTypeNames[] = { }; static_assert(IM_ARRAYSIZE(s_ValueTypeNames) == static_cast(ValueType::COUNT)); +static bool IsIntegerType(ValueType type) +{ + return type != ValueType::Float + && type != ValueType::Double + && type != ValueType::COUNT; +} + +static std::string GetScanInput(const std::string& input, ValueType type, bool isHex) +{ + if (!isHex || !IsIntegerType(type)) { + return input; + } + + const size_t first = input.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + return input; + } + const size_t last = input.find_last_not_of(" \t\r\n"); + std::string value = input.substr(first, last - first + 1); + const size_t prefix = value.front() == '+' || value.front() == '-' ? 1 : 0; + if (value.size() >= prefix + 2 + && value[prefix] == '0' + && (value[prefix + 1] == 'x' || value[prefix + 1] == 'X')) { + return value; + } + value.insert(prefix, "0x"); + return value; +} + MemoryScannerPane::MemoryScannerPane(State& state) : Pane(ICON_MDI_RADAR " Memory Scanner", state) { @@ -44,7 +73,32 @@ void MemoryScannerPane::Draw(double deltaTime) { ImGui::Begin(m_Name.c_str(), &m_Open); - ImGui::Text("Found %llu", m_Scanner ? m_Scanner->GetResults().size() : 0); + const uint32_t processId = m_State.Process && m_State.Process->IsValid() + ? m_State.Process->GetPid() + : 0; + if (processId != m_ProcessId) { + m_Scanner.reset(); + m_ProcessId = processId; + m_ModuleFilter = ModuleFilter::Any; + m_SelectedModuleIndex = 0; + m_MinAddress = 0; + m_MaxAddress = 0x7FFFFFFFFFFF; + } + if (m_ModuleFilter == ModuleFilter::Specific + && (m_SelectedModuleIndex < 0 + || static_cast(m_SelectedModuleIndex) >= m_State.Modules.size())) { + m_ModuleFilter = ModuleFilter::Any; + m_SelectedModuleIndex = 0; + m_MinAddress = 0; + m_MaxAddress = 0x7FFFFFFFFFFF; + } + + size_t resultCount = 0; + if (m_Scanner) { + std::lock_guard lock(m_Scanner->GetMutex()); + resultCount = m_Scanner->GetResults().size(); + } + ImGui::Text("Found %llu", static_cast(resultCount)); if (ImGui::BeginTable("MemoryScannerView", 2, ImGuiTableFlags_Resizable)) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); @@ -104,18 +158,13 @@ void MemoryScannerPane::Draw(double deltaTime) ImGui::SetNextItemWidth(100.0f); if (m_Scanner) { - const bool isDisabled = m_Scanner->IsScanning(); - if (isDisabled) { - ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); - } - if (ImGui::Button("New Scan", ImVec2 { 80.0f, 0.0f })) { + if (m_Scanner->IsScanning()) { + if (ImGui::Button("Cancel Scan", ImVec2 { 80.0f, 0.0f })) { + m_Scanner->Cancel(); + } + } else if (ImGui::Button("New Scan", ImVec2 { 80.0f, 0.0f })) { m_Scanner.reset(); } - if (isDisabled) { - ImGui::PopStyleVar(); - ImGui::PopItemFlag(); - } } else { if (ImGui::Button("First Scan", ImVec2 { 80.0f, 0.0f })) { StartFirstScan(); @@ -131,7 +180,9 @@ void MemoryScannerPane::Draw(double deltaTime) if (ImGui::Button("Next Scan", ImVec2 { 80.0f, 0.0f })) { if (m_Scanner) { - m_Scanner->NextScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, m_ScanInputLower, m_ScanInputUpper); + const std::string lowerValue = GetScanInput(m_ScanInputLower, m_ValueType, m_IsInputHex); + const std::string upperValue = GetScanInput(m_ScanInputUpper, m_ValueType, m_IsInputHex); + m_Scanner->NextScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, lowerValue, upperValue); } } @@ -178,6 +229,9 @@ void MemoryScannerPane::Draw(double deltaTime) ImGui::EndCombo(); } + if (m_Scanner) { + ImGui::BeginDisabled(); + } if (ImGui::BeginCombo("Value Type", s_ValueTypeNames[static_cast(m_ValueType)])) { for (size_t n = 0; n < IM_ARRAYSIZE(s_ValueTypeNames); n++) { bool isSelected = (m_ValueType == static_cast(n)); @@ -190,6 +244,9 @@ void MemoryScannerPane::Draw(double deltaTime) } ImGui::EndCombo(); } + if (m_Scanner) { + ImGui::EndDisabled(); + } std::string comboLabel = (m_ModuleFilter == ModuleFilter::Any) ? "Any" : m_State.Modules[m_SelectedModuleIndex].szModule; if (ImGui::BeginCombo("Module", comboLabel.c_str())) { @@ -211,7 +268,10 @@ void MemoryScannerPane::Draw(double deltaTime) m_ModuleFilter = ModuleFilter::Specific; m_SelectedModuleIndex = i; m_MinAddress = reinterpret_cast(m_State.Modules[i].modBaseAddr); - m_MaxAddress = m_MinAddress + m_State.Modules[i].modBaseSize; + const uintptr_t moduleSize = static_cast(m_State.Modules[i].modBaseSize); + m_MaxAddress = moduleSize > std::numeric_limits::max() - m_MinAddress + ? std::numeric_limits::max() + : m_MinAddress + moduleSize; } if (isSelected) { ImGui::SetItemDefaultFocus(); @@ -231,9 +291,16 @@ void MemoryScannerPane::Draw(double deltaTime) void MemoryScannerPane::StartFirstScan() { + if (!m_State.Process || !m_State.Process->IsValid()) { + ERR("Select a valid process before starting a memory scan"); + return; + } + INFO("Scanning..."); + const std::string lowerValue = GetScanInput(m_ScanInputLower, m_ValueType, m_IsInputHex); + const std::string upperValue = GetScanInput(m_ScanInputUpper, m_ValueType, m_IsInputHex); m_Scanner = std::make_unique(m_State.Process); - if (!m_Scanner->FirstScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, m_ScanInputLower, m_ScanInputUpper)) { + if (!m_Scanner->FirstScan(m_ValueType, m_ScanType, m_MinAddress, m_MaxAddress, lowerValue, upperValue)) { m_Scanner = nullptr; } } diff --git a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h index adc9b36..cd6f5e6 100644 --- a/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h +++ b/CheatStengine/src/CheatStengine/Panes/MemoryScannerPane.h @@ -35,6 +35,7 @@ class MemoryScannerPane final : public Pane { uintptr_t m_MaxAddress = 0x7FFFFFFFFFFF; ModuleFilter m_ModuleFilter = ModuleFilter::Any; int m_SelectedModuleIndex = 0; // Index in the modules vector + uint32_t m_ProcessId = 0; // Scan Results std::unique_ptr m_Scanner; diff --git a/CheatStengine/src/CheatStengine/Process/Device.cpp b/CheatStengine/src/CheatStengine/Process/Device.cpp new file mode 100644 index 0000000..91f653f --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Device.cpp @@ -0,0 +1,828 @@ +#include "Device.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr DWORD ControlTimeoutMilliseconds = 5000; +constexpr DWORD CancellationTimeoutMilliseconds = 1000; +constexpr wchar_t ControlDevicePath[] = L"\\\\.\\MemoryAccess"; + +enum class InterfaceResult { + Found, + Missing, + Failed, +}; + +struct InterfacePaths { + InterfaceResult Result = InterfaceResult::Failed; + std::vector Paths; + DWORD Error = ERROR_GEN_FAILURE; +}; + +struct PendingControl { + OVERLAPPED Overlapped {}; + HANDLE Event = nullptr; + std::vector Input; + std::vector Buffer; + + ~PendingControl() + { + if (Event) { + CloseHandle(Event); + } + } +}; + +InterfacePaths FindInterfacePaths() +{ + GUID interfaceGuid = Protocol::DeviceInterfaceGuid; + ULONG pathLength = 0; + CONFIGRET result = CM_Get_Device_Interface_List_SizeW( + &pathLength, + &interfaceGuid, + nullptr, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (result != CR_SUCCESS) { + return { + InterfaceResult::Failed, + {}, + CM_MapCrToWin32Err(result, ERROR_GEN_FAILURE), + }; + } + if (pathLength <= 1) { + return { InterfaceResult::Missing, {}, ERROR_FILE_NOT_FOUND }; + } + + std::vector buffer(pathLength, L'\0'); + result = CM_Get_Device_Interface_ListW( + &interfaceGuid, + nullptr, + buffer.data(), + pathLength, + CM_GET_DEVICE_INTERFACE_LIST_PRESENT); + if (result != CR_SUCCESS) { + return { + InterfaceResult::Failed, + {}, + CM_MapCrToWin32Err(result, ERROR_GEN_FAILURE), + }; + } + + InterfacePaths paths; + paths.Result = InterfaceResult::Found; + paths.Error = ERROR_SUCCESS; + const wchar_t* current = buffer.data(); + const wchar_t* end = buffer.data() + buffer.size(); + while (current < end && *current) { + const size_t remaining = static_cast(end - current); + const size_t length = wcsnlen_s(current, remaining); + if (length == remaining) { + return { InterfaceResult::Failed, {}, ERROR_INVALID_DATA }; + } + paths.Paths.emplace_back(current, length); + current += length + 1; + } + + if (paths.Paths.empty()) { + paths.Result = InterfaceResult::Missing; + paths.Error = ERROR_FILE_NOT_FOUND; + } + return paths; +} + +bool IsResponseValid( + const Protocol::PacketHeader& header, + size_t expectedSize, + DWORD transferred) +{ + return header.Size >= expectedSize + && header.Size <= transferred + && header.MajorVersion == Protocol::VersionMajor + && header.MinorVersion >= Protocol::VersionMinor + && header.Flags == 0 + && header.Reserved == 0; +} + +bool IsAddressRangeValid(uintptr_t address, size_t size) +{ + return address != 0 + && size != 0 + && size - 1 <= std::numeric_limits::max() - address; +} + +void WaitForAbandonedControl(std::shared_ptr control) +{ + WaitForSingleObject(control->Event, INFINITE); +} + +} + +Device::Device() +{ + static_cast(Open()); +} + +Device::~Device() +{ + Close(); +} + +bool Device::IsOpen() const +{ + std::scoped_lock lock(m_ControlMutex); + return m_Handle != INVALID_HANDLE_VALUE; +} + +bool Device::Bind(uint32_t processId) const +{ + if (processId == 0) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::BindProcess)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + Protocol::BindProcessRequest request {}; + request.Header = Protocol::CreateHeader(); + request.ProcessId = processId; + + DWORD transferred = 0; + if (!Control( + Protocol::BindProcessControlCode, + &request, + sizeof(request), + nullptr, + nullptr, + 0, + transferred)) { + return false; + } + if (transferred != 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + return true; +} + +std::optional Device::Ping(uint64_t nonce) const +{ + if (!HasCapability(Protocol::Ping)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::PingRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Nonce = nonce; + Protocol::PingResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::PingControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Nonce != nonce + || (response.Capabilities & Protocol::RequiredCapabilities) != Protocol::RequiredCapabilities + || response.MaximumTransferSize == 0 + || response.IdleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || response.IdleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds + || response.RemainingIdleSeconds > response.IdleTimeoutSeconds + || response.LoadMode == static_cast(Protocol::LoadMode::Unknown) + || response.LoadMode > static_cast(Protocol::LoadMode::Mapped) + || response.State > static_cast(Protocol::RuntimeState::Removed) + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +std::optional Device::Heartbeat(uint64_t sequence) const +{ + if (!HasCapability(Protocol::Heartbeat)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::HeartbeatRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Sequence = sequence; + Protocol::HeartbeatResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::HeartbeatControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Sequence != sequence + || response.State > static_cast(Protocol::RuntimeState::Removed)) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +std::optional Device::Configure(uint32_t idleTimeoutSeconds) const +{ + if (idleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || idleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds) { + SetError(ERROR_INVALID_PARAMETER); + return std::nullopt; + } + if (!HasCapability(Protocol::Configure)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::ConfigureRequest request {}; + request.Header = Protocol::CreateHeader(); + request.IdleTimeoutSeconds = idleTimeoutSeconds; + Protocol::ConfigureResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::ConfigureControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.IdleTimeoutSeconds < Protocol::MinimumIdleTimeoutSeconds + || response.IdleTimeoutSeconds > Protocol::MaximumIdleTimeoutSeconds + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response; +} + +bool Device::Read(uintptr_t address, void* buffer, size_t size) const +{ + if (size == 0) { + SetError(ERROR_SUCCESS); + return true; + } + if (!buffer || !IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::ReadMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = GetMaximumTransferSize(); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + size_t offset = 0; + while (offset < size) { + const size_t chunk = static_cast(std::min( + maximumTransferSize, + static_cast(size - offset))); + Protocol::ReadMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address + offset); + request.Size = static_cast(chunk); + + DWORD transferred = 0; + if (!Control( + Protocol::ReadMemoryControlCode, + &request, + sizeof(request), + nullptr, + static_cast(buffer) + offset, + static_cast(chunk), + transferred)) { + return false; + } + if (transferred != chunk) { + SetError(ERROR_PARTIAL_COPY); + return false; + } + offset += chunk; + } + + SetError(ERROR_SUCCESS); + return true; +} + +bool Device::Write(uintptr_t address, const void* buffer, size_t size) const +{ + if (size == 0) { + SetError(ERROR_SUCCESS); + return true; + } + if (!buffer || !IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::WriteMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = GetMaximumTransferSize(); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + size_t offset = 0; + while (offset < size) { + const size_t chunk = static_cast(std::min( + maximumTransferSize, + static_cast(size - offset))); + Protocol::WriteMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address + offset); + request.Size = static_cast(chunk); + + DWORD transferred = 0; + if (!Control( + Protocol::WriteMemoryControlCode, + &request, + sizeof(request), + static_cast(buffer) + offset, + nullptr, + static_cast(chunk), + transferred)) { + return false; + } + if (transferred != chunk) { + SetError(ERROR_PARTIAL_COPY); + return false; + } + offset += chunk; + } + + SetError(ERROR_SUCCESS); + return true; +} + +std::optional Device::Query(uintptr_t address) const +{ + if (!HasCapability(Protocol::QueryMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::QueryMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + Protocol::QueryMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::QueryMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.RegionSize == 0 + || response.RegionSize > std::numeric_limits::max()) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + MEMORY_BASIC_INFORMATION information {}; + information.BaseAddress = reinterpret_cast(static_cast(response.BaseAddress)); + information.AllocationBase = reinterpret_cast(static_cast(response.AllocationBase)); + information.AllocationProtect = response.AllocationProtect; + information.RegionSize = static_cast(response.RegionSize); + information.State = response.State; + information.Protect = response.Protect; + information.Type = response.Type; + SetError(ERROR_SUCCESS); + return information; +} + +uintptr_t Device::Allocate( + size_t size, + uint32_t protection, + uint32_t allocationType, + uintptr_t preferredAddress) const +{ + if (size == 0 + || (preferredAddress != 0 + && size - 1 > std::numeric_limits::max() - preferredAddress)) { + SetError(ERROR_INVALID_PARAMETER); + return 0; + } + if (!HasCapability(Protocol::AllocateMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return 0; + } + + Protocol::AllocateMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(preferredAddress); + request.Size = static_cast(size); + request.AllocationType = allocationType; + request.Protect = protection; + Protocol::AllocateMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::AllocateMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return 0; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Address == 0 + || response.Address > std::numeric_limits::max() + || response.Size < size) { + SetError(ERROR_INVALID_DATA); + return 0; + } + + SetError(ERROR_SUCCESS); + return static_cast(response.Address); +} + +bool Device::Free(uintptr_t address, uint32_t freeType, size_t size) const +{ + if (address == 0) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + if (!HasCapability(Protocol::FreeMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + Protocol::FreeMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + request.Size = static_cast(size); + request.FreeType = freeType; + + DWORD transferred = 0; + if (!Control( + Protocol::FreeMemoryControlCode, + &request, + sizeof(request), + nullptr, + nullptr, + 0, + transferred)) { + return false; + } + if (transferred != 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + return true; +} + +std::optional Device::Protect( + uintptr_t address, + size_t size, + uint32_t protection) const +{ + if (!IsAddressRangeValid(address, size)) { + SetError(ERROR_INVALID_PARAMETER); + return std::nullopt; + } + if (!HasCapability(Protocol::ProtectMemory)) { + SetError(ERROR_NOT_SUPPORTED); + return std::nullopt; + } + + Protocol::ProtectMemoryRequest request {}; + request.Header = Protocol::CreateHeader(); + request.Address = static_cast(address); + request.Size = static_cast(size); + request.Protect = protection; + Protocol::ProtectMemoryResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::ProtectMemoryControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return std::nullopt; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.Address == 0 + || response.Size == 0 + || response.Reserved != 0) { + SetError(ERROR_INVALID_DATA); + return std::nullopt; + } + + SetError(ERROR_SUCCESS); + return response.OldProtect; +} + +Protocol::LoadMode Device::GetMode() const +{ + return m_Mode.load(std::memory_order_relaxed); +} + +uint64_t Device::GetCapabilities() const +{ + return m_Capabilities.load(std::memory_order_relaxed); +} + +uint64_t Device::GetMaximumTransferSize() const +{ + return m_MaximumTransferSize.load(std::memory_order_relaxed); +} + +DWORD Device::GetError() const +{ + return m_Error.load(std::memory_order_relaxed); +} + +bool Device::Open() +{ + const InterfacePaths interfaces = FindInterfacePaths(); + DWORD pnpError = interfaces.Error; + + if (interfaces.Result == InterfaceResult::Found) { + for (const std::wstring& path : interfaces.Paths) { + if (OpenPath(path, Protocol::LoadMode::Pnp)) { + return true; + } + pnpError = GetError(); + } + } + + if (OpenPath(ControlDevicePath, Protocol::LoadMode::Mapped)) { + return true; + } + + const DWORD mappedError = GetError(); + const bool mappedMissing = mappedError == ERROR_FILE_NOT_FOUND + || mappedError == ERROR_PATH_NOT_FOUND; + const bool pnpMissing = pnpError == ERROR_FILE_NOT_FOUND + || pnpError == ERROR_PATH_NOT_FOUND; + if (mappedMissing && !pnpMissing && pnpError != ERROR_SUCCESS) { + SetError(pnpError); + } + return false; +} + +bool Device::OpenPath(const std::wstring& path, Protocol::LoadMode mode) +{ + HANDLE handle = CreateFileW( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + nullptr); + if (handle == INVALID_HANDLE_VALUE) { + SetError(GetLastError()); + return false; + } + + { + std::scoped_lock lock(m_ControlMutex); + m_Handle = handle; + m_Mode.store(mode, std::memory_order_relaxed); + } + + if (ReadInformation()) { + return true; + } + + const DWORD error = GetError(); + Close(); + SetError(error); + return false; +} + +bool Device::ReadInformation() +{ + Protocol::InformationRequest request {}; + request.Header = Protocol::CreateHeader(); + Protocol::InformationResponse response {}; + DWORD transferred = 0; + if (!Control( + Protocol::InformationControlCode, + &request, + sizeof(request), + nullptr, + &response, + sizeof(response), + transferred)) { + return false; + } + if (transferred != sizeof(response) + || !IsResponseValid(response.Header, sizeof(response), transferred) + || response.LoadMode == static_cast(Protocol::LoadMode::Unknown) + || response.LoadMode > static_cast(Protocol::LoadMode::Mapped) + || response.Reserved != 0) { + SetError(ERROR_REVISION_MISMATCH); + return false; + } + if ((response.Capabilities & Protocol::RequiredCapabilities) + != Protocol::RequiredCapabilities + || response.MaximumTransferSize == 0) { + SetError(ERROR_NOT_SUPPORTED); + return false; + } + + const uint64_t maximumTransferSize = std::min({ + response.MaximumTransferSize, + Protocol::MaximumTransferSize, + static_cast(std::numeric_limits::max()), + }); + if (maximumTransferSize == 0) { + SetError(ERROR_INVALID_DATA); + return false; + } + + m_Capabilities.store(response.Capabilities, std::memory_order_relaxed); + m_MaximumTransferSize.store(maximumTransferSize, std::memory_order_relaxed); + m_Mode.store(static_cast(response.LoadMode), std::memory_order_relaxed); + SetError(ERROR_SUCCESS); + return true; +} + +bool Device::HasCapability(uint64_t capability) const +{ + return (GetCapabilities() & capability) == capability; +} + +bool Device::Control( + DWORD code, + const void* input, + DWORD inputSize, + const void* directInput, + void* output, + DWORD bufferSize, + DWORD& transferred) const +{ + transferred = 0; + if ((inputSize != 0 && !input) + || (bufferSize != 0 && !directInput && !output) + || (directInput && output)) { + SetError(ERROR_INVALID_PARAMETER); + return false; + } + + std::shared_ptr control; + try { + control = std::make_shared(); + control->Input.resize(inputSize); + control->Buffer.resize(bufferSize); + } catch (const std::bad_alloc&) { + SetError(ERROR_NOT_ENOUGH_MEMORY); + return false; + } + + if (inputSize != 0) { + std::memcpy(control->Input.data(), input, inputSize); + } + if (bufferSize != 0 && directInput) { + std::memcpy(control->Buffer.data(), directInput, bufferSize); + } + + control->Event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (!control->Event) { + SetError(GetLastError()); + return false; + } + control->Overlapped.hEvent = control->Event; + + std::scoped_lock lock(m_ControlMutex); + if (m_Handle == INVALID_HANDLE_VALUE) { + SetError(ERROR_INVALID_HANDLE); + return false; + } + + DWORD immediateTransferred = 0; + const BOOL started = DeviceIoControl( + m_Handle, + code, + inputSize == 0 ? nullptr : control->Input.data(), + inputSize, + bufferSize == 0 ? nullptr : control->Buffer.data(), + bufferSize, + &immediateTransferred, + &control->Overlapped); + if (!started) { + const DWORD error = GetLastError(); + if (error != ERROR_IO_PENDING) { + SetError(error); + return false; + } + } + + const DWORD waitResult = WaitForSingleObject( + control->Event, + ControlTimeoutMilliseconds); + if (waitResult != WAIT_OBJECT_0) { + const DWORD waitError = waitResult == WAIT_TIMEOUT ? ERROR_TIMEOUT : GetLastError(); + CancelIoEx(m_Handle, &control->Overlapped); + const DWORD cancellationResult = WaitForSingleObject( + control->Event, + CancellationTimeoutMilliseconds); + if (cancellationResult != WAIT_OBJECT_0) { + CloseLocked(); + try { + std::thread(WaitForAbandonedControl, control).detach(); + } catch (...) { + WaitForSingleObject(control->Event, INFINITE); + } + } + SetError(waitError); + return false; + } + + if (!GetOverlappedResult( + m_Handle, + &control->Overlapped, + &transferred, + FALSE)) { + SetError(GetLastError()); + return false; + } + if (transferred > bufferSize) { + SetError(ERROR_INVALID_DATA); + return false; + } + if (output && transferred != 0) { + std::memcpy(output, control->Buffer.data(), transferred); + } + + SetError(ERROR_SUCCESS); + return true; +} + +void Device::Close() +{ + std::scoped_lock lock(m_ControlMutex); + CloseLocked(); +} + +void Device::CloseLocked() const +{ + if (m_Handle != INVALID_HANDLE_VALUE) { + CloseHandle(m_Handle); + m_Handle = INVALID_HANDLE_VALUE; + } + m_Mode.store(Protocol::LoadMode::Unknown, std::memory_order_relaxed); + m_Capabilities.store(0, std::memory_order_relaxed); + m_MaximumTransferSize.store(0, std::memory_order_relaxed); +} + +void Device::SetError(DWORD error) const +{ + m_Error.store(error, std::memory_order_relaxed); +} diff --git a/CheatStengine/src/CheatStengine/Process/Device.h b/CheatStengine/src/CheatStengine/Process/Device.h new file mode 100644 index 0000000..65b3ca4 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Device.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +class Device final { +public: + Device(); + ~Device(); + + Device(const Device&) = delete; + Device& operator=(const Device&) = delete; + Device(Device&&) = delete; + Device& operator=(Device&&) = delete; + + [[nodiscard]] bool IsOpen() const; + [[nodiscard]] bool Bind(uint32_t processId) const; + + [[nodiscard]] std::optional Ping(uint64_t nonce = 0) const; + [[nodiscard]] std::optional Heartbeat(uint64_t sequence) const; + [[nodiscard]] std::optional Configure(uint32_t idleTimeoutSeconds) const; + + [[nodiscard]] bool Read(uintptr_t address, void* buffer, size_t size) const; + [[nodiscard]] bool Write(uintptr_t address, const void* buffer, size_t size) const; + + [[nodiscard]] std::optional Query(uintptr_t address) const; + [[nodiscard]] uintptr_t Allocate( + size_t size, + uint32_t protection, + uint32_t allocationType = MEM_COMMIT | MEM_RESERVE, + uintptr_t preferredAddress = 0) const; + [[nodiscard]] bool Free( + uintptr_t address, + uint32_t freeType = MEM_RELEASE, + size_t size = 0) const; + [[nodiscard]] std::optional Protect( + uintptr_t address, + size_t size, + uint32_t protection) const; + + [[nodiscard]] Protocol::LoadMode GetMode() const; + [[nodiscard]] uint64_t GetCapabilities() const; + [[nodiscard]] uint64_t GetMaximumTransferSize() const; + [[nodiscard]] DWORD GetError() const; + +private: + [[nodiscard]] bool Open(); + [[nodiscard]] bool OpenPath(const std::wstring& path, Protocol::LoadMode mode); + [[nodiscard]] bool ReadInformation(); + [[nodiscard]] bool HasCapability(uint64_t capability) const; + [[nodiscard]] bool Control( + DWORD code, + const void* input, + DWORD inputSize, + const void* directInput, + void* output, + DWORD bufferSize, + DWORD& transferred) const; + + void Close(); + void CloseLocked() const; + void SetError(DWORD error) const; + +private: + mutable std::mutex m_ControlMutex; + mutable HANDLE m_Handle = INVALID_HANDLE_VALUE; + mutable std::atomic m_Mode = Protocol::LoadMode::Unknown; + mutable std::atomic m_Capabilities = 0; + mutable std::atomic m_MaximumTransferSize = 0; + mutable std::atomic m_Error = ERROR_SUCCESS; +}; diff --git a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp index 78dda9c..830090e 100644 --- a/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp +++ b/CheatStengine/src/CheatStengine/Process/KernelProcess.cpp @@ -2,168 +2,74 @@ #include -#include - #include -#define IOCTL_CS_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, 0x6969, METHOD_BUFFERED, FILE_ANY_ACCESS) - -struct CommandHeader { - enum Type : uint32_t { - ReadMemory = 0, - WriteMemory = 1, - QueryMemory = 2, - AllocateMemory = 3, - FreeMemory = 4, - ProtectMemory = 5, - } Type; - - uint32_t Pid; - - union { - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } ReadMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } WriteMemoryData; - - struct - { - uintptr_t Address; - MEMORY_BASIC_INFORMATION* Mbi; - } QueryMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t AllocationType; - uint32_t Protect; - uintptr_t* AllocatedAddressPtr; - } AllocateMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t FreeType; - } FreeMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t NewProtect; - uint32_t* OldProtectPtr; - } ProtectMemoryData; - }; -}; - -static bool SendBuffer(HANDLE deviceHandle, CommandHeader* command, void* outputBuffer = nullptr, size_t outputBufferSize = 0) -{ - DWORD bytesReturned = 0; - BOOL result = FALSE; - result = DeviceIoControl( - deviceHandle, - IOCTL_CS_COMMAND, - command, - sizeof(CommandHeader), - outputBuffer, - outputBufferSize, - &bytesReturned, - nullptr); - if (!result) { - ERR("DeviceIoControl failed. Error Code: {}", GetLastError()); - return false; - } - - return true; -} - -KernelProcess::KernelProcess(DWORD pid) - : Process(pid) -{ - m_DeviceHandle = CreateFileW( - L"\\\\.\\CheatStengineDriver", - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - 0, - nullptr); - if (m_DeviceHandle == INVALID_HANDLE_VALUE) { - ERR("Failed to open handle to driver. Error Code: {}", GetLastError()); - } -} +namespace { -KernelProcess::KernelProcess(const std::string& procName) - : KernelProcess(0) +DWORD FindProcessId(std::string_view processName) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { - return; + return 0; } - PROCESSENTRY32 entry; - entry.dwSize = sizeof(PROCESSENTRY32); + const std::string name(processName); + DWORD processId = 0; + PROCESSENTRY32 entry {}; + entry.dwSize = sizeof(entry); if (Process32First(snapshot, &entry)) { do { - if (strncmp(entry.szExeFile, procName.c_str(), procName.size()) == 0) { - m_Pid = entry.th32ProcessID; + if (_stricmp(entry.szExeFile, name.c_str()) == 0) { + processId = entry.th32ProcessID; break; } } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); + return processId; } -KernelProcess::~KernelProcess() -{ } -uintptr_t KernelProcess::Allocate(size_t size, uint32_t protection, uint32_t allocationType) const +KernelProcess::KernelProcess(DWORD pid) + : Process(pid) { - if (!IsValid()) { - return 0; + if (Process::IsValid()) { + m_Bound = m_Device.Bind(m_Pid); + if (!m_Bound) { + ERR("Failed to bind the driver to process {}: {}", m_Pid, m_Device.GetError()); + } } +} - CommandHeader command {}; - command.Type = CommandHeader::Type::AllocateMemory; - command.Pid = m_Pid; - command.AllocateMemoryData.Address = 0; - command.AllocateMemoryData.Size = size; - command.AllocateMemoryData.AllocationType = allocationType; - command.AllocateMemoryData.Protect = protection; +KernelProcess::KernelProcess(const std::string& procName) + : Process(FindProcessId(procName)) +{ + if (Process::IsValid()) { + m_Bound = m_Device.Bind(m_Pid); + if (!m_Bound) { + ERR("Failed to bind the driver to process {}: {}", m_Pid, m_Device.GetError()); + } + } +} - uintptr_t allocatedAddress = 0; - command.AllocateMemoryData.AllocatedAddressPtr = &allocatedAddress; +KernelProcess::~KernelProcess() = default; - SendBuffer(m_DeviceHandle, &command); - return allocatedAddress; +uintptr_t KernelProcess::Allocate(size_t size, uint32_t protection, uint32_t allocationType) const +{ + if (!IsValid() || size == 0) { + return 0; + } + return m_Device.Allocate(size, protection, allocationType); } bool KernelProcess::Free(uintptr_t address, uint32_t freeType) const { - if (!IsValid()) { + if (!IsValid() || address == 0) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::FreeMemory; - command.Pid = m_Pid; - command.FreeMemoryData.Address = address; - command.FreeMemoryData.Size = 0; - command.FreeMemoryData.FreeType = freeType; - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Free(address, freeType); } std::optional KernelProcess::Query(uintptr_t address) const @@ -171,37 +77,18 @@ std::optional KernelProcess::Query(uintptr_t address) if (!IsValid()) { return std::nullopt; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::QueryMemory; - command.Pid = m_Pid; - command.QueryMemoryData.Address = address; - MEMORY_BASIC_INFORMATION mbi {}; - if (!SendBuffer(m_DeviceHandle, &command, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) { - return std::nullopt; - } - return mbi; + return m_Device.Query(address); } -std::optional KernelProcess::Protect(uintptr_t address, size_t size, uint32_t protection) const +std::optional KernelProcess::Protect( + uintptr_t address, + size_t size, + uint32_t protection) const { - if (!IsValid()) { - return false; - } - - CommandHeader command {}; - command.Type = CommandHeader::Type::ProtectMemory; - command.Pid = m_Pid; - command.ProtectMemoryData.Address = address; - command.ProtectMemoryData.Size = size; - command.ProtectMemoryData.NewProtect = protection; - - uint32_t oldProtect = 0; - command.ProtectMemoryData.OldProtectPtr = &oldProtect; - if (!SendBuffer(m_DeviceHandle, &command)) { + if (!IsValid() || address == 0 || size == 0) { return std::nullopt; } - return oldProtect; + return m_Device.Protect(address, size, protection); } bool KernelProcess::ReadBuffer(uintptr_t address, void* buffer, size_t size) const @@ -209,14 +96,7 @@ bool KernelProcess::ReadBuffer(uintptr_t address, void* buffer, size_t size) con if (!IsValid()) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::ReadMemory; - command.Pid = m_Pid; - command.ReadMemoryData.Address = address; - command.ReadMemoryData.Size = size; - command.ReadMemoryData.Buffer = buffer; - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Read(address, buffer, size); } bool KernelProcess::WriteBuffer(uintptr_t address, const void* buffer, size_t size) const @@ -224,14 +104,7 @@ bool KernelProcess::WriteBuffer(uintptr_t address, const void* buffer, size_t si if (!IsValid()) { return false; } - - CommandHeader command {}; - command.Type = CommandHeader::Type::WriteMemory; - command.Pid = m_Pid; - command.WriteMemoryData.Address = address; - command.WriteMemoryData.Size = size; - command.WriteMemoryData.Buffer = const_cast(buffer); // It's up to the kernel driver to ensure this is not written to. - return SendBuffer(m_DeviceHandle, &command); + return m_Device.Write(address, buffer, size); } MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const @@ -241,12 +114,12 @@ MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const return {}; } - MODULEENTRY32 entry; + const std::string moduleName(name); + MODULEENTRY32 entry {}; entry.dwSize = sizeof(entry); - if (Module32First(snapshot, &entry)) { do { - if (_stricmp(entry.szModule, name.data()) == 0) { + if (_stricmp(entry.szModule, moduleName.c_str()) == 0) { CloseHandle(snapshot); return entry; } @@ -259,20 +132,19 @@ MODULEENTRY32 KernelProcess::GetModuleEntry(std::string_view name) const std::vector KernelProcess::GetModuleEntries(bool refresh) const { + static_cast(refresh); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, m_Pid); if (snapshot == INVALID_HANDLE_VALUE) { return {}; } - MODULEENTRY32 entry; + MODULEENTRY32 entry {}; entry.dwSize = sizeof(entry); - std::vector modules; + std::unordered_set seenModules; if (Module32First(snapshot, &entry)) { - std::unordered_set seenModules; do { - std::string moduleName = entry.szModule; - if (seenModules.insert(moduleName).second) { + if (seenModules.insert(entry.szModule).second) { modules.push_back(entry); } } while (Module32Next(snapshot, &entry)); @@ -289,22 +161,21 @@ std::string KernelProcess::GetName() } HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (snapshot == INVALID_HANDLE_VALUE) - return ""; - - PROCESSENTRY32 pe; - pe.dwSize = sizeof(PROCESSENTRY32); + if (snapshot == INVALID_HANDLE_VALUE) { + return {}; + } - if (Process32First(snapshot, &pe)) { + PROCESSENTRY32 entry {}; + entry.dwSize = sizeof(entry); + if (Process32First(snapshot, &entry)) { do { - if (pe.th32ProcessID == m_Pid) { - CloseHandle(snapshot); - m_Name = pe.szExeFile; - return pe.szExeFile; + if (entry.th32ProcessID == m_Pid) { + m_Name = entry.szExeFile; + break; } - } while (Process32Next(snapshot, &pe)); + } while (Process32Next(snapshot, &entry)); } CloseHandle(snapshot); - return {}; + return m_Name; } diff --git a/CheatStengine/src/CheatStengine/Process/KernelProcess.h b/CheatStengine/src/CheatStengine/Process/KernelProcess.h index 1d7ad28..2f8e88f 100644 --- a/CheatStengine/src/CheatStengine/Process/KernelProcess.h +++ b/CheatStengine/src/CheatStengine/Process/KernelProcess.h @@ -1,5 +1,6 @@ #pragma once +#include "Device.h" #include "Process.h" class KernelProcess final : public Process { @@ -9,7 +10,7 @@ class KernelProcess final : public Process { ~KernelProcess() override; [[nodiscard]] uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const override; - bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const override; + bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const override; [[nodiscard]] std::optional Query(uintptr_t address) const override; std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const override; @@ -21,9 +22,10 @@ class KernelProcess final : public Process { [[nodiscard]] std::vector GetModuleEntries(bool refresh = false) const override; [[nodiscard]] std::string GetName() override; - [[nodiscard]] bool IsValid() const override { return Process::IsValid() && m_DeviceHandle != INVALID_HANDLE_VALUE; } + [[nodiscard]] bool IsValid() const override { return Process::IsValid() && m_Bound && m_Device.IsOpen(); } private: - HANDLE m_DeviceHandle = nullptr; + Device m_Device; + bool m_Bound = false; std::string m_Name; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Process/Monitor.cpp b/CheatStengine/src/CheatStengine/Process/Monitor.cpp new file mode 100644 index 0000000..6e30c3b --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Monitor.cpp @@ -0,0 +1,125 @@ +#include "Monitor.h" + +#include "Device.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +Monitor::~Monitor() +{ + Stop(); +} + +void Monitor::Start() +{ + std::scoped_lock lock(m_ThreadMutex); + if (m_Thread.joinable()) { + return; + } + + MonitorSnapshot snapshot; + snapshot.Running = true; + SetSnapshot(snapshot); + m_Running.store(true, std::memory_order_release); + m_Thread = std::jthread([this](std::stop_token stopToken) { + Run(stopToken); + }); +} + +void Monitor::Stop() +{ + std::scoped_lock lock(m_ThreadMutex); + if (m_Thread.joinable()) { + m_Thread.request_stop(); + m_Wake.notify_all(); + m_Thread.join(); + } + + m_Running.store(false, std::memory_order_release); + MonitorSnapshot snapshot = GetSnapshot(); + snapshot.Running = false; + snapshot.Connected = false; + snapshot.Mode = Protocol::LoadMode::Unknown; + SetSnapshot(snapshot); +} + +bool Monitor::IsRunning() const +{ + return m_Running.load(std::memory_order_acquire); +} + +MonitorSnapshot Monitor::GetSnapshot() const +{ + std::scoped_lock lock(m_SnapshotMutex); + return m_Snapshot; +} + +void Monitor::Run(std::stop_token stopToken) +{ + std::unique_ptr device; + while (!stopToken.stop_requested()) { + bool newConnection = false; + if (!device) { + device = std::make_unique(); + newConnection = device->IsOpen(); + } + + MonitorSnapshot snapshot = GetSnapshot(); + snapshot.Running = true; + snapshot.Connected = device->IsOpen(); + snapshot.Mode = device->GetMode(); + snapshot.Error = device->GetError(); + + if (snapshot.Connected) { + const uint64_t sequence = m_NextSequence.fetch_add(1, std::memory_order_relaxed); + if (sequence == 0 + || sequence > static_cast(std::numeric_limits::max())) { + snapshot.Connected = false; + snapshot.Error = ERROR_ARITHMETIC_OVERFLOW; + SetSnapshot(snapshot); + break; + } + + const std::optional response = device->Heartbeat(sequence); + if (response) { + if (newConnection) { + snapshot.Generation = m_NextGeneration.fetch_add(1, std::memory_order_relaxed); + } + snapshot.Connected = true; + snapshot.Sequence = response->Sequence; + snapshot.KernelTime = response->KernelTime; + snapshot.RemainingIdleSeconds = response->RemainingIdleSeconds; + snapshot.State = static_cast(response->State); + snapshot.Error = ERROR_SUCCESS; + } else { + snapshot.Connected = false; + snapshot.Error = device->GetError(); + device.reset(); + } + } else { + device.reset(); + } + + SetSnapshot(snapshot); + if (Wait(stopToken)) { + break; + } + } +} + +bool Monitor::Wait(std::stop_token stopToken) +{ + std::unique_lock lock(m_WaitMutex); + return m_Wake.wait_for(lock, 5s, [&stopToken] { + return stopToken.stop_requested(); + }); +} + +void Monitor::SetSnapshot(const MonitorSnapshot& snapshot) +{ + std::scoped_lock lock(m_SnapshotMutex); + m_Snapshot = snapshot; +} diff --git a/CheatStengine/src/CheatStengine/Process/Monitor.h b/CheatStengine/src/CheatStengine/Process/Monitor.h new file mode 100644 index 0000000..3cd533d --- /dev/null +++ b/CheatStengine/src/CheatStengine/Process/Monitor.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include +#include + +struct MonitorSnapshot { + bool Running = false; + bool Connected = false; + Protocol::LoadMode Mode = Protocol::LoadMode::Unknown; + Protocol::RuntimeState State = Protocol::RuntimeState::Starting; + uint64_t Sequence = 0; + uint64_t Generation = 0; + uint64_t KernelTime = 0; + uint32_t RemainingIdleSeconds = 0; + DWORD Error = ERROR_SUCCESS; +}; + +class Monitor final { +public: + Monitor() = default; + ~Monitor(); + + Monitor(const Monitor&) = delete; + Monitor& operator=(const Monitor&) = delete; + Monitor(Monitor&&) = delete; + Monitor& operator=(Monitor&&) = delete; + + void Start(); + void Stop(); + + [[nodiscard]] bool IsRunning() const; + [[nodiscard]] MonitorSnapshot GetSnapshot() const; + +private: + void Run(std::stop_token stopToken); + [[nodiscard]] bool Wait(std::stop_token stopToken); + void SetSnapshot(const MonitorSnapshot& snapshot); + +private: + mutable std::mutex m_ThreadMutex; + std::jthread m_Thread; + std::atomic m_Running = false; + std::atomic m_NextSequence = 1; + std::atomic m_NextGeneration = 1; + + mutable std::mutex m_SnapshotMutex; + MonitorSnapshot m_Snapshot; + + std::mutex m_WaitMutex; + std::condition_variable m_Wake; +}; diff --git a/CheatStengine/src/CheatStengine/Process/Process.h b/CheatStengine/src/CheatStengine/Process/Process.h index ff19579..d5f03b0 100644 --- a/CheatStengine/src/CheatStengine/Process/Process.h +++ b/CheatStengine/src/CheatStengine/Process/Process.h @@ -26,7 +26,7 @@ class Process { virtual ~Process() = default; [[nodiscard]] virtual uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const = 0; - virtual bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const = 0; + virtual bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const = 0; [[nodiscard]] virtual std::optional Query(uintptr_t address) const = 0; virtual std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const = 0; @@ -36,11 +36,7 @@ class Process { [[nodiscard]] virtual MODULEENTRY32 GetModuleEntry(std::string_view name) const = 0; [[nodiscard]] virtual std::vector GetModuleEntries(bool refresh = false) const = 0; - [[nodiscard]] virtual std::string GetName() = 0; - - // Implemented methods that rely on the above virtual methods - [[nodiscard]] bool IsAddressReadable(uintptr_t address) const; template @@ -108,4 +104,4 @@ class Process { protected: DWORD m_Pid = 0; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h b/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h index e35fe4b..6aa6c16 100644 --- a/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h +++ b/CheatStengine/src/CheatStengine/Process/WinAPIProcess.h @@ -9,7 +9,7 @@ class WinAPIProcess final : public Process { ~WinAPIProcess() override; [[nodiscard]] uintptr_t Allocate(size_t size, uint32_t protection, uint32_t allocationType = MEM_COMMIT | MEM_RESERVE) const override; - bool Free(uintptr_t address, uint32_t freeType = MEM_DECOMMIT) const override; + bool Free(uintptr_t address, uint32_t freeType = MEM_RELEASE) const override; [[nodiscard]] std::optional Query(uintptr_t address) const override; std::optional Protect(uintptr_t address, size_t size, uint32_t protection) const override; @@ -25,4 +25,4 @@ class WinAPIProcess final : public Process { private: HANDLE m_Handle = nullptr; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp index f71884d..ffafd20 100644 --- a/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp +++ b/CheatStengine/src/CheatStengine/Server/ToolRegistry.cpp @@ -687,7 +687,9 @@ namespace Server { if (context.Scanner->IsScanning()) { Fail("a scan is already in progress"); } - context.Scanner->NextScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper); + if (!context.Scanner->NextScan(valueType, scanType, 0x0, 0x7FFFFFFFFFFF, value, upper)) { + Fail("failed to start scan refinement (invalid value or incompatible value type?)"); + } } else { if (context.Scanner && context.Scanner->IsScanning()) { Fail("a scan is already in progress; poll scan_results until scanning=false"); @@ -738,8 +740,4 @@ namespace Server { }); } -} - - - - +} \ No newline at end of file diff --git a/CheatStengine/src/CheatStengine/Settings/EnumSetting.h b/CheatStengine/src/CheatStengine/Settings/EnumSetting.h index 6e4d428..b2663a9 100644 --- a/CheatStengine/src/CheatStengine/Settings/EnumSetting.h +++ b/CheatStengine/src/CheatStengine/Settings/EnumSetting.h @@ -103,6 +103,7 @@ class EnumSetting final : public Setting { void Apply() override { m_Value = m_TempValue; } [[nodiscard]] EnumType GetValue() const { return m_Value; } + [[nodiscard]] EnumType GetPendingValue() const { return m_TempValue; } [[nodiscard]] const char* GetCurrentItemName() const { return EnumTraits::GetName(m_Value); } [[nodiscard]] std::string GetName() const override { return m_Name; } @@ -120,4 +121,4 @@ class EnumSetting final : public Setting { std::string m_Description; EnumType m_Value; EnumType m_TempValue; -}; \ No newline at end of file +}; diff --git a/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp b/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp new file mode 100644 index 0000000..79598c1 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/KernelSettings.cpp @@ -0,0 +1,317 @@ +#include "KernelSettings.h" + +#include + +#include + +#include +#include +#include +#include + +namespace { + +const char* GetModeName(Protocol::LoadMode mode) +{ + switch (mode) { + case Protocol::LoadMode::Pnp: return "PnP"; + case Protocol::LoadMode::Service: return "Service"; + case Protocol::LoadMode::Mapped: return "Mapped"; + default: return "Unknown"; + } +} + +const char* GetStateName(Protocol::RuntimeState state) +{ + switch (state) { + case Protocol::RuntimeState::Starting: return "Starting"; + case Protocol::RuntimeState::Ready: return "Ready"; + case Protocol::RuntimeState::Idle: return "Idle"; + case Protocol::RuntimeState::Stopping: return "Stopping"; + case Protocol::RuntimeState::Removed: return "Removed"; + default: return "Unknown"; + } +} + +std::string GetErrorText(DWORD error) +{ + if (error == ERROR_SUCCESS) { + return "None"; + } + return std::format("{} ({})", std::system_category().message(error), error); +} + +void DrawRow(const char* name, const std::string& value) +{ + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::TextDisabled("%s", name); + ImGui::TableSetColumnIndex(1); + ImGui::TextUnformatted(value.c_str()); +} + +} + +KernelSettings::KernelSettings( + std::string name, + Monitor& monitor, + std::function visible) + : m_Name(std::move(name)) + , m_Monitor(monitor) + , m_Visible(std::move(visible)) +{ +} + +void KernelSettings::Draw() +{ + PollTasks(); + if (m_Visible && !m_Visible()) { + return; + } + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + const bool connectionChanged = monitor.Connected + && monitor.Generation != m_LastPingGeneration; + if ((!m_LastPing || connectionChanged) && !m_PingTask.valid()) { + StartPing(); + } + + ImGui::PushID(m_Name.c_str()); + ImGui::Separator(); + ImGui::Spacing(); + ImGui::TextUnformatted("Kernel Mode"); + ImGui::Spacing(); + + DrawStatus(); + + const bool pinging = m_PingTask.valid(); + if (pinging) { + ImGui::BeginDisabled(); + } + if (ImGui::Button(pinging ? "Testing..." : "Ping Test")) { + StartPing(); + } + if (pinging) { + ImGui::EndDisabled(); + } + + int timeout = static_cast(m_TempValue); + ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.4f); + if (ImGui::SliderInt( + "Idle timeout", + &timeout, + static_cast(Protocol::MinimumIdleTimeoutSeconds), + static_cast(Protocol::MaximumIdleTimeoutSeconds), + "%d seconds")) { + m_TempValue = static_cast(timeout); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Time before the driver enters its idle state after the last client disconnects."); + } + + if (m_ConfigureTask.valid() || m_QueuedConfigure) { + ImGui::TextDisabled("Applying idle timeout..."); + } else if (m_LastConfigure) { + if (m_LastConfigure->Success) { + ImGui::TextDisabled("Idle timeout applied: %u seconds", m_LastConfigure->EffectiveSeconds); + } else { + const std::string error = GetErrorText(m_LastConfigure->Error); + ImGui::TextColored(ImVec4 { 1.0f, 0.4f, 0.4f, 1.0f }, "Apply failed: %s", error.c_str()); + } + } + + ImGui::PopID(); +} + +void KernelSettings::Restore() +{ + m_TempValue = m_Value; +} + +void KernelSettings::Apply() +{ + m_Value = m_TempValue; + if (!m_Visible || m_Visible()) { + QueueConfigure(m_Value); + } +} + +std::string KernelSettings::GetName() const +{ + return m_Name; +} + +std::string KernelSettings::GetDescription() const +{ + return {}; +} + +bool KernelSettings::HasValueChanged() const +{ + return m_Value != m_TempValue; +} + +void KernelSettings::PollTasks() +{ + if (m_PingTask.valid() + && m_PingTask.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + m_LastPing = m_PingTask.get(); + m_LastPingGeneration = m_PendingPingGeneration; + } + + if (m_ConfigureTask.valid() + && m_ConfigureTask.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + ConfigureResult result = m_ConfigureTask.get(); + if (result.Success) { + m_Value = result.EffectiveSeconds; + if (m_TempValue == result.RequestedSeconds) { + m_TempValue = result.EffectiveSeconds; + } + } + m_LastConfigure = result; + if (result.Success && !m_PingTask.valid()) { + StartPing(); + } + } + + StartConfigure(); +} + +void KernelSettings::StartPing() +{ + if (m_PingTask.valid()) { + return; + } + + const uint64_t nonce = m_NextNonce++; + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + m_PendingPingGeneration = monitor.Connected ? monitor.Generation : 0; + m_PingTask = std::async(std::launch::async, [nonce] { + return RunPing(nonce); + }); +} + +void KernelSettings::QueueConfigure(uint32_t seconds) +{ + m_QueuedConfigure = seconds; + StartConfigure(); +} + +void KernelSettings::StartConfigure() +{ + if (m_ConfigureTask.valid() || !m_QueuedConfigure) { + return; + } + + const uint32_t seconds = *m_QueuedConfigure; + m_QueuedConfigure.reset(); + m_ConfigureTask = std::async(std::launch::async, [seconds] { + return RunConfigure(seconds); + }); +} + +void KernelSettings::DrawStatus() const +{ + const MonitorSnapshot monitor = m_Monitor.GetSnapshot(); + Protocol::LoadMode mode = monitor.Mode; + Protocol::RuntimeState state = monitor.State; + DWORD error = monitor.Error; + const bool currentPing = m_LastPing + && m_LastPing->Response + && (!monitor.Connected || m_LastPingGeneration == monitor.Generation); + + if (m_LastPing) { + if (!monitor.Connected) { + mode = m_LastPing->Mode; + } + if (!monitor.Connected || m_LastPingGeneration == monitor.Generation) { + error = m_LastPing->Error; + } + if (!monitor.Connected && m_LastPing->Response) { + state = static_cast(m_LastPing->Response->State); + } + } + + if (ImGui::BeginTable("Status", 2, ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_BordersInnerH)) { + DrawRow( + "Connection", + monitor.Connected + ? "Connected" + : currentPing + ? "Reachable" + : monitor.Running ? "Connecting" + : "Stopped"); + DrawRow("Load", GetModeName(mode)); + DrawRow("State", GetStateName(state)); + + if (currentPing) { + const Protocol::PingResponse& response = *m_LastPing->Response; + DrawRow("Driver", std::format( + "{}.{}.{}", + response.DriverMajorVersion, + response.DriverMinorVersion, + response.DriverPatchVersion)); + DrawRow("Protocol", std::format( + "{}.{}", + response.Header.MajorVersion, + response.Header.MinorVersion)); + DrawRow("Capabilities", std::format("0x{:016X}", response.Capabilities)); + DrawRow("Active sessions", std::to_string(response.ActiveSessions)); + DrawRow("Idle timeout", std::format("{} seconds", response.IdleTimeoutSeconds)); + DrawRow( + "Idle remaining", + std::format( + "{} seconds", + monitor.Connected ? monitor.RemainingIdleSeconds : response.RemainingIdleSeconds)); + DrawRow("RTT", std::format("{:.3f} ms", m_LastPing->Milliseconds)); + } else { + DrawRow("Heartbeat", monitor.Sequence == 0 ? "Waiting" : std::format("#{}", monitor.Sequence)); + DrawRow("Idle remaining", std::format("{} seconds", monitor.RemainingIdleSeconds)); + } + + DrawRow("Error", GetErrorText(error)); + ImGui::EndTable(); + } + + ImGui::Spacing(); +} + +KernelSettings::PingResult KernelSettings::RunPing(uint64_t nonce) +{ + PingResult result; + Device device; + result.Mode = device.GetMode(); + if (!device.IsOpen()) { + result.Error = device.GetError(); + return result; + } + + const auto started = std::chrono::steady_clock::now(); + result.Response = device.Ping(nonce); + const auto finished = std::chrono::steady_clock::now(); + result.Milliseconds = std::chrono::duration(finished - started).count(); + result.Error = device.GetError(); + if (result.Response) { + result.Mode = static_cast(result.Response->LoadMode); + } + return result; +} + +KernelSettings::ConfigureResult KernelSettings::RunConfigure(uint32_t seconds) +{ + ConfigureResult result; + result.RequestedSeconds = seconds; + + Device device; + if (!device.IsOpen()) { + result.Error = device.GetError(); + return result; + } + + const std::optional response = device.Configure(seconds); + result.Error = device.GetError(); + if (response) { + result.EffectiveSeconds = response->IdleTimeoutSeconds; + result.Success = true; + } + return result; +} diff --git a/CheatStengine/src/CheatStengine/Settings/KernelSettings.h b/CheatStengine/src/CheatStengine/Settings/KernelSettings.h new file mode 100644 index 0000000..16d1908 --- /dev/null +++ b/CheatStengine/src/CheatStengine/Settings/KernelSettings.h @@ -0,0 +1,69 @@ +#pragma once + +#include "Setting.h" + +#include + +#include + +#include + +#include +#include +#include +#include +#include + +class KernelSettings final : public Setting { +public: + KernelSettings(std::string name, Monitor& monitor, std::function visible); + + void Draw() override; + void Restore() override; + void Apply() override; + + [[nodiscard]] std::string GetName() const override; + [[nodiscard]] std::string GetDescription() const override; + [[nodiscard]] bool HasValueChanged() const override; + +private: + struct PingResult { + std::optional Response; + Protocol::LoadMode Mode = Protocol::LoadMode::Unknown; + DWORD Error = ERROR_SUCCESS; + double Milliseconds = 0.0; + }; + + struct ConfigureResult { + uint32_t RequestedSeconds = 0; + uint32_t EffectiveSeconds = 0; + DWORD Error = ERROR_SUCCESS; + bool Success = false; + }; + + void PollTasks(); + void StartPing(); + void QueueConfigure(uint32_t seconds); + void StartConfigure(); + void DrawStatus() const; + + [[nodiscard]] static PingResult RunPing(uint64_t nonce); + [[nodiscard]] static ConfigureResult RunConfigure(uint32_t seconds); + +private: + std::string m_Name; + Monitor& m_Monitor; + std::function m_Visible; + uint32_t m_Value = Protocol::DefaultIdleTimeoutSeconds; + uint32_t m_TempValue = Protocol::DefaultIdleTimeoutSeconds; + uint64_t m_NextNonce = 1; + uint64_t m_PendingPingGeneration = 0; + uint64_t m_LastPingGeneration = 0; + + std::future m_PingTask; + std::optional m_LastPing; + + std::future m_ConfigureTask; + std::optional m_QueuedConfigure; + std::optional m_LastConfigure; +}; diff --git a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp index 3b89e78..61f91a3 100644 --- a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp +++ b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.cpp @@ -1,9 +1,230 @@ #include "MemoryScanner.h" +#include #include #include +#include +#include +#include #include +#include +#include +#include +#include + +namespace { + + constexpr size_t ScanReadChunkSize = 1024 * 1024; + constexpr size_t MaximumConsecutiveQueryFailures = 16; + + std::unique_ptr CloneProcess(const std::unique_ptr& process) + { + if (!process || !process->IsValid()) { + return {}; + } + + const ProcessMode mode = dynamic_cast(process.get()) + ? ProcessMode::Kernel + : ProcessMode::WinAPI; + try { + std::unique_ptr clone = Process::Create(process->GetPid(), mode); + if (!clone || !clone->IsValid() || !clone->Query(0)) { + WARN("Failed to open an independent process connection for the memory scan"); + return {}; + } + return clone; + } catch (const std::exception& error) { + WARN("Failed to open an independent process connection for the memory scan: {}", error.what()); + return {}; + } + } + + std::string_view Trim(std::string_view value) + { + const size_t first = value.find_first_not_of(" \t\r\n"); + if (first == std::string_view::npos) { + return {}; + } + + const size_t last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); + } + + template + T ParseInteger(std::string_view input) + { + static_assert(std::is_integral_v); + + const std::string_view trimmed = Trim(input); + if (trimmed.empty()) { + throw std::invalid_argument("value is empty"); + } + + const size_t prefixOffset = (trimmed.front() == '+' || trimmed.front() == '-') ? 1 : 0; + const bool isNegative = trimmed.front() == '-'; + const bool isHex = trimmed.size() >= prefixOffset + 2 + && trimmed[prefixOffset] == '0' + && (trimmed[prefixOffset + 1] == 'x' || trimmed[prefixOffset + 1] == 'X'); + const int base = isHex ? 16 : 10; + const std::string text(trimmed); + size_t parsedCharacters = 0; + + if constexpr (std::is_unsigned_v) { + if (isNegative) { + throw std::out_of_range("negative value cannot be stored in an unsigned type"); + } + + const unsigned long long parsed = std::stoull(text, &parsedCharacters, base); + if (parsedCharacters != text.size() || parsed > std::numeric_limits::max()) { + throw std::out_of_range("value is outside the selected type's range"); + } + return static_cast(parsed); + } else { + if (isHex && !isNegative) { + using UnsignedT = std::make_unsigned_t; + const unsigned long long parsed = std::stoull(text, &parsedCharacters, base); + if (parsedCharacters != text.size() || parsed > std::numeric_limits::max()) { + throw std::out_of_range("value is outside the selected type's range"); + } + + const UnsignedT bits = static_cast(parsed); + T value {}; + std::memcpy(&value, &bits, sizeof(value)); + return value; + } + + const long long parsed = std::stoll(text, &parsedCharacters, base); + if (parsedCharacters != text.size() + || parsed < static_cast(std::numeric_limits::min()) + || parsed > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("value is outside the selected type's range"); + } + return static_cast(parsed); + } + } + + template + T ParseFloatingPoint(std::string_view input) + { + const std::string_view trimmed = Trim(input); + if (trimmed.empty()) { + throw std::invalid_argument("value is empty"); + } + + const std::string text(trimmed); + size_t parsedCharacters = 0; + T value {}; + if constexpr (std::is_same_v) { + value = std::stof(text, &parsedCharacters); + } else { + value = std::stod(text, &parsedCharacters); + } + + if (parsedCharacters != text.size()) { + throw std::invalid_argument("value contains invalid characters"); + } + return value; + } + + ScanValue ParseScanValue(std::string_view input, ValueType type) + { + switch (type) { + case ValueType::Int8: return ParseInteger(input); + case ValueType::Int16: return ParseInteger(input); + case ValueType::Int32: return ParseInteger(input); + case ValueType::Int64: return ParseInteger(input); + case ValueType::UInt8: return ParseInteger(input); + case ValueType::UInt16: return ParseInteger(input); + case ValueType::UInt32: return ParseInteger(input); + case ValueType::UInt64: return ParseInteger(input); + case ValueType::Float: return ParseFloatingPoint(input); + case ValueType::Double: return ParseFloatingPoint(input); + default: throw std::invalid_argument("invalid value type"); + } + } + + template + T ReadUnaligned(const uint8_t* data) + { + T value {}; + std::memcpy(&value, data, sizeof(value)); + return value; + } + + ScanValue ScanValueFromBytes(ValueType type, const uint8_t* data) + { + switch (type) { + case ValueType::Int8: return ReadUnaligned(data); + case ValueType::Int16: return ReadUnaligned(data); + case ValueType::Int32: return ReadUnaligned(data); + case ValueType::Int64: return ReadUnaligned(data); + case ValueType::UInt8: return ReadUnaligned(data); + case ValueType::UInt16: return ReadUnaligned(data); + case ValueType::UInt32: return ReadUnaligned(data); + case ValueType::UInt64: return ReadUnaligned(data); + case ValueType::Float: return ReadUnaligned(data); + case ValueType::Double: return ReadUnaligned(data); + default: throw std::invalid_argument("invalid value type"); + } + } + + bool TryReadScanValue(const Process& process, uintptr_t address, const ScanValue& previousValue, ScanValue& value) + { + return std::visit([&](auto previous) { + using T = decltype(previous); + T current {}; + if (!process.ReadBuffer(address, ¤t, sizeof(current))) { + return false; + } + value = current; + return true; + }, + previousValue); + } + + bool AreBoundsOrdered(const ScanValue& lower, const ScanValue& upper) + { + return std::visit([&](auto lowerValue) { + using T = decltype(lowerValue); + const T* upperValue = std::get_if(&upper); + return upperValue && lowerValue <= *upperValue; + }, + lower); + } + + template + bool CompareRawValues(ScanType scanType, const uint8_t* data, + const std::vector& target1, const std::vector& target2) + { + if (scanType == ScanType::UnknownInitialValue) { + return true; + } + if (target1.size() != sizeof(T)) { + return false; + } + + const T currentValue = ReadUnaligned(data); + const T lowerValue = ReadUnaligned(target1.data()); + switch (scanType) { + case ScanType::ExactValue: return currentValue == lowerValue; + case ScanType::BiggerThan: return currentValue > lowerValue; + case ScanType::SmallerThan: return currentValue < lowerValue; + case ScanType::ValueBetween: + return target2.size() == sizeof(T) + && currentValue >= lowerValue + && currentValue <= ReadUnaligned(target2.data()); + default: return false; + } + } + +} // namespace + +MemoryScanner::MemoryScanner(const std::unique_ptr& process) + : m_Process(CloneProcess(process)) +{ + m_CurrentResults.reserve(100000); +} std::string ValueTypeToString(ValueType type) { @@ -66,18 +287,15 @@ std::string MemoryAddress::ReadValue(const Process& proc) const bool MemoryAddress::WriteValue(const Process& proc, const std::string& value) const { - switch (Type) { - case ValueType::Int8: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int16: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int32: return proc.Write(Address, static_cast(std::stoi(value))); - case ValueType::Int64: return proc.Write(Address, static_cast(std::stoll(value))); - case ValueType::UInt8: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt16: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt32: return proc.Write(Address, static_cast(std::stoul(value))); - case ValueType::UInt64: return proc.Write(Address, static_cast(std::stoull(value))); - case ValueType::Float: return proc.Write(Address, std::stof(value)); - case ValueType::Double: return proc.Write(Address, std::stod(value)); - default: return false; + try { + const ScanValue parsedValue = ParseScanValue(value, Type); + return std::visit([&](auto typedValue) { + return proc.Write(Address, typedValue); + }, + parsedValue); + } catch (const std::exception& error) { + ERR("Invalid value '{}': {}", value, error.what()); + return false; } } @@ -100,325 +318,400 @@ std::string ScanValueToString(const ScanValue& value, bool isHex) bool MemoryScanner::FirstScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - if (m_Scanning) { + const size_t typeSize = GetTypeSize(valueType); + if (!m_Process || !m_Process->IsValid() || typeSize == 0 + || scanType < ScanType::ExactValue || scanType >= ScanType::COUNT + || minAddress >= maxAddress) { + ERR("Invalid memory scan configuration"); return false; } - std::vector targetValue1, targetValue2; - if (scanType != ScanType::UnknownInitialValue) { - if (lowerValue.empty()) { - return false; - } - targetValue1 = StringToBytes(lowerValue, valueType); - } - if (scanType == ScanType::ValueBetween) { - if (upperValue.empty()) { - return false; - } - targetValue2 = StringToBytes(upperValue, valueType); - } - std::thread([this](ValueType valueType, ScanType scanType, - uintptr_t minAddress, uintptr_t maxAddress, - const std::vector& targetValue1, const std::vector& targetValue2) { - auto start = std::chrono::high_resolution_clock::now(); - - { - std::lock_guard lock(m_Mutex); - m_CurrentResults.clear(); + std::vector targetValue1; + std::vector targetValue2; + try { + if (scanType != ScanType::UnknownInitialValue) { + if (lowerValue.empty()) { + ERR("A scan value is required"); + return false; + } + targetValue1 = StringToBytes(lowerValue, valueType); } - m_Scanning = true; - std::vector> regions; - regions.reserve(256); - uintptr_t currentAddress = minAddress; - while (currentAddress < maxAddress) { - std::optional mbi = m_Process->Query(currentAddress); - if (!mbi) { - currentAddress += 0x1000; - continue; + if (scanType == ScanType::ValueBetween) { + if (upperValue.empty()) { + ERR("An upper scan value is required"); + return false; } - - uintptr_t base = reinterpret_cast(mbi->BaseAddress); - if (!IsValidMemoryRegion(*mbi)) { - currentAddress = base + mbi->RegionSize; - continue; + targetValue2 = StringToBytes(upperValue, valueType); + if (!AreBoundsOrdered(StringToValue(lowerValue, valueType), StringToValue(upperValue, valueType))) { + ERR("The lower scan value must not exceed the upper scan value"); + return false; } - - regions.emplace_back(base, mbi->RegionSize); - currentAddress = base + mbi->RegionSize; } + } catch (const std::exception& error) { + ERR("Invalid scan value: {}", error.what()); + return false; + } + + std::scoped_lock threadLock(m_ThreadMutex); + if (m_Scanning.exchange(true, std::memory_order_acq_rel)) { + return false; + } + m_ValueType = valueType; + + try { + m_ScanThread = std::jthread([this, valueType, scanType, minAddress, maxAddress, + typeSize, targetValue1 = std::move(targetValue1), + targetValue2 = std::move(targetValue2)](std::stop_token stopToken) { + try { + auto startTime = std::chrono::high_resolution_clock::now(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults.clear(); + } + + std::vector> regions; + regions.reserve(256); + uintptr_t currentAddress = minAddress; + size_t consecutiveQueryFailures = 0; + while (currentAddress < maxAddress && !stopToken.stop_requested()) { + const std::optional mbi = m_Process->Query(currentAddress); + if (!mbi) { + ++consecutiveQueryFailures; + if (consecutiveQueryFailures >= MaximumConsecutiveQueryFailures) { + WARN("Stopping memory-region discovery after {} consecutive query failures at 0x{:X}", + consecutiveQueryFailures, currentAddress); + break; + } + currentAddress += std::min(0x1000, maxAddress - currentAddress); + continue; + } + consecutiveQueryFailures = 0; - INFO("Found {} regions to scan", regions.size()); - INFO("Took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count()); - start = std::chrono::high_resolution_clock::now(); - size_t typeSize = GetTypeSize(valueType); + const uintptr_t base = reinterpret_cast(mbi->BaseAddress); + uintptr_t regionEnd = std::numeric_limits::max(); + if (mbi->RegionSize <= std::numeric_limits::max() - base) { + regionEnd = base + static_cast(mbi->RegionSize); + } - std::vector>> futures; - futures.reserve(regions.size()); + if (regionEnd <= currentAddress) { + currentAddress += std::min(0x1000, maxAddress - currentAddress); + continue; + } - for (const auto& [address, size] : regions) { - futures.emplace_back(std::async(std::launch::async, [this, address, size, typeSize, scanType, valueType, &targetValue1, &targetValue2]() -> std::vector { - std::vector buffer(size); - if (!m_Process->ReadBuffer(address, buffer.data(), size)) { - return {}; + const uintptr_t scanStart = std::max({ minAddress, currentAddress, base }); + const uintptr_t scanEnd = std::min(maxAddress, regionEnd); + if (IsValidMemoryRegion(*mbi) + && scanEnd > scanStart + && scanEnd - scanStart >= typeSize) { + regions.emplace_back(scanStart, static_cast(scanEnd - scanStart)); + } + currentAddress = regionEnd; } - std::vector localResults; - for (size_t offset = 0; offset < size; offset += typeSize) { - uintptr_t currentAddress = address + offset; - - if (CompareByteArrays(scanType, valueType, buffer.data() + offset, targetValue1, targetValue2)) { - switch (valueType) { - case ValueType::Int8: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int16: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int32: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Int64: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt8: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt16: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt32: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::UInt64: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Float: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; - case ValueType::Double: localResults.emplace_back(currentAddress, *reinterpret_cast(buffer.data() + offset)); break; + INFO("Found {} regions to scan", regions.size()); + INFO("Region query took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - startTime).count()); + startTime = std::chrono::high_resolution_clock::now(); + + std::vector newResults; + if (!regions.empty() && !stopToken.stop_requested()) { + const size_t hardwareThreads = std::max(1, std::thread::hardware_concurrency()); + const size_t workerCount = std::min(hardwareThreads, regions.size()); + const size_t regionsPerWorker = (regions.size() + workerCount - 1) / workerCount; + + std::vector>> futures; + futures.reserve(workerCount); + for (size_t worker = 0; worker < workerCount; ++worker) { + const size_t firstRegion = worker * regionsPerWorker; + const size_t lastRegion = std::min(firstRegion + regionsPerWorker, regions.size()); + if (firstRegion >= lastRegion) { + break; } + + futures.emplace_back(std::async(std::launch::async, + [this, ®ions, firstRegion, lastRegion, typeSize, scanType, valueType, + &targetValue1, &targetValue2, stopToken] { + std::vector localResults; + for (size_t regionIndex = firstRegion; + regionIndex < lastRegion && !stopToken.stop_requested(); + ++regionIndex) { + const auto [address, regionSize] = regions[regionIndex]; + std::vector buffer(std::min(ScanReadChunkSize, regionSize)); + + for (size_t regionOffset = 0; + regionOffset < regionSize && !stopToken.stop_requested();) { + const size_t readSize = std::min(buffer.size(), regionSize - regionOffset); + if (m_Process->ReadBuffer(address + regionOffset, buffer.data(), readSize)) { + for (size_t offset = 0; offset + typeSize <= readSize; offset += typeSize) { + const uint8_t* valueBytes = buffer.data() + offset; + if (CompareByteArrays(scanType, valueType, valueBytes, + targetValue1, targetValue2)) { + localResults.emplace_back(address + regionOffset + offset, + ScanValueFromBytes(valueType, valueBytes)); + } + } + } + regionOffset += readSize; + } + } + return localResults; + })); + } + + for (auto& future : futures) { + std::vector results = future.get(); + newResults.insert(newResults.end(), + std::make_move_iterator(results.begin()), + std::make_move_iterator(results.end())); } - // Additional scan types can be implemented here. } - return localResults; - })); - } - for (auto& future : futures) { - std::vector results = future.get(); - std::lock_guard lock(m_Mutex); - m_CurrentResults.insert(m_CurrentResults.end(), - std::make_move_iterator(results.begin()), - std::make_move_iterator(results.end())); - } + if (!stopToken.stop_requested()) { + const size_t resultCount = newResults.size(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults = std::move(newResults); + } + + INFO("Found {} results", resultCount); + INFO("Memory scan took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - startTime).count()); + } + } catch (const std::exception& error) { + ERR("Memory scan failed: {}", error.what()); + } catch (...) { + ERR("Memory scan failed with an unknown error"); + } + m_Scanning.store(false, std::memory_order_release); + }); + } catch (const std::exception& error) { + m_ValueType = ValueType::COUNT; + m_Scanning.store(false, std::memory_order_release); + ERR("Failed to start memory scan: {}", error.what()); + return false; + } - INFO("Found {} results", m_CurrentResults.size()); - INFO("Took: {}ms", std::chrono::duration(std::chrono::high_resolution_clock::now() - start).count()); - m_Scanning = false; - }, - valueType, scanType, minAddress, maxAddress, targetValue1, targetValue2) - .detach(); return true; } -void MemoryScanner::NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) +bool MemoryScanner::NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - if (m_CurrentResults.empty()) { - ERR("Not previous scan results"); - return; + const size_t typeSize = GetTypeSize(valueType); + if (!m_Process || !m_Process->IsValid() || typeSize == 0 + || scanType < ScanType::ExactValue || scanType >= ScanType::COUNT + || minAddress >= maxAddress) { + ERR("Invalid memory scan configuration"); + return false; } - if (m_Scanning) { - return; + if (valueType != m_ValueType) { + ERR("The value type cannot be changed while refining a scan"); + return false; } - std::thread([this](ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue, const std::string& upperValue) { - INFO("Refining {} results...", m_CurrentResults.size()); + { + std::lock_guard lock(m_Mutex); + if (m_CurrentResults.empty()) { + ERR("No previous scan results"); + return false; + } + } - ScanValue targetValue1, targetValue2; - if (scanType != ScanType::UnknownInitialValue && !lowerValue.empty()) { + ScanValue targetValue1 = int8_t {}; + ScanValue targetValue2 = int8_t {}; + try { + if (scanType != ScanType::UnknownInitialValue) { + if (lowerValue.empty()) { + ERR("A scan value is required"); + return false; + } targetValue1 = StringToValue(lowerValue, valueType); } - if (scanType == ScanType::ValueBetween && !upperValue.empty()) { + if (scanType == ScanType::ValueBetween) { + if (upperValue.empty()) { + ERR("An upper scan value is required"); + return false; + } targetValue2 = StringToValue(upperValue, valueType); + if (!AreBoundsOrdered(targetValue1, targetValue2)) { + ERR("The lower scan value must not exceed the upper scan value"); + return false; + } } + } catch (const std::exception& error) { + ERR("Invalid scan value: {}", error.what()); + return false; + } - m_Scanning = true; - - uint32_t numThreads = std::thread::hardware_concurrency(); - if (numThreads == 0) { - numThreads = 4; - } - - size_t totalAddresses = m_CurrentResults.size(); - size_t chunkSize = (totalAddresses + numThreads - 1) / numThreads; + std::scoped_lock threadLock(m_ThreadMutex); + if (m_Scanning.exchange(true, std::memory_order_acq_rel)) { + return false; + } - std::vector>> futures; - for (size_t i = 0; i < numThreads; ++i) { - size_t start = i * chunkSize; - size_t end = std::min(start + chunkSize, totalAddresses); - if (start >= totalAddresses) { - break; - } + try { + m_ScanThread = std::jthread([this, scanType, minAddress, maxAddress, typeSize, + targetValue1 = std::move(targetValue1), + targetValue2 = std::move(targetValue2)](std::stop_token stopToken) { + try { + size_t totalAddresses = 0; + { + std::lock_guard lock(m_Mutex); + totalAddresses = m_CurrentResults.size(); + } + INFO("Refining {} results...", totalAddresses); + + const size_t hardwareThreads = std::max(1, std::thread::hardware_concurrency()); + const size_t workerCount = std::max(1, std::min(hardwareThreads, totalAddresses)); + const size_t addressesPerWorker = (totalAddresses + workerCount - 1) / workerCount; + + std::vector>> futures; + futures.reserve(workerCount); + for (size_t worker = 0; worker < workerCount; ++worker) { + const size_t firstAddress = worker * addressesPerWorker; + const size_t lastAddress = std::min(firstAddress + addressesPerWorker, totalAddresses); + if (firstAddress >= lastAddress) { + break; + } - futures.push_back(std::async(std::launch::async, [this, scanType, &targetValue1, &targetValue2, minAddress, maxAddress](size_t startIdx, size_t endIdx) { - std::vector threadResults; + futures.emplace_back(std::async(std::launch::async, + [this, scanType, &targetValue1, &targetValue2, minAddress, maxAddress, typeSize, + firstAddress, lastAddress, stopToken] { + std::vector localResults; + for (size_t index = firstAddress; + index < lastAddress && !stopToken.stop_requested(); + ++index) { + const ScannedAddress& address = m_CurrentResults[index]; + if (address.Address < minAddress || address.Address >= maxAddress + || typeSize > maxAddress - address.Address) { + continue; + } + + ScanValue value = address.PreviousValue; + if (!TryReadScanValue(*m_Process, address.Address, address.PreviousValue, value)) { + continue; + } + if (CompareValues(scanType, value, targetValue1, targetValue2)) { + localResults.push_back(address); + localResults.back().PreviousValue = value; + } + } + return localResults; + })); + } - for (size_t j = startIdx; j < endIdx; j++) { - ScannedAddress& addr = m_CurrentResults[j]; - if (addr.Address < minAddress || addr.Address >= maxAddress) { - continue; + std::vector newResults; + for (auto& future : futures) { + std::vector results = future.get(); + newResults.insert(newResults.end(), + std::make_move_iterator(results.begin()), + std::make_move_iterator(results.end())); } - ScanValue value = addr.ReadValue(*m_Process); - if (CompareValues(scanType, value, targetValue1, targetValue2)) { - threadResults.push_back(addr); - threadResults.back().PreviousValue = value; + + if (!stopToken.stop_requested()) { + const size_t resultCount = newResults.size(); + { + std::lock_guard lock(m_Mutex); + m_CurrentResults = std::move(newResults); + } + INFO("Found {} results", resultCount); } + } catch (const std::exception& error) { + ERR("Memory scan refinement failed: {}", error.what()); + } catch (...) { + ERR("Memory scan refinement failed with an unknown error"); } + m_Scanning.store(false, std::memory_order_release); + }); + } catch (const std::exception& error) { + m_Scanning.store(false, std::memory_order_release); + ERR("Failed to start memory scan refinement: {}", error.what()); + return false; + } - return threadResults; }, start, end)); - } - - std::vector newResults; - for (auto& future : futures) { - auto threadResult = future.get(); - newResults.insert(newResults.end(), - std::make_move_iterator(threadResult.begin()), - std::make_move_iterator(threadResult.end())); - } - - { - std::lock_guard lock(m_Mutex); - m_CurrentResults = std::move(newResults); - } - m_Scanning = false; - INFO("Found {} results", m_CurrentResults.size()); - }, - valueType, scanType, minAddress, maxAddress, lowerValue, upperValue) - .detach(); + return true; } -bool MemoryScanner::CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2) +void MemoryScanner::Cancel() { - if (scanType == ScanType::ExactValue) { - return value == targetValue1; - } - - if (scanType == ScanType::BiggerThan) { - return std::visit(overloads { - [&](int8_t v) { return v > std::get(targetValue1); }, - [&](int16_t v) { return v > std::get(targetValue1); }, - [&](int32_t v) { return v > std::get(targetValue1); }, - [&](int64_t v) { return v > std::get(targetValue1); }, - [&](uint8_t v) { return v > std::get(targetValue1); }, - [&](uint16_t v) { return v > std::get(targetValue1); }, - [&](uint32_t v) { return v > std::get(targetValue1); }, - [&](uint64_t v) { return v > std::get(targetValue1); }, - [&](float v) { return v > std::get(targetValue1); }, - [&](double v) { return v > std::get(targetValue1); }, - }, - value); - } - - if (scanType == ScanType::SmallerThan) { - return std::visit(overloads { - [&](int8_t v) { return v < std::get(targetValue1); }, - [&](int16_t v) { return v < std::get(targetValue1); }, - [&](int32_t v) { return v < std::get(targetValue1); }, - [&](int64_t v) { return v < std::get(targetValue1); }, - [&](uint8_t v) { return v < std::get(targetValue1); }, - [&](uint16_t v) { return v < std::get(targetValue1); }, - [&](uint32_t v) { return v < std::get(targetValue1); }, - [&](uint64_t v) { return v < std::get(targetValue1); }, - [&](float v) { return v < std::get(targetValue1); }, - [&](double v) { return v < std::get(targetValue1); }, - }, - value); + std::scoped_lock lock(m_ThreadMutex); + if (m_ScanThread.joinable()) { + m_ScanThread.request_stop(); } - return false; } -bool MemoryScanner::CompareByteArrays(ScanType scanType, ValueType valueType, uint8_t* data, const std::vector& target1, const std::vector& target2) +bool MemoryScanner::CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2) { - if (scanType == ScanType::ExactValue) { - return memcmp(data, target1.data(), target1.size()) == 0; + if (scanType == ScanType::UnknownInitialValue) { + return true; } - if (scanType == ScanType::BiggerThan) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data > *(int8_t*)target1.data(); - case ValueType::Int16: return *(int16_t*)data > *(int16_t*)target1.data(); - case ValueType::Int32: return *(int32_t*)data > *(int32_t*)target1.data(); - case ValueType::Int64: return *(int64_t*)data > *(int64_t*)target1.data(); - case ValueType::UInt8: return *(uint8_t*)data > *(uint8_t*)target1.data(); - case ValueType::UInt16: return *(uint16_t*)data > *(uint16_t*)target1.data(); - case ValueType::UInt32: return *(uint32_t*)data > *(uint32_t*)target1.data(); - case ValueType::UInt64: return *(uint64_t*)data > *(uint64_t*)target1.data(); - case ValueType::Float: return *(float*)data > *(float*)target1.data(); - case ValueType::Double: return *(double*)data > *(double*)target1.data(); - default: return false; + return std::visit([&](auto currentValue) { + using T = decltype(currentValue); + const T* lowerValue = std::get_if(&targetValue1); + if (!lowerValue) { + return false; } - } - if (scanType == ScanType::SmallerThan) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data < *(int8_t*)target1.data(); - case ValueType::Int16: return *(int16_t*)data < *(int16_t*)target1.data(); - case ValueType::Int32: return *(int32_t*)data < *(int32_t*)target1.data(); - case ValueType::Int64: return *(int64_t*)data < *(int64_t*)target1.data(); - case ValueType::UInt8: return *(uint8_t*)data < *(uint8_t*)target1.data(); - case ValueType::UInt16: return *(uint16_t*)data < *(uint16_t*)target1.data(); - case ValueType::UInt32: return *(uint32_t*)data < *(uint32_t*)target1.data(); - case ValueType::UInt64: return *(uint64_t*)data < *(uint64_t*)target1.data(); - case ValueType::Float: return *(float*)data < *(float*)target1.data(); - case ValueType::Double: return *(double*)data < *(double*)target1.data(); + switch (scanType) { + case ScanType::ExactValue: return currentValue == *lowerValue; + case ScanType::BiggerThan: return currentValue > *lowerValue; + case ScanType::SmallerThan: return currentValue < *lowerValue; + case ScanType::ValueBetween: { + const T* upperValue = std::get_if(&targetValue2); + return upperValue && currentValue >= *lowerValue && currentValue <= *upperValue; + } default: return false; } - } + }, + value); +} - if (scanType == ScanType::ValueBetween) { - switch (valueType) { - case ValueType::Int8: return *(int8_t*)data >= *(int8_t*)target1.data() && *(int8_t*)data <= *(int8_t*)target2.data(); - case ValueType::Int16: return *(int16_t*)data >= *(int16_t*)target1.data() && *(int16_t*)data <= *(int16_t*)target2.data(); - case ValueType::Int32: return *(int32_t*)data >= *(int32_t*)target1.data() && *(int32_t*)data <= *(int32_t*)target2.data(); - case ValueType::Int64: return *(int64_t*)data >= *(int64_t*)target1.data() && *(int64_t*)data <= *(int64_t*)target2.data(); - case ValueType::UInt8: return *(uint8_t*)data >= *(uint8_t*)target1.data() && *(uint8_t*)data <= *(uint8_t*)target2.data(); - case ValueType::UInt16: return *(uint16_t*)data >= *(uint16_t*)target1.data() && *(uint16_t*)data <= *(uint16_t*)target2.data(); - case ValueType::UInt32: return *(uint32_t*)data >= *(uint32_t*)target1.data() && *(uint32_t*)data <= *(uint32_t*)target2.data(); - case ValueType::UInt64: return *(uint64_t*)data >= *(uint64_t*)target1.data() && *(uint64_t*)data <= *(uint64_t*)target2.data(); - case ValueType::Float: return *(float*)data >= *(float*)target1.data() && *(float*)data <= *(float*)target2.data(); - case ValueType::Double: return *(double*)data >= *(double*)target1.data() && *(double*)data <= *(double*)target2.data(); - default: return false; - } +bool MemoryScanner::CompareByteArrays(ScanType scanType, ValueType valueType, const uint8_t* data, const std::vector& target1, const std::vector& target2) +{ + if (!data) { + return false; } - return true; + switch (valueType) { + case ValueType::Int8: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int16: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int32: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Int64: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt8: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt16: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt32: return CompareRawValues(scanType, data, target1, target2); + case ValueType::UInt64: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Float: return CompareRawValues(scanType, data, target1, target2); + case ValueType::Double: return CompareRawValues(scanType, data, target1, target2); + default: return false; + } } std::vector MemoryScanner::StringToBytes(const std::string& str, ValueType type) { - std::string val = str.starts_with("0x") ? str.substr(2) : str; - switch (type) { - case ValueType::Int8: return ValueToBytes(std::stoi(val)); - case ValueType::UInt8: return ValueToBytes(std::stoul(val)); - case ValueType::Int16: return ValueToBytes(std::stoi(val)); - case ValueType::UInt16: return ValueToBytes(std::stoul(val)); - case ValueType::Int32: return ValueToBytes(std::stol(val)); - case ValueType::UInt32: return ValueToBytes(std::stoul(val)); - case ValueType::Int64: return ValueToBytes(std::stoll(val)); - case ValueType::UInt64: return ValueToBytes(std::stoull(val)); - case ValueType::Float: return ValueToBytes(std::stof(val)); - case ValueType::Double: return ValueToBytes(std::stod(val)); - default: return {}; - } + const ScanValue value = ParseScanValue(str, type); + return std::visit([](auto typedValue) { + return ValueToBytes(typedValue); + }, + value); } ScanValue MemoryScanner::StringToValue(const std::string& str, ValueType type) { - switch (type) { - case ValueType::Int8: return static_cast(std::stoi(str)); - case ValueType::UInt8: return static_cast(std::stoul(str)); - case ValueType::Int16: return static_cast(std::stoi(str)); - case ValueType::UInt16: return static_cast(std::stoul(str)); - case ValueType::Int32: return static_cast(std::stol(str)); - case ValueType::UInt32: return static_cast(std::stoul(str)); - case ValueType::Int64: return static_cast(std::stoll(str)); - case ValueType::UInt64: return static_cast(std::stoull(str)); - case ValueType::Float: return static_cast(std::stof(str)); - case ValueType::Double: return static_cast(std::stod(str)); - default: return {}; - } + return ParseScanValue(str, type); } bool MemoryScanner::IsValidMemoryRegion(const MEMORY_BASIC_INFORMATION& memInfo) { - return (memInfo.State == MEM_COMMIT) - && (memInfo.Protect & PAGE_GUARD) == 0 - && (memInfo.Protect != PAGE_NOACCESS) - && ((memInfo.Protect & PAGE_READWRITE) - || (memInfo.Protect & PAGE_READONLY) - || (memInfo.Protect & PAGE_EXECUTE_READ) - || (memInfo.Protect & PAGE_EXECUTE_READWRITE)); -} \ No newline at end of file + if (memInfo.State != MEM_COMMIT || (memInfo.Protect & PAGE_GUARD) != 0) { + return false; + } + + switch (memInfo.Protect & 0xFF) { + case PAGE_READONLY: + case PAGE_READWRITE: + case PAGE_WRITECOPY: + case PAGE_EXECUTE_READ: + case PAGE_EXECUTE_READWRITE: + case PAGE_EXECUTE_WRITECOPY: return true; + default: return false; + } +} diff --git a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h index e366699..8ef205d 100644 --- a/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h +++ b/CheatStengine/src/CheatStengine/Tools/MemoryScanner.h @@ -6,8 +6,10 @@ #include #include +#include #include #include +#include #include #include @@ -68,26 +70,24 @@ enum class ScanType { class MemoryScanner { public: - explicit MemoryScanner(const std::unique_ptr& proc) - : m_Process(proc) - { - m_CurrentResults.reserve(100000); - } + explicit MemoryScanner(const std::unique_ptr& process); bool FirstScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue = "", const std::string& upperValue = ""); - void NextScan(ValueType valueType, ScanType scanType, + bool NextScan(ValueType valueType, ScanType scanType, uintptr_t minAddress, uintptr_t maxAddress, const std::string& lowerValue = "", const std::string& upperValue = ""); + void Cancel(); [[nodiscard]] const std::vector& GetResults() const { return m_CurrentResults; } [[nodiscard]] std::mutex& GetMutex() { return m_Mutex; } [[nodiscard]] bool IsScanning() const { return m_Scanning; } + [[nodiscard]] uint32_t GetProcessId() const { return m_Process ? m_Process->GetPid() : 0; } private: template @@ -98,18 +98,19 @@ class MemoryScanner { return bytes; } static bool CompareValues(ScanType scanType, const ScanValue& value, const ScanValue& targetValue1, const ScanValue& targetValue2); - static bool CompareByteArrays(ScanType scanType, ValueType valueType, uint8_t* data, const std::vector& target1, const std::vector& target2); + static bool CompareByteArrays(ScanType scanType, ValueType valueType, const uint8_t* data, const std::vector& target1, const std::vector& target2); static std::vector StringToBytes(const std::string& str, ValueType type); static ScanValue StringToValue(const std::string& str, ValueType type); static bool IsValidMemoryRegion(const MEMORY_BASIC_INFORMATION& memInfo); private: - const std::unique_ptr& m_Process; + std::unique_ptr m_Process; std::vector m_CurrentResults; std::mutex m_Mutex; - // Written on the detached scan thread, read from the main/HTTP thread; atomic - // so those accesses aren't a data race. + std::mutex m_ThreadMutex; + std::atomic m_ValueType = ValueType::COUNT; std::atomic m_Scanning = false; + std::jthread m_ScanThread; }; diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp index 218dc3e..dc4cf22 100644 --- a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.cpp @@ -130,8 +130,6 @@ uint64_t IconCache::CreateTextureFromIcon(HICON icon) return 0; } - // Swap B and R (GDI gives BGRA, D3D wants RGBA) while checking whether the - // colour bitmap carried any real alpha. bool hasAlpha = false; for (uint32_t& pixel : pixels) { if (pixel & 0xFF000000) { @@ -141,10 +139,6 @@ uint64_t IconCache::CreateTextureFromIcon(HICON icon) uint32_t r = (pixel & 0x00FF0000) >> 16; pixel = (pixel & 0xFF00FF00) | (b << 16) | r; } - - // Legacy icons store no per-pixel alpha; transparency lives in the AND mask - // (a set bit means transparent). Without this, such icons decode to alpha 0 - // everywhere and render fully invisible. Rebuild alpha from the mask. if (!hasAlpha) { std::vector mask(static_cast(width) * height); if (GetDIBits(screenDc, iconInfo.hbmMask, 0, height, mask.data(), &bmi, DIB_RGB_COLORS) != 0) { @@ -153,7 +147,6 @@ uint64_t IconCache::CreateTextureFromIcon(HICON icon) pixels[i] = transparent ? (pixels[i] & 0x00FFFFFF) : (pixels[i] | 0xFF000000); } } else { - // Mask unreadable: make the icon opaque rather than invisible. for (uint32_t& pixel : pixels) { pixel |= 0xFF000000; } diff --git a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h index d7acc58..c028688 100644 --- a/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h +++ b/CheatStengine/src/CheatStengine/UI/ImGui/IconCache.h @@ -9,54 +9,21 @@ struct ID3D11ShaderResourceView; struct HICON__; using HICON = HICON__*; -// Turns Windows shell icons into ImGui-renderable textures and caches them. -// -// The process/window selector wants a small icon in front of every row. Pulling -// and uploading an icon is comparatively expensive (shell query + a GPU texture -// per icon), so results are cached: process icons by pid, file icons by path. -// Textures live for the lifetime of the cache and are released together in -// Shutdown(), which the owner calls before the D3D11 device goes away. -// -// Everything here is best-effort: if an icon can't be extracted the lookup -// returns 0 and the caller simply draws nothing (or a shared fallback glyph), -// never an error. class IconCache { public: IconCache() = default; ~IconCache(); - // Binds the cache to the live D3D11 device. Must be called once, on the main - // thread, before any Get* call. Passing nullptr disables texture creation so - // the selector still works headless (every lookup just returns 0). void Initialize(ID3D11Device* device); - - // Releases every cached texture. Safe to call more than once; the destructor - // calls it too. void Shutdown(); - // Icon for a running process, resolved from its executable path and keyed by - // pid. Returns an ImTextureID-compatible handle (the SRV pointer as a - // uint64_t) or 0 when unavailable. [[nodiscard]] uint64_t GetProcessIcon(uint32_t pid); - - // Icon for the window's owning process, keyed by pid so it shares the process - // cache. hwnd is only used to find that pid. [[nodiscard]] uint64_t GetWindowIcon(void* hwnd); - - // A neutral fallback (the generic application icon) for rows whose real icon - // couldn't be extracted. Cached once. [[nodiscard]] uint64_t GetFallbackIcon(); private: - // Extracts the shell icon for a file path and uploads it as a texture. - // Returns 0 on any failure. [[nodiscard]] uint64_t CreateTextureFromPath(const std::wstring& path); - - // Rasterizes an HICON into BGRA pixels and uploads a texture. Takes ownership - // of nothing; the caller still destroys the HICON. [[nodiscard]] uint64_t CreateTextureFromIcon(HICON icon); - - // Resolves a pid to its executable path, or an empty string on failure. [[nodiscard]] static std::wstring GetProcessPath(uint32_t pid); ID3D11Device* m_Device = nullptr; diff --git a/CheatStengine/vendor/IconFontCppHeaders b/CheatStengine/vendor/IconFontCppHeaders new file mode 160000 index 0000000..210b5a3 --- /dev/null +++ b/CheatStengine/vendor/IconFontCppHeaders @@ -0,0 +1 @@ +Subproject commit 210b5a399a64270674560d633638952d1e8d804d diff --git a/CheatStengine/vendor/cpp-httplib b/CheatStengine/vendor/cpp-httplib new file mode 160000 index 0000000..a7bc00e --- /dev/null +++ b/CheatStengine/vendor/cpp-httplib @@ -0,0 +1 @@ +Subproject commit a7bc00e3307fecdb4d67545e93be7b88cfb1e186 diff --git a/CheatStengine/vendor/json b/CheatStengine/vendor/json new file mode 160000 index 0000000..9cca280 --- /dev/null +++ b/CheatStengine/vendor/json @@ -0,0 +1 @@ +Subproject commit 9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 diff --git a/CheatStengine/vendor/zasm b/CheatStengine/vendor/zasm new file mode 160000 index 0000000..813a187 --- /dev/null +++ b/CheatStengine/vendor/zasm @@ -0,0 +1 @@ +Subproject commit 813a1875d1f45d6c27d938eb10b5f96c05757a1b diff --git a/CheatStengineDriver/CMakeLists.txt b/CheatStengineDriver/CMakeLists.txt index ca615e5..fb3b33f 100644 --- a/CheatStengineDriver/CMakeLists.txt +++ b/CheatStengineDriver/CMakeLists.txt @@ -1,12 +1,63 @@ -project(CheatStengineDriver) +project(CheatStengineDriver LANGUAGES CXX) include(vendor/FindWDK.cmake) find_package(WDK REQUIRED) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) +set(DRIVER_WINVER 0x0A00) +set(DRIVER_NTDDI_VERSION 0x0A000008) + +if (MSVC) + string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +endif () + wdk_add_driver(${PROJECT_NAME} - NTDDI_VERSION 0x0A000010 - ${SOURCE_FILES}) + WINVER ${DRIVER_WINVER} + NTDDI_VERSION ${DRIVER_NTDDI_VERSION} + src/CheatStengineDriver/Control.cpp + src/CheatStengineDriver/Control.h + src/CheatStengineDriver/Device.cpp + src/CheatStengineDriver/Device.h + src/CheatStengineDriver/Driver.cpp + src/CheatStengineDriver/Driver.h + src/CheatStengineDriver/Mapper.cpp + src/CheatStengineDriver/Mapper.h + src/CheatStengineDriver/Memory.cpp + src/CheatStengineDriver/Memory.h + src/CheatStengineDriver/Pnp.cpp + src/CheatStengineDriver/Pnp.h + src/CheatStengineDriver/Process.cpp + src/CheatStengineDriver/Process.h + Driver.inf + ../Shared/Protocol.h +) + +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../Shared +) + +target_compile_definitions(${PROJECT_NAME} PRIVATE + _KMODE + WINVER=${DRIVER_WINVER} +) + +target_compile_options(${PROJECT_NAME} PRIVATE + /W4 + /GR- + /EHs-c- +) + +target_link_libraries(${PROJECT_NAME} WDK::WDMSEC) + +get_target_property(DRIVER_LINK_FLAGS ${PROJECT_NAME} LINK_FLAGS) +string(REPLACE [=[/DRIVER ]=] [=[/DRIVER:WDM ]=] DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +string(REPLACE [=[/MERGE:_TEXT=.text;_PAGE=PAGE ]=] "" DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +string(REPLACE [=[/MERGE:_TEXT=.text_PAGE=PAGE ]=] "" DRIVER_LINK_FLAGS ${DRIVER_LINK_FLAGS}) +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS ${DRIVER_LINK_FLAGS}) +set_source_files_properties(Driver.inf PROPERTIES HEADER_FILE_ONLY TRUE) -target_compile_definitions(${PROJECT_NAME} PRIVATE _KMODE) +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/Driver.inf + $/Driver.inf +) diff --git a/CheatStengineDriver/Driver.inf b/CheatStengineDriver/Driver.inf new file mode 100644 index 0000000..de56726 --- /dev/null +++ b/CheatStengineDriver/Driver.inf @@ -0,0 +1,63 @@ +[Version] +Signature="$WINDOWS NT$" +Class=System +ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318} +Provider=%ProviderName% +CatalogFile.NTamd64=Driver.cat +DriverVer=07/21/2026,1.0.0.0 +PnpLockdown=1 + +[Manufacturer] +%ManufacturerName%=Models,NTamd64.10.0...19041 + +[Models.NTamd64.10.0...19041] +%DeviceName%=Device_Install,Root\CheatStengine + +[DestinationDirs] +DriverCopyFiles=13 + +[SourceDisksNames] +1=%DiskName%,,, + +[SourceDisksFiles] +CheatStengineDriver.sys=1 + +[Device_Install.NT] +CopyFiles=DriverCopyFiles + +[Device_Install.NT.HW] +AddReg=DeviceSecurity + +[DeviceSecurity] +HKR,,DeviceCharacteristics,0x00010001,0x00000100 +HKR,,Security,0x00000000,"D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[Device_Install.NT.Interfaces] +AddInterface={8D3F3F2D-3D46-4A66-8F9B-46E6D5A67831},,Interface_Install + +[Interface_Install] +AddReg=InterfaceSecurity + +[InterfaceSecurity] +HKR,,Security,0x00000000,"D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[Device_Install.NT.Services] +AddService=CheatStengineDriver,0x00000002,Service_Install + +[Service_Install] +DisplayName=%ServiceName% +ServiceType=1 +StartType=3 +ErrorControl=1 +ServiceBinary=%13%\CheatStengineDriver.sys +Security="D:P(A;;GA;;;SY)(A;;GA;;;BA)" + +[DriverCopyFiles] +CheatStengineDriver.sys + +[Strings] +ProviderName="Cheat Stengine" +ManufacturerName="Cheat Stengine" +DeviceName="Cheat Stengine Device" +ServiceName="Cheat Stengine Driver" +DiskName="Cheat Stengine Installation Media" diff --git a/CheatStengineDriver/src/CheatStengineDriver/Control.cpp b/CheatStengineDriver/src/CheatStengineDriver/Control.cpp new file mode 100644 index 0000000..2c3079b --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Control.cpp @@ -0,0 +1,598 @@ +#include "Control.h" + +#include "Memory.h" +#include "Process.h" + +namespace { + +using Handler = NTSTATUS (*)( + DeviceExtension*, + PFILE_OBJECT, + PIRP, + PIO_STACK_LOCATION, + PULONG_PTR); + +struct Route { + ULONG Code; + Handler Function; + bool WakesDevice; +}; + +uint64_t GetCapabilities() +{ + uint64_t capabilities = Protocol::BindProcess + | Protocol::QueryMemory + | Protocol::AllocateMemory + | Protocol::FreeMemory + | Protocol::Ping + | Protocol::Heartbeat + | Protocol::Configure; + if (CanCopyProcessMemory()) { + capabilities |= Protocol::ReadMemory | Protocol::WriteMemory; + } + if (CanProtectProcessMemory()) { + capabilities |= Protocol::ProtectMemory; + } + return capabilities; +} + +template +NTSTATUS GetRequest(PIRP irp, ULONG inputLength, const T** request, bool negotiate = false) +{ + if (!request || !irp->AssociatedIrp.SystemBuffer || inputLength < sizeof(T)) { + return STATUS_BUFFER_TOO_SMALL; + } + + const T* packet = static_cast(irp->AssociatedIrp.SystemBuffer); + const Protocol::PacketHeader& header = packet->Header; + if (header.Size < sizeof(T) || header.Size > inputLength + || header.Flags != 0 || header.Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + if (header.MajorVersion != Protocol::VersionMajor + || (!negotiate && header.MinorVersion > Protocol::VersionMinor)) { + return STATUS_REVISION_MISMATCH; + } + + *request = packet; + return STATUS_SUCCESS; +} + +template +T* GetResponse(PIRP irp, ULONG outputLength, PULONG_PTR information) +{ + if (!irp->AssociatedIrp.SystemBuffer || outputLength < sizeof(T)) { + *information = sizeof(T); + return nullptr; + } + return static_cast(irp->AssociatedIrp.SystemBuffer); +} + +PVOID GetDirectBuffer(PIRP irp, ULONG length) +{ + if (!irp->MdlAddress || length == 0 || MmGetMdlByteCount(irp->MdlAddress) != length) { + return nullptr; + } + return MmGetSystemAddressForMdlSafe( + irp->MdlAddress, + static_cast(NormalPagePriority | MdlMappingNoExecute)); +} + +NTSTATUS HandleInformation( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::InformationRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request, + true); + if (!NT_SUCCESS(status)) { + return status; + } + + Protocol::InformationResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Capabilities = GetCapabilities(); + response->MaximumTransferSize = Protocol::MaximumTransferSize; + response->LoadMode = static_cast(extension->Mode); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandlePing( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::PingRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::PingRequest request = *requestPointer; + + Protocol::PingResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Nonce = request.Nonce; + response->KernelTime = KeQueryInterruptTime(); + response->Capabilities = GetCapabilities(); + response->MaximumTransferSize = Protocol::MaximumTransferSize; + response->DriverMajorVersion = Protocol::DriverVersionMajor; + response->DriverMinorVersion = Protocol::DriverVersionMinor; + response->DriverPatchVersion = Protocol::DriverVersionPatch; + response->ActiveSessions = GetSessionCount(extension); + response->IdleTimeoutSeconds = GetIdleTimeout(extension); + response->RemainingIdleSeconds = GetRemainingIdleTime(extension); + response->LoadMode = static_cast(extension->Mode); + response->State = static_cast(GetRuntimeState(extension)); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleHeartbeat( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::HeartbeatRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::HeartbeatRequest request = *requestPointer; + + Protocol::HeartbeatResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + status = ReferenceSession(extension, fileObject, &session); + if (!NT_SUCCESS(status)) { + return status; + } + status = UpdateHeartbeat(session, request.Sequence); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + TouchDevice(extension); + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Sequence = request.Sequence; + response->KernelTime = KeQueryInterruptTime(); + response->RemainingIdleSeconds = GetRemainingIdleTime(extension); + response->State = static_cast(GetRuntimeState(extension)); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleConfigure( + DeviceExtension* extension, + PFILE_OBJECT, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::ConfigureRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status) || requestPointer->Reserved != 0) { + return NT_SUCCESS(status) ? STATUS_INVALID_PARAMETER : status; + } + const Protocol::ConfigureRequest request = *requestPointer; + + Protocol::ConfigureResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + status = SetIdleTimeout(extension, request.IdleTimeoutSeconds); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->IdleTimeoutSeconds = GetIdleTimeout(extension); + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleBind( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR) +{ + const Protocol::BindProcessRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->ProcessId == 0 || request->Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + return BindSession(extension, fileObject, request->ProcessId); +} + +NTSTATUS HandleRead( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength; + const ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength; + const Protocol::ReadMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest(irp, inputLength, &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Size == 0 || request->Size > Protocol::MaximumTransferSize + || request->Size != outputLength) { + return STATUS_INVALID_BUFFER_SIZE; + } + + PVOID buffer = GetDirectBuffer(irp, outputLength); + if (!buffer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + SIZE_T transferred = 0; + status = ReadProcessMemory( + process, + request->Address, + buffer, + static_cast(request->Size), + &transferred); + ReleaseSession(session); + *information = transferred; + return status; +} + +NTSTATUS HandleWrite( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const ULONG inputLength = stack->Parameters.DeviceIoControl.InputBufferLength; + const ULONG outputLength = stack->Parameters.DeviceIoControl.OutputBufferLength; + const Protocol::WriteMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest(irp, inputLength, &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Size == 0 || request->Size > Protocol::MaximumTransferSize + || request->Size != outputLength) { + return STATUS_INVALID_BUFFER_SIZE; + } + + PVOID buffer = GetDirectBuffer(irp, outputLength); + if (!buffer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + SIZE_T transferred = 0; + status = WriteProcessMemory( + process, + request->Address, + buffer, + static_cast(request->Size), + &transferred); + ReleaseSession(session); + *information = transferred; + return status; +} + +NTSTATUS HandleQuery( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::QueryMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::QueryMemoryRequest request = *requestPointer; + + Protocol::QueryMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + MEMORY_BASIC_INFORMATION memoryInformation {}; + status = QueryProcessMemory(process, request.Address, &memoryInformation); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->BaseAddress = reinterpret_cast(memoryInformation.BaseAddress); + response->AllocationBase = reinterpret_cast(memoryInformation.AllocationBase); + response->RegionSize = memoryInformation.RegionSize; + response->AllocationProtect = memoryInformation.AllocationProtect; + response->State = memoryInformation.State; + response->Protect = memoryInformation.Protect; + response->Type = memoryInformation.Type; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleAllocate( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + const Protocol::AllocateMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status)) { + return status; + } + const Protocol::AllocateMemoryRequest request = *requestPointer; + + Protocol::AllocateMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + UINT64 address = 0; + UINT64 size = 0; + status = AllocateProcessMemory( + process, + request.Address, + request.Size, + request.AllocationType, + request.Protect, + &address, + &size); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Address = address; + response->Size = size; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +NTSTATUS HandleFree( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR) +{ + const Protocol::FreeMemoryRequest* request = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &request); + if (!NT_SUCCESS(status)) { + return status; + } + if (request->Reserved != 0) { + return STATUS_INVALID_PARAMETER; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + status = FreeProcessMemory( + process, + request->Address, + request->Size, + request->FreeType); + ReleaseSession(session); + return status; +} + +NTSTATUS HandleProtect( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + PULONG_PTR information) +{ + if (!CanProtectProcessMemory()) { + return STATUS_NOT_SUPPORTED; + } + + const Protocol::ProtectMemoryRequest* requestPointer = nullptr; + NTSTATUS status = GetRequest( + irp, + stack->Parameters.DeviceIoControl.InputBufferLength, + &requestPointer); + if (!NT_SUCCESS(status) || requestPointer->Reserved != 0) { + return NT_SUCCESS(status) ? STATUS_INVALID_PARAMETER : status; + } + const Protocol::ProtectMemoryRequest request = *requestPointer; + + Protocol::ProtectMemoryResponse* response = GetResponse( + irp, + stack->Parameters.DeviceIoControl.OutputBufferLength, + information); + if (!response) { + return STATUS_BUFFER_TOO_SMALL; + } + + Session* session = nullptr; + PEPROCESS process = nullptr; + status = ReferenceSessionProcess(extension, fileObject, &session, &process); + if (!NT_SUCCESS(status)) { + return status; + } + + ULONG oldProtect = 0; + UINT64 address = 0; + UINT64 size = 0; + status = ProtectProcessMemory( + process, + request.Address, + request.Size, + request.Protect, + &oldProtect, + &address, + &size); + ReleaseSession(session); + if (!NT_SUCCESS(status)) { + return status; + } + + *response = {}; + response->Header = Protocol::CreateHeader(); + response->Address = address; + response->Size = size; + response->OldProtect = oldProtect; + *information = sizeof(*response); + return STATUS_SUCCESS; +} + +const Route Routes[] = { + { Protocol::InformationControlCode, HandleInformation, true }, + { Protocol::PingControlCode, HandlePing, true }, + { Protocol::HeartbeatControlCode, HandleHeartbeat, true }, + { Protocol::ConfigureControlCode, HandleConfigure, true }, + { Protocol::BindProcessControlCode, HandleBind, false }, + { Protocol::ReadMemoryControlCode, HandleRead, false }, + { Protocol::WriteMemoryControlCode, HandleWrite, false }, + { Protocol::QueryMemoryControlCode, HandleQuery, false }, + { Protocol::AllocateMemoryControlCode, HandleAllocate, false }, + { Protocol::FreeMemoryControlCode, HandleFree, false }, + { Protocol::ProtectMemoryControlCode, HandleProtect, false }, +}; + +} + +NTSTATUS DispatchControlRequest( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + ULONG_PTR* information) +{ + const ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; + for (const Route& route : Routes) { + if (route.Code != code) { + continue; + } + if (GetRuntimeState(extension) == Protocol::RuntimeState::Idle + && !route.WakesDevice) { + return STATUS_DEVICE_NOT_READY; + } + if (route.WakesDevice) { + TouchDevice(extension); + } + const NTSTATUS status = route.Function( + extension, + fileObject, + irp, + stack, + information); + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + return status; + } + return STATUS_INVALID_DEVICE_REQUEST; +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Control.h b/CheatStengineDriver/src/CheatStengineDriver/Control.h new file mode 100644 index 0000000..07e7068 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Control.h @@ -0,0 +1,10 @@ +#pragma once + +#include "Device.h" + +NTSTATUS DispatchControlRequest( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + PIRP irp, + PIO_STACK_LOCATION stack, + ULONG_PTR* information); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Device.cpp b/CheatStengineDriver/src/CheatStengineDriver/Device.cpp new file mode 100644 index 0000000..b6ae396 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Device.cpp @@ -0,0 +1,478 @@ +#include "Device.h" + +#include "Control.h" + +namespace { + +constexpr ULONG PoolTag = 'sSeC'; +constexpr LONG64 TimeUnitsPerSecond = 10'000'000; + +VOID IdleTimerCallback(PEX_TIMER, PVOID context) +{ + DeviceExtension* extension = static_cast(context); + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + const LONG64 deadline = InterlockedCompareExchange64(&extension->IdleDeadline, 0, 0); + if (deadline == 0 + || static_cast(KeQueryInterruptTime()) < deadline + || GetDeviceState(extension) != DeviceState::Started) { + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + InterlockedCompareExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Idle), + static_cast(Protocol::RuntimeState::Ready)); + InterlockedCompareExchange64(&extension->IdleDeadline, 0, deadline); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); +} + +Session* TakeSession(DeviceExtension* extension, PFILE_OBJECT fileObject) +{ + if (!fileObject) { + return nullptr; + } + + ExAcquireFastMutex(&extension->SessionLock); + Session* session = static_cast(InterlockedExchangePointer( + reinterpret_cast(&fileObject->FsContext), + nullptr)); + ExReleaseFastMutex(&extension->SessionLock); + return session; +} + +void DestroySession(Session* session) +{ + if (!session) { + return; + } + + ExWaitForRundownProtectionRelease(&session->Rundown); + PEPROCESS process = static_cast(InterlockedExchangePointer( + reinterpret_cast(&session->Process), + nullptr)); + if (process) { + ObDereferenceObject(process); + } + + DeviceExtension* extension = session->Device; + InterlockedDecrement(&extension->SessionCount); + TouchDevice(extension); + ExFreePoolWithTag(session, PoolTag); +} + +} + +DeviceExtension* GetExtension(PDEVICE_OBJECT deviceObject) +{ + return static_cast(deviceObject->DeviceExtension); +} + +DeviceState GetDeviceState(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange(&extension->State, 0, 0)); +} + +void SetDeviceState(DeviceExtension* extension, DeviceState state) +{ + InterlockedExchange(&extension->State, static_cast(state)); +} + +NTSTATUS InitializeDevice(PDEVICE_OBJECT deviceObject, Protocol::LoadMode mode, DeviceState state) +{ + DeviceExtension* extension = GetExtension(deviceObject); + RtlZeroMemory(extension, sizeof(*extension)); + extension->DeviceObject = deviceObject; + extension->Mode = mode; + extension->State = static_cast(state); + extension->Runtime = static_cast( + state == DeviceState::Started + ? Protocol::RuntimeState::Ready + : Protocol::RuntimeState::Starting); + extension->IdleTimeoutSeconds = Protocol::DefaultIdleTimeoutSeconds; + + IoInitializeRemoveLock(&extension->RemoveLock, PoolTag, 0, 0); + ExInitializeRundownProtection(&extension->IoRundown); + ExInitializeFastMutex(&extension->SessionLock); + KeInitializeSpinLock(&extension->IdleLock); + extension->IdleTimer = ExAllocateTimer(IdleTimerCallback, extension, EX_TIMER_NO_WAKE); + if (!extension->IdleTimer) { + return STATUS_INSUFFICIENT_RESOURCES; + } + + extension->LastActivity = static_cast(KeQueryInterruptTime()); + if (state == DeviceState::Started) { + TouchDevice(extension); + } + return STATUS_SUCCESS; +} + +void DestroyDevice(DeviceExtension* extension) +{ + SetDeviceState(extension, DeviceState::Removed); + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Removed)); + InterlockedExchange64(&extension->IdleDeadline, 0); + PEX_TIMER timer = extension->IdleTimer; + extension->IdleTimer = nullptr; + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + + if (timer) { + ExDeleteTimer(timer, TRUE, TRUE, nullptr); + } + + if (InterlockedCompareExchange(&extension->IoDrained, 1, 0) == 0) { + ExWaitForRundownProtectionRelease(&extension->IoRundown); + } +} + +void StartDevice(DeviceExtension* extension) +{ + if (InterlockedCompareExchange(&extension->IoDrained, 0, 1) == 1) { + ExReInitializeRundownProtection(&extension->IoRundown); + } + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Ready)); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + TouchDevice(extension); +} + +void StopDevice(DeviceExtension* extension) +{ + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Stopping)); + InterlockedExchange64(&extension->IdleDeadline, 0); + if (extension->IdleTimer) { + ExCancelTimer(extension->IdleTimer, nullptr); + } + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + if (InterlockedCompareExchange(&extension->IoDrained, 1, 0) == 0) { + ExWaitForRundownProtectionRelease(&extension->IoRundown); + } +} + +bool AcquireDeviceIo(DeviceExtension* extension) +{ + if (!ExAcquireRundownProtection(&extension->IoRundown)) { + return false; + } + if (GetDeviceState(extension) != DeviceState::Started) { + ExReleaseRundownProtection(&extension->IoRundown); + return false; + } + return true; +} + +void ReleaseDeviceIo(DeviceExtension* extension) +{ + ExReleaseRundownProtection(&extension->IoRundown); +} + +void TouchDevice(DeviceExtension* extension) +{ + KIRQL oldIrql = PASSIVE_LEVEL; + KeAcquireSpinLock(&extension->IdleLock, &oldIrql); + if (GetDeviceState(extension) != DeviceState::Started || !extension->IdleTimer) { + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + const ULONG seconds = static_cast(InterlockedCompareExchange( + &extension->IdleTimeoutSeconds, + 0, + 0)); + const LONG64 now = static_cast(KeQueryInterruptTime()); + InterlockedExchange64(&extension->LastActivity, now); + InterlockedExchange( + &extension->Runtime, + static_cast(Protocol::RuntimeState::Ready)); + + if (seconds == 0) { + InterlockedExchange64(&extension->IdleDeadline, 0); + ExCancelTimer(extension->IdleTimer, nullptr); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); + return; + } + + const LONG64 duration = static_cast(seconds) * TimeUnitsPerSecond; + InterlockedExchange64(&extension->IdleDeadline, now + duration); + ExSetTimer(extension->IdleTimer, -duration, 0, nullptr); + KeReleaseSpinLock(&extension->IdleLock, oldIrql); +} + +NTSTATUS SetIdleTimeout(DeviceExtension* extension, ULONG seconds) +{ + if (seconds != 0 + && (seconds < Protocol::MinimumIdleTimeoutSeconds + || seconds > Protocol::MaximumIdleTimeoutSeconds)) { + return STATUS_INVALID_PARAMETER; + } + + InterlockedExchange(&extension->IdleTimeoutSeconds, static_cast(seconds)); + TouchDevice(extension); + return STATUS_SUCCESS; +} + +ULONG GetIdleTimeout(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange( + &extension->IdleTimeoutSeconds, + 0, + 0)); +} + +ULONG GetRemainingIdleTime(DeviceExtension* extension) +{ + const LONG64 deadline = InterlockedCompareExchange64(&extension->IdleDeadline, 0, 0); + if (deadline == 0) { + return 0; + } + + const LONG64 now = static_cast(KeQueryInterruptTime()); + if (now >= deadline) { + return 0; + } + + const ULONGLONG remaining = static_cast(deadline - now); + return static_cast((remaining + TimeUnitsPerSecond - 1) / TimeUnitsPerSecond); +} + +Protocol::RuntimeState GetRuntimeState(DeviceExtension* extension) +{ + const DeviceState state = GetDeviceState(extension); + if (state == DeviceState::Removed) { + return Protocol::RuntimeState::Removed; + } + if (state != DeviceState::Started) { + return state == DeviceState::Added + ? Protocol::RuntimeState::Starting + : Protocol::RuntimeState::Stopping; + } + return static_cast( + InterlockedCompareExchange(&extension->Runtime, 0, 0)); +} + +ULONG GetSessionCount(DeviceExtension* extension) +{ + return static_cast(InterlockedCompareExchange(&extension->SessionCount, 0, 0)); +} + +NTSTATUS ReferenceSession(DeviceExtension* extension, PFILE_OBJECT fileObject, Session** session) +{ + if (!extension || !fileObject || !session) { + return STATUS_INVALID_PARAMETER; + } + + ExAcquireFastMutex(&extension->SessionLock); + Session* context = static_cast(fileObject->FsContext); + const bool acquired = context && ExAcquireRundownProtection(&context->Rundown); + ExReleaseFastMutex(&extension->SessionLock); + if (!acquired) { + return STATUS_FILE_CLOSED; + } + + *session = context; + return STATUS_SUCCESS; +} + +void ReleaseSession(Session* session) +{ + ExReleaseRundownProtection(&session->Rundown); +} + +NTSTATUS BindSession( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + ULONG processId) +{ + if (processId == 0) { + return STATUS_INVALID_PARAMETER; + } + + Session* session = nullptr; + NTSTATUS status = ReferenceSession(extension, fileObject, &session); + if (!NT_SUCCESS(status)) { + return status; + } + + PEPROCESS process = nullptr; + status = PsLookupProcessByProcessId( + reinterpret_cast(static_cast(processId)), + &process); + if (NT_SUCCESS(status)) { + PVOID previous = InterlockedCompareExchangePointer( + reinterpret_cast(&session->Process), + process, + nullptr); + if (previous) { + ObDereferenceObject(process); + status = STATUS_ALREADY_REGISTERED; + } + } + + ReleaseSession(session); + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + return status; +} + +NTSTATUS ReferenceSessionProcess( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + Session** session, + PEPROCESS* process) +{ + if (!process) { + return STATUS_INVALID_PARAMETER; + } + + NTSTATUS status = ReferenceSession(extension, fileObject, session); + if (!NT_SUCCESS(status)) { + return status; + } + + PEPROCESS boundProcess = static_cast(InterlockedCompareExchangePointer( + reinterpret_cast(&(*session)->Process), + nullptr, + nullptr)); + if (!boundProcess) { + ReleaseSession(*session); + *session = nullptr; + return STATUS_INVALID_DEVICE_STATE; + } + + *process = boundProcess; + return STATUS_SUCCESS; +} + +NTSTATUS UpdateHeartbeat(Session* session, UINT64 sequence) +{ + if (sequence == 0 || sequence > MAXLONG64) { + return STATUS_INVALID_PARAMETER; + } + + LONG64 current = InterlockedCompareExchange64(&session->HeartbeatSequence, 0, 0); + while (sequence > static_cast(current)) { + const LONG64 previous = InterlockedCompareExchange64( + &session->HeartbeatSequence, + static_cast(sequence), + current); + if (previous == current) { + return STATUS_SUCCESS; + } + current = previous; + } + return STATUS_INVALID_PARAMETER; +} + +NTSTATUS DispatchCreate(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + const bool ioAcquired = AcquireDeviceIo(extension); + if (!ioAcquired) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + if (!stack->FileObject) { + status = STATUS_INVALID_PARAMETER; + } else { + ExAcquireFastMutex(&extension->SessionLock); + if (stack->FileObject->FsContext) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + Session* session = static_cast(ExAllocatePool2( + POOL_FLAG_NON_PAGED, + sizeof(Session), + PoolTag)); + if (!session) { + status = STATUS_INSUFFICIENT_RESOURCES; + } else { + RtlZeroMemory(session, sizeof(*session)); + ExInitializeRundownProtection(&session->Rundown); + session->Device = extension; + stack->FileObject->FsContext = session; + InterlockedIncrement(&extension->SessionCount); + status = STATUS_SUCCESS; + } + } + ExReleaseFastMutex(&extension->SessionLock); + } + ReleaseDeviceIo(extension); + } + + if (NT_SUCCESS(status)) { + TouchDevice(extension); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); +} + +NTSTATUS DispatchCleanup(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, irp); + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + DestroySession(TakeSession(extension, stack->FileObject)); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLock(&extension->RemoveLock, irp); + } + return CompleteIrp(irp, STATUS_SUCCESS); +} + +NTSTATUS DispatchClose(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, irp); + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + DestroySession(TakeSession(extension, stack->FileObject)); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLock(&extension->RemoveLock, irp); + } + return CompleteIrp(irp, STATUS_SUCCESS); +} + +NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + ULONG_PTR information = 0; + if (!AcquireDeviceIo(extension)) { + status = STATUS_INVALID_DEVICE_STATE; + } else { + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + if (!stack->FileObject) { + status = STATUS_INVALID_PARAMETER; + } else { + status = DispatchControlRequest( + extension, + stack->FileObject, + irp, + stack, + &information); + } + ReleaseDeviceIo(extension); + } + + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status, information); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Device.h b/CheatStengineDriver/src/CheatStengineDriver/Device.h new file mode 100644 index 0000000..b1a13d4 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Device.h @@ -0,0 +1,82 @@ +#pragma once + +#include "Driver.h" + +#include + +enum class DeviceState : LONG { + Added, + Started, + StopPending, + Stopped, + RemovePending, + SurpriseRemoved, + Removed, +}; + +struct DeviceExtension { + PDEVICE_OBJECT DeviceObject; + PDEVICE_OBJECT LowerDeviceObject; + PDEVICE_OBJECT PhysicalDeviceObject; + IO_REMOVE_LOCK RemoveLock; + EX_RUNDOWN_REF IoRundown; + FAST_MUTEX SessionLock; + KSPIN_LOCK IdleLock; + UNICODE_STRING InterfaceName; + PEX_TIMER IdleTimer; + volatile LONG State; + volatile LONG IoDrained; + volatile LONG Runtime; + volatile LONG SessionCount; + volatile LONG IdleTimeoutSeconds; + volatile LONG64 LastActivity; + volatile LONG64 IdleDeadline; + Protocol::LoadMode Mode; + BOOLEAN InterfaceEnabled; +}; + +struct Session { + EX_RUNDOWN_REF Rundown; + DeviceExtension* Device; + PEPROCESS Process; + volatile LONG64 HeartbeatSequence; +}; + +DeviceExtension* GetExtension(PDEVICE_OBJECT deviceObject); +DeviceState GetDeviceState(DeviceExtension* extension); +void SetDeviceState(DeviceExtension* extension, DeviceState state); + +NTSTATUS InitializeDevice(PDEVICE_OBJECT deviceObject, Protocol::LoadMode mode, DeviceState state); +void DestroyDevice(DeviceExtension* extension); +void StartDevice(DeviceExtension* extension); +void StopDevice(DeviceExtension* extension); +bool AcquireDeviceIo(DeviceExtension* extension); +void ReleaseDeviceIo(DeviceExtension* extension); +void TouchDevice(DeviceExtension* extension); +NTSTATUS SetIdleTimeout(DeviceExtension* extension, ULONG seconds); +ULONG GetIdleTimeout(DeviceExtension* extension); +ULONG GetRemainingIdleTime(DeviceExtension* extension); +Protocol::RuntimeState GetRuntimeState(DeviceExtension* extension); +ULONG GetSessionCount(DeviceExtension* extension); + +NTSTATUS BindSession(DeviceExtension* extension, PFILE_OBJECT fileObject, ULONG processId); +NTSTATUS ReferenceSessionProcess( + DeviceExtension* extension, + PFILE_OBJECT fileObject, + Session** session, + PEPROCESS* process); +NTSTATUS ReferenceSession(DeviceExtension* extension, PFILE_OBJECT fileObject, Session** session); +void ReleaseSession(Session* session); +NTSTATUS UpdateHeartbeat(Session* session, UINT64 sequence); + +_Dispatch_type_(IRP_MJ_CREATE) +DRIVER_DISPATCH DispatchCreate; + +_Dispatch_type_(IRP_MJ_CLEANUP) +DRIVER_DISPATCH DispatchCleanup; + +_Dispatch_type_(IRP_MJ_CLOSE) +DRIVER_DISPATCH DispatchClose; + +_Dispatch_type_(IRP_MJ_DEVICE_CONTROL) +DRIVER_DISPATCH DispatchDeviceControl; diff --git a/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp b/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp index 929cce8..1214c9e 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Driver.cpp @@ -1,202 +1,70 @@ -#include "driver.h" -#include "process.h" +#include "Driver.h" -static PDEVICE_OBJECT g_DeviceObject = nullptr; -static UNICODE_STRING g_DevName {}; -static UNICODE_STRING g_SymLink {}; +#include "Device.h" +#include "Mapper.h" +#include "Memory.h" +#include "Pnp.h" +#include "Process.h" -static NTSTATUS CreateClose(PDEVICE_OBJECT deviceObject, PIRP irp) +namespace { + +NTSTATUS DispatchUnsupported(PDEVICE_OBJECT, PIRP irp) { - UNREFERENCED_PARAMETER(deviceObject); + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); +} - irp->IoStatus.Status = STATUS_SUCCESS; - irp->IoStatus.Information = 0; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_SUCCESS; } -static NTSTATUS DeviceControl(PDEVICE_OBJECT deviceObject, PIRP irp) +NTSTATUS CompleteIrp(PIRP irp, NTSTATUS status, ULONG_PTR information) { - UNREFERENCED_PARAMETER(deviceObject); - - PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(irp); - - const ULONG ioCode = irpSp->Parameters.DeviceIoControl.IoControlCode; - if (ioCode != IOCTL_CS_COMMAND) { - DbgPrint("[CheatStengine] Unknown IOCTL: 0x%08X\n", ioCode); - irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST; - irp->IoStatus.Information = 0; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_INVALID_DEVICE_REQUEST; - } - - const ULONG inputLen = irpSp->Parameters.DeviceIoControl.InputBufferLength; - const ULONG outputLen = irpSp->Parameters.DeviceIoControl.OutputBufferLength; - if (inputLen < sizeof(CommandHeader) || !irp->AssociatedIrp.SystemBuffer) { - DbgPrint("[CheatStengine] Buffer too small or null (%lu bytes)\n", inputLen); - irp->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_BUFFER_TOO_SMALL; - } - - CommandHeader* cmd = static_cast(irp->AssociatedIrp.SystemBuffer); - DbgPrint("[CheatStengine] Command type=%d PID=%lu\n", cmd->Type, cmd->Pid); - - // Resolve target process. - PEPROCESS targetProcess = nullptr; - NTSTATUS status = PsLookupProcessByProcessId(reinterpret_cast(cmd->Pid), &targetProcess); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PsLookupProcessByProcessId failed for PID %lu: 0x%08X\n", - cmd->Pid, status); - irp->IoStatus.Status = STATUS_INVALID_PARAMETER; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return STATUS_INVALID_PARAMETER; - } - - ULONG bytesReturned = 0; - - // Dispatch command. - switch (cmd->Type) { - case CommandHeader::ReadMemory: - status = ReadProcessMemory( - targetProcess, - cmd->ReadMemoryData.Address, - cmd->ReadMemoryData.Buffer, - cmd->ReadMemoryData.Size); - break; - - case CommandHeader::WriteMemory: - status = WriteProcessMemory( - targetProcess, - cmd->WriteMemoryData.Address, - cmd->WriteMemoryData.Buffer, - cmd->WriteMemoryData.Size); - break; - - case CommandHeader::QueryMemory: - if (outputLen != sizeof(MEMORY_BASIC_INFORMATION)) { - DbgPrint("[CheatStengine] Buffer isn't the size of a MEMORY_BASIC_INFORMATION: %lu bytes\n", outputLen); - status = STATUS_BUFFER_TOO_SMALL; - break; - } - status = QueryProcessMemory( - targetProcess, - cmd->QueryMemoryData.Address, - static_cast(irp->AssociatedIrp.SystemBuffer)); - bytesReturned = sizeof(MEMORY_BASIC_INFORMATION); - break; - - case CommandHeader::AllocateMemory: - status = AllocateProcessMemory( - targetProcess, - cmd->AllocateMemoryData.Address, - cmd->AllocateMemoryData.Size, - cmd->AllocateMemoryData.AllocationType, - cmd->AllocateMemoryData.Protect, - cmd->AllocateMemoryData.AllocatedAddressPtr); - break; - - case CommandHeader::FreeMemory: - status = FreeProcessMemory( - targetProcess, - cmd->FreeMemoryData.Address, - cmd->FreeMemoryData.Size, - cmd->FreeMemoryData.FreeType); - break; - - case CommandHeader::ProtectMemory: - status = ProtectProcessMemory( - targetProcess, - cmd->ProtectMemoryData.Address, - cmd->ProtectMemoryData.Size, - cmd->ProtectMemoryData.NewProtect, - cmd->ProtectMemoryData.OldProtectPtr); - break; - - default: - DbgPrint("[CheatStengine] Unhandled command type: %d\n", cmd->Type); - status = STATUS_INVALID_PARAMETER; - break; - } - - ObDereferenceObject(targetProcess); - irp->IoStatus.Status = status; - irp->IoStatus.Information = bytesReturned; + irp->IoStatus.Information = information; IoCompleteRequest(irp, IO_NO_INCREMENT); return status; } -static void DriverUnload(PDRIVER_OBJECT driverObject) +void ConfigureDispatch(PDRIVER_OBJECT driverObject, bool pnp) { - UNREFERENCED_PARAMETER(driverObject); - - NTSTATUS status = IoDeleteSymbolicLink(&g_SymLink); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoDeleteSymbolicLink failed: 0x%08X\n", status); - return; + for (ULONG index = 0; index <= IRP_MJ_MAXIMUM_FUNCTION; ++index) { + driverObject->MajorFunction[index] = pnp ? DispatchPassThrough : DispatchUnsupported; } - IoDeleteDevice(driverObject->DeviceObject); - DbgPrint("[CheatStengine] Driver unloaded.\n"); + driverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; + driverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchCleanup; + driverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; + driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl; + if (pnp) { + driverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp; + driverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower; + driverObject->DriverExtension->AddDevice = AddDevice; + } + driverObject->DriverUnload = pnp ? DriverUnload : nullptr; } -NTSTATUS UnsupportedDispatch(PDEVICE_OBJECT driverObject, PIRP irp) +void DriverUnload(PDRIVER_OBJECT driverObject) { - UNREFERENCED_PARAMETER(driverObject); - irp->IoStatus.Status = STATUS_NOT_SUPPORTED; - IoCompleteRequest(irp, IO_NO_INCREMENT); - return irp->IoStatus.Status; + PDEVICE_OBJECT deviceObject = driverObject->DeviceObject; + while (deviceObject) { + PDEVICE_OBJECT nextDeviceObject = deviceObject->NextDevice; + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode == Protocol::LoadMode::Service) { + DestroyControlDevice(deviceObject); + } + deviceObject = nextDeviceObject; + } } -NTSTATUS InitializeDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) +extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) { - DbgPrint("[CheatStengine] DriverEntry.\n"); - - RtlInitUnicodeString(&g_DevName, L"\\Device\\CheatStengineDriver"); - RtlInitUnicodeString(&g_SymLink, L"\\DosDevices\\CheatStengineDriver"); - - PDEVICE_OBJECT deviceObject = nullptr; - NTSTATUS status = IoCreateDevice( - driverObject, - 0, - &g_DevName, - FILE_DEVICE_UNKNOWN, - FILE_DEVICE_SECURE_OPEN, - FALSE, - &deviceObject); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoCreateDevice failed: 0x%08X\n", status); - return status; - } - - status = IoCreateSymbolicLink(&g_SymLink, &g_DevName); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] IoCreateSymbolicLink failed: 0x%08X\n", status); - IoDeleteDevice(deviceObject); - return status; + if (!registryPath) { + return BootstrapMappedDriver(); } - - for (int i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++) { - driverObject->MajorFunction[i] = UnsupportedDispatch; + if (!driverObject || !driverObject->DriverExtension) { + return STATUS_INVALID_PARAMETER; } - driverObject->MajorFunction[IRP_MJ_CREATE] = CreateClose; - driverObject->MajorFunction[IRP_MJ_CLOSE] = CreateClose; - driverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl; - driverObject->DriverUnload = DriverUnload; - deviceObject->Flags |= DO_BUFFERED_IO; - deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; - - DbgPrint("[CheatStengine] Driver loaded.\n"); - return STATUS_SUCCESS; + InitializeMemorySupport(); + InitializeProcessSupport(); + ConfigureDispatch(driverObject, true); + return CreateControlDevice(driverObject, Protocol::LoadMode::Service); } - -extern "C" NTSTATUS NTAPI IoCreateDriver(PUNICODE_STRING driverName, PDRIVER_INITIALIZE initializationFunction); -extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) -{ - UNREFERENCED_PARAMETER(registryPath); - // return InitializeDriver(driverObject, registryPath); - return IoCreateDriver(nullptr, InitializeDriver); -} \ No newline at end of file diff --git a/CheatStengineDriver/src/CheatStengineDriver/Driver.h b/CheatStengineDriver/src/CheatStengineDriver/Driver.h index 7da2453..01eee15 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Driver.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Driver.h @@ -1,66 +1,9 @@ #pragma once -#include #include -#include -#define IOCTL_CS_COMMAND \ - CTL_CODE(FILE_DEVICE_UNKNOWN, 0x6969, METHOD_BUFFERED, FILE_ANY_ACCESS) +extern "C" DRIVER_INITIALIZE DriverEntry; +DRIVER_UNLOAD DriverUnload; -struct CommandHeader { - enum Type : uint32_t { - ReadMemory = 0, - WriteMemory = 1, - QueryMemory = 2, - AllocateMemory = 3, - FreeMemory = 4, - ProtectMemory = 5, - } Type; - - uint32_t Pid; - - union { - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } ReadMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - void* Buffer; - } WriteMemoryData; - - struct - { - uintptr_t Address; - } QueryMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t AllocationType; - uint32_t Protect; - uintptr_t* AllocatedAddressPtr; - } AllocateMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t FreeType; - } FreeMemoryData; - - struct - { - uintptr_t Address; - size_t Size; - uint32_t NewProtect; - uint32_t* OldProtectPtr; - } ProtectMemoryData; - }; -}; \ No newline at end of file +void ConfigureDispatch(PDRIVER_OBJECT driverObject, bool pnp); +NTSTATUS CompleteIrp(PIRP irp, NTSTATUS status, ULONG_PTR information = 0); \ No newline at end of file diff --git a/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp b/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp new file mode 100644 index 0000000..b661a9a --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Mapper.cpp @@ -0,0 +1,116 @@ +#include "Mapper.h" + +#include "Memory.h" +#include "Process.h" + +#include + +namespace { + +using CreateDriverRoutine = NTSTATUS (NTAPI*)(PUNICODE_STRING, PDRIVER_INITIALIZE); + +constexpr GUID DeviceClassGuid = { + 0xa7e1dd95, + 0x2cb7, + 0x46a5, + { 0x90, 0x1e, 0x23, 0x7a, 0x1f, 0xb7, 0x69, 0x5c } +}; + +UNICODE_STRING GetDeviceName() +{ + UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\Device\\MemoryAccess"); + return name; +} + +UNICODE_STRING GetSymbolicLinkName() +{ + UNICODE_STRING name = RTL_CONSTANT_STRING(L"\\DosDevices\\MemoryAccess"); + return name; +} + +} + +NTSTATUS BootstrapMappedDriver() +{ + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"IoCreateDriver"); + const auto createDriver = reinterpret_cast( + MmGetSystemRoutineAddress(&routineName)); + if (!createDriver) { + return STATUS_NOT_SUPPORTED; + } + + UNICODE_STRING driverName = RTL_CONSTANT_STRING(L"\\Driver\\CheatStengineDriver"); + return createDriver(&driverName, InitializeMappedDriver); +} + +NTSTATUS InitializeMappedDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath) +{ + UNREFERENCED_PARAMETER(registryPath); + if (!driverObject || !driverObject->DriverExtension) { + return STATUS_INVALID_PARAMETER; + } + + InitializeMemorySupport(); + InitializeProcessSupport(); + ConfigureDispatch(driverObject, false); + + return CreateControlDevice(driverObject, Protocol::LoadMode::Mapped); +} + +NTSTATUS CreateControlDevice(PDRIVER_OBJECT driverObject, Protocol::LoadMode mode) +{ + if (!driverObject + || (mode != Protocol::LoadMode::Service && mode != Protocol::LoadMode::Mapped)) { + return STATUS_INVALID_PARAMETER; + } + + UNICODE_STRING deviceName = GetDeviceName(); + PDEVICE_OBJECT deviceObject = nullptr; + NTSTATUS status = IoCreateDeviceSecure( + driverObject, + sizeof(DeviceExtension), + &deviceName, + Protocol::DeviceType, + FILE_DEVICE_SECURE_OPEN, + FALSE, + &SDDL_DEVOBJ_SYS_ALL_ADM_ALL, + &DeviceClassGuid, + &deviceObject); + if (!NT_SUCCESS(status)) { + return status; + } + + status = InitializeDevice(deviceObject, mode, DeviceState::Started); + if (!NT_SUCCESS(status)) { + IoDeleteDevice(deviceObject); + return status; + } + + UNICODE_STRING symbolicLinkName = GetSymbolicLinkName(); + status = IoCreateSymbolicLink(&symbolicLinkName, &deviceName); + if (!NT_SUCCESS(status)) { + DestroyDevice(GetExtension(deviceObject)); + IoDeleteDevice(deviceObject); + return status; + } + + deviceObject->Flags |= DO_DIRECT_IO | DO_POWER_PAGABLE; + deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; + return STATUS_SUCCESS; +} + +void DestroyControlDevice(PDEVICE_OBJECT deviceObject) +{ + DeviceExtension* extension = GetExtension(deviceObject); + const NTSTATUS lockStatus = IoAcquireRemoveLock(&extension->RemoveLock, deviceObject); + SetDeviceState(extension, DeviceState::Removed); + StopDevice(extension); + if (NT_SUCCESS(lockStatus)) { + IoReleaseRemoveLockAndWait(&extension->RemoveLock, deviceObject); + } + + UNICODE_STRING symbolicLinkName = GetSymbolicLinkName(); + IoDeleteSymbolicLink(&symbolicLinkName); + DestroyDevice(extension); + IoDeleteDevice(deviceObject); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Mapper.h b/CheatStengineDriver/src/CheatStengineDriver/Mapper.h new file mode 100644 index 0000000..670db69 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Mapper.h @@ -0,0 +1,8 @@ +#pragma once + +#include "Device.h" + +NTSTATUS InitializeMappedDriver(PDRIVER_OBJECT driverObject, PUNICODE_STRING registryPath); +NTSTATUS BootstrapMappedDriver(); +NTSTATUS CreateControlDevice(PDRIVER_OBJECT driverObject, Protocol::LoadMode mode); +void DestroyControlDevice(PDEVICE_OBJECT deviceObject); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp b/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp index 5f30a6e..70abdf2 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Memory.cpp @@ -1,209 +1,122 @@ -#include "memory.h" +#include "Memory.h" -#include +namespace { -// Credit to https://github.com/ryan-weil/ReadWriteDriver +using CopyRoutine = NTSTATUS (NTAPI*)( + PEPROCESS, + PVOID, + PEPROCESS, + PVOID, + SIZE_T, + KPROCESSOR_MODE, + PSIZE_T); -static constexpr UINT32 PAGE_OFFSET_BITS = 12; -static constexpr UINT64 PTE_PHYS_MASK = (~0xFULL << 8) & 0xFFF'FFFF'FFULL; -static constexpr UINT64 PTE_PHYS_MASK_WIDE = 0x000F'FFFF'FFFF'F000ULL; +PVOID volatile CopyAddress = nullptr; -namespace { - // RTL_OSVERSIONINFOW build numbers - enum WindowsBuild : ULONG { - Build_1803 = 17134, - Build_1809 = 17763, - Build_1903 = 18362, - Build_1909 = 18363, - Build_2004 = 19041, - Build_20H2 = 19569, - Build_21H1 = 20180, - Build_22H1 = 22000, - Build_22H2 = 22621, - Build_22H22 = 19045, - Build_Win11 = 24345, - }; - - INT32 UserDirBaseOffset() - { - RTL_OSVERSIONINFOW ver {}; - ver.dwOSVersionInfoSize = sizeof(ver); - RtlGetVersion(&ver); - - switch (ver.dwBuildNumber) { - case Build_1803: - case Build_1809: - return 0x0278; - - case Build_1903: - case Build_1909: - return 0x0280; - - case Build_2004: - case Build_20H2: - case Build_21H1: - case Build_22H1: - case Build_22H2: - case Build_22H22: - case Build_Win11: - default: - return 0x0388; - } +NTSTATUS TransferStatus(NTSTATUS status, SIZE_T transferred, SIZE_T requested) +{ + if (transferred == requested) { + return STATUS_SUCCESS; + } + if (transferred != 0) { + return STATUS_PARTIAL_COPY; } + return NT_SUCCESS(status) ? STATUS_PARTIAL_COPY : status; } -UINT64 GetProcessCr3(PEPROCESS process) +CopyRoutine GetCopyRoutine() { - PUCHAR processBytes = reinterpret_cast(process); - ULONG_PTR dirBase = *reinterpret_cast(processBytes + 0x28); - if (dirBase != 0) { - return dirBase; - } + return reinterpret_cast( + InterlockedCompareExchangePointer(&CopyAddress, nullptr, nullptr)); +} - INT32 offset = UserDirBaseOffset(); - return *reinterpret_cast(processBytes + offset); } -bool IsValidUserModeAddress(uintptr_t address, size_t size) +void InitializeMemorySupport() { - uintptr_t end = address + size; - if (end < address) { - // overflow check - return false; - } - - return address < MmUserProbeAddress && end <= MmUserProbeAddress; + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"MmCopyVirtualMemory"); + InterlockedExchangePointer( + &CopyAddress, + MmGetSystemRoutineAddress(&routineName)); } -NTSTATUS PhysRead(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesRead) +bool CanCopyProcessMemory() { - MM_COPY_ADDRESS src {}; - src.PhysicalAddress.QuadPart = reinterpret_cast(physAddress); - return MmCopyMemory(buffer, src, size, MM_COPY_MEMORY_PHYSICAL, bytesRead); + return GetCopyRoutine() != nullptr; } -NTSTATUS PhysWrite(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesWritten) +bool IsUserAddressRange(UINT64 address, UINT64 size) { - if (!physAddress) { - return STATUS_INVALID_PARAMETER; - } - - PHYSICAL_ADDRESS pa {}; - pa.QuadPart = reinterpret_cast(physAddress); - - PVOID mapped = MmMapIoSpaceEx(pa, size, PAGE_READWRITE); - if (!mapped) { - return STATUS_INSUFFICIENT_RESOURCES; + if (address == 0 || size == 0) { + return false; } - RtlCopyMemory(mapped, buffer, size); - *bytesWritten = size; - MmUnmapIoSpace(mapped, size); - return STATUS_SUCCESS; + const UINT64 maximumAddress = reinterpret_cast(MmHighestUserAddress); + return address <= maximumAddress && size - 1 <= maximumAddress - address; } -UINT64 TranslateLinear(UINT64 dtb, UINT64 va) +NTSTATUS ReadProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - dtb &= ~0xFULL; - - const UINT64 pageOffset = va & ~(~0ULL << PAGE_OFFSET_BITS); - const UINT64 pteIndex = (va >> 12) & 0x1FFULL; - const UINT64 ptIndex = (va >> 21) & 0x1FFULL; - const UINT64 pdIndex = (va >> 30) & 0x1FFULL; - const UINT64 pdpIndex = (va >> 39) & 0x1FFULL; - - SIZE_T br = 0; - - // --- Level 4 -> PDPTE --- - UINT64 pdpe = 0; - PhysRead(reinterpret_cast(dtb + 8 * pdpIndex), &pdpe, sizeof(pdpe), &br); - if (!(pdpe & 1)) { - return 0; - } - - // --- Level 3 --> PDE --- - UINT64 pde = 0; - PhysRead(reinterpret_cast((pdpe & PTE_PHYS_MASK) + 8 * pdIndex), &pde, sizeof(pde), &br); - if (!(pde & 1)) { - return 0; - } - - if (pde & (1ULL << 7)) { - // 1 GiB huge page - return (pde & (~0ULL << 42 >> 12)) + (va & ~(~0ULL << 30)); - } - - // --- Level 2 -> PTE --- - UINT64 pteAddr = 0; - PhysRead(reinterpret_cast((pde & PTE_PHYS_MASK) + 8 * ptIndex), &pteAddr, sizeof(pteAddr), &br); - if (!(pteAddr & 1)) { - return 0; + if (!process || !buffer || !bytesTransferred || !IsUserAddressRange(address, size)) { + return STATUS_INVALID_PARAMETER; } - - if (pteAddr & (1ULL << 7)) { - // 2 MiB large page - return (pteAddr & PTE_PHYS_MASK) + (va & ~(~0ULL << 21)); + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } - // --- Level 1 -> physical page --- - UINT64 pte = 0; - PhysRead(reinterpret_cast((pteAddr & PTE_PHYS_MASK) + 8 * pteIndex), &pte, sizeof(pte), &br); - UINT64 physBase = pte & PTE_PHYS_MASK; - if (!physBase) { - return 0; + const CopyRoutine copyRoutine = GetCopyRoutine(); + if (!copyRoutine) { + return STATUS_NOT_SUPPORTED; } - return physBase + pageOffset; + *bytesTransferred = 0; + SIZE_T transferred = 0; + const NTSTATUS status = copyRoutine( + process, + reinterpret_cast(address), + PsGetCurrentProcess(), + buffer, + size, + KernelMode, + &transferred); + *bytesTransferred = transferred; + return TransferStatus(status, transferred, size); } -bool IsPageNoAccess(UINT64 dtb, UINT64 va) +NTSTATUS WriteProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - const UINT64 pteIndex = (va >> 12) & 0x1FFULL; - const UINT64 ptIndex = (va >> 21) & 0x1FFULL; - const UINT64 pdIndex = (va >> 30) & 0x1FFULL; - const UINT64 pdpIndex = (va >> 39) & 0x1FFULL; - - SIZE_T br = 0; - UINT64 entry = 0; - - // PDPTE - PhysRead(reinterpret_cast((dtb & ~0xFFFULL) + pdpIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; + if (!process || !buffer || !bytesTransferred || !IsUserAddressRange(address, size)) { + return STATUS_INVALID_PARAMETER; } - - // PDE - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + pdIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; - } - if (entry & (1ULL << 7)) { - return false; // 1 GiB page – present - } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } - // PT entry (2 MiB check) - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + ptIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - if (!(entry & 1)) { - return true; - } - if (entry & (1ULL << 7)) { - return false; // 2 MiB page – present - } + const CopyRoutine copyRoutine = GetCopyRoutine(); + if (!copyRoutine) { + return STATUS_NOT_SUPPORTED; } - // PTE - { - UINT64 base = entry & PTE_PHYS_MASK_WIDE; - PhysRead(reinterpret_cast(base + pteIndex * sizeof(UINT64)), - &entry, sizeof(entry), &br); - return !(entry & 1); - } -} \ No newline at end of file + *bytesTransferred = 0; + SIZE_T transferred = 0; + const NTSTATUS status = copyRoutine( + PsGetCurrentProcess(), + buffer, + process, + reinterpret_cast(address), + size, + KernelMode, + &transferred); + *bytesTransferred = transferred; + return TransferStatus(status, transferred, size); +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Memory.h b/CheatStengineDriver/src/CheatStengineDriver/Memory.h index dd1265a..439386e 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Memory.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Memory.h @@ -2,10 +2,21 @@ #include -NTSTATUS PhysRead(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesRead); -NTSTATUS PhysWrite(PVOID physAddress, PVOID buffer, SIZE_T size, SIZE_T* bytesWritten); +void InitializeMemorySupport(); +bool CanCopyProcessMemory(); -UINT64 TranslateLinear(UINT64 dtb, UINT64 va); -bool IsPageNoAccess(UINT64 dtb, UINT64 va); -bool IsValidUserModeAddress(uintptr_t address, size_t size); -UINT64 GetProcessCr3(PEPROCESS process); \ No newline at end of file +bool IsUserAddressRange(UINT64 address, UINT64 size); + +NTSTATUS ReadProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); + +NTSTATUS WriteProcessAddressSpace( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); diff --git a/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp b/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp new file mode 100644 index 0000000..8485c82 --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Pnp.cpp @@ -0,0 +1,270 @@ +#include "Pnp.h" + +namespace { + +NTSTATUS SetInterface(DeviceExtension* extension, BOOLEAN enabled) +{ + if (extension->InterfaceEnabled == enabled) { + return STATUS_SUCCESS; + } + + const NTSTATUS status = IoSetDeviceInterfaceState( + &extension->InterfaceName, + enabled ? TRUE : FALSE); + if (NT_SUCCESS(status)) { + extension->InterfaceEnabled = enabled; + } + return status; +} + +NTSTATUS ForwardSynchronously(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + KEVENT event {}; + KeInitializeEvent(&event, NotificationEvent, FALSE); + + IoCopyCurrentIrpStackLocationToNext(irp); + IoSetCompletionRoutine( + irp, + CompleteSynchronousIrp, + &event, + TRUE, + TRUE, + TRUE); + + const NTSTATUS callStatus = IoCallDriver(extension->LowerDeviceObject, irp); + if (callStatus == STATUS_PENDING) { + KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, nullptr); + } + return irp->IoStatus.Status; +} + +NTSTATUS ForwardWithRemoveLock(PDEVICE_OBJECT deviceObject, PIRP irp, bool power) +{ + DeviceExtension* extension = GetExtension(deviceObject); + IoCopyCurrentIrpStackLocationToNext(irp); + IoSetCompletionRoutine( + irp, + ReleaseRemoveLock, + extension, + TRUE, + TRUE, + TRUE); + return power + ? PoCallDriver(extension->LowerDeviceObject, irp) + : IoCallDriver(extension->LowerDeviceObject, irp); +} + +bool BeginPendingState(DeviceExtension* extension, DeviceState state) +{ + if (GetDeviceState(extension) != DeviceState::Started) { + return false; + } + + SetDeviceState(extension, state); + SetInterface(extension, FALSE); + StopDevice(extension); + return true; +} + +void RestorePendingState(DeviceExtension* extension, DeviceState state) +{ + if (GetDeviceState(extension) != state) { + return; + } + + StartDevice(extension); + SetInterface(extension, TRUE); + SetDeviceState(extension, DeviceState::Started); + TouchDevice(extension); +} + +} + +NTSTATUS CompleteSynchronousIrp(PDEVICE_OBJECT, PIRP, PVOID context) +{ + KeSetEvent(static_cast(context), IO_NO_INCREMENT, FALSE); + return STATUS_MORE_PROCESSING_REQUIRED; +} + +NTSTATUS ReleaseRemoveLock(PDEVICE_OBJECT, PIRP irp, PVOID context) +{ + if (irp->PendingReturned) { + IoMarkIrpPending(irp); + } + IoReleaseRemoveLock(&static_cast(context)->RemoveLock, irp); + return STATUS_CONTINUE_COMPLETION; +} + +NTSTATUS DispatchPnp(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + + PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(irp); + switch (stack->MinorFunction) { + case IRP_MN_START_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + if (NT_SUCCESS(status)) { + StartDevice(extension); + status = SetInterface(extension, TRUE); + if (NT_SUCCESS(status)) { + SetDeviceState(extension, DeviceState::Started); + TouchDevice(extension); + } else { + SetDeviceState(extension, DeviceState::Stopped); + StopDevice(extension); + } + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_QUERY_STOP_DEVICE: { + const bool transitioned = BeginPendingState(extension, DeviceState::StopPending); + status = ForwardSynchronously(deviceObject, irp); + if (!NT_SUCCESS(status) && transitioned) { + RestorePendingState(extension, DeviceState::StopPending); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + } + + case IRP_MN_QUERY_REMOVE_DEVICE: { + const bool transitioned = BeginPendingState(extension, DeviceState::RemovePending); + status = ForwardSynchronously(deviceObject, irp); + if (!NT_SUCCESS(status) && transitioned) { + RestorePendingState(extension, DeviceState::RemovePending); + } + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + } + + case IRP_MN_CANCEL_STOP_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + RestorePendingState(extension, DeviceState::StopPending); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_CANCEL_REMOVE_DEVICE: + status = ForwardSynchronously(deviceObject, irp); + RestorePendingState(extension, DeviceState::RemovePending); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_STOP_DEVICE: + SetDeviceState(extension, DeviceState::Stopped); + SetInterface(extension, FALSE); + StopDevice(extension); + status = ForwardSynchronously(deviceObject, irp); + IoReleaseRemoveLock(&extension->RemoveLock, irp); + return CompleteIrp(irp, status); + + case IRP_MN_SURPRISE_REMOVAL: + SetDeviceState(extension, DeviceState::SurpriseRemoved); + SetInterface(extension, FALSE); + StopDevice(extension); + return ForwardWithRemoveLock(deviceObject, irp, false); + + case IRP_MN_REMOVE_DEVICE: + SetDeviceState(extension, DeviceState::Removed); + SetInterface(extension, FALSE); + StopDevice(extension); + IoReleaseRemoveLockAndWait(&extension->RemoveLock, irp); + IoSkipCurrentIrpStackLocation(irp); + status = IoCallDriver(extension->LowerDeviceObject, irp); + IoDetachDevice(extension->LowerDeviceObject); + DestroyDevice(extension); + RtlFreeUnicodeString(&extension->InterfaceName); + IoDeleteDevice(deviceObject); + return status; + + default: + return ForwardWithRemoveLock(deviceObject, irp, false); + } +} + +NTSTATUS DispatchPower(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + const NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + return ForwardWithRemoveLock(deviceObject, irp, true); +} + +NTSTATUS DispatchPassThrough(PDEVICE_OBJECT deviceObject, PIRP irp) +{ + DeviceExtension* extension = GetExtension(deviceObject); + if (extension->Mode != Protocol::LoadMode::Pnp) { + return CompleteIrp(irp, STATUS_INVALID_DEVICE_REQUEST); + } + const NTSTATUS status = IoAcquireRemoveLock(&extension->RemoveLock, irp); + if (!NT_SUCCESS(status)) { + return CompleteIrp(irp, status); + } + return ForwardWithRemoveLock(deviceObject, irp, false); +} + +NTSTATUS AddDevice(PDRIVER_OBJECT driverObject, PDEVICE_OBJECT physicalDeviceObject) +{ + PDEVICE_OBJECT deviceObject = nullptr; + NTSTATUS status = IoCreateDevice( + driverObject, + sizeof(DeviceExtension), + nullptr, + Protocol::DeviceType, + FILE_DEVICE_SECURE_OPEN, + FALSE, + &deviceObject); + if (!NT_SUCCESS(status)) { + return status; + } + + status = InitializeDevice(deviceObject, Protocol::LoadMode::Pnp, DeviceState::Added); + if (!NT_SUCCESS(status)) { + IoDeleteDevice(deviceObject); + return status; + } + + DeviceExtension* extension = GetExtension(deviceObject); + extension->PhysicalDeviceObject = physicalDeviceObject; + status = IoAttachDeviceToDeviceStackSafe( + deviceObject, + physicalDeviceObject, + &extension->LowerDeviceObject); + if (!NT_SUCCESS(status)) { + DestroyDevice(extension); + IoDeleteDevice(deviceObject); + return status; + } + + GUID interfaceGuid = Protocol::DeviceInterfaceGuid; + status = IoRegisterDeviceInterface( + physicalDeviceObject, + &interfaceGuid, + nullptr, + &extension->InterfaceName); + if (!NT_SUCCESS(status)) { + IoDetachDevice(extension->LowerDeviceObject); + DestroyDevice(extension); + IoDeleteDevice(deviceObject); + return status; + } + + deviceObject->Flags |= DO_DIRECT_IO; + if ((extension->LowerDeviceObject->Flags & DO_POWER_PAGABLE) != 0) { + deviceObject->Flags |= DO_POWER_PAGABLE; + } + deviceObject->Flags &= ~DO_DEVICE_INITIALIZING; + return STATUS_SUCCESS; +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Pnp.h b/CheatStengineDriver/src/CheatStengineDriver/Pnp.h new file mode 100644 index 0000000..442267e --- /dev/null +++ b/CheatStengineDriver/src/CheatStengineDriver/Pnp.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Device.h" + +DRIVER_ADD_DEVICE AddDevice; + +_Dispatch_type_(IRP_MJ_PNP) +DRIVER_DISPATCH DispatchPnp; + +_Dispatch_type_(IRP_MJ_POWER) +DRIVER_DISPATCH DispatchPower; + +DRIVER_DISPATCH DispatchPassThrough; +IO_COMPLETION_ROUTINE CompleteSynchronousIrp; +IO_COMPLETION_ROUTINE ReleaseRemoveLock; diff --git a/CheatStengineDriver/src/CheatStengineDriver/Process.cpp b/CheatStengineDriver/src/CheatStengineDriver/Process.cpp index 28528b3..7e11ad0 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Process.cpp +++ b/CheatStengineDriver/src/CheatStengineDriver/Process.cpp @@ -1,232 +1,201 @@ -#include "process.h" -#include "memory.h" +#include "Process.h" -#include +#include "Memory.h" -extern "C" NTSYSAPI NTSTATUS NTAPI ZwProtectVirtualMemory( - IN HANDLE ProcessHandle, - IN OUT PVOID* BaseAddress, - IN OUT PSIZE_T NumberOfBytesToProtect, - IN ULONG NewAccessProtection, - OUT PULONG OldAccessProtection); +namespace { -#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +using ProtectRoutine = NTSTATUS (NTAPI*)( + HANDLE, + PVOID*, + PSIZE_T, + ULONG, + PULONG); -NTSTATUS ReadProcessMemory(PEPROCESS process, uintptr_t address, void* buffer, size_t size) -{ - DbgPrint("[CheatStengine] ReadProcessMemory addr=0x%p size=%zu\n", - reinterpret_cast(address), size); - - if (!buffer || size == 0) { - return STATUS_INVALID_PARAMETER; - } +PVOID volatile ProtectAddress = nullptr; - if (!IsValidUserModeAddress(address, size)) { - DbgPrint("[CheatStengine] ReadProcessMemory: Invalid user mode address 0x%p\n", - reinterpret_cast(address)); - return STATUS_ACCESS_VIOLATION; - } - - const UINT64 dtb = GetProcessCr3(process); - if (IsPageNoAccess(dtb, address)) { - DbgPrint("[CheatStengine] ReadProcessMemory: Address 0x%p is marked as PAGE_NOACCESS\n", - reinterpret_cast(address)); - return STATUS_ACCESS_DENIED; - } - - const UINT64 physAddr = TranslateLinear(dtb, address); - if (!physAddr) { - DbgPrint("[CheatStengine] ReadProcessMemory: Failed to translate linear address 0x%p\n", - reinterpret_cast(address)); - return STATUS_UNSUCCESSFUL; - } - - // Clamp to the end of the current physical page. - const ULONG64 chunk = MIN(PAGE_SIZE - static_cast(physAddr & 0xFFF), size); - SIZE_T bytesRead = 0; - NTSTATUS status = PhysRead(reinterpret_cast(physAddr), buffer, chunk, &bytesRead); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PhysRead failed: 0x%08X\n", status); - } - return status; +bool IsNativeSize(UINT64 size) +{ + return size <= static_cast(MAXULONG_PTR); } -NTSTATUS WriteProcessMemory(PEPROCESS process, uintptr_t address, void* buffer, size_t size) +bool IsUserAddress(UINT64 address) { - DbgPrint("[CheatStengine] WriteProcessMemory addr=0x%p size=%zu\n", - reinterpret_cast(address), size); - - if (!buffer || size == 0) { - return STATUS_INVALID_PARAMETER; - } + return address <= reinterpret_cast(MmHighestUserAddress); +} - if (!IsValidUserModeAddress(address, size)) { - return STATUS_ACCESS_VIOLATION; - } +} - const UINT64 dtb = GetProcessCr3(process); - if (IsPageNoAccess(dtb, address)) { - return STATUS_ACCESS_DENIED; - } +void InitializeProcessSupport() +{ + UNICODE_STRING routineName = RTL_CONSTANT_STRING(L"ZwProtectVirtualMemory"); + PVOID routine = MmGetSystemRoutineAddress(&routineName); + InterlockedExchangePointer(&ProtectAddress, routine); +} - const UINT64 physAddr = TranslateLinear(dtb, address); - if (!physAddr) { - return STATUS_UNSUCCESSFUL; - } +bool CanProtectProcessMemory() +{ + return InterlockedCompareExchangePointer(&ProtectAddress, nullptr, nullptr) != nullptr; +} - const ULONG64 chunk = MIN(PAGE_SIZE - static_cast(physAddr & 0xFFF), size); - SIZE_T bytesWritten = 0; - NTSTATUS status = PhysWrite(reinterpret_cast(physAddr), buffer, chunk, &bytesWritten); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] PhysWrite failed: 0x%08X\n", status); - } - return status; +NTSTATUS ReadProcessMemory( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) +{ + return ReadProcessAddressSpace(process, address, buffer, size, bytesTransferred); } -NTSTATUS QueryProcessMemory(PEPROCESS process, uintptr_t address, MEMORY_BASIC_INFORMATION* mbi) +NTSTATUS WriteProcessMemory( + PEPROCESS process, + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred) { - DbgPrint("[CheatStengine] QueryProcessMemory addr=0x%p, mbi=0x%p\n", - reinterpret_cast(address), reinterpret_cast(mbi)); + return WriteProcessAddressSpace(process, address, buffer, size, bytesTransferred); +} - if (!process || !mbi) { +NTSTATUS QueryProcessMemory( + PEPROCESS process, + UINT64 address, + PMEMORY_BASIC_INFORMATION memoryInformation) +{ + if (!process || !memoryInformation || !IsUserAddress(address)) { return STATUS_INVALID_PARAMETER; } - - if (!IsValidUserModeAddress(address, 1)) { - DbgPrint("[CheatStengine] QueryProcessMemory: Invalid user mode address 0x%p\n", - reinterpret_cast(address)); - return STATUS_ACCESS_VIOLATION; + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; } NTSTATUS status = STATUS_SUCCESS; - KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - SIZE_T returnLength = 0; - - status = ZwQueryVirtualMemory( - ZwCurrentProcess(), - reinterpret_cast(address), - MemoryBasicInformation, - mbi, - sizeof(MEMORY_BASIC_INFORMATION), - &returnLength); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in QueryProcessMemory: 0x%08X\n", status); - } + SIZE_T returnLength = 0; + status = ZwQueryVirtualMemory( + ZwCurrentProcess(), + reinterpret_cast(address), + MemoryBasicInformation, + memoryInformation, + sizeof(*memoryInformation), + &returnLength); KeUnstackDetachProcess(&apcState); - - DbgPrint("[CheatStengine] ZwQueryVirtualMemory returned 0x%08X, BaseAddress=0x%p, RegionSize=0x%p, State=0x%X, Protect=0x%X, Type=0x%X\n", - status, mbi->BaseAddress, reinterpret_cast(mbi->RegionSize), mbi->State, mbi->Protect, mbi->Type); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwQueryVirtualMemory failed: 0x%08X\n", status); - } return status; } -NTSTATUS AllocateProcessMemory(PEPROCESS process, uintptr_t address, size_t size, - uint32_t allocationType, uint32_t protect, - uintptr_t* outAddress) +NTSTATUS AllocateProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG allocationType, + ULONG protect, + PUINT64 allocatedAddress, + PUINT64 allocatedSize) { - if (!process || !outAddress || size == 0) + if (!process || !allocatedAddress || !allocatedSize || size == 0 + || !IsNativeSize(size) + || (address != 0 && !IsUserAddressRange(address, size))) { return STATUS_INVALID_PARAMETER; + } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } NTSTATUS status = STATUS_SUCCESS; PVOID baseAddress = reinterpret_cast(address); - SIZE_T regionSize = size; - + SIZE_T regionSize = static_cast(size); KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwAllocateVirtualMemory( - ZwCurrentProcess(), - &baseAddress, - 0, - ®ionSize, - allocationType, - protect); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in AllocateProcessMemory: 0x%08X\n", status); - } + status = ZwAllocateVirtualMemory( + ZwCurrentProcess(), + &baseAddress, + 0, + ®ionSize, + allocationType, + protect); KeUnstackDetachProcess(&apcState); if (NT_SUCCESS(status)) { - *outAddress = reinterpret_cast(baseAddress); - } else { - DbgPrint("[CheatStengine] ZwAllocateVirtualMemory failed: 0x%08X\n", status); + *allocatedAddress = reinterpret_cast(baseAddress); + *allocatedSize = regionSize; } return status; } -NTSTATUS FreeProcessMemory(PEPROCESS process, uintptr_t address, size_t size, uint32_t freeType) +NTSTATUS FreeProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG freeType) { - if (!process || address == 0) { + const bool release = freeType == MEM_RELEASE && size == 0; + const bool decommit = freeType == MEM_DECOMMIT + && size != 0 + && IsUserAddressRange(address, size); + if (!process || address == 0 || !IsNativeSize(size) || (!release && !decommit) + || !IsUserAddress(address)) { return STATUS_INVALID_PARAMETER; } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } NTSTATUS status = STATUS_SUCCESS; PVOID baseAddress = reinterpret_cast(address); - SIZE_T regionSize = size; - + SIZE_T regionSize = static_cast(size); KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwFreeVirtualMemory( - ZwCurrentProcess(), - &baseAddress, - ®ionSize, - freeType); - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in FreeProcessMemory: 0x%08X\n", status); - } + status = ZwFreeVirtualMemory( + ZwCurrentProcess(), + &baseAddress, + ®ionSize, + freeType); KeUnstackDetachProcess(&apcState); - - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwFreeVirtualMemory failed: 0x%08X\n", status); - } return status; } -NTSTATUS ProtectProcessMemory(PEPROCESS process, uintptr_t address, size_t size, - uint32_t newProtect, uint32_t* oldProtect) +NTSTATUS ProtectProcessMemory( + PEPROCESS process, + UINT64 address, + UINT64 size, + ULONG newProtect, + PULONG oldProtect, + PUINT64 protectedAddress, + PUINT64 protectedSize) { - if (!process || address == 0 || size == 0 || !oldProtect) { + if (!process || !oldProtect || !protectedAddress || !protectedSize + || !IsNativeSize(size) || !IsUserAddressRange(address, size)) { return STATUS_INVALID_PARAMETER; } + if (KeGetCurrentIrql() != PASSIVE_LEVEL) { + return STATUS_INVALID_DEVICE_STATE; + } - if (!IsValidUserModeAddress(address, size)) { - return STATUS_ACCESS_VIOLATION; + const auto protectRoutine = reinterpret_cast( + InterlockedCompareExchangePointer(&ProtectAddress, nullptr, nullptr)); + if (!protectRoutine) { + return STATUS_NOT_SUPPORTED; } NTSTATUS status = STATUS_SUCCESS; - ULONG oldProtectValue = 0; - + PVOID baseAddress = reinterpret_cast(address); + SIZE_T regionSize = static_cast(size); + ULONG previousProtect = 0; KAPC_STATE apcState {}; KeStackAttachProcess(process, &apcState); - __try { - status = ZwProtectVirtualMemory( - ZwCurrentProcess(), - reinterpret_cast(&address), - &size, - newProtect, - &oldProtectValue); - - if (NT_SUCCESS(status)) { - *oldProtect = oldProtectValue; - } - } __except (EXCEPTION_EXECUTE_HANDLER) { - status = GetExceptionCode(); - DbgPrint("[CheatStengine] Exception in ProtectProcessMemory: 0x%08X\n", status); - } + status = protectRoutine( + ZwCurrentProcess(), + &baseAddress, + ®ionSize, + newProtect, + &previousProtect); KeUnstackDetachProcess(&apcState); - if (!NT_SUCCESS(status)) { - DbgPrint("[CheatStengine] ZwProtectVirtualMemory failed: 0x%08X\n", status); + if (NT_SUCCESS(status)) { + *oldProtect = previousProtect; + *protectedAddress = reinterpret_cast(baseAddress); + *protectedSize = regionSize; } return status; -} \ No newline at end of file +} diff --git a/CheatStengineDriver/src/CheatStengineDriver/Process.h b/CheatStengineDriver/src/CheatStengineDriver/Process.h index 97c4d03..c335df8 100644 --- a/CheatStengineDriver/src/CheatStengineDriver/Process.h +++ b/CheatStengineDriver/src/CheatStengineDriver/Process.h @@ -1,42 +1,49 @@ #pragma once -#include #include +void InitializeProcessSupport(); +bool CanProtectProcessMemory(); + NTSTATUS ReadProcessMemory( PEPROCESS process, - uintptr_t address, - void* buffer, - size_t size); + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); NTSTATUS WriteProcessMemory( PEPROCESS process, - uintptr_t address, - void* buffer, - size_t size); + UINT64 address, + PVOID buffer, + SIZE_T size, + PSIZE_T bytesTransferred); NTSTATUS QueryProcessMemory( PEPROCESS process, - uintptr_t address, - PMEMORY_BASIC_INFORMATION mbi); + UINT64 address, + PMEMORY_BASIC_INFORMATION memoryInformation); NTSTATUS AllocateProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t allocationType, - uint32_t protect, - uintptr_t* outAddress); + UINT64 address, + UINT64 size, + ULONG allocationType, + ULONG protect, + PUINT64 allocatedAddress, + PUINT64 allocatedSize); NTSTATUS FreeProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t freeType); + UINT64 address, + UINT64 size, + ULONG freeType); NTSTATUS ProtectProcessMemory( PEPROCESS process, - uintptr_t address, - size_t size, - uint32_t newProtect, - uint32_t* oldProtect); \ No newline at end of file + UINT64 address, + UINT64 size, + ULONG newProtect, + PULONG oldProtect, + PUINT64 protectedAddress, + PUINT64 protectedSize); diff --git a/CheatStengineDriver/vendor/FindWDK b/CheatStengineDriver/vendor/FindWDK new file mode 160000 index 0000000..ff531a9 --- /dev/null +++ b/CheatStengineDriver/vendor/FindWDK @@ -0,0 +1 @@ +Subproject commit ff531a9533b6d38f0ab62e23da2205b0cfe0d51b diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 41fe01a..27e69fa 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -3,15 +3,26 @@ project(Engine LANGUAGES CXX) include(vendor/imgui.cmake) include(vendor/spdlog.cmake) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_library(${PROJECT_NAME} ${SOURCE_FILES}) +add_library(${PROJECT_NAME} + src/Engine/Core/Application.cpp + src/Engine/Core/Layers/Layer.cpp + src/Engine/Core/Layers/LayerStack.cpp + src/Engine/Core/Log.cpp + src/Engine/Core/Window.cpp + src/Engine/Renderer/GraphicsContext.cpp + src/Platform/DX11/DX11Context.cpp + src/Platform/Win32/Win32Window.cpp +) + +target_compile_definitions(imgui PUBLIC IMGUI_USE_WCHAR32) +target_compile_definitions(${PROJECT_NAME} PUBLIC ENGINE_IMGUI) if (WIN32) - add_definitions(-DPLATFORM_WINDOWS -DNOMINMAX) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_WINDOWS NOMINMAX) elseif (UNIX AND NOT APPLE) - add_definitions(-DPLATFORM_LINUX) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_LINUX) elseif (APPLE) - add_definitions(-DPLATFORM_MACOS) + target_compile_definitions(${PROJECT_NAME} PUBLIC PLATFORM_MACOS) endif () target_include_directories(${PROJECT_NAME} PUBLIC src) diff --git a/Engine/vendor/imgui b/Engine/vendor/imgui new file mode 160000 index 0000000..2a1b69f --- /dev/null +++ b/Engine/vendor/imgui @@ -0,0 +1 @@ +Subproject commit 2a1b69f05748ad909f03acf4533447cac1331611 diff --git a/Engine/vendor/spdlog b/Engine/vendor/spdlog new file mode 160000 index 0000000..7e635fc --- /dev/null +++ b/Engine/vendor/spdlog @@ -0,0 +1 @@ +Subproject commit 7e635fca68d014934b4af8a1cf874f63989352b7 diff --git a/README.md b/README.md index 91a94c2..7fc5389 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,8 @@ features for reverse engineering, debugging, memory analysis, and game hacking. > [!WARNING] > Cheat Stengine is still in the very early stages of development. Many features are not yet implemented, and the tool > may be unstable. Expect bugs, crashes, and breaking changes between versions. - -> [!NOTE] -> If you're on an AMD chipset, you might face some issues. If you do, please copy the generated dump file and create an issue +> +> If you're on an AMD, you might face some issues. If you do, please copy the generated dump file and create an issue > with any relevant information that could help us reproduce the problem. > > We are not responsible for individual data loss, system instability, or other issues caused by using Cheat Stengine. @@ -33,26 +32,19 @@ cmake --build build --config Release ## MCP Server -Cheat Stengine ships with a built-in MCP server so an AI agent can drive the +Cheat Stengine comes with a built-in MCP server so an AI agent can drive the engine: list and attach processes, read/write and scan memory, disassemble, -assemble, dissect structs, and generate byte signatures. +assemble, dissect structs, and generate byte signatures -The server starts automatically on launch, binds to loopback, and prints its -URL and bearer token to the log. Open the **MCP** tab in Settings +The server starts automatically on launch +To configure open the **MCP** tab in Settings (`Ctrl+,`) to: -* toggle the server on or off, -* require the bearer token (off by default, since the loopback bind is the - gate), -* copy the endpoint and token, -* open the web dashboard and configuration pages, and +* toggle the server on or off +* require the bearer token (off by default) +* copy the endpoint and token * reinstall the client configs for Claude Code, Cursor, Claude Desktop, Codex - and the skill. - -The dashboard (served at the printed URL) shows live sessions, jobs and the -tool catalogue, and lets you enable or disable individual tools. The -configuration page at `/config.html` manages the connection and install -targets. + and the skill(optional but saves you around 100 tokens by explaining the ai how tools work) ## Features @@ -79,12 +71,4 @@ targets. * [ ] Thread Explorer * [ ] Handle Viewer * [ ] Plugin System -* [ ] DBVM - -## Development - -Cheat Stengine is under active development. Features marked as incomplete are planned or currently being worked on. -The project is still experimental, so functionality and APIs may change as development progresses. - -Bug reports and contributions are welcome. If you encounter a crash, please include relevant logs or dump files when -creating an issue. \ No newline at end of file +* [ ] DBVM \ No newline at end of file diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..f14c282 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,29 @@ +--- +name: cheatstengine +description: Drive Cheat Stengine (a Windows reverse-engineering tool) over MCP: list/attach processes, read/write and scan memory, disassemble, assemble, dissect structs, and generate byte signatures. Use when inspecting or modifying a running process's memory on this machine. +--- + +# Cheat Stengine MCP + +This tool exposes a live reverse-engineering engine over MCP at `http://127.0.0.1:13777/mcp`. +Auth is off by default, so you can connect with just the URL. If the user turns auth on, send `Authorization: Bearer 2187c0e6df08f002e36a6303069998a5341e94ade9aec213cf81cfe083229c3a`. + +## Workflow + +1. `list_processes` - find the target's pid. +2. `open_process` { pid } - attach. Everything below needs an attached process. +3. Inspect: `list_modules`, `read_memory`, `query_memory`, `disassemble`, `dissect_struct`. +4. Resolve addresses symbolically with `resolve_address` { expression: "module.dll+0x1234" }; most tools also accept an expression string directly in place of a numeric address. +5. Modify: `write_memory` { address, hex }, or `assemble` { source, address, write: true }. + +## Async tools (poll, don't block) + +- `pattern_scan` { pattern } returns a `jobId`. Poll `job_status` { id } until status is `completed`, then read `result.matches`. +- `scan_value` { valueType, scanType, value } starts a value scan. Poll `scan_results` until `scanning` is false. Refine with `scan_value` { ..., next: true }. + +## Notes + +- Addresses in results are hex strings like `0x7FF6...`. +- Value types: int8/16/32/64, uint8/16/32/64, float, double. +- Scan types: exact, bigger, smaller, between, unknown. +- `pattern` is space-separated hex with `??` wildcards, e.g. `48 8B ?? C3`. diff --git a/SampleApp/CMakeLists.txt b/SampleApp/CMakeLists.txt index e128067..0fbabcd 100644 --- a/SampleApp/CMakeLists.txt +++ b/SampleApp/CMakeLists.txt @@ -1,9 +1,10 @@ project(SampleApp) -file(GLOB_RECURSE SOURCE_FILES src/*.cpp) -add_executable(${PROJECT_NAME} ${SOURCE_FILES}) +add_executable(${PROJECT_NAME} + src/SampleApp/main.cpp +) target_include_directories(${PROJECT_NAME} PUBLIC src) target_link_libraries(${PROJECT_NAME} PUBLIC Engine -) \ No newline at end of file +) diff --git a/Shared/Protocol.h b/Shared/Protocol.h new file mode 100644 index 0000000..c8ed1c3 --- /dev/null +++ b/Shared/Protocol.h @@ -0,0 +1,255 @@ +#pragma once + +#ifdef _KMODE +#include +#else +#include +#include +#endif + +#include + +namespace Protocol { + +inline constexpr uint16_t VersionMajor = 1; +inline constexpr uint16_t VersionMinor = 2; +inline constexpr uint16_t DriverVersionMajor = 1; +inline constexpr uint16_t DriverVersionMinor = 0; +inline constexpr uint32_t DriverVersionPatch = 0; +inline constexpr uint32_t DeviceType = 0x8000; +inline constexpr uint64_t MaximumTransferSize = 16ULL * 1024ULL * 1024ULL; +inline constexpr uint32_t DefaultIdleTimeoutSeconds = 60; +inline constexpr uint32_t MinimumIdleTimeoutSeconds = 15; +inline constexpr uint32_t MaximumIdleTimeoutSeconds = 3600; + +inline constexpr GUID DeviceInterfaceGuid = { + 0x8d3f3f2d, + 0x3d46, + 0x4a66, + { 0x8f, 0x9b, 0x46, 0xe6, 0xd5, 0xa6, 0x78, 0x31 } +}; + +enum Capability : uint64_t { + BindProcess = 1ULL << 0, + ReadMemory = 1ULL << 1, + WriteMemory = 1ULL << 2, + QueryMemory = 1ULL << 3, + AllocateMemory = 1ULL << 4, + FreeMemory = 1ULL << 5, + ProtectMemory = 1ULL << 6, + Ping = 1ULL << 7, + Heartbeat = 1ULL << 8, + Configure = 1ULL << 9, +}; + +inline constexpr uint64_t RequiredCapabilities = BindProcess + | ReadMemory + | WriteMemory + | QueryMemory + | AllocateMemory + | FreeMemory + | Ping + | Heartbeat + | Configure; + +inline constexpr uint64_t Capabilities = RequiredCapabilities | ProtectMemory; + +enum class LoadMode : uint32_t { + Unknown, + Pnp, + Service, + Mapped, +}; + +enum class RuntimeState : uint32_t { + Starting, + Ready, + Idle, + Stopping, + Removed, +}; + +struct PacketHeader { + uint32_t Size; + uint16_t MajorVersion; + uint16_t MinorVersion; + uint32_t Flags; + uint32_t Reserved; +}; + +struct InformationRequest { + PacketHeader Header; +}; + +struct InformationResponse { + PacketHeader Header; + uint64_t Capabilities; + uint64_t MaximumTransferSize; + uint32_t LoadMode; + uint32_t Reserved; +}; + +struct PingRequest { + PacketHeader Header; + uint64_t Nonce; +}; + +struct PingResponse { + PacketHeader Header; + uint64_t Nonce; + uint64_t KernelTime; + uint64_t Capabilities; + uint64_t MaximumTransferSize; + uint16_t DriverMajorVersion; + uint16_t DriverMinorVersion; + uint32_t DriverPatchVersion; + uint32_t ActiveSessions; + uint32_t IdleTimeoutSeconds; + uint32_t RemainingIdleSeconds; + uint32_t LoadMode; + uint32_t State; + uint32_t Reserved; +}; + +struct HeartbeatRequest { + PacketHeader Header; + uint64_t Sequence; +}; + +struct HeartbeatResponse { + PacketHeader Header; + uint64_t Sequence; + uint64_t KernelTime; + uint32_t RemainingIdleSeconds; + uint32_t State; +}; + +struct ConfigureRequest { + PacketHeader Header; + uint32_t IdleTimeoutSeconds; + uint32_t Reserved; +}; + +struct ConfigureResponse { + PacketHeader Header; + uint32_t IdleTimeoutSeconds; + uint32_t Reserved; +}; + +struct BindProcessRequest { + PacketHeader Header; + uint32_t ProcessId; + uint32_t Reserved; +}; + +struct ReadMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct WriteMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct QueryMemoryRequest { + PacketHeader Header; + uint64_t Address; +}; + +struct QueryMemoryResponse { + PacketHeader Header; + uint64_t BaseAddress; + uint64_t AllocationBase; + uint64_t RegionSize; + uint32_t AllocationProtect; + uint32_t State; + uint32_t Protect; + uint32_t Type; +}; + +struct AllocateMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t AllocationType; + uint32_t Protect; +}; + +struct AllocateMemoryResponse { + PacketHeader Header; + uint64_t Address; + uint64_t Size; +}; + +struct FreeMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t FreeType; + uint32_t Reserved; +}; + +struct ProtectMemoryRequest { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t Protect; + uint32_t Reserved; +}; + +struct ProtectMemoryResponse { + PacketHeader Header; + uint64_t Address; + uint64_t Size; + uint32_t OldProtect; + uint32_t Reserved; +}; + +inline constexpr uint32_t InformationControlCode = CTL_CODE(DeviceType, 0x800, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t BindProcessControlCode = CTL_CODE(DeviceType, 0x801, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS); +inline constexpr uint32_t ReadMemoryControlCode = CTL_CODE(DeviceType, 0x802, METHOD_OUT_DIRECT, FILE_READ_ACCESS); +inline constexpr uint32_t WriteMemoryControlCode = CTL_CODE(DeviceType, 0x803, METHOD_IN_DIRECT, FILE_WRITE_ACCESS); +inline constexpr uint32_t QueryMemoryControlCode = CTL_CODE(DeviceType, 0x804, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t AllocateMemoryControlCode = CTL_CODE(DeviceType, 0x805, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t FreeMemoryControlCode = CTL_CODE(DeviceType, 0x806, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t ProtectMemoryControlCode = CTL_CODE(DeviceType, 0x807, METHOD_BUFFERED, FILE_WRITE_ACCESS); +inline constexpr uint32_t PingControlCode = CTL_CODE(DeviceType, 0x808, METHOD_BUFFERED, FILE_READ_ACCESS); +inline constexpr uint32_t HeartbeatControlCode = CTL_CODE(DeviceType, 0x809, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS); +inline constexpr uint32_t ConfigureControlCode = CTL_CODE(DeviceType, 0x80A, METHOD_BUFFERED, FILE_WRITE_ACCESS); + +template +constexpr PacketHeader CreateHeader() +{ + return { + static_cast(sizeof(T)), + VersionMajor, + VersionMinor, + 0, + 0, + }; +} + +static_assert(sizeof(PacketHeader) == 16); +static_assert(sizeof(InformationRequest) == 16); +static_assert(sizeof(InformationResponse) == 40); +static_assert(sizeof(PingRequest) == 24); +static_assert(sizeof(PingResponse) == 80); +static_assert(sizeof(HeartbeatRequest) == 24); +static_assert(sizeof(HeartbeatResponse) == 40); +static_assert(sizeof(ConfigureRequest) == 24); +static_assert(sizeof(ConfigureResponse) == 24); +static_assert(sizeof(BindProcessRequest) == 24); +static_assert(sizeof(ReadMemoryRequest) == 32); +static_assert(sizeof(WriteMemoryRequest) == 32); +static_assert(sizeof(QueryMemoryRequest) == 24); +static_assert(sizeof(QueryMemoryResponse) == 56); +static_assert(sizeof(AllocateMemoryRequest) == 40); +static_assert(sizeof(AllocateMemoryResponse) == 32); +static_assert(sizeof(FreeMemoryRequest) == 40); +static_assert(sizeof(ProtectMemoryRequest) == 40); +static_assert(sizeof(ProtectMemoryResponse) == 40); + +}