Skip to content

Instantly share code, notes, and snippets.

@CTurt
Created February 23, 2022 00:07
Show Gist options
  • Save CTurt/ff3a46da2d20a4ab52b2ca5a83cea1e5 to your computer and use it in GitHub Desktop.
Save CTurt/ff3a46da2d20a4ab52b2ca5a83cea1e5 to your computer and use it in GitHub Desktop.
Proof-of-concept Offensive Combat server
/*
# Proof-of-concept Offensive Combat server
I produced this because I was curious to try reversing some C# code,
and partly also for nostalgia reasons.
Since the official servers have been down it has been impossible to
even play the offline game modes. With this proof-of-concept, you can
at least launch into the tutorial for a bit.
Feel free to take this further; with some more work it should be
possible to play all the offline game modes, and with a lot of work
you could even fully reimplement multiplayer.
# Usage
- Delete your old save data (C:\Users\yourname\AppData\LocalLow\Three Gates AB),
- Open this config file in a text editor: C:\Program Files (x86)\Steam\steamapps\common\Offensive Combat Redux\OC.config
- Change the HostURI to point to localhost:
SCP:QuickStart.HostURI=http://localhost:8080/
- Install golang if you don't have it already,
- Run this script by entering this command in cmd: go run ocserver.go
- Launch the game, and it should load into the tutorial!
# Reversing notes
Patch PeerBase.debugOut to ALL (5) to see network debug statements in the log file
(C:\Program Files (x86)\Steam\steamapps\common\Offensive Combat Redux\OC_Data\output_log.txt),
- CTurt
*/
package main
import (
"fmt"
"log"
"strings"
"net"
"net/http"
"compress/flate"
"bytes"
)
func abc(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "http://localhost:8080/")
}
func version(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "3720")
}
func servers(w http.ResponseWriter, req *http.Request) {
// EUCentral == 21
fmt.Fprintf(w, "{ \"servers\": [ { \"HostName\": \"bla\", \"PublicIP\": \"127.0.0.1\", \"ServerRegion\": 21 } ] }")
}
func other(w http.ResponseWriter, req *http.Request) {
fmt.Println("req " + req.URL.Path)
if(strings.HasPrefix(req.URL.Path, "/auth/st/")) {
fmt.Fprintf(w, "{ \"Id\": \"c75d06a8-a705-48ec-b6b3-9076becf20f4\", \"AccountName\": \"CTurt\", \"Token\": \"2222222222\", \"RealName\": \"CTurt\", \"Email\": \"cturt@localhost\", \"Created\": \"2000/11/11 11:11:11\", \"HasEmail\": true, \"LoginMethod\": \"1\", \"LoginUserId\": \"1\", \"LastLoginIP\": \"127.0.0.1\", \"IsActivated\": true, \"IsAdmin\": false }")
} else if(strings.HasPrefix(req.URL.Path, "/system/")) {
fmt.Fprintf(w, "{ \"servers\": [ { \"HostName\": \"bla\", \"PublicIP\": \"127.0.0.1\", \"ServerRegion\": 21 } ] }")
}
}
func deserializeU16(buf []byte) uint16 {
return (uint16(buf[0]) << 8) | uint16(buf[1]);
}
func deserializeU32(buf []byte) uint32 {
return (uint32(buf[0]) << 24) | (uint32(buf[1]) << 16) | (uint32(buf[2]) << 8) | uint32(buf[3]);
}
func serializeU16(s uint16) []byte {
return []byte { byte(s >> 8), byte(s) }
}
func serializeU32(s uint32) []byte {
return []byte { byte(s >> 24), byte(s >> 16), byte(s >> 8), byte(s) }
}
func serializeU32b(s uint32) []byte {
return []byte { byte(s), byte(s >> 8), byte(s >> 16), byte(s >> 24) }
}
type packetHeader struct {
peerId uint16
commandCount byte
time uint32
challenge uint32
}
func deserializePacketHeader(buf []byte) packetHeader {
return packetHeader {
peerId: deserializeU16(buf[0:2]),
// zero
commandCount: buf[3],
time: deserializeU32(buf[4:8]),
challenge: deserializeU32(buf[8:12]),
}
}
func serializePacketHeader(h packetHeader) []byte {
resp := []byte{}
resp = append(resp, 0, 0)
resp = append(resp, 0)
resp = append(resp, h.commandCount)
resp = append(resp, serializeU32(h.time)...)
resp = append(resp, serializeU32(h.challenge)...)
return resp
}
const COMMAND_TYPE_ACK = 1
const COMMAND_TYPE_CONNECT = 2
const COMMAND_TYPE_VERIFY_CONNECT = 3
const COMMAND_TYPE_DISCONNECT = 4
const COMMAND_TYPE_PING = 5
const COMMAND_TYPE_SEND_RELIABLE = 6
const COMMAND_TYPE_EG_SERVERTIME = 12
const COMMAND_FLAG_ACK = 1
var currentReliableSequenceNumber uint32 = 0
func assignReliableSequenceNumber() uint32 {
currentReliableSequenceNumber += 1
return currentReliableSequenceNumber - 1
}
type command struct {
commandType uint8
commandChannelID uint8
commandFlags uint8
commandLength uint32
reliableSequenceNumber uint32
}
func deserializeCommand(buf []byte) command {
return command {
commandType: buf[0],
commandChannelID: buf[1],
commandFlags: buf[2],
// four
commandLength: deserializeU32(buf[4:8]),
reliableSequenceNumber: deserializeU32(buf[8:12]),
}
}
func serializeCommand(c command) []byte {
resp := []byte{}
resp = append(resp, c.commandType)
resp = append(resp, c.commandChannelID)
resp = append(resp, c.commandFlags)
resp = append(resp, 4) // reserved
resp = append(resp, serializeU32(c.commandLength)...)
resp = append(resp, serializeU32(c.reliableSequenceNumber)...)
return resp
}
func getProfileData() (int, []byte) {
// PlayerProfile
primary := "weap-ar-arbiter"
secondary := "weap-pi-minarchdp"
melee := "weap-me-combatknife"
grenade := "weap-gr-fraggrenade"
b := []byte("{ \"AccountId\": \"c75d06a8-a705-48ec-b6b3-9076becf20f4\", \"SteamId\": 1234, \"AccountName\": \"CTurt\", \"Avatar\": { \"PartsByIndex\": { 0: { \"Id\": 0 }, 1: { \"Id\": 0 }, 2: { \"Id\": 0 }, 3: { \"Id\": 0 } } }, \"Weapons\": { \"Loadout\": { \"PrimaryWeaponID\": \"" + primary + "\", \"SecondaryWeaponID\": \"" + secondary + "\", \"MeleeWeaponID\": \"" + melee + "\", \"GrenadeWeaponID\": \"" + grenade + "\" } }, \"ByID\": { \"" + primary + "\": { \"Id\": \"" + primary + "\", \"Owned\": true }, \"" + secondary + "\": { \"Id\": \"" + secondary + "\", \"Owned\": true }, \"" + melee + "\": { \"Id\": \"" + melee + "\", \"Owned\": true }, \"" + grenade + "\": { \"Id\": \"" + grenade + "\", \"Owned\": true } } }")
compressedData := new(bytes.Buffer)
compressedData.Write([]byte{ 0x08, 0x1d }) // ICSharpCode.SharpZipLib.Zip.Compression.Inflater.DecodeHeader
zw, err := flate.NewWriter(compressedData, flate.BestSpeed)
if err != nil {
log.Fatal(err)
}
if _, err := zw.Write(b[:]); err != nil {
log.Fatal(err)
}
if err := zw.Flush(); err != nil {
log.Fatal(err)
}
if err := zw.Close(); err != nil {
log.Fatal(err)
}
return len(b), compressedData.Bytes()
}
func serve(pc net.PacketConn, addr net.Addr, buf []byte) {
//fmt.Printf("got %d\n", len(buf))
//for i := 0; i < len(buf); i++ {
// fmt.Printf("%x ", buf[i])
//}
//fmt.Println("")
header := deserializePacketHeader(buf)
var commandOffset uint32 = 12
for i := 0; i < int(header.commandCount); i++ {
c := deserializeCommand(buf[commandOffset:])
if c.commandType == COMMAND_TYPE_CONNECT {
resp := serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: COMMAND_TYPE_VERIFY_CONNECT,
commandChannelID: c.commandChannelID,
commandFlags: 0,
commandLength: 0,
reliableSequenceNumber: assignReliableSequenceNumber(),
})...)
resp = append(resp,
serializeU16(0x4141)... // peerId
)
pc.WriteTo(resp, addr)
} else if c.commandType == COMMAND_TYPE_SEND_RELIABLE {
payload := buf[commandOffset + 12 : commandOffset + c.commandLength]
fmt.Println("Command reliable - with payload:")
for i := 0; i < len(payload); i++ {
fmt.Printf("%x ", payload[i])
}
fmt.Println("")
if payload[0] == 0xf3 {
// Connect - these magic bytes set by ExitGames.Client.Photon.PeerBase
if payload[1] == 0 && payload[2] == 1 && payload[3] == 6 && payload[4] == 1 && payload[5] == 3 && payload[6] == 0 && payload[7] == 1 && payload[8] == 7 {
fmt.Println(" Connect from ")
// app name string from payload[9:]
// DeserializeMessageAndCallback
resp := serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: 7,
commandChannelID: c.commandChannelID,
commandFlags: 1, // 1 is for reliable
commandLength: 12 + 4 + 2,
reliableSequenceNumber: assignReliableSequenceNumber(),
})...)
resp = append(resp, serializeU32(0)...) // unreliableSequenceNumber
resp = append(resp,
0xf3,
1, // INIT_RESPONSE
)
pc.WriteTo(resp, addr)
} else if payload[1] == 2 && payload[2] == 1 {
fmt.Println(" Auth token")
// We'll send our profile data here as well
ogLen, compressedProfile := getProfileData()
profilejsonobject := []byte{};
profilejsonobject = append(profilejsonobject,
serializeU32b(uint32(len(compressedProfile) + 4))... // size of the 4 bytes for decompressed size + compressed size
)
profilejsonobject = append(profilejsonobject,
serializeU32b(uint32(ogLen))... // decompressed size
)
profilejsonobject = append(profilejsonobject, compressedProfile...)
resp := serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: 7,
commandChannelID: c.commandChannelID,
commandFlags: 1, // 1 is for reliable,
commandLength: 12 + 4 + 14 + uint32(len(profilejsonobject)),
reliableSequenceNumber: assignReliableSequenceNumber(),
})...)
resp = append(resp, serializeU32(0)...) // unreliableSequenceNumber
resp = append(resp,
0xf3,
3, // op response
10, // opcode - 10 == GetProfile
0, 0, // return code
// debug message
0, // type - none
// parameters
0, 1, // num
0, // key
120, // byte array
)
resp = append(resp, serializeU32(uint32(len(profilejsonobject)))...)
resp = append(resp, profilejsonobject...)
pc.WriteTo(resp, addr)
// Reply to the auth message
resp = serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: 7,
commandChannelID: c.commandChannelID,
commandFlags: 1, // 1 is for reliable
commandLength: 12 + 4 + 8,
reliableSequenceNumber: assignReliableSequenceNumber(),
})...)
resp = append(resp, serializeU32(0)...) // unreliableSequenceNumber
resp = append(resp,
0xf3,
3, // op response
1, // opcode - 1 == auth
0, 0, // return code
// debug message
0, // type - none
// parameters
0, 0, // num
)
pc.WriteTo(resp, addr)
} else {
fmt.Println(" Unknown op")
}
}
} else if c.commandType == COMMAND_TYPE_ACK {
// Nothing needed
} else if c.commandType == COMMAND_TYPE_DISCONNECT {
fmt.Println("Disconnect!")
} else if c.commandType == COMMAND_TYPE_EG_SERVERTIME {
fmt.Println("Eg")
// Not sure what this is; possibly requesting encryption key exchange
} else if c.commandType == COMMAND_TYPE_PING {
fmt.Println("Ping")
// TODO: I think this is wrong
resp := serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: 7,
commandChannelID: c.commandChannelID,
commandFlags: 1, // 1 is for reliable
commandLength: 12 + 4 + 9,
reliableSequenceNumber: assignReliableSequenceNumber(),
})...)
resp = append(resp, serializeU32(0)...) // unreliableSequenceNumber
resp = append(resp,
0xf0, // ping response
)
resp = append(resp,
serializeU32(0)...,
)
resp = append(resp,
serializeU32(0)...,
)
pc.WriteTo(resp, addr)
} else {
fmt.Println("Command type ", c.commandType)
}
if c.commandFlags & COMMAND_FLAG_ACK == COMMAND_FLAG_ACK {
resp := serializePacketHeader(packetHeader { commandCount: 1, challenge: header.challenge, time: header.time })
resp = append(resp, serializeCommand(command {
commandType: 1,
commandChannelID: c.commandChannelID,
commandFlags: 0,
commandLength: 0,
reliableSequenceNumber: 0, // Don't assign
})...)
resp = append(resp,
buf[20], buf[21], buf[22], buf[23], // ackReceivedReliableSequenceNumber
0, 0, 0, 0, // ackReceivedSentTime
)
pc.WriteTo(resp, addr)
}
commandOffset += c.commandLength
}
}
func main() {
http.HandleFunc("/abc", abc)
http.HandleFunc("/version/", version)
http.HandleFunc("/servers", servers)
http.HandleFunc("/", other)
go http.ListenAndServe(":8080", nil)
pc, err := net.ListenPacket("udp", ":5056")
if err != nil {
fmt.Println(err)
}
defer pc.Close()
for {
buf := make([]byte, 1024)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
continue
}
go serve(pc, addr, buf[:n])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment