起因
俺是个记性不好的人,经常记不住牌,所以很少能赢。于是俺就写了个程序来记录出过的牌。
开始
因为是网游,所以就开始监听网络包。因为不需要改网络包,所以俺就选择了cap_ip。cap_ip是一个通过设置网卡混乱模式来监听网络包的源码,很容易在网上找到。
代码 U_GameCap
U_GameCap 用于游戏数据的网络包的监听和处理。
unit U_GameCap;interface
uses windows, classes, conn, cap_ip;
constmaxRecCount = 25 * 4 * 2; //一局牌的最多出牌信息数typeTGameCap = classprivateFPlayHallPort: integer;Fcap_ip: Tcap_ip;FconnList: TconnList;FMsgHandle: THandle;FMsg: UINT;FDebugPath: string;procedure CreatCap;procedure beginCap;procedure endCap;procedure freeCap;procedure OnCap(ip, proto, sourceIP, destIP, SourcePort, DestPort: string;header: pchar; header_size: integer; data: pchar; data_size: integer);procedure OnCapError(Error: string);procedure OnNewConn(sender: TObject; var conn: Tconn);procedure OnConnBuff(sender: TObject; var Buff: array of Byte; var ReceBuff_Len: integer);protectedprocedure ConnBuff(port: integer; var Buff: array of Byte; var ReceBuff_Len: integer); virtual;procedure postmsg(wParam: WPARAM; lParam: LPARAM);procedure debugfile(data: pchar; data_size: integer);publicUserID: integer;OutCardCounts: array[0..255] of Byte;OutCardCount: integer;GroundCards: array[0..52] of Byte;GroundCardCount: integer;GroundCards2: array[0..52] of Byte;GroundCardCount2: integer;maincard: Integer;chairid: Integer;constructor Create();destructor Destroy; override;procedure addCapPort(ip: string; port: Integer);procedure start(Port: Integer);procedure stop;property PlayHallPort: integer read FPlayHallPort;property MsgHandle: THandle read FMsgHandle write FMsgHandle;property Msg: UINT read FMsg write FMsg;property DebugPath: string read FDebugPath write FDebugPath;end;implementationuses SysUtils;{ TGameCap }procedure TGameCap.addCapPort(ip: string; port: Integer);
beginFconnList.addCapPort(ip, port);
end;procedure TGameCap.beginCap;
beginFconnList.clearCapPort;FconnList.clearconn;FconnList.addCapPort('', FPlayHallPort);Fcap_ip.StartCap;
end;procedure TGameCap.ConnBuff(port: integer; var Buff: array of Byte;var ReceBuff_Len: integer);
beginReceBuff_Len := 0;
end;procedure TGameCap.CreatCap;
beginFcap_ip := Tcap_ip.Create(nil);Fcap_ip.OnCap := OnCap;Fcap_ip.OnError := OnCapError;
end;constructor TGameCap.Create;
beginUserID := -1;maincard := -1;chairid := -1;OutCardCount := 0;GroundCardCount := 0;GroundCardCount2 := 0;FconnList := TconnList.Create;FconnList.OnNewConn := OnNewConn;CreatCap;end;procedure TGameCap.debugfile(data: pchar; data_size: integer);
varfn: string;ms: TMemoryStream;
beginms := TMemoryStream.Create;ms.Write(data[0], data_size);fn := DebugPath + FormatDateTime('HHNNSSzzz', now);while FileExists(fn) dofn := fn + '_';ForceDirectories(ExtractFilePath(fn));ms.SaveToFile(fn);FreeAndNil(ms);
end;destructor TGameCap.Destroy;
beginstop;freeCap;FreeAndNil(FconnList);inherited;
end;procedure TGameCap.endCap;
beginFcap_ip.StopCap;
end;procedure TGameCap.freeCap;
beginendCap;Fcap_ip.OnCap := nil;Fcap_ip.OnError := nil;FreeAndNil(Fcap_ip);
end;procedure TGameCap.OnCap(ip, proto, sourceIP, destIP, SourcePort,DestPort: string; header: pchar; header_size: integer; data: pchar;data_size: integer);
varPort: integer;
beginif data_size <= 0 thenexit;if proto <> 'TCP' thenexit;if SourcePort = '80' thenexit;if ip <> destIP thenexit;Port := StrToIntDef(SourcePort, 0);if Port = 0 thenexit;FconnList.processbuff(sourceIP, Port, data, data_size);
end;procedure TGameCap.OnCapError(Error: string);
begin
//
end;procedure TGameCap.OnConnBuff(sender: TObject; var Buff: array of Byte;var ReceBuff_Len: integer);
varport: integer;
beginport := Tconn(sender).port;ConnBuff(port, Buff, ReceBuff_Len);
end;procedure TGameCap.OnNewConn(sender: TObject; var conn: Tconn);
beginconn.OnConnBuff := OnConnBuff;
end;procedure TGameCap.postmsg(wParam: WPARAM; lParam: LPARAM);
beginPostMessage(FMsgHandle, FMsg, wParam, lParam);
end;procedure TGameCap.start(Port: Integer);
beginFPlayHallPort := Port;beginCap;
end;procedure TGameCap.stop;
beginendCap;
end;end.
数据解析 U_27w
//MainID: 101, AssID: 3为游戏房间数据包。
//变长,包头之后8个字节,前4为KindID,后4为游戏的NameID;
//之后为结构体,
//依次为4字节长度,4字节类型标识,4字节游戏类型,4字节KindID,4字节NameID,4字节RoomID,4字节在线人数,4字节每桌玩家数,4字节房间桌子数,
//4字节房间端口号,
//25字节房间IP地址(c格式字符串),
//31字节房间名称(含中文)。
type
PNetMessageHead = ^NetMessageHead;
NetMessageHead = packed record
uMessageSize: Cardinal; //数据包大小
bMainID: Cardinal; //处理主类型
bAssistantID: Cardinal; //辅助处理类型 ID
bHandleCode: Cardinal; //数据包处理代码
bReserve: Cardinal; //保留字段
end;
PRec = ^TRec;
TRec = packed record
nextPerId: integer; //下家
mainCardCount_0: integer; //主牌数量
mainCardCount_1: integer;
mainCardCount_2: integer;
mainCardCount_3: integer;
mainCardPairCount_0: byte; //主牌数量
mainCardPairCount_1: byte;
mainCardPairCount_2: byte;
mainCardPairCount_3: byte;
CardCount: byte; //牌数
PerId: byte; //出牌人
Cards: array[0..maxCardCount - 1] of byte; //牌
end;
T27w = class(TGameCap)
private
function willprocess(port: integer; pHead: PNetMessageHead): Boolean;
procedure process(port: integer; bMainID: Cardinal; bAssistantID: Cardinal; data: pchar; size: integer);
procedure setUserID(data: pchar; data_size: integer);
procedure setPort(data: pchar; data_size: integer);
procedure addrec(data: pchar; data_size: integer);
procedure setground(data: pchar; data_size: integer);
procedure setmain(data: pchar; data_size: integer);
procedure setBegin(data: pchar; data_size: integer);
procedure setground2(data: pchar; data_size: integer);
procedure setChairid(data: pchar; data_size: integer);
procedure newRound;
protected
procedure ConnBuff(port: integer; var Buff: array of Byte; var ReceBuff_Len: integer); override;
public
RecList: array[0..maxRecCount] of TRec;
RecCount: integer;
function ismaincard(v: Integer): boolean;
end;
unit U_27w;interface
uses Windows, U_GameCap;
constmessage_maxLen = 1024 * 4;maxCardCount = 40; //一轮牌的最多出牌信息数wp_addrec = 1;wp_setground = 2;wp_setmain = 3;wp_setBegin = 4;wp_setground2 = 5;wp_setChairid = 6;
typePNetMessageHead = ^NetMessageHead;NetMessageHead = packed recorduMessageSize: Cardinal; //数据包大小bMainID: Cardinal; //处理主类型bAssistantID: Cardinal; //辅助处理类型 IDbHandleCode: Cardinal; //数据包处理代码bReserve: Cardinal; //保留字段end;PRec = ^TRec;TRec = packed recordnextPerId: integer; //下家mainCardCount_0: integer; //主牌数量mainCardCount_1: integer;mainCardCount_2: integer;mainCardCount_3: integer;mainCardPairCount_0: byte; //主牌数量mainCardPairCount_1: byte;mainCardPairCount_2: byte;mainCardPairCount_3: byte;CardCount: byte; //牌数PerId: byte; //出牌人Cards: array[0..maxCardCount - 1] of byte; //牌end;T27w = class(TGameCap)privatefunction willprocess(port: integer; pHead: PNetMessageHead): Boolean;procedure process(port: integer; bMainID: Cardinal; bAssistantID: Cardinal; data: pchar; size: integer);procedure setUserID(data: pchar; data_size: integer);procedure setPort(data: pchar; data_size: integer);procedure addrec(data: pchar; data_size: integer);procedure setground(data: pchar; data_size: integer);procedure setmain(data: pchar; data_size: integer);procedure setBegin(data: pchar; data_size: integer);procedure setground2(data: pchar; data_size: integer);procedure setChairid(data: pchar; data_size: integer);procedure newRound;protectedprocedure ConnBuff(port: integer; var Buff: array of Byte; var ReceBuff_Len: integer); override;publicRecList: array[0..maxRecCount] of TRec;RecCount: integer;function ismaincard(v: Integer): boolean;end;
implementation
{ T27w }
uses math, SysUtils;procedure T27w.addrec(data: pchar; data_size: integer);
varsize: integer;i: Byte;nowPRec: PRec;OldPRec: PRec;
beginsize := sizeof(TRec) - maxCardCount + PRec(data)^.CardCount;if size > data_size thenbegin//包不对;exit;end;i := PRec(data)^.PerId;if i > 3 thenbegin//包不对;exit;end;nowPRec := PRec(data);if RecCount > 0 thenbegin //校验重复牌for i := RecCount - 1 downto max(0, RecCount - 4) dobeginOldPRec := @RecList[i];if (OldPRec^.nextPerId < 0) or (OldPRec^.nextPerId > 3) then //轮 判断beginBreak;end;if (nowPRec^.nextPerId = OldPRec^.nextPerId) and (nowPRec^.PerId = OldPRec^.PerId) thenexit;end;end;Move(PRec(data)^, RecList[RecCount], size);inc(RecCount);nowPRec := @RecList[RecCount - 1];for i := 0 to nowPRec^.CardCount - 1 doinc(OutCardCounts[nowPRec^.Cards[i]]);inc(OutCardCount, nowPRec^.CardCount);postmsg(wp_addrec, RecCount - 1);
end;procedure T27w.ConnBuff(port: integer; var Buff: array of Byte;var ReceBuff_Len: integer);
varheadsize: integer;data: pchar;data_size: integer;size: Integer;
beginheadsize := sizeof(NetMessageHead);data_size := ReceBuff_Len;data := @Buff[0];while (data_size > headsize) dobeginsize := PNetMessageHead(data)^.uMessageSize;if size <= headsize thenbegininc(data, 1);dec(data_size, 1);Continue; // size 比 包头 的 size 还小 错包end;if size > message_maxLen thenbegininc(data, 1);dec(data_size, 1);Continue; // size 太大 错包end;with PNetMessageHead(data)^ doif (bMainID = 0) or (bAssistantID = 0) or (bReserve <> 0) thenbegininc(data, 1);dec(data_size, 1);Continue; // 错包end;//以上 是为了 判断 出第一个包//因为 有可能 是程序 可能 后开 接收到了 某个包的 后半端//所以 要找出 第一个 uMessageSize 的位置if size > data_size thenBreak; // 没收全if willprocess(port, PNetMessageHead(data)) thenbeginwith PNetMessageHead(data)^ doprocess(port, bMainID, bAssistantID, @(data[headsize]), size - headsize);inc(data, size);dec(data_size, size);Continue;end;inc(data, size); // inc(data, 1);dec(data_size, size); // inc(data, 1);end;if data_size <= 0 thenbeginReceBuff_Len := 0;endelsebeginif ReceBuff_Len <> data_size thenbeginMove(Buff[ReceBuff_Len - data_size], Buff[0], data_size);ReceBuff_Len := data_size;end;end;
end;function T27w.ismaincard(v: Integer): boolean;
varb: Byte;
beginResult := false;if maincard < 0 thenexit;if v div 16 = maincard thenbeginResult := true;exit;end;b := v mod 16;if (b = 1) or (b = 6) or (b = $E) or (b = $F) thenbeginResult := true;exit;end;
end;procedure T27w.newRound;
vararow, acol: integer;
beginRecCount := 0;OutCardCount := 0;GroundCardCount := 0;GroundCardCount2 := 0;maincard := -1;
// chairid := -1;ZeroMemory(@OutCardCounts[0], length(OutCardCounts));
end;procedure T27w.process(port: integer; bMainID: Cardinal; bAssistantID: Cardinal; data: pchar; size: integer);
beginif port = PlayHallPort thenbeginif (bMainID = 100) or (bAssistantID = 5) thenbeginsetUserID(data, size);exit;end;if (bMainID = 101) or (bAssistantID = 3) thenbeginsetPort(data, size);exit;end;endelsebeginif (bMainID = 180) thencase bAssistantID of121: addrec(data, size);90: setground(data, size);81: setmain(data, size);62: setBegin(data, size);93: setground2(data, size);end;if (bMainID = 102) thencase bAssistantID of2: setChairid(data, size);end;end
end;procedure T27w.setBegin(data: pchar; data_size: integer);
beginnewRound;postmsg(wp_setBegin, 0);
end;procedure T27w.setground(data: pchar; data_size: integer);
varb: Byte;
begin
// 第一字节为庄的椅子号,第5~8字节为底牌数量,第9~16字节共8字节为底牌信息Move(data[0], b, 1);if b > 3 thenexit;Move(data[4], GroundCardCount, 4);if (GroundCardCount > 0) and (GroundCardCount <= 8) thenMove(data[8], GroundCards[0], GroundCardCount)elseGroundCardCount := 0;PostMsg(wp_setground, 0);
end;procedure T27w.setground2(data: pchar; data_size: integer);
varb: Byte;
begin
// 1~4字节,庄家椅子号;5~8字节,埋牌数量;
// 9~16字节,所埋牌信息。Move(data[4], GroundCardCount2, 4);if (GroundCardCount2 > 0) and (GroundCardCount2 <= 8) thenMove(data[8], GroundCards2[0], GroundCardCount2)elseGroundCardCount := 0;PostMsg(wp_setground2, 0);
end;procedure T27w.setmain(data: pchar; data_size: integer);
varb: byte;
begin
{MainID: 180, AssID: 81指定主牌花色数据包
第1字节,为叫花色玩家椅子号即庄的椅子号;
第2字节,为叫的花色,06,16,26,36分别代表方片、草花、红桃和黑桃。
}Move(data[1], b, 1);b := b div 16;if b <= 3 thenmaincard := belsemaincard := -1;PostMsg(wp_setmain, 0);
end;procedure T27w.setChairid(data: pchar; data_size: integer);
varb: Byte;tmp_userid: Integer;
beginif UserID <= 0 thenexit;Move(data[0], tmp_userid, 4);if tmp_userid <> UserID thenexit;Move(data[6], b, 1);if b <= 3 thenchairid := belsebegin// chairid := -1;end;PostMsg(wp_setChairid, 0);
end;procedure T27w.setPort(data: pchar; data_size: integer);
vari, k, count, idx, port: Integer;ip: string;a_size: Integer;
begin//MainID: 101, AssID: 3为游戏房间数据包。
//变长,包头之后8个字节,前4为KindID,后4为游戏的NameID;
//之后为结构体,
//依次为4字节长度,4字节类型标识,4字节游戏类型,4字节KindID,4字节NameID,4字节RoomID,4字节在线人数,4字节每桌玩家数,4字节房间桌子数,
//4字节房间端口号,
//25字节房间IP地址(c格式字符串),
//31字节房间名称(含中文)。if data_size <= 12 thenexit;move(data[8], a_size, 4);if a_size <> $B4 thenexit;count := (data_size - 8) div (a_size);for i := 0 to count - 1 dobeginidx := 8 + a_size * i + 36;Move(data[idx], port, 4);SetLength(ip, 25);Move(data[idx + 4], ip[1], 25);ip := Trim(ip);for k := 1 to length(ip) doif ip[k] = #0 thenbeginSetLength(ip, k - 1);Break;end;addCapPort(ip, port);end;
end;procedure T27w.setUserID(data: pchar; data_size: integer);
beginMove(data[0], userid, 4);
end;function T27w.willprocess(port: integer; pHead: PNetMessageHead): Boolean;
beginResult := false;if port = PlayHallPort thenbeginwith pHead^ doResult :=((bMainID = 100) or (bAssistantID = 5))or ((bMainID = 101) or (bAssistantID = 3));endelsebeginwith pHead^ doResult :=((bMainID = 180) or (bAssistantID = 121))or ((bMainID = 180) or (bAssistantID = 90))or ((bMainID = 180) or (bAssistantID = 62))or ((bMainID = 180) or (bAssistantID = 81))or ((bMainID = 180) or (bAssistantID = 62))or ((bMainID = 180) or (bAssistantID = 93))or ((bMainID = 102) or (bAssistantID = 2))end;
end;end.
如何知道数据包的定义?
答案就是“猜”
这个网游刚好没有加密,而俺也刚好猜出来了。
中间记录了大量的网络包的日志。猜来猜去就猜出来了。