Skip to content

Instantly share code, notes, and snippets.

@esterTion
Last active August 22, 2024 08:08
Show Gist options
  • Save esterTion/c163e01a491b96525523750a59d07f2b to your computer and use it in GitHub Desktop.
Save esterTion/c163e01a491b96525523750a59d07f2b to your computer and use it in GitHub Desktop.
KService intercepter

ET 框架的一个常用通信通道KService,用来游戏和服务器进行rpc通信

本质是一个或多个kcp包拼接起来,并再追加一个会话识别编号

抓包并自动提取protobuf rpc内容

if NOT [%1]==[] GOTO single
start boot.bat 11002
start boot.bat 11003
start boot.bat 11004
exit
:single
php proxy.php %1 | cat
<?php
chdir(__DIR__);
require 'UnityBundle.php';
use Workerman\Connection\UdpConnection;
use Workerman\Connection\AsyncUdpConnection;
use Workerman\Worker;
use Workerman\Timer;
require_once __DIR__ . '/vendor/autoload.php';
$linkPool = [];
$port = 11004;
if (isset($argv[1])) {
$port = $argv[1];
}
define('PORT', $port);
define('ID_MAP', json_decode(file_get_contents('../idMap.json'), true));
$proxy = new Worker('udp://0.0.0.0:'.PORT);
$proxy->onWorkerStart = function ($worker) {
};
$prevConnection = null;
$upstream = new AsyncUdpConnection('udp://alpha.anxiens.com:'.PORT);
$upstream->onMessage = function ($c, $data) {
global $prevConnection;
if (!$prevConnection) return;
$data = str_replace(['106.75.223.112:11003','106.75.223.112:11004'], ['192.168.2.198:11003 ','192.168.2.198:11004 '], $data);
$prevConnection->send($data);
echo PORT." s->c\t" . strlen($data) . "\n";
try {
readKServiceBody($data);
} catch (Exception $_) {}
};
$proxy->onMessage = function ($connection, $data) {
echo PORT." c->s\t" . strlen($data) . "\n";
global $prevConnection;
global $upstream;
// if ($prevConnection) {
// $prevConnection->close();
// }
$prevConnection = $connection;
$upstream->connect();
$upstream->send($data);
try {
readKServiceBody($data);
} catch (Exception $_) {}
};
$fragments = [];
function getFullDataStart($conv, $sn, $frg) {
global $fragments;
$convFrag = &$fragments[$conv];
if ($frg === 0) {
if ($sn === 0) return 0;
// 往前找到上一个frg为0的分片
for ($i=$sn-1; $i>=0; $i--) {
if (!isset($convFrag[$i])) return -1;
if ($convFrag[$i] === true) return $i+1;
if ($convFrag[$i]['frg'] === 0) return $i+1;
}
return -1;
}
if (isset($convFrag[$sn]) && $convFrag[$sn] === true) return -1;
if (!isset($convFrag[$sn + $frg]) || $convFrag[$sn + $frg] === true) return -1;
return getFullDataStart($conv, $sn + $frg, 0);
}
function receivedBody($info, $body) {
global $fragments;
$conv = $info['conv'];
$sn = $info['sn'];
$frg = $info['frg'];
if (!isset($fragments[$conv])) $fragments[$conv] = [];
$fragments[$conv][$sn] = ['body' => $body, 'frg' => $frg];
if (($start = getFullDataStart($conv, $sn, $frg)) !== -1) {
$count = $fragments[$conv][$start]['frg'];
$body = '';
for ($i=$start; $i<=$start+$count; $i++) {
$body .= $fragments[$conv][$i]['body'];
$fragments[$conv][$i] = true;
}
$size = strlen($body);
echo "$sn\t$frg\tfound\t$start count $count\tsize $size\n";
return $body;
}
return null;
}
function readKServiceBody($data) {
$type = ord($data[0]);
switch ($type) {
case 1: {
echo "\t\t\tConnect\n";
break;
}
case 2: {
echo "\t\t\tConnect Ack\n";
break;
}
case 3: {
echo "\t\t\tKService Recv Fin\n";
break;
}
case 4: {
// echo "\t\t\tKService Data\n";
$kcpStream = new MemoryStream($data);
$kcpStream->littleEndian = true;
$kcpStream->position = 5;
while ($kcpStream->position < $kcpStream->size) {
// echo "kcp\n";
$info = [
'conv' => $kcpStream->long,
'cmd' => ord($kcpStream->byte),
'frg' => ord($kcpStream->byte),
'wnd' => $kcpStream->short,
'ts' => $kcpStream->long,
'sn' => $kcpStream->long,
'una' => $kcpStream->long,
'len' => $kcpStream->long,
];
$len = $info['len'];
if ($len <= 2) {
$kcpStream->position += $len;
continue;
}
$body = $kcpStream->readData($len);
$body = receivedBody($info, $body);
if ($body === null) continue;
$payload = new MemoryStream($body);
$payload->littleEndian = true;
$messageId = $payload->short;
if (!isset(ID_MAP[$messageId])) {
echo "Unknown message id: $messageId\n";
continue;
}
$message = ID_MAP[$messageId];
if (in_array($message, ['C2G_Ping', 'G2C_Ping'])) {
// skip
continue;
}
echo "$message: ";
$proto = parseProto('../'.$message.'_gen.proto');
$data = parseProtoBuf($message, $payload->size - 2, $payload, $proto);
// print_r($data);
processDict($data);
$output = prettifyJSON($data);
echo $output."\n";
@mkdir("log/".PORT, 0777, true);
file_put_contents("log/".PORT."/".date('Ymd-His').'-'.$info['sn'].'-'.$message.".json", $output);
}
echo "\n";
break;
}
}
}
function parseProto($file) {
$proto = file_get_contents($file);
$proto = preg_replace('(//.+\n)', "\n", $proto);
preg_match_all('(message (\w+) \{([\w\W]+?)\})', $proto, $messages);
$output = [];
for ($i=0; $i<count($messages[0]); $i++) {
$messageName = $messages[1][$i];
$lines = explode(';', trim($messages[2][$i], "; \n"));
$output[$messageName] = [];
foreach ($lines as $line) {
$fields = explode('=', trim($line));
$tag = trim($fields[1]);
$fields = explode(' ', trim($fields[0]));
$output[$messageName][$tag] = array(
'repeated' => $fields[0] == 'repeated',
'type' => $fields[1],
'name' => $fields[2]
);
}
}
return $output;
}
function readVarInt32($pb) {
$b = ord($pb->readData(1));
$result = $b & 0x7f;
$shift = 0;
while ($b & 0x80) {
$shift += 7;
if ($shift > 64) throw new Exception('Too many bytes for varint');
$b=ord($pb->readData(1));
$result |= ($b & 0x7f) << $shift;
}
return $result;
}
function setValue(&$arr, $key, &$val, $isArr) {
if ($isArr) {
if (!isset($arr[$key])) $arr[$key] = [];
$arr[$key][] = $val;
} else {
$arr[$key] = $val;
}
}
function parseProtoBuf($messageName, $end, &$pb, &$proto, $messageCallback = null, $level = 1) {
$message = [];
while ($pb->position() < $end){
$id = readVarInt32($pb);
$type = $id & 7;
$id = $id >> 3;
if (isset($proto[$messageName][$id])) {
$def = $proto[$messageName][$id];
} else {
$def = array('type'=>'byte', 'name'=>'UNKNOWN_'.$id, 'repeated'=>false);
}
$typeName = $def['type'];
$name = $def['name'];
$isRepeated = $def['repeated'];
switch($type){
case 0:
$data = readVarInt32($pb);
setValue($message, $name, $data, $isRepeated);
break;
case 1:
$data = $pb->readData(8);
if ($typeName == 'double') {
$data = unpack('d', $data)[1];
} else {
$data = '<fix64> '.bin2hex($data);
}
setValue($message, $name, $data, $isRepeated);
break;
case 2:
$length = readVarInt32($pb);
if (isset($proto[$typeName])) {
$sub = parseProtoBuf($typeName, $pb->position()+$length, $pb, $proto, $messageCallback, $level + 1);
if ($messageCallback === null || call_user_func($messageCallback, $name, $sub, $level)) {
setValue($message, $name, $sub, $isRepeated);
}
} else if ($length > 0) {
$data = $pb->readData($length);
if ($typeName == 'string') {
} else {
if (mb_detect_encoding($data, 'UTF-8', true)) {
$data = '<guessed string> '.$data;
} else {
$data = '<byte> '.bin2hex($data);
}
}
setValue($message, $name, $data, $isRepeated);
}
break;
case 5:
$data = $pb->readData(4);
if ($typeName == 'float') {
$data = unpack('f', $data)[1];
} else {
$data = '<fix32> '.bin2hex($data);
}
setValue($message, $name, $data, $isRepeated);
break;
default:
throw new Exception('unsupported type '.$type);
}
}
return $message;
}
function processDict(&$arr) {
foreach ($arr as $key=>&$val) {
if (gettype($val) == 'array') {
if (isset($val[0]) && gettype($val[0]) == 'array') {
$keys = array_keys($val[0]);
if (count($keys) == 2
&& substr($keys[0], -4, 4) == '_key'
&& substr($keys[1], -6, 6) == '_value') {
$newDict = [];
foreach ($val as &$item) {
$newDict[ $item[$keys[0]] ] = $item[$keys[1]];
}
unset($arr[$key]);
$arr[$key] = $newDict;
$val = &$arr[$key];
}
}
processDict($val);
$keys = array_keys($val);
if (count($keys) == 1 && $keys[0] === 'entries') {
$arr[$key] = $val['entries'];
}
}
}
}
function prettifyJSON($in, Stream $out = NULL, $returnData = true) {
if ($out == NULL) $out = new MemoryStream('');
$a = $in;
if (gettype($a) == 'string') $a = json_decode($a);
$a = json_encode($a, JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES+JSON_PRETTY_PRINT);
$a = preg_replace("/([^\]\}]),\n +/", "$1, ", $a);
$a = preg_replace('/("[^"]+?":) /', '$1', $a);
$a = preg_replace_callback("/\n +/", function ($m) { return "\n".str_repeat(' ', (strlen($m[0])-1) / 2); }, $a);
$out->write($a);
if (!$returnData) return;
$out->seek(0);
$output = $out->readData($out->size);
unset($out);
return $output;
}
Worker::runAll();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment