Created
February 23, 2022 00:07
-
-
Save CTurt/ff3a46da2d20a4ab52b2ca5a83cea1e5 to your computer and use it in GitHub Desktop.
Proof-of-concept Offensive Combat server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
# 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