閨蜜的電腦是新買的臺式的,看著它我就想起了在學校上電腦課,偷偷玩蜘蛛紙牌和掃雷的事情。還記得當時玩掃雷的時候心情總是非常的緊張,倒不是因為害怕老師發現,而是害怕自己突然被雷"炸死"。
然而當我打開電腦的時候,我并沒有找到掃雷這款游戲,也沒有辦法下載,我問我閨蜜,她說也表示不知道原因,所以最后我就直接上西瓜視頻尋找答案了。
最后我終于在西瓜視頻創作人"GM的秘密基地"制作的視頻"掃雷為什么在win10上消失了?原因竟然和國際反地組織相關!"里面,找到了我想要的答案。
1. 為減少麻煩,最初微軟將掃雷換成了"花田"
上世紀末,在國際上興起了一股反地雷風潮,參加的國家高達100多個。這100個國家還簽署了一份公約,并呼吁全球禁止生產和使用地雷。這本來是一件好事,因為地雷的威脅的確很大,現在我國都有戰士因為掃雷而不斷犧牲。
但是萬萬沒有想到的是,一位意大利網友竟然趁著這次機會開始針對微軟的掃雷游戲,并且還發起了一場"國際禁止掃雷運動",受到了很多人的鼓舞與支持,而這場運動簡稱ICBW。
他們要求微軟刪除掉系統里的掃雷游戲,因為掃雷傷害到了冒著生命危險排雷的戰士,以及廣大的地雷受害者,這么聽來,好像也沒有什么毛病。而當時微軟也為了避免麻煩和影響的進一步擴散,就很快修改了意大利版本的windows,把里面的掃雷換皮成了一個叫"花田"的游戲。
2. 網友不買賬,微軟迫不得已只有刪除
微軟以為這樣做事情就可以告一段落了,但是后來證明并沒有。因為那位意大利網友看到微軟的處理結果之后,又帶領著ICBW開始抗議。
于是,微軟百般無奈之下,只有在后來的vista和win7中把所有版本的掃雷全部換成了"花田"。到了win8開始甚至都不再自帶掃雷,只能靠主動去搜索來玩這款經典的游戲了。
雖然沒有了這些游戲讓人覺得很難過,但是我們還可以從另一角度來看待這個問題,那就是微軟發明這些自帶的小游戲,其實完全是為了讓用戶熟悉鼠標的用法,而現在隨著電腦的普及,人們早就學會了如何使用鼠標,所以自然也到了它們退出歷史舞臺的時候了。
關于"win10為什么沒有掃雷"的科普就到這里了,但是西瓜視頻上的科普一直都在增加,所以趕快上西瓜視頻搜索:GM的秘密基地,然后觀看視頻漲知識吧!
前逆過XP下的掃雷程序,感覺XP下的掃雷很簡單,但是發現網上對于Win7下的掃雷逆向很少很少,于是就試著繼續逆一下Win7下的掃雷。這一逆發現難度提升了不只一個等級啊,經過兩天的努力,終于整個逆完了它的掃雷算法。
首先在Win7下的掃雷不再是像XP一樣在一開始就布置好雷區,這樣我們就可以在一開始就讀取雷區內存,比較坑的是win7下的掃雷是在你點擊第一塊兒方塊時才開始布置雷區。這樣我首先在rand函數下斷點,發現有好多地方會調用rand函數,我把每個調用rand函數的地方下了斷點,然后把一直在調用的rand函數的那幾個函數斷點給去掉,這樣我們就找到了程序的突破口。
不斷退出當前調用,并在上層函數的call調用處下斷點,直到找到了一個疑是算法入口的函數。
跟進函數,又發現一個call,繼續跟進
我發現這個函數便是調用rand函數的地方,估計核心就在此了,開干
我們要對每個call都倍加小心,需要都看一下,我們發現這里好像是一個申請數組空間并填充的操作
經過多次循環后,發現數組填充完畢,之后觀察一下申請的數組空間中存儲的東西
發現沒有了00 01 09 0A四個值。因為我是點擊的第一個方塊,我們可以重新調試,點擊其他方塊試一下,發現這個數組會將點擊方塊周圍的的9個值去掉(包括點擊方塊自己),這樣我們就理解了,程序不會在第一次點擊方塊周圍產生雷。
這時候我們估計就對這個程序有了一點點理解了,在點擊第一塊兒方塊的時候,程序開始申請內存。這里它會有一個結構體存儲了隨機雷數組已用大小和總空間,然后生成一個數組,并將各個雷進行編號存入數組中。之后rand函數產生的隨機雷就在這些數組中產生。接下來驗證我們的想法:
跟進下一個call,發現這里申請數組空間,并存儲隨機出來的雷值
繼續單步,發現有個小循環比較有意思
看一下rax存儲了什么?
這好像是存儲了多個數組的首地址啊,正好我們現在設定了9x9的雷區,這里正好9個地址,我們再跟進去這些地址看一下
這里+10處又存儲了一個地址,繼續觀察,發現有個byte數組,存儲了雷的狀態,有雷就是1,無雷就是0
這個時候我們就基本搞明白了這個Win7下掃雷是怎么布置的了。可是問題來了,最初的記錄雷區各個數組的地址從哪得啊?我們逆著代碼去溯源。我們發現這個值是rax+0x10處的存儲的值,而rax是rsi+0x58處存儲的值,這個rsi是rcx作為上層調用函數傳過來的參,我們走出這個函數看看這個參數從哪里得到。
我們找到了這樣一個值,在FFCFAA38中存儲了我們所想要的rsi的值。我們知道,這是一個全局變量,存儲了rsi地址。但是這個值由于RSLR機制而導致每次地址不一樣。我們有一種方法得到這個值,我們先看當前模塊加載基地址,然后用FFCFAA38(全局變量地址)-FFC500000(當前模塊加載地址)=AAA38(相對當前模塊偏移)。這樣我們可以用GetModuleHandle函數得到當前模塊加載基地址,然后加上這個偏移AAA38就得到了全局變量地址。
這樣我們就有了得到數組地址的方法:
Address=[[[[hModule+0xAAA38]+0x18]+0x58]+0x10]
這時Address就是存儲雷區數組的首地址,每個雷區地址+0x10處就是雷區列狀態數組(byte)地址。
其實,到這里我們也開始明白了,它所使用的應該是C++的vector,一個個push才產生這樣的內存空間的,不得不說,這C++功力已經爐火存青了,各種數據結構弄得頭都大了。
找到了雷區布置數組就可以進行下一步動作了,我們通過計算鼠標坐標值來獲得雷區格子,每個格子是17*17像素大小并加上1像素的邊,所以每個格子大小為18像素,雷區邊界為30像素。你問我這些怎么得到的?這些值肯定在某個內存存著,你可以下斷點在GetCursorPos處,在你移動鼠標時會觸發斷點,然后跳出函數,發現下邊有一個GetWindowRect函數,這個函數會傳遞窗口句柄,窗口句柄存儲在一個全局內存中,我們可以得到這個窗口句柄。但是我用了更簡單的方法,既然有窗口,我直接用工具測一下就知道每個格子大小了么。
這樣我們就得到了鼠標坐標轉換格子的公式:
int x=(xPos - 30) / 18; //列
int y=(yPos - 30) / 18; //行
最后,上輔助代碼:
WNDPROC g_oldProc=NULL;
DWORD
dwArrOffset=0xAAA38;
//這是重定向之前全局變量相對于模塊基地址的位置
DWORD64
dwMineAddress=0;
//雷區列數組位置
BYTE
*pMineMap;
//自定義一個數組存儲雷的布局,便于訪問
DWORD
rows=0;
//行
DWORD
cols=0;
//列
LRESULT
CALLBACK WindowProc(
_In_
HWND
hwnd,
_In_
UINT
uMsg,
_In_
WPARAM
wParam,
_In_
LPARAM
lParam
)
{
if
(uMsg==WM_MOUSEMOVE)
{
int
xPos=GET_X_LPARAM(lParam);
int
yPos=GET_Y_LPARAM(lParam);
int
x=(xPos - 30) / 18;
//列
int
y=(yPos - 30) / 18;
//行
int
nWidth=30 + 18 * cols;
int
nHight=30 + 18 * rows;
if
(x>=0&&y>=0&&x<nWidth&&y<nHight)
{
if
(pMineMap[x*rows + y]==(
BYTE
)0x01)
{
SetWindowText(hwnd, L
"!!!有雷!!!"
);
}
else
{
SetWindowText(hwnd, L
"掃雷"
);
}
}
else
{
SetWindowText(hwnd, L
"掃雷"
);
}
}
//F12鍵一鍵掃雷
if
(wParam==VK_F12)
{
int
i=0;
do
{
for
(
int
j=0; j < rows; j++)
{
if
(pMineMap[i*rows + j] !=(
BYTE
)0x01)
{
int
x=(i * 18) + 30 + 9;
//定位到格子中心
int
y=(j * 18) + 30 + 9;
LPARAM
point=MAKELPARAM(x, y);
//發送鼠標點擊消息
PostMessage(hwnd, WM_LBUTTONDOWN, NULL, point);
PostMessage(hwnd, WM_LBUTTONUP, NULL, point);
}
}
i++;
}
while
(i!=cols);
}
return
CallWindowProc(g_oldProc, hwnd, uMsg, wParam, lParam);
}
// CMineSweeperWaiguaApp 初始化
BOOL
CMineSweeperWaiguaApp::InitInstance()
{
CWinApp::InitInstance();
OutputDebugString(L
"已加載!\n"
);
//獲得雷區數組地址
HMODULE
hModule=GetModuleHandle(NULL);
// 雷區列數組獲得公式 [[[[hModule+0xAAA38]+0x18]+0x58]+0x10]
DWORD64
v1=*(
DWORD64
*)((
DWORD64
)hModule + dwArrOffset);
//v2+0x8處存儲了雷總數(DWORD),v2+0x0C處存儲了行總數(DWORD),v2+0x10處存儲了列總數(DWORD)
DWORD64
v2=*(
DWORD64
*)(v1 + 0x18);
//申請一個存儲雷區的數組空間
rows=*(
DWORD
*)(v2+0x0C);
//行
cols=*(
DWORD
*)(v2+0x10);
//列
pMineMap=(
BYTE
*)VirtualAlloc(NULL,rows*cols, MEM_COMMIT, PAGE_READWRITE);
if
(pMineMap==NULL)
{
OutputDebugString(L
"申請內存失敗!\n"
);
}
OutputDebugString(L
"申請內存成功!\n"
);
//v1處存儲了列總數(DWORD)
v1=*(
DWORD64
*)(v2 + 0x58);
dwMineAddress=*(
DWORD64
*)(v1 + 0x10);
//存儲雷區列數組地址,有多少列就有多少數組
//數組地址首位顯示的是行數
for
(
int
i=0;i<*((
DWORD
*)(v2+0x10));i++)
{
//v1處前4字節(DWORD)儲了行總數 ,后邊兩個內容沒搞懂(10 10)
v1=*(
DWORD64
*)(dwMineAddress + i * 8);
BYTE
*v5=(
BYTE
*)(*(
DWORD64
*)(v1 + 0x10));
for
(
int
j=0; j < *((
DWORD
*)(v2 + 0x0C)); j++)
{
//將雷區狀態賦值到數組中去
pMineMap[i*rows +j]=*v5;
//第i列第j行
v5++;
}
}
OutputDebugString(L
"雷區賦值成功!\n"
);
HWND
hWnd=FindWindow(NULL, L
"掃雷"
);
if
(hWnd==NULL)
{
OutputDebugString(L
"未找到目標窗口!\n"
);
return
FALSE;
}
//更改指定窗口的屬性
//返回值是之前的窗口函數
g_oldProc=(WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (
LONG_PTR
)WindowProc);
return
TRUE;
}
到此,我們大功告成,需要注意的是每次要點擊一下一個格子再注入動態庫。不過,我的F12一鍵掃雷并沒成功有人知道是怎么回事么?希望不吝賜教幫我解決一下,嘻嘻~~
作者:ixiaohuo ,原文發表于看雪論壇 —【軟件逆向】版塊
責編:王冬奇
中關村在線消息:在全新的Windows 10操作系統中,諸如開始菜單等經典功能部分回歸。而國外有消息透露,非常經典的《紙牌(Solitaire)》游戲也將正式回歸Windows操作系統,讓不少網友驚呼《掃雷(Minesweeper)》還會遠嗎?
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
報道稱,目前《紙牌》游戲的細節仍然稀缺,暫時還沒有任何證實消息的截圖。不過,有使用Windows 8/8.1操作系統的用戶在Windows Store應用程序商店內找到了微軟官方出品的《微軟紙牌游戲合集(MicrosoftSolitaire Collection)》。
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
在APP介紹中,微軟也表示《微軟紙牌游戲合集》始終保持著最受歡迎電腦游戲的桂冠,并且伴隨Windows系統已20年有余。該合集將5種不同的紙牌游戲玩法集合在了起來,用戶還可將紙牌游戲的分數與好友一較高下。
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
現在,這個《微軟紙牌游戲合集》應用程序將重新默認預裝到Windows 10上,并融入了新Windows更清晰的現代用戶界面設計風格,還支持通過觸屏操作玩游戲。
《紙牌》游戲將正式回歸Windows 10(圖片來自neowin)
在Windows 10 build 10056截圖中也可以看到,《微軟紙牌游戲合集》已經回到Windows上,但名稱中仍印有“Preview”,還處于早期測試當中。所以,目前商店里的“紙牌”游戲版本可能與最終版本又有所不同。