Thursday 23 December 2010

@hướng dẫn viết auto cho Game

Tổng quát
AutoPlay nhìn chung được chia làm 2 loại:
- Là các macro thay thế thao tác người chơi, phân tích dữ liệu đồ họa
- Thực thi lệnh game bằng cách thay đổi dữ liệu game ( mem cheat, packet editor )
Việc phân loại trên chỉ là tương đối do có thể kết hợp 2 loại trên

Loại Macro:
- Chạy dễ dàng với mọi phiên bản, hơi chậm nhưng ổn định, dễ có sai sót do hình ảnh khuất, không chạy được ở chế độ FULL screen và 3D ( 2 chế độ này không bắt được hình ). Người viết cần kiến thức đồ họa cao

Loại Cheat, hook:
- Chạy riêng từng phiên bản, nhanh nhưng có phần bất ổn, ít khi sai sót khi xử lý, chạy được ở mọi chế độ hình ảnh

Ở đây tôi chỉ nói đến loại 2 do loại này chỉ yêu cầu kiến thức lập trình, không yêu cầu kiến thức đồ họa

Cách Viết AutoPlay loại Hook, mem cheat:
Yêu cầu: - Có các công cụ Game Cheat như Tsearch, MemFinder
- Biết assembler
- Nắm rõ kỹ thuật hook, packet editor

Các thông tin mà AutoPlay có được là do đọc dữ liệu trong bộ nhớ game, địa chỉ có thể là DMA hay Static nhưng đều có thể tìm ra bằng các tool trên.
Tôi chỉ có thể hướng dẫn các bạn sử dụng Mem Cheat Tool, và hiểu rõ về Hook, các bạn phải tự học ASM

Về Game Hook: Thực chất file hook.dll là dùng để thay đổi dữ liệu trong game
các bạn tham khảo về Hook tại:
http://msdn.microsoft.com/library/de...dn_hooks32.asp

Cách thức Hook họat động:
- Khi không có Hook
Windows Message -> Game Windows -> Game Message Patch
- Khi có Hook:
Windows Message -> Game Windows -> Hook.dll -> Game Message Patch
**Lưu ý: Game sẽ thực thi mã trong hook.dll chú không phải AutoPlay thực thi mã trong hook.dll, AutoPlay chỉ gửi các thông điệp đặc biệt để hook.dll biết đó là lệnh của AutoPlay

Hướng dẫn chi tiết:
Bài 1 – Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ
Bài 3 – Điều khiển nhân vật chạy


Ở đây không bàn đến việc dùng kỹ thuật đồ họa và chiếm chuột để điều khiển nhân vật. Các phần mềm hiện nay thường dùng chương trình chính Autoplay thực hiện việc đọc bộ nhớ Game lấy thông tin và dùng cơ chế Hook để ghi thông tin cần thay đổi vào bộ nhớ Game. Một điểm lưu ý là khi Hook sẽ chiếm tài nguyên, nên ta có thể dùng một mẹo nhỏ, sau khi Hook ta Unhook ngay lập tức và dùng LoadLibrary để nạp hook.dll vào bộ nhớ game, thay đổi địa chỉ WndProc để thực hiện nhiều ý đồ khác.
(Các bạn có thể tham khảo bài viêt sau để biết cách dùng Hook và mẹo này:http://www.codeproject.com/threads/winspy.asp). Ngoài ra, để chương trình có thêm nhiều chức năng, có thể ta sẽ phải gọi các hàm trong game_y.exe và khi đó ta phải mở rộng cái Hook.dll để có thể hook + inject code vào Game process (sẽ nói trong bài 3).

Bài 1 - Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Tất cả các nhân vật (người chơi, quái, Npc...) được lưu thông tin trong mảng gồm 256 đối tượng, mỗi đối tượng kích thước 0x7E4C, địa chỉ lưu địa chỉ của mảng trên là 0x00D3A570. Đối tượng 0 để trống, đối tượng 1 là người chơi, còn lại là quái & Npc.
Khi chưa dùng cơ chế inject code nói trên ta có thể đọc bộ nhớ của Game bằng hàm ReadProcessMemory. Dưới đây là minh họa lớp đối tượng Npc và một số biến thành viên cần dùng (phần ... là phần dữ liệu chưa dùng tới, các bạn phải đặt một mảng BYTE với kích thước tương ứng vào đó để các địa chỉ OFFSET của các biến được chính xác)

Code:
#define NPC_BASE_ADD 0x00D3A570
#define NPC_DATA_SIZE 0x00007E4C
#define MAX_NPC 256
#define PLAYER_INDEX 1

class CNpc
{
...
DWORD m_NpcKind; //0x0020 1=mod, 1=player,...
...
DWORD m_Doing; //0x00EC 1=stand, 2=walk, 3=run...
...
int m_CurLife; //0x0B44
int m_CurLifeMax;
int m_CurLifeRep;
int m_CurMana;
int m_CurManaMax;
...
BOOL m_bRideHorse; //0x0D58
char Name[32]; //0x0D5C Ten nhan vat
int m_n***;
...
BOOL m_FightMode; //0x0EAC

};

// Tim cua so va xac dinh Game Process Handle o day
...
CNpc Npc;
LPBYTE lpBaseAdd, lpCurAdd;
//Read start Address of Npc Array
ReadProcessMemory(m_hVLProcess, (LPBYTE)NPC_BASE_ADD, (LPVOID)&lpBaseAdd, 4, NULL);
//Start Address of PLAYER
lpCurAdd = lpBaseAdd + PLAYER_INDEX*NPC_DATA_SIZE;
//Read PLAYER ìnormation
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
...

Khi đọc được các thông số máu và mana rồi ta có thể dùng hàm PostMessage(hWnd, WM_KEYDOWN...) để gửi phím 1, 2, 3. Các bạn thấy ở trên có 1 biến m_FightMode dùng để xác định nhân vật đang ở chế độ đánh (ở bản đồ có quái) hoặc đang ở trong thành (không dùng chiêu được). Căn cứ vào đó ta có thể gửi phím 1, 2, 3 khi cần thiết (m_FightMode=TRUE). Nếu nhân vật về thành rồi khỏi bơm máu nữa (nhiều khi dính độc về cả phút mà cứ đứng trong thành bơm máu thì phí quá).

Linh tinh khác: Game VLTK có định nghĩa một số hàm macro có thể dùng ở dòng gõ CHAT, ví dụ:
“/Switch Horse”, “/Switch Sit”, “/SayPhrase 0”, , “/SayPhrase 1” các bạn thử mà xem, cũng thũ vị lắm. Các bạn mở file UIconfig.ini sẽ thấy nhiều hàm hơn. Dưới đây là đoạn code mà có thể thêm vào chương trình của mình dùng để chat hoặc chạy một hàm macro trong Game. Bạn có thể lập trình lên xuống ngựa hay ngồi xuống, đứng lên, ... mà ko phải dùng WM_KEYDOWN gửi phím tắt V, M (vì ở chế độ phím Mặc định, nhấn V sẽ ko ngồi đâu).

Code:
void PostChatMessage(LPCTSTR szChatMsg)
{
//Set focus to CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
//Clear CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_DOWN, 0x00500001);
while (szChatMsg[0])
{
::PostMessage(m_hVLWin, WM_CHAR, LOBYTE(szChatMsg[0]), 0);
szChatMsg++;
}
//Set focus to Game Window
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
}

Đến đây các bạn có thể lập được một chương trình tự buff mana, ngồi rao bán hàng rồi đấy. Ở bài 2 tôi sẽ nói về cách xác định tọa độ nhân vật (cũng giống như của các Npc khác) và đồ. Địa chỉ OFFSET tọa độ của nhân vật & Npc không phải là 0x10F8 như các bạn tưởng đâu, đó là địa chỉ đích Npc sẽ chạy tới, sẽ đánh tới... nếu nhân vật ngồi xuống, giá trị này sẽ bằng 0.

Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ

Hix, được các bác admin quan tâm, em lại viết tiếp.
Bây giờ đến phần toạ độ của nhân vật. Bổ sung thêm các biến thành viên vào lớp CNpc, chúng ta có các thông tin về toạ độ và thông tin đối tượng cần đánh/ theo sau/ đồ cần nhặt:

Code:
class CNpc
{
public:
...
int m_NextAdd; //0x000C – xac dinh Npc ton tai hay ko
...
int m_ActiveSkill; //0x0114 – ky nang dang su dung
...
int m_MapX, m_MapY, m_MapZ; //0x0D0C – toa do Cell
int m_OffX, m_OffY; //0x0D18 – toa do Npc trong Cell (*1024)
...
int m_nPeopleIdx; //0x0DE0 Npc can tan cong, hoac theo sau
...
int m_nObjectIdx; //0x0DEC Obj can nhat
...
WORD m_RegX; //0x0EC4 – toa do Region
WORD m_RegY;
public:
void GetMapPos(int *nX, int *nY);
};
Toàn bàn đồ được chia thành nhiều vùng, có kích thước 512x1024, mỗi vùng lại chia thành nhiều cell, có kích thước là 32x32. Hai biến WORD RegX, RegY lưu giá trị vùng (Region), các biến MapX, MapY lưu giá trị của cell; OffX,OffY là toạ độ Npc trong ô đó (giá trị = 1024/pixel).

Toạ độ của nhân vật được tính như sau:

Code:
#define REGION_WIDTH 512
#define REGION_HEIGHT 1024
#define CELL_WIDTH 32
#define CELL_HEIGHT 32

void CNpc::GetMapPos(int *nX, int *nY)
{
*nX = m_RegX*REGION_WIDTH + m_MapX*CELL_WIDTH + (m_OffX>>10);
*nY = m_RegY*REGION_HEIGHT + m_MapY*CELL_HEIGHT + (m_OffY>>10);
}
Hàm sẽ trả về nX, nY giá trị toạ độ tuyệt đối của nhân vật/ Npc trên bản đồ, tính theo pixel (không phải toạ độ vẫn thường nói trong GAME đâu). Còn toạ độ trong Game (thể hiện trên bản đồ nhỏ) sẽ được tính bằng:

Code:
nOrgX = nX>>8; // dich phai 8 bit, chia cho 256
nOrgY = nY>>9; // dich phai 9 bit, chia cho 512
Đến đây các bạn có thể xác định được toạ độ của nhân vật cũng như của Npc bất kỳ trong mảng 256 Npc rồi, và do đó sẽ xác định được khoảng các giữa chúng. Vấn đề là khi quái bị chết, không phải tất cả dữ liệu trong đối tượng bị xoá, trừ khi có quái hoặc player khác được nạp vào vùng nhớ đó. Tôi có thấy 1 địa chỉ ở OFFSET 0x000C, tạm đặt tên biến là m_NextAdd vì thấy nó lưu các địa chỉ quanh quanh trong mảng 256 Npc, khi quái chết, hoặc ra khỏi vùng nhớ (đi ra xa), giá trị m_NextAdd được đặt về 0.

Vậy thuật toán đơn giản để tìm quái để tấn công sẽ như sau:

Code:
CNpc Player; Npc;
int nX, nY;
int nMinIndex, nMinDistance=2000;
//Read PLAYER information
lpCurAdd = lpBaseAdd + NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Player, sizeof(CNpc), NULL);
for (int i=2; i< MAX_NPC; i++)
{
//Read NPC information
lpCurAdd = lpBaseAdd + i*NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
if (!Npc.m_NextAdd) continue; // Npc does not exist
if (Npc.m_CurLife<=0 || Npc.m_NpcKind != 0) continue;
//Xac dinh toa do quai
Npc.GetMapPos(int &nX, &nY);
//Tinh khoang cach den nhan vat
int nDistance = ...
//Kiem tra xem khoang cach la nho nhat de tan cong truoc
if (nDistance<=nMinDistance)
{
nMinDistance = nDistance;
nMinIndex = i;
}
}
//Player.m_nPeopleIdx = nMinIndex; // Tan cong quai o gan nhat
SendMessage(WM_HOOK_WRITE,...); // Ghi gia tri vao bo nho Game thong qua Hook
Vòng for trên tôi viết đơn giản hoá để dễ hiểu, các bạn có thể viết thêm các chức năng kiểm tra lag (sau khi đánh một lúc mà quái ko mất máu thì đổi con khác). Có 1 lưu ý nhỏ ở đây là khi máu quái = 0 nó vẫn sống, < 0 nó mới chết nên trong việc kiểm tra đánh quái, các bạn đánh đến khi máu < 0 thì mới search tiếp con khác. Đó là việc ở trước vòng for này, còn trong vòng for ở trên vì sao tôi vẫn kiểm tra Npc.m_CurLife<=0 thì bỏ qua Npc, các bạn có thể tự tìm hiểu nhé.

Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ (tiếp theo)


