
前幾天搬工作室,電腦什么的都搬到百米遠的地方,激光切割機因為體積大而且笨重,更重要的的是排煙的新工作室不好安裝,所幸就暫時留在那了。最開始得到時候切割東西都是這邊做好微信傳輸過去,拿筆記本在連接上切割機的WIFI,進行切割的,一整二整反復修改切割,除了百米“賽跑”外還要不停的切換WIFI,整的火氣比較大。后來優化了一下,把切割機的路由器做了一下設置,橋接到主路由無線上(如果不重新設置切割機的IP,就使用子網的方式,映射IP端口到外部;設置的話可以選擇把路由器當交換機使用),這樣調整后的好處是不需要拷貝到另外一臺電腦再操作,直接傳輸即可。但是又想要的新東西,在切割機工作的時候,軟件(LaserCAD)并不會顯示當前的是否處于“運行”還是“空閑”狀態,雖然它在運行中的時候加載文件,會提示當前正在運行,并不是直觀。所以決定在軟件的控制面板的底下添加一個用來顯示當前狀態的文本作為提示,然后也就有了這篇文章。
1. 獲取激光切割機的狀態
2. 在軟件中添加用于顯示狀態的Static
3. 使用程序控制狀態顯示
獲取激光切割機的狀態
獲取狀態,就需要搞清楚軟件和激光切割機是如何通信的,獲取狀態所發送的消息內容是什么,這些都是封裝在程序里面的,并且這個軟件并不開源,從源代碼獲取是不可能了,而且文檔上也沒有說到傳輸協議這個信息,還好知道它是有用網絡通信的,這就是我們的突破口。
準備軟件:
Wireshark (用于網絡抓包) 選擇要抓包的網卡,然后設置過濾 ip.addr == 192.168.102.15
(后面的IP地址是
)
點擊獲取當前坐標位置:

通過捉取的信息我們可以發現激光切割機監聽的端口為 2027 ,之后我們也是需要把數據發送到這個端口上的,協議為UDP
在我還不知道切割機器在運行中加載文檔時會提示:“傳輸失敗:機器正在工作或暫停!”時,我才用的方法時獲取兩次當前的坐標位置,如果坐標位置一樣就說明機器已經處于空閑狀態,否則正在運行。有了這個思路,就用node.js 寫了一個簡易版本,測試是否可行。先看看獲取當前坐標位置到做了什么事:

這個是發送的16進制數據,看Ascii碼明顯是加密過的可以使用右鍵復制值,或則…as HEX Dump

對其進行解密得到:
00 00 00 01 d7 00 04 21 d7 00 04 31 d7 00 04 41 d7 00 04 51
解密方法為對每一個字節(bit):~bit & 0xff ^ 0x82
比如開始:7d 7d 7d 7c a5 3e00 00 00 01 d8 43
暫停/繼續:7d 7d 7d 7c a5 390 00 00 01 d8 44
停止:7d 7d 7d 7c a5 3800 00 00 01 d8 45
前面的 00 00 00 01 表示發送
后面的 d7 00 表示類型 也可以嘗試轉成十進制
再后面攜帶的數據 接收的數據:

解密后:00 00 00 02 d7 01 04 21 00 00 0b 22 6c d7 01 04 31 00 00 03 69 0f d7 01 04 41 00 00 18 35 00 d7 01 04 51 00 00 00 00 00
前面的 00 00 00 02 表示接收
后面是每個軸和對應的坐標
d7 01 04 21 00 00 0b 22 6c X坐標 = 184.06
d7 01 04 31 00 00 03 69 0f Y坐標 = 62.61
d7 01 04 41 00 00 18 35 00 Z坐標 = 400d7 01 04 51 00 00 00 00 00

#include <iostream>#include <iomanip>using namespace std; int decode(int data[5]){ if (data[0] < 0 || data[1] < 0 || data[2] < 0 || data[3] < 0 || data[4] < 0) return 0; return data[4] & 0x7F | ((data[3] & 0x7F | ((data[2] & 0x7F | ((((unsigned __int8)data[0] << 7) | data[1] & 0x7F) << 7)) << 7)) << 7);} int main() { int dataX[] = { 0x00, 0x00, 0x0B, 0x22, 0x6C }; int dataY[] = { 0x00, 0x00, 0x03, 0x69, 0x0F }; int dataZ[] = { 0x00, 0x00, 0x18, 0x35, 0x00 }; cout << "X=" << fixed << setprecision(2) << decode(dataX) * 0.001 << endl; cout << "Y=" << fixed << setprecision(2) << decode(dataY) * 0.001 << endl; cout << "Z=" << fixed << setprecision(2) << decode(dataZ) * 0.001 << endl; return 0;}

解密的函數是通過 x64dbg 調試獲得的



最后的這個匯編函數就是對應上面解析坐標的C++代碼
Node.js 使用坐標判斷是否停止
const dgram = require("dgram"); const client = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x5c, 0xaa, 0x7d, 0x79, 0x4c, 0xaa, 0x7d, 0x79, 0x3c, 0xaa, 0x7d, 0x79, 0x2c]) setInterval(() => { client.send(buffer, 2027, "192.168.102.15", (err, bytes) => { if (err) console.error(err); // console.log(bytes); });}, 2000) let same = false let prePosition = []function isSame(msg) { for (let i = 0; i < msg.length; i++) { if (msg[i] !== prePosition[i]) { prePosition = msg return false } } return true}client.on('message', (msg, rinfo) => { if (isSame(msg)) { if (!same) { // 系統提示音 setTimeout(() => { process.stdout.write('\x07') }, 10) setTimeout(() => { process.stdout.write('\x07') }, 1000) setTimeout(() => { process.stdout.write('\x07') }, 2000) } same = true console.log('位置相同'); } else { same = false console.log('.'); } // client.close();}); client.on("close", () => { console.log("close");});

后面發現了激光切割機在運行的時候加載文檔會提示:

所以通過網絡抓包獲得

發送:7d7d7d7caa7d797daa7d797c
接收:7d7d7d7faa7c797d7d7d7d7d7caa7c797c7d7d7d7d7d
接收的數據其中的第 13 個字節:
0x7d:~(0x7d ^ 0x82) & 0xFF = 0 空閑
0x7c:~(0x7c ^ 0x82) & 0xFF = 1 工作或暫停
改進一下js代碼:
const dgram = require("dgram"); const client = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x7d, 0xaa, 0x7d, 0x79, 0x7c]) function send() { client.send(buffer, 2027, "192.168.101.15", (err, bytes) => { if (err) console.error(err); // console.log(bytes); });} setInterval(send, 2000)send() let preStatus = 0 client.on('message', (msg, rinfo) => { let status = ~(msg[12] ^ 0x82) & 0xFF // console.log('status', status); if (status === 0) { if (preStatus !== status) { // 系統提示音 process.stdout.write('\x07') } console.log('空閑'); } else { console.log('.'); } preStatus = status // client.close();}); client.on("close", () => { console.log("close");});
有了上面獲取狀態的基礎,下一步就可以在界面上做文章了
首先在界面上添加兩個標簽文本(Static)
我這里使用的是 XNResourceEditor 原因是可以選擇控件繪制,至于其他的方式你們都可以嘗試,甚至可以使用C/C++ 動態創建

給靜態文本設置一個不重復的ID

保存好后,下面開始在程序運行的時候獲取到這個“靜態文本”的句柄這里使用微軟的Spy++ (安裝Visual Studio 并且選擇使用c++的桌面開發的工作負荷會有自帶的,在工具菜單里面)當然也可以自行下載或使用其他的類似工具

點擊查找窗口,并把那個定位的圖標拖動到你新添加的顯示狀態的靜態文本上方,然后點擊OK

從上方看要獲取到這個窗口的句柄需要至少5次查找
獲取主窗口的句柄Afx:400000:b:10005:6:26060cc7(激光雕刻切割控制系統 V7.92.2 - Untitled) à 獲取ProfUIS-DockBar à 獲取ProfUIS-ControlBar(控制面板) à 獲取#32770(控制面板)à 獲取Static(停止)
通常獲取窗口會使用 FindWindow API
[DllImport("user32.dll", SetLastError = truestatic extern IntPtr FindWindow(string lpClassName, string lpWindowName);lpClassName 類名 lpWindowName 窗口標題
獲取子窗口會使用FindWindowEx API
[DllImport("user32.dll", SetLastError = truepublic static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, string windowTitle);parentHandle 父窗口句柄 hWndChildAfter前一個子窗口句柄 className類名 windowTitle窗口標題
經過實驗主窗口標題和類名都是會改變的,所以獲取的時候都不能FindWindow
使用進程的MainWindowHandle來代替
下面為C#代碼

var processList = Process.GetProcessesByName("laserCAD");if (processList.Length > 0) { IntPtr hwn = processList[0].MainWindowHandle; IntPtr t = FindWindowEx(hwn, IntPtr.Zero, "ProfUIS-DockBar", null); IntPtr t2; do { t2 = FindWindowEx(hwn, t, "ProfUIS-DockBar", null); if (t2 != IntPtr.Zero) t = t2; } while (t2 != IntPtr.Zero); t = FindWindowEx(t, IntPtr.Zero, "ProfUIS-ControlBar", "控制面板"); t = FindWindowEx(t, IntPtr.Zero, "#32770", "控制面板"); t2 = FindWindowEx(t, IntPtr.Zero, "Static", "當前狀態:"); IntPtr statusHwn = FindWindowEx(t, t2, "Static", null);}
有了句柄現在可以設置靜態文本文字了
使用的 SendMessage API
[DllImport("user32.dll", CharSet = CharSet.Auto)]public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, uint wParam, StringBuilder lParam);const uint WM_SETTEXT = 0x000C; SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("你好"));

這樣就可以開始寫UDP發送和接收數據了
使用定時器每一秒發送一次請求
void sendMsg() { // 從程序中獲取選定的IP地址 EndPoint point = new IPEndPoint(IPAddress.Parse("192.168.102.15"), 2027); var buffer = new byte[]{0x7d,0x7d,0x7d,0x7c,0xaa,0x7d,0x79,0x7d,0xaa,0x7d,0x79,0x7c}; client.SendTo(buffer, point); EndPoint point2 = new IPEndPoint(IPAddress.Any, 0); //用來保存發送方的ip和端口號 byte[] buffer2 = new byte[1024]; int length = client.ReceiveFrom(buffer2, ref point2); //接收數據報 var status = ~(buffer2[12] ^ 0x82) & 0xFF; if (status == 0) { if (preStatus != status) { // 系統提示 //MessageBox.Show(form,"切割完成"); } Console.WriteLine("空閑"); SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("空閑")); } else { Console.WriteLine("."); SendMessage(statusHwn, WM_SETTEXT, 20, new StringBuilder("運行")); } preStatus = status;}
在定時器中調用
client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);Thread threadSendMsg = new Thread(sendMsg);threadSendMsg.Start();
經過測試發現在加載文檔的時候沖突比較大,所以在加載文檔框彈出來的時候不去獲取狀態
private bool isDocLoadOpen(){ var wn = FindWindow("#32770", "文檔加載"); return wn != IntPtr.Zero;}
在定時器開始的時候調用如果true直接return
// 跳過文檔加載if (isDocLoadOpen()) return;
在程序中是可以選擇哪個IP地址的,所以我們需從程序內存中拿到選擇的IP地址

上面的條注釋就可以在內存中獲取IP地址了,是通過32位10進制存儲的

IntPtr ipIndexAddr = IntPtr.Zero;private string getSelectIp() { //if (cacheIp != "") return cacheIp; var _memoryUtils = new MemoryUtils(needList.Last().Id); if (ipIndexAddr == IntPtr.Zero) ipIndexAddr = _memoryUtils.GetMemoryAddress("LaserCAD2.1.exe", 0x1B22E8); log("ipIndexAddr:" + ipIndexAddr); var ipIndex = _memoryUtils.ReadToInt(ipIndexAddr); log("ipIndex:" + ipIndex); //var ipAddr = _memoryUtils.GetMemoryAddress("LaserCAD2.1.exe", 0x1B21EC + 8 * ipIndex); var ipAddr = ipIndexAddr - 0x1B22E8 + 0x1B21EC + 8 * ipIndex; log("ipAddr:" + ipAddr); var ip = _memoryUtils.ReadToInt(ipAddr); log("ip:" + ip); var ipStr = "" + (ip >> 24 & 0xff) + "." + (ip >> 16 & 0xff) + "." + (ip >> 8 & 0xff) + "." + (ip & 0xff); cacheIp = ipStr; return ipStr;}

除此之外,基本功能已經完成,但總是覺得不夠完善,比如運行和空閑狀態顯示紅色和綠色區分更為明顯,使用DLL的方式是,更為便捷
接下來繼續實現
程序中需要改變Static的顏色,從網絡上查到的資料,都說需要實現函數重載WndProc,苦于沒有源碼實現不了(或則說難度比較大),于是乎,想到了HDC自己繪制一個。
var title = new StringBuilder(" 空閑 ");SendMessage(statusHwn, WM_SETTEXT, 20, title);var hdc = GetDC(statusHwn);log("hdc:" + hdc);SetBkColor(hdc, 0xF2F2F2);SetTextColor(hdc, ColorTranslator.ToWin32(Color.Green));var rcTitle = new Rectangle(0, 0, 100, 50);ExtTextOut(hdc, 0, 0, ETO_OPAQUE, ref rcTitle, null, 0, IntPtr.Zero);DrawText(hdc, title, title.Length + 1, ref rcTitle, 0);ReleaseDC(statusHwn, hdc);

效果其實還不錯,就是布局刷新后需要下一次等下一次重繪才會有顏色,不過已經夠用了下面是一些用到的導入函數
[DllImport("gdi32.dll")]static extern uint SetBkColor(IntPtr hdc, int crColor); [DllImport("gdi32.dll")]static extern uint SetTextColor(IntPtr hdc, int crColor); [DllImport("user32.dll", SetLastError = true)]static extern IntPtr GetDC(IntPtr hWnd); [DllImport("user32.dll")]static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); [DllImport("gdi32.dll", EntryPoint = "ExtTextOutW")]static extern bool ExtTextOut(IntPtr hdc, int X, int Y, uint fuOptions, [In] ref Rectangle lprc, [MarshalAs(UnmanagedType.LPWStr)] string lpString, uint cbCount, [In] IntPtr lpDx); [DllImport("user32.dll")]static extern int DrawText(IntPtr hdc, StringBuilder lpchText, int cchText, ref Rectangle lprc, uint dwDTFormat);
至于DLL的方式,因為懶的去寫一份C++的代碼,所以又整了好幾天,問題出在C#的生成的dll沒有導出表,重網上得知一個叫 UnmanagedExports 的Nuget包可以實現,但是應該是過時了(有個新的DllExport有興趣的朋友可以試試),需要.Net 3.5的環境,新版本的VS 需要 Visual Studio BuildTools 2015,還需要一個UnmanagedExportLibrary.zip(項目模板放在C:\Users\x\Documents\Visual Studio 2022\Templates\ProjectTemplates\Visual C# 目錄里

)經歷一番波折后需要把解決方案的平臺設置為x86

系統的語言設置成英語
這樣編譯出來的DLL才有導出標(早知道選擇C++了……,還是太犟了……)

DLL中對獲取IP地址的優化(因為在同一進程下)
private static string getSelectIp() { var baseAddress = Process.GetCurrentProcess().MainModule.BaseAddress; log("baseAddress:" + baseAddress); var ipIndexAddr = baseAddress + 0x1B22E8; log("ipIndexAddr:" + ipIndexAddr); var ipIndex = Marshal.ReadInt32(ipIndexAddr); log("ipIndex:" + ipIndex); var ipAddr = baseAddress + 0x1B21EC + 8 * ipIndex; log("ipAddr:" + ipAddr); var ip = Marshal.ReadInt32(ipAddr); log("ip:" + ip); var ipStr = "" + (ip >> 24 & 0xff) + "." + (ip >> 16 & 0xff) + "." + (ip >> 8 & 0xff) + "." + (ip & 0xff); log("ipStr:" + ipStr); return ipStr;}
有了DLL現在需要把DLL添加到程序的導入表中
使用LoadPE工具
開始前記得備份一下

直接將LaserCAD.exe拖拽到LoadPE上方

點擊目錄

然后點擊輸入表后面的兩個點按鈕

點擊右鍵添加導入表

輸入DLL的名稱和需要導入的API名稱,然后點擊+號按鈕

加入列表后點擊確定

這個地址需要在匯編的時候調用
將LaserCAD.exe加載到x32dbg中 點擊運行,會停在入口處

F8步過
如果卡在循環里可以點擊循環跳轉前的下一行

然后點擊運行到選區即可跳過循環

一直運行到這里程序處于運行狀態中
這里就是我們要插入DLL調用的地方(雖然這個地方不是很理想)
在這個地方先下一個斷點,一會再回來,按空格把匯編代碼復制到剪貼板,并記錄下一行地址
call 0x005310C6
00530840
把滾動條拉向下拉,找到都是00的區塊

在這里寫入一些匯編代碼 pushad 和 popad 保證堆棧平衡
調用的DLL中的init函數的地址為 call dword ptr ds:[ 0x0061F00C]
0x0061F00C 為基值 0x400000 + ThunkRAV(0x21F00C)
然后再跳回之前的下一行地址 jmp 0x00530840
復制pushad 的地址 00540637
再斷點里面找到之前下的斷點,雙擊回到原來的地方

修改成 jmp 0x00540637
右鍵點擊補丁


點擊修補文件,保存成另外一個文件文名
可以把名字改回來
至此所有的功能已經實現,感謝大家耐心的閱讀!
模擬DUP(部分)服務的JS代碼
const dgram = require("dgram"); const server = dgram.createSocket("udp4"); let buffer = Buffer.from([0x7d, 0x7d, 0x7d, 0x7f, 0xaa, 0x7c, 0x79, 0x5c, 0x7d, 0x7d, 0x76, 0x5f, 0x11, 0xaa, 0x7c, 0x79, 0x4c, 0x7d, 0x7d, 0x7e, 0x14, 0x72, 0xaa, 0x7c, 0x79, 0x3c, 0x7d, 0x7d, 0x65, 0x48, 0x7d, 0xaa, 0x7c, 0x79, 0x2c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d]) let bufferStatus = Buffer.from([0x7d, 0x7d, 0x7d, 0x7f, 0xaa, 0x7c, 0x79, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d, 0xaa, 0x7c, 0x79, 0x7c, 0x7d, 0x7d, 0x7d, 0x7d, 0x7d]) // 接收的狀態請求const bufferReceiveStatus = Buffer.from([0x7d, 0x7d, 0x7d, 0x7c, 0xaa, 0x7d, 0x79, 0x7d, 0xaa, 0x7d, 0x79, 0x7c]) function equals(msg, buf) { for (let i = 0; i < msg.length; i++) { if (msg[i] !== buf[i]) { return false } } return true} server.on("error", function (err) { console.log("server error:\n" + err.stack); server.close();}); let count = 0 server.on("message", function (msg, rinfo) { console.log("server got: " + msg + " from " + rinfo.address + ":" + rinfo.port); let sendBuff = buffer if (equals(msg, bufferReceiveStatus)) { // 狀態 ++count; sendBuff = bufferStatus if (count % 5 == 0) { sendBuff[12] = 0x7d } else { sendBuff[12] = 0x7c } } server.send(sendBuff, rinfo.port, rinfo.address, (err, bytes) => { if (err) console.error(err); console.log(bytes, '發送成功'); });}); server.on("listening", function () { var address = server.address(); console.log("server listening " + address.address + ":" + address.port);}); server.bind(2027);
Andrew
Anna艷娜
虹科網絡安全
X0_0X
Anna艷娜
安全俠
Anna艷娜
Anna艷娜
Andrew
看雪學苑
一顆小胡椒