Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sci Chicken

A PUBG pcap playback system that does not hog CPU/GPU, works with version 3.7.19.
A PUBG pcap playback system that does not hog CPU/GPU, works with version 3.7.28.9.

## Sniff

Expand Down
11 changes: 10 additions & 1 deletion backend/bunchstash.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ const bunchStash = {
return [{ type: CONSTS.EventTypes.GAMESTOP, time: inputEvent.time }]
}
const messageType = bunch.uebuffer.readUInt8()
if (messageType === 1 /* NMT_Welcome, only care about this */) {
if (messageType === 0 /* NMT_Hello */) {
const IsLittleEndian = bunch.uebuffer.readUInt8()
const RemoteNetworkVersion = bunch.uebuffer.readUInt32()
const EncryptionToken = bunch.uebuffer.readString()
if (EncryptionToken.length === 24) {
return [{ type: CONSTS.EventTypes.ENCRYPTIONKEY, time: inputEvent.time, data: { EncryptionToken } }]
} else {
return null
}
} else if (messageType === 1 /* NMT_Welcome */) {
const map = bunch.uebuffer.readString()
const gameMode = bunch.uebuffer.readString()
const unknown = bunch.uebuffer.readString()
Expand Down
1 change: 1 addition & 0 deletions backend/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
// bunch process -> new event
SELFLOCEX: Symbol('SelfLocEx'),
CARSYNC: Symbol('CarSync'),
ENCRYPTIONKEY: Symbol('EncryptionKey'),
GAMESTART: Symbol('GameStart'),
GAMESTOP: Symbol('GameStop'),
ACTOROPEN: Symbol('ActorOpen'),
Expand Down
115 changes: 87 additions & 28 deletions backend/packetparser.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,93 @@
const CONSTS = require('./constants')
const decoders = require('cap').decoders
const PROTOCOL = decoders.PROTOCOL
const UEBuffer = require('./uebuffer')
const logger = require('./logger')
const Bunch = require('./bunch')
const crypto = require('crypto');

const allsame = input => {
return input === 255 || input === 0
}

const packetParserProto = {
EncryptionToken: '',
haveEncryptionToken: false,

readRawBit (rawPacket, posBits) {
const SHIFTS = [1, 2, 4, 8, 16, 32, 64, 128]
const b = rawPacket[posBits >> 3] & SHIFTS[posBits & 0b0111] // x & 0111 means x % 8 -> 0..7
return b != 0
},

// server 7000..7999 -> Game PC packet, UDP. Contains a set of bunches to a certain channel
parseUEBunchPacket (rawPacket, payloadLength, pindex, isOutTraffic) {
if (!payloadLength) {
return
}
const buf = UEBuffer(rawPacket)
let buf = UEBuffer(rawPacket)
buf.skipBits(42 * 8) // skip to udp payload
// The end of packet might still have udp padding, so use payloadLength
buf.remainingBits = payloadLength * 8
buf._localRemainingBits = payloadLength * 8

if (!isOutTraffic || isOutTraffic) {
// adjust size, according to NetConnection.cpp:701
let lastByte = buf.buffer[41 + payloadLength]
if (lastByte > 0) {
// NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this
let finalBitSize = buf.remainingBits - 2
while ((lastByte & 0x80) === 0) {
lastByte = lastByte * 2
finalBitSize--
}
buf.remainingBits = finalBitSize
buf._localRemainingBits = finalBitSize
} else {
logger.fatal('Got 0 in last byte of udp payload, which looks wrong')
return
let IsHandshake = this.readRawBit(rawPacket, (42 * 8))
if (IsHandshake) {
return null
}
let IsEncrypted = this.readRawBit(rawPacket, (42 * 8) + 1)

// adjust size, according to NetConnection.cpp:701
let lastByte = buf.buffer[41 + payloadLength] & 0xFF
if (lastByte > 0) {
// NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this
let finalBitSize = (payloadLength * 8) - (IsEncrypted ? 1 : 2)
while ((lastByte & 0x80) === 0) {
lastByte = lastByte * 2
finalBitSize--
}
buf.remainingBits = finalBitSize
buf._localRemainingBits = finalBitSize
} else {
logger.fatal('Got 0 in last byte of udp payload, which looks wrong')
return
}

if (buf.readBit() || buf.readBit()) {
logger.warn({ pindex }, 'Got handshake or encrypted.')
return null // 1st 2nd bit -> handshake or encrypted
IsHandshake = buf.readBit()
IsEncrypted = buf.readBit()

if (IsEncrypted && !this.haveEncryptionToken) {
//logger.warn('Got encrypted packet but don\'t have EncryptionToken')
return null
} else if (IsEncrypted && this.haveEncryptionToken) {
const nonce = new Buffer(buf.readBits(96))
const tag = new Buffer(buf.readBits(128))
const ciphertext = new Buffer(buf.readBits(buf.remainingBits))
const cipher = crypto.createDecipheriv('aes-192-gcm', this.EncryptionToken, nonce);
cipher.setAuthTag(tag)
try {
let decryptedRaw = cipher.update(ciphertext)
decryptedRaw = Buffer.concat([decryptedRaw, cipher.final()]);
lastByte = decryptedRaw[decryptedRaw.length - 1] & 0xFF
if (lastByte > 0) {
// NOTE: UE4 code is remainingBitss - 1, I changed to 2, maybe PUBG modified this
finalBitSize = (decryptedRaw.length * 8) - 2
while ((lastByte & 0x80) === 0) {
lastByte = lastByte * 2
finalBitSize--
}
buf = UEBuffer(decryptedRaw)
buf.remainingBits = finalBitSize
buf._localRemainingBits = finalBitSize
} else {
logger.fatal('Got 0 in last byte of udp payload, which looks wrong')
return
}
} catch (ex) {
logger.warn('Missed decrypting packet')
return null
}
}

// this is Unreal packet id, 15 bit
const packetId = buf.readInt(CONSTS.MAX_PACKETID)
const event = { type: CONSTS.EventTypes.UEBUNCHES, valid: true, uepid: packetId, totalBunchesBits: buf.remainingBits, data: [] }
Expand Down Expand Up @@ -99,6 +144,7 @@ const packetParserProto = {
const chType = bReliable || bOpen
? buf.readInt(CONSTS.CHTYPE_MAX)
: CONSTS.CHTYPE_NONE // when it is not Open or reliable, type = none

const bunchDataBits = buf.readInt(CONSTS.MAX_BUNCH_DATA_BITS)
let preLeft = buf.remainingBits
if (bunchDataBits > preLeft) {
Expand All @@ -111,26 +157,27 @@ const packetParserProto = {
// END OF getting bunch header, now get bunch data
if (chType > 4) {
logger.fatal({ pindex, chType }, 'Got unknown channel type')
return null
}
// ignore file and voice and unknown channel type
if (chType === CONSTS.CHTYPE_FILE || chType === CONSTS.CHTYPE_VOICE || chType > 4) {
if (chType === CONSTS.CHTYPE_FILE || chType === CONSTS.CHTYPE_VOICE) {
buf.skipBits(bunchDataBits)
continue
}
// assume -> control channel always got channel index = 0
if (chType === CONSTS.CHTYPE_CONTROL) {
if (isOutTraffic) {
// for outTraffic, we dont care control channel
buf.skipBits(bunchDataBits)
continue
}
//if (isOutTraffic) {
// // for outTraffic, we dont care control channel
// buf.skipBits(bunchDataBits)
// continue
//}
if (chIndex !== 0 && !isOutTraffic) {
logger.fatal({ chIndex, pindex }, 'Found non-zero Index CONTROL CHANNEL, not good!')
buf.skipBits(bunchDataBits)
continue
}
}

// if we are here, we can assume the bunch is valid, bunch can be partial
// since bunch might be stashed for a while, we need to copy bits from the current Buffer
event.data.push(Bunch(bunchDataBits,
Expand All @@ -149,7 +196,7 @@ const packetParserProto = {
14 /* ether header length offset */
)
let result = null
if (ipv4Info.info.protocol === PROTOCOL.IP.UDP) {
if (ipv4Info.info.protocol === decoders.PROTOCOL.IP.UDP) {
const udpInfo = decoders.UDP(rawPacket, ipv4Info.offset)
if (udpInfo.info.srcport >= 7000 && udpInfo.info.srcport <= 7999) {
// the actor bunch packets, sent by channels
Expand All @@ -162,6 +209,18 @@ const packetParserProto = {
result.time = time
}
return result
},

setEncryptionToken (EncryptionToken) {
if (!this.haveEncryptionToken) {
this.EncryptionToken = EncryptionToken
this.haveEncryptionToken = true
}
},

clearEncryptionToken () {
this.haveEncryptionToken = false
this.EncryptionToken = ''
}
}

Expand Down
114 changes: 84 additions & 30 deletions backend/processor/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,90 +25,144 @@ function processGame (buf, actor, repObj, waitingHandle, dataOut, i) {
case 23:
dataOut.matchShortGuid = buf.readString() // matchShortGuid !
break
case 24:
buf.readBit() // bIsCustomGame
break
case 25:
buf.readBit() // bIsWinnerombieTeam
break
case 26:
buf.readUInt32() // RemainingTime
buf.readUInt32() // NumTeams
break
case 27:
buf.readUInt32() // MatchElapsedMinutes
buf.readUInt32() // RemainingTime
break
case 28:
buf.readUInt32() // bTimerPaused
buf.readUInt32() // MatchElapsedMinutes
break
case 29:
buf.readBit() // bShowLastCircleMark
buf.readFloat() // MatchElapsedTimeSec
break
case 30:
buf.readBit() // bCanShowLastCircleMark
buf.readBit() // bTimerPaused
break
case 31:
buf.readUInt32() // numAlivePlayers, never hit this
buf.readBit() // bShowLastCircleMark
break
case 32:
buf.readUInt32() // numJoinPlayers
buf.readBit() // bCanShowLastCircleMark
break
case 33:
buf.readUInt32() // NumAlivePlayers
buf.readBit() // bCanKillerSpectate
break
case 34:
buf.readUInt32() // NumAliveZombiePlayers
buf.readUInt32() // NumJoinPlayers
break
case 35:
buf.readUInt32() // NumAliveTeams
buf.readUInt32() // NumAlivePlayers
break
case 36:
buf.readUInt32() //NumStartPlayers
buf.readUInt32() // NumAliveZombiePlayers
break
case 37:
buf.readUInt32() // NumStartTeams
buf.readUInt32() // NumAliveTeams
break
case 38:
dataOut.poison = buf.readFloatVector()
buf.readUInt32() //NumStartPlayers
break
case 39:
dataOut.poisonRadius = buf.readFloat() // poison radius
buf.readUInt32() // NumStartTeams
break
case 40:
dataOut.safezone = buf.readFloatVector()
dataOut.safezone = buf.readFloatVector() // SafetyZonePosition
break
case 41:
dataOut.safezoneRadius = buf.readFloat() // safezone radius
dataOut.safezoneRadius = buf.readFloat() // SafetyZoneRadius
break
case 42:
buf.readFloatVector() // redzone !
dataOut.poison = buf.readFloatVector() // PoisonGasWarningPosition
break
case 43:
buf.readFloat() // redzone radius !
dataOut.poisonRadius = buf.readFloat() // PoisonGasWarningRadius
break
case 44:
buf.readFloat() // TotalReleaseDuration
buf.readFloatVector() // RedZonePosition
break
case 45:
buf.readFloat() // ElapsedReleaseDuration
break
case 46:
buf.readFloat() // TotalWarningDuration
buf.readFloat() // RedZoneRadius
break
//case 46:
// // LastCirclePosition
// break
case 47:
buf.readFloat() // ElapsedWarningDuration
buf.readFloat() // TotalReleaseDuration
break
case 48:
buf.readFloat() // not sure what, but it is float
buf.readFloat() // ElapsedReleaseDuration
break
case 49:
buf.readBit() // bIsTeamMatch
buf.readFloat() // TotalWarningDuration
break
case 50:
buf.readBit() // bIsZombieMode
buf.readFloat() // ElapsedWarningDuration
break
case 51:
buf.readFloatVector() // SafetyZoneBeginPosition
buf.readBit() // bIsGasRelease
break
case 52:
buf.readFloat()
buf.readBit() // bIsTeamMatch
break
case 53:
buf.readByte() // MatchStartType
buf.readBit() // bIsZombieMode
break
case 54:
buf.readBit() // bUseXboxUnauthorizedDevice
break
case 55:
buf.readFloatVector() // SafetyZoneBeginPosition
break
case 56:
buf.readFloat() // SafetyZoneBeginRadius
break
case 57:
buf.readBit() // MatchStartType
break
case 58:
buf.readBit() // bShowAircraftRoute
break
case 59:
buf.readBit() // bIsWarMode
break
case 60:
buf.readUInt32() // GoalScore
break
//case 61:
// // TeamScores
// break
//case 62:
// // NextRespawnTimeTick
// break
//case 63:
// // TimeLimitTick
// break
//case 64:
// // bIsTeamElimination
// break
//case 65:
// // bUseWarRoyaleBluezone
// break
//case 66:
// // bUsingSquadInTeam
// break
//case 67:
// // TeamIds
// break
//case 68:
// // TeamIndices
// break
//case 69:
// // TeamLeaderNames
// break
default:
return false
}
Expand Down
Loading