Khi các bạn đã có thể lấy toạ độ quái thì nên chọn thuật toán đánh quái ở gần trước. Theo tôi mình ko nên chọn phương pháp đánh quái ít máu trước vì sẽ bị KS hết điểm kn và nhân vật phải chạy tới chạy lui tít xa, không tối ưu.

Tiếp theo ta sẽ nghiên cứu mảng lưu thông tin đồ rớt ra. Mảng này gồm 256 đối tượng, nằm bắt đầu ở địa chỉ tĩnh 0x00DC2750, kích thước mỗi đối tượng là 0x03C4. Ta chỉ cần lấy phần dữ liệu ở đầu đối tượng và định nghĩa thành lớp CObj của chúng ta như dưới đây.

Code:
class CObj //dung nham lan voi CObject cua VC++ day nhe
{
public:
int m_nID; // -1 la do ko ton tai, > 0 la ton tai
int m_nData1; // Unused data
int m_ObjKind; // 3=item, 4=money,..10=Property Box
int m_Index;
WORD m_RegX, m_RegY; // Toa do Region
int m_nData2[2]; // Unused data
int m_MapX, m_MapY; // 0x001C, toa do Cell
int m_OffX, m_OffY; // 0x0024, toa do Off trong Cell
int m_nData3[13]; // Unused data
char m_szName[32]; // 0x0060
int m_nData4; // Unused data
int m_nColor; // Mau cua do (0=trang, 3=xanh, ?=vang...)
int m_nData5[3]; // Unused data
int m_nMoneyNum; // Luong tien
public:
void GetMapPos(int *nX, int *nY);
};
Chặc, đến đây các bạn lại thấy quá dễ để tính tọa độ của đồ rồi đúng ko, cũng theo cách tính như ở CNpc thôi. Nhặt đồ thì dễ hơn đánh quái nhiều, tìm cái đồ nào ở gần thì nhặt. Khoảng cách có thể nhặt đô trong Game đặt là 64, nhưng nếu cần các bạn cứ tìm trong khoảng 200-300 gì đó, nhân vật chỉ cần nhích đi một chút là nhặt được đồ. Các bạn còn nhớ cái biến thành viên m_nObjectIdx trong lớp CNpc ko? Nó sẽ xác định địa chỉ cần gán cho đối tượng người chơi để nhặt đồ (cũng theo vị trí của đồ trong mảng 256 đối tượng kia)

Bổ sung: Lần đầu tiên khi nhân vật bắt đầu đánh, các bạn phải gán chiêu thức vào m_ActiveSkill ở OFFSET 0x0114 nhé (tham khảo thêm bài viết của TinhLaGi). Ngoài ra ở phần trên tôi có bỏ sót một biến thành viên tại địa chỉ Offset 0x0C58, đó là m_nSkillRadius - bán kính hiệu quả của chiêu thức. Trước khi gán lệnh đánh, bạn gán giá trị này một số nhỏ, ví dụ 75 chẳng hạn là bán kính quyền cơ bản, nhân vật sẽ tiếp sát rồi mới đánh quái. Nếu ko để chế độ đánh tiếp cận, sau khi đánh 1 chiêu có bán kính hiệu quả = 0 (ví dụ NLTD), các bạn lại phải gán lại số này đủ lớn, nếu ko nhân vật sẽ chạy lại sát quái mà vẫn ko với tay đánh được. Có thể gán = 75 hoặc thậm chí = 5000, game sẽ tự reset lại.

Thêm nữa, phần đọc tọa độ của nhân vật, Npc & Obj có một số bạn dùng địa chỉ offset 0x0D20, 0x1088.. gì đó. Thực ra các địa chỉ này chỉ là kết quả của các tính toán hoặc gán tham số tọa độ cho các thao tác của nhân vật nên đôi khi mình đọc ra sẽ thu được một kết quả không chính xác.

Bài viết tiếp theo tôi sẽ đề cập đến việc điều khiển nhân vật chạy đến một tọa độ cho trước.

Bài 3 – Điều khiển nhân vật chạy


Nào hôm nay chúng ta sẽ nghiên cứu đến cái việc điều khiển quái chạy. Dưới đây là các biến thành viên bổ sung cho lớp CNpc:
Code:
class CNpc
{
public:

...
int CmdKind; //0x1088, 2=walk, 3=run
int Param_X;
int Param_Y;
};
Để cho nhân vật chạy hoặc đi bộ đến 1 điểm nào đó, các bạn gán các giá trị thích hợp vào các địa chỉ trên, bao gồm loại lệnh (chạy, đi bộ) và tọa độ X, Y.

SendMessage(WM_HOOK_WRITE, ...3);
SendMessage(WM_HOOK_WRITE, ...nX);
SendMessage(WM_HOOK_WRITE, ...nY);

Tuy nhiên, công việc ko chỉ dừng lại ở đó, nếu ko nhân vật sẽ tự giật lại vị trí cũ. Bước tiếp theo là chúng ta phải gửi một gói tin đến server, báo rằng nhân vật di chuyển đến tọa độ mới. Như các bạn biết, đôi khi mạng lag, ta điều khiển nhân vật chạy được một đoạn nó lại giật lại chỗ cũ là vì gói tin báo đã ko gửi đi được.

Để làm được việc đó ta sẽ viết code gọi hàm gửi tin đến server. Sau khi nghiên cứu, tôi đã tìm được địa chỉ các hàm gửi lệnh RUN và hàm gửi lệnh WALK trong game_y.exe. Cũng may là mấy hàm này ko viết trong Class nên việc gọi cũng đỡ cực hơn. Trong chương trình HookDll, các bạn bổ sung các đoạn code sau:

Code:
//Global Functions
void (*SendCmdRunToServer)(int, int);
void (*SendCmdWalkToServer)(int, int);

#define RUN_FUNC_ADD 0x004C41B7 // Send Client DoRun Function
#define WLK_FUNC_ADD 0x004C4204 // Send Client DoWalk Function
Và khởi tạo các địa chỉ hàm SendCmdRunToServer & hàm SendCmdWalkToServer bằng cách chèn đoạn code sau vào vị trí nào thích hợp (nhớ rằng chúng ta phải thực hiện việc này trong HookProc, tức là đang ở trong Process của Game. Việc gán trực tiếp bằng toán tử gán ( = ) sẽ ko thực hiện được vì VC++ sẽ báo lỗi nên ta dùng đoạn mã ASM.

Code:
__asm
{
mov SendCmdRunToServer, RUN_FUNC_ADD;
mov SendCmdWalkToServer, WLK_FUNC_ADD;
}
Đến đây chúng ta đã hoàn tất 90% công việc rồi, quay trở lại phần code của chương trình chính, sau khi ra đã gán các giá trị CmdKind, Param_X, Param_Y ta phải gửi một thông điệp WM_HOOK_WRITE nữa để báo HookProc chạy hàm SendCmdRunToServer hoặc SendCmdWalkToServer.

Đoạn mã đó như sau:
Code:
//SendMessage(m_hVLWindow, WM_HOOK_WRITE, 2, 0); // Send Walk Cmd
SendMessage(m_hVLWindow, WM_HOOK_WRITE, 3, 0); // Send Run Cmd
Còn ở HookProc, các bạn cải tiến một chút như dưới đây thì ta sẽ có được đoạn gửi lệnh RUN, WALK lên Server.

Code:
#define pCW ((CWPSTRUCT*)lParam)
LRESULT HookProc (int nCode, WPARAM wParam, LPARAM lParam)
{
int nAdd = pCW->wParam;
int nVal = pCW->lParam;
int *pX, *pY;
switch (nAdd)
{
case 2: // DO WALK
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdWalkToServer(*pX, *pY);
break;
case 3: // DO RUN
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdRunToServer(*pX, *pY);
break;
default: // ASSIGN OTHER VALUES
pX = (int*)nAdd;
*pX = nVal;
}
}
Kết thúc loạt bài về viết chương trình AutoPlay. Từ đây các bạn có thể cải tiến chương trình theo ý thích của mình, làm bao nhiêu chức năng tùy ý. Và từ sau bài này các bạn vào trao đổi và có cao kiến hay thắc mắc gì cứ nêu ra chúng ta cùng bàn luận nhé. Các đoạn code trên có nhiều chỗ tôi gõ trực tiếp lên đây (ko phải copy từ chương trình ra vì chỉ cần lấy phần cơ bản cho dễ hiểu) nên đôi khi gõ tắt hoặc gõ lỗi tí xíu, các bạn tự sửa lấy nhé.

Nguồn từ Cộng đồng Aptechite Việt Nam

@Hướng dẫn viết hack game - part5

Chap 5 : Cheat mode
hôm nay ko rảnh rảnh nhưng vẫn ngồi viết tiếp phần mở đầu cho chap 5
I. Fix lại lỗi bị dis khi chơi :
khi chúng ta chơi pikachu, chúng ta thấy game bị dis mỗi khi bài hát hết, bực mình thật đó,
ko biết làm sao cả, tôi lại phân tích thế này
sau khi unpack và dùng reshack tôi phát hiện ra trong source dùng midi làm nhạc nền cho app, có 3 bài nhạc nền
vậy là đầu tiên chúng chơi 1 bài, sau đó đổi bài chơi bài tiếp
vậy là lỗi khi đổi bài,

xem tiếp source decompile 1 lúc, chưa thấm gì, dùng vbdecompiler 7.9 thì thấy nó decompiler ra cả source vb chứ ko phải là asm nữa,
tôi thì chưa từng học vb bao giờ cả
nhưng xem source vb này, sơ sơ tôi đánh giá được như này nè
ở trong vb5.0 nó có thể play midi mà ko cần đến việc quan tâm tạo ra 1 player như trong C++ mà chỉ việc bắt play music còn lại thì msvbvm quyết định,
sound play được nhúng vào từ wmmlib chỉ là để play các .wav thôi chứ lại ko play midi......
tớ thấy là thế, các vb coder sai ở đâu thì giúp tớ chỉ bảo tớ nhé. cám ơn các bạn

tớ bắt đầu tìm thêm code thấy là nó như này, nó dùng 1 sự kiện timer của form để bắt đầu đếm xem đã play được đến đâu rồi, nếu mà hết bài thì nó thực hiện đoạn mã đổi bài
vụ này được nằm hết trong sự kiện timer của main form
mở lại VB Decompiler Pro ta thấy được dòng như sau

PHP Code:
Private Sub Timer_PlayMidi_Timer() 4B30D0
rất gợi mở phải ko nào

Load pikachu với olldbg
chuột phải vào dòng go to -> Expression
nhập địa chỉ 4B30D0 để jump đển hàm đó
chà, hàm này dài quá, loằng ngoằng nữa, tôi cũng hơi gà, ko biết đoạn lệnh nào là chuyển đổi bài hát cả,
thôi mạo muội đổi đổi luôn cái hàm này thành hàm ko có gì
chỉ vào dòng có địa chỉ 4B30D0, ấn space 1 cái, đổi mã của hàm này thành
PHP Code:
RET
(lúc ấn space xong, nó hiện ra 1 cái ô input nhỏ, cứ thể gõ RET vào rồi enter phát là được)
tức là câu lệnh return trong C ấy mà, có mã là C3
rồi ấn vào nút play ở trên menu (hoặc F9) để chạy thử cái, thấy ngon lành cành đào, đợi tiếp xem hết bài nhạc có dis ko, ko thấy dis, hì hì
bây giờ ngon rồi,

==>> ta sẽ xóa bỏ tất cả các dòng còn còn lại trong hàm đó bằng các : bôi đen toàn bộ dòng code của hàm này, chuột phải
vào edit binary
chỉ vào ô hex ở trong cửa sổ edit, ấn ctrl + a
rồi chuột phải tiếp vào set zero để toàn bộ vùng này có giá trị là 0 hết (0 là ko có gì )
==> ngon rồi, ta đã loại bỏ đi cái hàm thừa thãi ko cần thiết

làm thế nào nhỉ để lấy được cái chương trình đã debug này?
chuột phải, vào dòng dump debugged process rồi ấn dump là được
hi hi
tôi thấy lạ là tôi dùng windows7 dump thì được file chạy luôn
ở trong xp dump thì phải dùng ImportREC để rebuild lại mới chạy được


bây giờ được rồi, chạy hoài ko dis

nhưng mà thế này rõ ràng là thừa 2 file midi trong resource rồi
lại dùng reshack để del 2 bài midi thứ 2 và thứ 3 đi
thế là dung lượng nhỏ đi bao nhiêu


II. Sửa đổi màu nền khi chơi
trong quá trình tìm ra ma trận biểu diễn các con bài trên màn hình nền của form chính tôi phát hiện ra như sau :
new game ở pikachu rồi , bật artmoney lên, tìm chế độ unknown value, interger 1 byte (để dò từng byte 1) trong khoảng dữ liệu tôi tính (lần trước thấy cả 3 biến cần thiết ở cái vùng đó, nên rõ ràng tôi vẫn khoanh vùng đó đề tìm kiếm rồi )
lose game ở pikachu, new game mới rồi tìm lại các giá trị was changed
lại lose game ở pikachu, new game mới rồi tìm lại các giá trị was changed
lại lose game ở pikachu, new game mới rồi tìm lại các giá trị was changed
.....
ở lần thứ 3 tôi thấy cái ô này
PHP Code:
004B6070
có giá trị thay đổi theo mỗi lượt chơi,
thử thay đổi 1 phát
thấy màu nền thay đổi luôn
nghịch thêm tí thấy :
ô này là ô màu nền, có giá trị từ 0 đến 5
tương ứng với 6 màu khác nhau
==> code hack, tương tự như các code đã share ở trên thôi, chưa có gì đặc biệt cả

III. Ma trận các con bài
(đi ngủ thôi, mai viết tiếp)

Nguồn : http://forums.congdongcviet.com/showthread.php?t=35324 http://forums.congdongcviet.com/showthread.php?t=35324

@Hướng dẫn viết hack game - part4

Chap 4 : infinite time
Đây là chap tương đổi khó, để infinite time chúng ta phải đảm bảo được 2 điều
+ Tìm ra điạ chỉ của biến time
+ Tư tưởng đóng băng nó


I. Tìm ra địa chỉ của biến time :
Cách 1 : Tôi dò dưới chế độ Unknown value :
đầu tiên


Bước 2
Nghỉ tầm 1s để thanh time bar tụt đi 1 tí rồi tôi làm như này

sau đó cứ lặp lại bước 2 cho đến khi tìm thấy thì thôi
(Tốn khá nhiều time để bạn tìm thấy nó đấy)



Cách 2
: Cách 1 khá công phu và loằng ngoằng, nên theo kinh nghiệm, tôi ko làm như vậy, vào artmoney

xem vùng nhớ ở gần cái biến điểm tôi thấy có 1 vùng cứ nhẩy giá trị liên tằng tằng theo thời gian, tôi đoán đó là biến time

vô artmoney thử đóng băng cái giá trị đó tôi thấy thanh time bar nhúc nhíc quanh 1 điểm chứ ko tụt đi, vậy chính là nó
là 1 số thực 4byte (float) có địa chỉ là 004B6084 (Vậy là nó đứng ngay cạnh, đằng trước biến điểm)


II. Code đóng băng nó

thật ra cái biến time này nó là 1 biến float nhưng khi memory map tớ còn phát hiện ra như sau :
giả sử 4 byte của biến float đó là abcd
bỏ qua 2 byte a,b quan tâm đến c d tớ thấy
+ c chạy từ 0 đến 1 giá trị max rồi lại trở về 0 chạy tiếp, theo nhịp của trò chơi,
+ d càng nhỏ thì c chạy càng nhanh, khi d bằng 69 thì bắt đầu chạy từ từ, khi d cao đến gần 80 thì c chạy chậm hẳn
+ d=0 thì c chạy cực nhanh, cỡ mini dây, vụt 1 phát, d thành 69 luôn và c bắt đầu chạy từ từ với tộc dộ chậm dần
và vì vậy :
+ nếu set d thành số nhỏ hơn 69 thì ngay lập tức trở về bình thường
+ nếu set d thành -1 thì có nghĩa d chính là 255 (vì nó là 8bit mà) sẽ overflow ngay
+ nếu set cả biến float 4byte này thành 1 số âm thì theo cách tớ vừa mô tả ở trên d sẽ rất nhanh chóng phục hồi lại thành 69 và ko có tác dụng
+ nếu set cả biến float 4byte này thành 1 số lớn hơn mức cực đại thì sẽ kết thúc trận đấu luôn

==> thay đổi biến time này ko có hiệu quả
nhưng suy ra 1 cách đó là ta làm đóng băng giống như các cheat engine đóng băng : cứ 100ms thì lại set d=0 một lần, như thế thanh time bar vẫn max, time vẫn vô cùng




Tôi mất khá nhiều time để nghĩ và tìm ra cách đóng băng nó hay hơn
+ đầu tiên, đơn giản nhất, tôi nghĩ ra là sét giá trị cho nó 1 cách liên tục,
demo
PHP Code:
while (..)
{
set max time()
sleep(100)
}
nhưng như thế ko hay cho lắm, vậy cái hack tool và tôi viết ra lúc nào cũng phải chạy thì mới đóng băng được time, ko hay

+ tiếp theo : trong cái code decompile ra được, tôi thấy có 1 hàm sử lí TIMER, vậy tôi định kill timer của process này, nhưng còn loằng ngoằng hơn ..... và tôi chưa tìm được định danh của cái bộ timer này nên ..... lực bất tòng tâm


+ Cuối cùng, bực mình, sáng suốt tôi quyết định tìm ra nguyên nhân tay đổi nó.
ví dụ trong process có 1 đoạn là time=10+time/60 (ví dụ thế) .
Tôi quyết định dùng các kinh nghiệm đã có để tìm ra đoạn mã gán giá trị cho biến time này, rồi tôi thay đổi luôn đoạn mã này đi, thế là ok ? Đúng ko bạn

Bắt đầu nghĩ tôi thấy, time là 1 biến thực 4 byte, vậy với asm32 - float processing thì đoạn mã tôi cần tìm sẽ có dạng như sau

PHP Code:
fld real4 ptr [004B6084]
.....................
fstp real4 ptr [004B6084]
fld viết tắt của float load
fstp viết tắt của float store point
tạm dịch sang ngôn ngữ nói nó là
PHP Code:
fload time
.... xử lí linh tinh....
fsave time
Đến đây tôi có 2 cách :
Cách 1 đơn giản nhất mang tính tình huống, tôi mở VB Decompiler Pro với quyền admin, rồi decompile upikachu (xem chap1) , sau đó vào Filevào Save all in one module file Được 1 file .bas . Mở file này bằng notepad rồi ấn Ctrl + F tìm kiếm chuỗi 004B6084, tìm đúng chỗ nào sử dụng toán tử fld với tham số là 004B6084 ta được
PHP Code:
004B02C4: fld real4 ptr [004B6084h] ;
004B02CA: fadd real8 ptr var_80
004B02CD
: fild dword ptr var_14
004B02D0
: mov var_14, ebx
004B02D3
: fstp real8 ptr var_88
004B02D9
: fsub real8 ptr var_88
004B02DF
: fstp real4 ptr [004B6084h] ;
Cách 2 phức tạp hơn 1 tí : load Upokemon.exe bằng Ollydbg (Download tại đây) . Chú ý load file mà ta đã unpack rồi, sẽ dễ dàng hơn trong việc tìm kiếm
-> vào Ollybdb vào View vào Memory
-> Đúp chuột vào section .text
-> ra 1 cửa sổ dump, ta chuột phải, vào copy, vào select all
-> chuột phải lần nữa vào copy vào to file
-> được 1 file txt
-> tìm kiếm trên file này
chú ý : theo luật little-endian by order 004B6084 thì trong máy nó được tổ chức là 84 60 4B 00, nên chuỗi tìm kiếm của ta trong cách này phaỉ là 84604B00
tìm và thu được kết quả như sau
PHP Code:
004B02C4 D905 84604B00 FLD DWORD PTR [>
004B02CA DC45 80 FADD QWORD PTR >
004B02CD DB45 EC FILD DWORD PTR >
004B02D0 895D EC MOV [EBP-14],EB>
004B02D3 DD9D 78FFFFFF FSTP QWORD PTR >
004B02D9 DCA5 78FFFFFF FSUB QWORD PTR >
004B02DF D91D 84604B00 FSTP DWORD PTR >

III. Xử lý kết quả thu được như thế nào
Ta đã có đoạn mã và địa chỉ của đoạn mã này trong bộ nhớ rồi, ta phải làm thế nào bây giờ ?
Tôi nghĩ ra 1 cách , đó là tôi lái cái fld ra 1 chỗ khác, tôi cho nó fload sang ô điểm, hờ hờ hờ (như thế câu lệnh này sẽ trở thành)
PHP Code:
fload điểm
.... xử lí linh tinh....
fsave time
như thế này rõ ràng là chả có biến nào bị thay đổi cả rồi, hờ hờ hờ hờ (vì thằng điểm được load nhưng ko được save, thằng time được save nhưng bên trên đó lại chưa load, đâm ra chả có gì thay đổi cả)
ở đây , tại ô nhớ 004B02C4 câu lệnh fld real4 ptr [004B6084h] ; dài 6 byte 2 byte đầu chính là mã của toán tử fld(0x5D9) (Tôi biết là 6byte vì câu lệnh tiếp theo có địa chỉ 004B02CA), 4byte sau chính là tham số của toán tử này 004B6084h.


==> chung quy lại tôi sẽ viết 1 chương trình đổi giá trị tại địa chỉ 004B02C6 từ giá trị ban đầu là 004B6084 thành 004B6088

Code
PHP Code:
#include


void SetInfiniteTime()
{
HWND hwnd =FindWindow(L"ThunderRT6FormDC", L"³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,L"Bạn phải bật pikachu lên chơi trước đã",L"Thông báo",MB_OK);
}
else
{
DWORD pid;
int temp=0x4B6088;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B02C6;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,L"Đã set thành công",L"Thông báo",MB_OK);
}
}

void UnSetInfiniteTime()
{
HWND hwnd =FindWindow(L"ThunderRT6FormDC", L"³s³s¬Ý2");
if (
hwnd)
{
DWORD pid;
int temp=0x4B6084;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B02C6;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,L"Đã unset thành công",L"Thông báo",MB_OK);
}
}

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd)
{
SetInfiniteTime();
return
EXIT_SUCCESS;
}
Code cho Dev-C
PHP Code:
#include


void SetInfiniteTime()
{
HWND hwnd =FindWindow("ThunderRT6FormDC", "³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,"Ban phai bat pikachu lon choi truoc do","Thung boo",MB_OK);
}
else
{
DWORD pid;
int temp=0x4B6088;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B02C6;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,"Do set thanh cong","Thong bao",MB_OK);
}
}

void UnSetInfiniteTime()
{
HWND hwnd =FindWindow("ThunderRT6FormDC", "³s³s¬Ý2");
if (
hwnd)
{
DWORD pid;
int temp=0x4B6084;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B02C6;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,"Do unset thanh cong","Thong bao",MB_OK);
}
}

int main()
{
SetInfiniteTime();
return
1;
}
Kết quả thành công hơn cả mong đợi, sau khi set 1 lần (Set lúc nào cũng được, kể cả chưa vào game)thanh time bar sẽ đứng im luôn, ko dịch chuyển nữa, luôn ở trạng thái max, cho dù new game nhiều lần vẫn ko ảnh hưởng.

Kết luận : Với tất cả 4 chap trên, bạn hoàn toàn có thể viết 1 cái hack tool cho pikachu rồi đấy, nếu ko có kinh nghiệm tạo các ứng dụng trên windows, các bạn hoàn toàn có thể dùng DEV-C để viết lên 1 chương trình có menu như bình thường, nhập 1 để add point, 2 để hack mạng .......... Chúc các bạn thành công và vui vẻ với loạt tut này.

