diff --git a/README.md b/README.md index a31b11d..a0d524e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# tmp +# CrPatch v4.6.0 Test Repository diff --git a/crpatch_4.6.0.go b/crpatch_4.6.0.go new file mode 100644 index 0000000..e999ffa --- /dev/null +++ b/crpatch_4.6.0.go @@ -0,0 +1,322 @@ +//go:build ignore + +package main + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log" + "math" + "os" + "runtime" + "strings" + "time" + "unsafe" +) + +func patch() { + var name = `cloudreve` + if len(os.Args) == 3 { + name = os.Args[2] + } else if runtime.GOOS == `windows` { + name += `.exe` + } + + f, err := os.OpenFile(name, os.O_RDWR, 0666) + if err != nil { + log.Fatalln(`无法加载程序文件:`, err) + } + defer f.Close() + + offset, err := FindOffset(f, StringToBytes(`LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF0UXRuZmpOTGxuVnFTRG0rYXJNSApiaTFqQ2Zjam9WdzlTMnA5c2FhcEI3M1Z3YmdIT2FNT0lvbS9TMFNwQlpIYVZuamt3MDkzZHMraFhSam9ZRGFKCk1wLzlDMzlSWVJUL0svVHozYWJudFRXa2NrNk9EL3BhbGhqZWh4YXVqajJHdlprS2tPdW1lUXNxTzNjT3ZjcUEKb3JOd1ZCS1FtRGNkOE1nNlVIMHBnVjU1OEUwS1ZMazYxZzlWTlR4S09NS2Jvc1FXZ2xrc1ZvMjBGeUYzMko3WApzcExwcGR4WWF1U2NaYTk4NVNyKzd6QzV2NmNvTktZeDcrTjA4TkxQSk5PdW0vSko3T0RQSFJxVFUvd1hzSjd0Cm9ET01WSk9ncHF2UXVzV1ljbTdjUWU4Z25YYTlna0d3M1pqV1AwMm1zWXd6V3pjOHdFUFlaRk8wY2YwQXB6K3QKRXdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`)) + if err != nil { + log.Fatalln(`无法读取程序文件:`, err) + } + if offset == -1 { + log.Fatalln(`未找到数据偏移量,可能是已打补丁或程序更新`) + } + log.Println(`找到数据偏移量:`, offset) + + _, err = f.WriteAt(StringToBytes(`LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE1azlxa3pqTkkwNlc1ckg1WjFRRgpROVA4T2ltVytxTDhJOW5EZ1AwYnRKUzZFVzh4NW5GTkg4OEorNjJ5VXAxeEI1SXdpTVB2Qzl5QS82ekp6OW5WCk1udENJMU14WkNoQnpwSHZTakFieklqb0prY3hqOTJUZnpuVFNTOFROenVJU2luUHhZRWQ1Z1RLUE1GbCtiQzMKZ0R1VHQxSFNJVlUvNkhVMTZjRzFGMEpBZlZNcDN4RmV4b0pBdGQ4eDBMa0VsUTdESEhHQkJ5Y1ZpUm5rTngzTwpmTjFRRFMva2pQOUo5R0RvZVhRWitXZjVrN3BMMVB1blhHUmNmUEc1YjZpdDdnNXhWUGQ2cWJlTmpkMW9vUXdHCm9WMVI2WXhieXpuRnNGOFczRFpQeXVYOS9vdlNoSXdzc0tHZmY1WDhPa3dNNUwvVUhuRjhNWHdIN2QxM2ZRR20KMHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t`), offset) + if err != nil { + log.Fatalln(`无法写入程序文件:`, err) + } + log.Println(`补丁写入成功!`) +} + +func license() { + // 1. 序列化 LicenseContent 为 JSON(即 GCM 明文) + licenseContent := &LicenseContent{ + RootDomains: []string{`cloudreve.org`}, + Domains: []string{`plus.cloudreve.org`, `127.0.0.1`, `localhost`}, + UserID: `2018110303550773058`, + ValidSince: time.Now(), + ExpireAt: time.Unix(math.MaxInt32, 0), + VolEnabledDomain: []string{`cloudreve.org`}, + VolLicense: `eyJkb21haW5zIjpbImNsb3VkcmV2ZS5vcmciXSwic2lnbmVkX2F0IjoiMjAyNi0wMS0wNFQxOTowMTowOS40NjQxMDg2MzUrMDg6MDAifQ==`, + VolSign: `g8GD5F40cojyZaU6kMAjCHYBPmoI3RQ/45MyhruHRLXzCvLDLKgMZESgcm9QQRN8CxARReoBf3PwaayRQ0LzvMe0hWt5tAbZp3ilbca+09CUx3Sew7XADP0glXIzFS0ECigFTM8um5rgAnUgF2SNp5p5WDpcPTN/+nGVgpQ1eqJCqrOGzOmW7fSWa3ZkFzg5qIMQIDH6dkja8H4heG8mvPqYmMpjJ451aFJjnRDbNW7F3/lVVQbyE6nrb5jNRgxxgKp2D3uiq4KIohK6AheboEgPdnVdujwQJqj+bSSqOt0APMWOzDwMdch9zY+zt+8KkA1FVWyxhqRZPK+9sQkwOw==`, + } + if len(os.Args) == 3 { + licenseContent.Domains = append(strings.Split(os.Args[2], `,`), licenseContent.Domains...) + } + content, _ := json.Marshal(licenseContent) + + // 2. AES-GCM 加密 + aesKey, _ := HexDecode(StringToBytes(`93d384b2423f248457acd577f1e9dbc9576373da3272b49a424715f0829d5f89`)) + aesCipher, _ := aes.NewCipher(aesKey) + gcm, _ := cipher.NewGCM(aesCipher) + + nonce := RandomBytes(gcm.NonceSize()) + ciphertext := gcm.Seal(nil, nonce, content, nil) + contentEnc := append(nonce, ciphertext...) + + // 3. 计算 SHA1 摘要(与 loadLicense 中 crypto_sha1_Sum 对应) + sum := SumSHA1(content) + + // 4. 使用 RSA 私钥对 SHA1 摘要签名(与 validateWirthRSA 逆向逻辑对应) + pb, _ := pem.Decode(StringToBytes("-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5k9qkzjNI06W5rH5Z1QFQ9P8OimW+qL8I9nDgP0btJS6EW8x\n5nFNH88J+62yUp1xB5IwiMPvC9yA/6zJz9nVMntCI1MxZChBzpHvSjAbzIjoJkcx\nj92TfznTSS8TNzuISinPxYEd5gTKPMFl+bC3gDuTt1HSIVU/6HU16cG1F0JAfVMp\n3xFexoJAtd8x0LkElQ7DHHGBBycViRnkNx3OfN1QDS/kjP9J9GDoeXQZ+Wf5k7pL\n1PunXGRcfPG5b6it7g5xVPd6qbeNjd1ooQwGoV1R6YxbyznFsF8W3DZPyuX9/ovS\nhIwssKGff5X8OkwM5L/UHnF8MXwH7d13fQGm0wIDAQABAoIBABrjHJsRX4mGrEFE\nd8xjV7YCcojLwC+EhZEd5CAT7H8Zedt9MsMnaQyUnCLAtXfFn6N1oqrkzOyCkqDG\nqEzWrngVQbRkCvkZZg1apifwE96+APQvDY8xG/4XXx5FG6YwEgoudW4mU7o9xupu\n3ZvX0KRs0CLvr2tqy2o0/NVMYvrS7YN0VMt0j3DCK4wrY++nNRbV0G5rmIAxmarK\nGUv8wx8JD4M+bPw6LqiGhqtO9fxOeZuN+AwOsAndpn181haw0I3yDZviykqMwqRs\n7OEOK8UExT3D9BNEJVdBBgt0wxxSNknkJbULx0+8gNpGJRKT7nZ8rD8E4mBzD2NV\nBZuDTDECgYEA+VKdaLsCr5ygk2n+y/6Id8Knq/b0TpS67WK4HxcUmhJmOa/jl/1J\n3kthbAC/ZjqxyqHQyV+exnHxGwgwfSc5pvUogG2XmR/BRVoshQXaGQtd5JNQK002\nOD1GSzhhrXVYDik0tm3TGNrqWmNNxuHz7hLhktI+mLXG1xmG3/UkqSkCgYEA7Hpz\nGC02o3m6uDnxue/uNUo5nBt70+4vu7o/gSHXPu3SYWeBW8N/p5ihSmveppYcEist\nAteki8agFGorr1VJE2TRnMnIZzLug1Gm39wBN3Vj1LWWtAPPzbZWr5WvAah07AyI\nvm28PoMcMrcRDiO+1xoJQnqi7owWoXmdh0jtw5sCgYATi+iPV2oEfpEQPZUeRRxJ\n11bx4nCQ4JQvHDg3J8YTtjGRgDqlpdPuu5Yts3lN+HgL3NfTkc2O3Uv7xI6YoUSh\n1EKSnwTtmlG6sDEhVK4enIeF7g6w9mKz0gn6x7bQtIxsv8pEKyQLpM4OvA5S80/+\ncDiDHDyQGGA4aOXsFPoE0QKBgQDdVKJCn5bDHNzpLrqTOABw+h6/gG2+ZvFgOCrV\n1vGaIYdFyVWmqGzjVRSpDhsfOeHeE3EWdvS4KliLou9uNFmoADW8FJQwGthoKlUQ\nst6kpIeif68kn2iwdhfeUS+yUpPUlsqpyRVmQNp0kSkyXbxxoRnHkfbLZGyOB8Cr\n7VM0PwKBgG0RfIVLmcI2PslpZD0uVTVt3insBp/lbUQMbRGD5wCXzQNCJjOSICVG\nTeg2r6BsmLmRRwaK4Kfl2mk90lfF5PDQwoDmxzfj5XaAzjETaLpuTWf0v7azldT6\n2f/M9N+oH79iACS1JeO647coOsayLnpJsr7ks2DvQcy4cH/tw5dG\n-----END RSA PRIVATE KEY-----\n\n")) + priKey, _ := x509.ParsePKCS1PrivateKey(pb.Bytes) + + sig, err := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA1, sum) + if err != nil { + log.Fatalln(`无法签名授权文件:`, err) + } + + // 5. 组装 License 结构 + license := &License{ + Content: contentEnc, + Signature: sig, + } + // 6. JSON 序列化 License + data, _ := json.Marshal(license) + // 7. Base64 编码(与 loadLicense 开头的 DecodeString 对应) + enc := Base64Encode(base64.StdEncoding, data) + + err = os.WriteFile(`license`, enc, 0666) + if err != nil { + log.Fatalln(`无法写入授权文件:`, err) + } + log.Println(`授权文件写入成功!`) +} + +func valid() { + licenseTxt, err := os.ReadFile(`license`) + if err != nil { + log.Fatalln(`无法读取授权文件:`, err) + } + + licenseTxt, _ = Base64Decode(base64.StdEncoding, licenseTxt) + + var license *License + err = json.Unmarshal(licenseTxt, &license) + if err != nil { + log.Fatalln(`无法解析授权文件:`, err) + } + + aesKey, _ := HexDecode(StringToBytes(`93d384b2423f248457acd577f1e9dbc9576373da3272b49a424715f0829d5f89`)) + aesCipher, _ := aes.NewCipher(aesKey) + gcm, _ := cipher.NewGCM(aesCipher) + + plain, err := gcm.Open(nil, license.Content[:gcm.NonceSize()], license.Content[gcm.NonceSize():], nil) + if err != nil { + log.Fatalln(`无法解密文件内容:`, err) + } + fmt.Println(BytesToString(plain)) + + pb, _ := pem.Decode(StringToBytes("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5k9qkzjNI06W5rH5Z1QF\nQ9P8OimW+qL8I9nDgP0btJS6EW8x5nFNH88J+62yUp1xB5IwiMPvC9yA/6zJz9nV\nMntCI1MxZChBzpHvSjAbzIjoJkcxj92TfznTSS8TNzuISinPxYEd5gTKPMFl+bC3\ngDuTt1HSIVU/6HU16cG1F0JAfVMp3xFexoJAtd8x0LkElQ7DHHGBBycViRnkNx3O\nfN1QDS/kjP9J9GDoeXQZ+Wf5k7pL1PunXGRcfPG5b6it7g5xVPd6qbeNjd1ooQwG\noV1R6YxbyznFsF8W3DZPyuX9/ovShIwssKGff5X8OkwM5L/UHnF8MXwH7d13fQGm\n0wIDAQAB\n-----END PUBLIC KEY-----\n")) + pub, _ := x509.ParsePKIXPublicKey(pb.Bytes) + + err = rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA1, SumSHA1(plain), license.Signature) + if err != nil { + log.Fatalln(`签名验证失败!`) + } + log.Println(`是有效的破解版授权文件!`) +} + +func script() { + var err error + var sh bool = runtime.GOOS != `windows` + if len(os.Args) == 3 { + sh = os.Args[2] == `sh` + } + if sh { + err = os.WriteFile(`start.sh`, StringToBytes("#!/bin/bash\n\nexport CR_LICENSE_KEY=\"2018110303550773058\"\nexport CR_OFFLINE_LICENSE=$(cat license)\n\nexec ./cloudreve\n"), 0666) + } else { + err = os.WriteFile(`start.bat`, StringToBytes("@echo off\nsetlocal enabledelayedexpansion\n\nset \"CR_LICENSE_KEY=2018110303550773058\"\nset \"CR_OFFLINE_LICENSE=\"\n\nfor /f \"usebackq delims=\" %%a in (\"license\") do (\n set \"line=%%a\"\n set \"CR_OFFLINE_LICENSE=!CR_OFFLINE_LICENSE!!line!\"\n)\n\ncmd /c cloudreve.exe\n"), 0666) + } + if err != nil { + log.Fatalln(`写入文件失败:`, err) + } + log.Println(`启动脚本写入成功!`) +} + +func main() { + os.Stdout.WriteString("\n# CrPatch v4.6.0 by CloudrevePlus\n\n") + + const help = "Usage:\n 1. patch {file} - Patch Cloudreve exe\n 2. license {subDomain1,subDomain2} - Generate license file\n 3. valid (license) - Valid psedo license file?\n 4. script {sh|bat} - Generate startup script\n\n" + + if len(os.Args) == 1 { + os.Stdout.WriteString(help) + return + } + + switch os.Args[1] { + case `patch`: + patch() + case `license`: + license() + case `valid`: + valid() + case `script`: + script() + default: + os.Stdout.WriteString(help) + return + } +} + +/// util + +func FindOffset(f *os.File, target []byte) (int64, error) { + const chunkSize = 64 * 1024 // 64KB + buf := make([]byte, chunkSize) + var offset int64 = 0 + + // 用于处理跨块匹配 + var overlap []byte + + for { + n, err := f.Read(buf) + if n > 0 { + data := append(overlap, buf[:n]...) + + // 搜索目标内容 + idx := bytes.Index(data, target) + if idx != -1 { + return offset + int64(idx) - int64(len(overlap)), nil + } + + // 保留末尾 overlap + if len(target)-1 < len(data) { + overlap = data[len(data)-(len(target)-1):] + } else { + overlap = data + } + + offset += int64(n) + } + + if err == io.EOF { + break + } + if err != nil { + return -1, err + } + } + + return -1, nil +} + +/// types + +type License struct { + Content []byte + Signature []byte +} + +type LicenseContent struct { + RootDomains []string `json:"root_domains"` + Domains []string `json:"domains"` + UserID string `json:"user_id"` + ValidSince time.Time `json:"valid_since"` + ExpireAt time.Time `json:"expire_at"` + VolEnabledDomain []string `json:"vol_enabled_domain"` + VolLicense string `json:"vol_license"` + VolSign string `json:"vol_sign"` +} + +/// pkg + +//= bytesconv.go + +// StringToBytes converts string to byte slice without a memory allocation. +// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077. +func StringToBytes(s string) []byte { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} + +// BytesToString converts byte slice to string without a memory allocation. +// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077. +func BytesToString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} + +//= encoding.go + +//# base64 + +// Base64 编码 +func Base64Encode(enc *base64.Encoding, src []byte) []byte { + dst := MakeNoZero(enc.EncodedLen(len(src))) + enc.Encode(dst, src) + return dst +} + +// Base64 解码 +func Base64Decode(enc *base64.Encoding, src []byte) ([]byte, error) { + dst := MakeNoZero(enc.DecodedLen(len(src))) + n, err := enc.Decode(dst, src) + return dst[:n], err +} + +//# hex + +// Hex 编码 +func HexEncode(src []byte) []byte { + dst := MakeNoZero(hex.EncodedLen(len(src))) + hex.Encode(dst, src) + return dst +} + +// Hex 解码 +func HexDecode(src []byte) ([]byte, error) { + dst := MakeNoZero(hex.DecodedLen(len(src))) + _, err := hex.Decode(dst, src) + return dst, err +} + +//# hash + +// sha1 +func SumSHA1(src []byte) []byte { + s := sha1.Sum(src) + return s[:] +} + +//= random.go + +// 生成随机字节 +func RandomBytes(size int) []byte { + buf := MakeNoZero(size) + rand.Read(buf) + return buf +} + +//= util_v1.20.go + +// MakeNoZero 生成长度和容量为 n 的切片,此为Go1.20兼容版本。 +func MakeNoZero(n int) []byte { + return make([]byte, n) +} diff --git a/crpatch_4.6.0_linux_amd64v2 b/crpatch_4.6.0_linux_amd64v2 new file mode 100644 index 0000000..c730626 Binary files /dev/null and b/crpatch_4.6.0_linux_amd64v2 differ diff --git a/crpatch_4.6.0_windows_amd64v2.exe b/crpatch_4.6.0_windows_amd64v2.exe new file mode 100644 index 0000000..a9f51cc Binary files /dev/null and b/crpatch_4.6.0_windows_amd64v2.exe differ