@Hướng dẫn viết hack game - part3

Chap 3 : Vô hạn lượt đổi (unlimited continue)


Cũng như lần trước nhưng có 1 vài thay đổi,
đó là tôi biết địa chỉ của biến điểm là 004B6088
vậy tôi sẽ khoanh vùng lại tìm kiếm cho nhanh (theo kinh nghiệm thì 1 biến ở đó thì biến còn lại cũng chỉ ở gần gần đó thôi, ko đi xa đâu) nên tôi tìm như sau (lần này tôi tìm cả integer 4byte nữa) như trong hình sau


Thật tuyệt vời, chỉ tìm 1 lần là ra luôn, nó là 1 số nguyên 4byte và có địa chỉ là 004B60AA
vậy tôi demo luôn, cách thức cũng ko khác lần trước là mấy
PHP Code:
#include

void SetContinueTo999()
{
HWND hwnd =FindWindow(L"ThunderRT6FormDC", L"³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,L"Bạn phải bật pikachu lên chơi trước đã",L"Thông báo",MB_OK);
}
else
{
DWORD pid;
int temp=0;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B60AA;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,L"Đã add thành công",L"Thông báo",MB_OK);
}
}

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd)
{
SetContinueTo999();
return
EXIT_SUCCESS;
}
và code chạy trên DEV-C :
PHP Code:
#include

void SetContinueTo999()
{
HWND hwnd =FindWindow("ThunderRT6FormDC", "³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,"Ban phai bat pikachu len choi truoc da","Thông báo",MB_OK);
}
else
{
DWORD pid;
int temp=0;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid);
int *address=(int*)0x4B60AA;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,"Ðã add thành công","Thông báo",MB_OK);
}
}

int main()
{
SetContinueTo999();
return
1;
}

Nguồn:
http://forums.congdongcviet.com/showthread.php?t=35324

@Hướng dẫn viết hack game - part2

Chap 2: hack điểm

Khi chơi game, ta thấy điểm hiện lên trên màn hình, vậy thì chắc chắn nó sẽ được lưu trữ ở đâu đó trong bộ nhớ và sẽ có địa chỉ VA cụ thể. Dân lập trình chúng ta gọi chúng là biến, và có địa chỉ cụ thể, hj hj

Muốn biết VA là gì xem ở đây :
Portable Executable File Format


Để thay đổi điểm từ phía app của mình, đầu tiên chúng ta phải tìm được địa chỉ VA của biến điểm này đã nhỉ.

Để tìm được địa chỉ của biến này ko quá khó với 1 tool cơ bản như artmoney (Chưa có download ở đây, active code là dot68) :

Bước 1
Đầu tiên bật pikachu lên chơi lấy 20 điểm và bật artmoney lên,
đầu tiên là chọn tiến trình, pikachu ở đây có cái tên là D4S
rồi click vào Search lên 1 hộp thoại


Bước 2
click vào ... để chọn kiểu dữ liệu, mình hack nhiều lần rồi nên biết nó là kiểu float 4byte, nếu chưa hack bao giờ, các bạn có thể để ALL để tìm với mọi loại dữ liệu


Bước 3
chúng ta sẽ thu được 1 loạt địa chỉ đang chứa giá trị 20, bây giờ chúng ta vào trong game để chơi cho điểm trở thành 40 rồi vào artmoney, click vào nút Filter gõ giá trị mới là 40 rồi ok


Bước 4


Vậy là ta đã biết địa chỉ của biến điểm là 004B6088

Vậy ta sẽ làm 1 demo để hack điểm nhé :
Vào Visual Studio (Chưa có thì download tại đây) vào File -> New -> Project -> Win32 Project (đặt tên, đường dẫn) -> Next (chọn empty project) -> finish
chuột phải vào thư mục source code tạo 1 file cpp mới rồi gõ code

Để biết được tên lớp và tên cửa sổ của tiến trình pikachu ta chỉ việc mở spy++ (tool đính kèm khi cài VS rồi làm như hình sau)






demo chức năng đầu tiên như sau :

Demo1 : Set game point to 99999

PHP Code:
#include

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd)
{
HWND hwnd =FindWindow(L"ThunderRT6FormDC", L"³s³s¬Ý2"); // tìm cửa sổ pikachu
if (!hwnd) // Nếu ko tìm thấy
{
MessageBox(HWND_DESKTOP,L"Bạn phải bật pikachu lên chơi trước đã",L"Thông báo",MB_OK); //Hiện thông báo
}
else
{
DWORD pid;
float temp=99998;
GetWindowThreadProcessId(hwnd,&pid); // lấy định danh của process
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE,FALSE,pid); // mở process để láy handle
int *address=(int*)0x4B6088; // địa chỉ của biến điểm
WriteProcessMemory(handle,address,&temp,sizeof(temp),0); // ghi lên vùng nhớ đó
MessageBox(HWND_DESKTOP,L"Đã thiết lập xong",L"Thông báo",MB_OK); // thông báo kết quả
}
return
EXIT_SUCCESS;
}

Demo 2 :
Tăng điểm của người chơi (add 1000 to game point)
PHP Code:
#include

void Add1000ToPoint()
{
HWND hwnd =FindWindow(L"ThunderRT6FormDC", L"³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,L"Bạn phải bật pikachu lên chơi trước đã",L"Thông báo",MB_OK);
}
else
{
DWORD pid;
float temp=0;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ,FALSE,pid);
int *address=(int*)0x4B6088;
ReadProcessMemory(handle,address,&temp,sizeof(temp),0);
temp+=1000.f;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,L"Đã add thành công",L"Thông báo",MB_OK);
}
}


int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd)
{
Add1000ToPoint();
return
EXIT_SUCCESS;
}
Bây giờ bạn bật pikachu lên , vô game chơi rồi chạy thử cái tool bạn vừa làm xong xem thế nào, hj hj hj hj

Đặc biệt


1. Code demo sau chạy ngon trên Dev-C :
PHP Code:
#include

void Add1000ToPoint()
{
HWND hwnd =FindWindow("ThunderRT6FormDC", "³s³s¬Ý2");
if (!
hwnd)
{
MessageBox(HWND_DESKTOP,"Ban phai bat pikachu len choi truoc da","Thong bao",MB_OK);
}
else
{
DWORD pid;
float temp=0;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ,FALSE,pid);
int *address=(int*)0x4B6088;
ReadProcessMemory(handle,address,&temp,sizeof(temp),0);
temp+=1000.f;
WriteProcessMemory(handle,address,&temp,sizeof(temp),0);
MessageBox(HWND_DESKTOP,"Da add thanh cong","Thong bao",MB_OK);
}
}


int main()
{
Add1000ToPoint();
return
1;
}
2. Và giải thích
PHP Code:
HWND hwnd =FindWindow("ThunderRT6FormDC", "³s³s¬Ý2");
Câu lệnh này giúp tìm ra mã số của cửa sổ và lưu vào biến hwnd . Trong windows thì mỗi cửa số có 1 mã số là 1 số nguyên 32 bit,


PHP Code:
if (!hwnd)
{
MessageBox(HWND_DESKTOP,"Ban phai bat pikachu len choi truoc da","Thong bao",MB_OK);
}
nếu không tìm thấy thì hiển thị lên 1 hộp thông báo với nội dung như các bạn nhìn thấy

PHP Code:
else
{
DWORD pid;
float temp=0;
GetWindowThreadProcessId(hwnd,&pid); // lẫy mã của tiến trình đang chạy
HANDLE handle=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ,FALSE,pid); // mở tiến trình đó để đọc ghi dữ liệu, dạng dạng như mở file ấy
int *address=(int*)0x4B6088; // gán cho biến này địa chỉ của biến điểm mà ta đã tìm ra
ReadProcessMemory(handle,address,&temp,sizeof(temp),0); // đọc giá trị của biến điểm lưu vào temp
temp+=1000; // tăng temp lên 1000
WriteProcessMemory(handle,address,&temp,sizeof(temp),0); // ghi lại giá trị mới của temp vào trong ô nhớ của biến điểm
MessageBox(HWND_DESKTOP,"Da add thanh cong","Thong bao",MB_OK); //thông báo đã ghi thành công
}

xong cháp 2, nghỉ ngơi , học bài mai viết tiếp chap 3