854 lines
26 KiB
Go
854 lines
26 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"debug/pe"
|
|
"encoding/binary"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/big"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
// why tf do I have to reimplement .rsrc parsing? Shouldn't debug/pe be doing this for me?
|
|
|
|
type ImageResourceDirectory struct {
|
|
Characteristics uint32
|
|
Timestamp uint32
|
|
MajorVersion uint16
|
|
MinorVersion uint16
|
|
NumberOfNamedEntries uint16
|
|
NumberOfIdEntries uint16
|
|
}
|
|
|
|
type ImageResourceDirectoryEntry struct {
|
|
NameId uint32
|
|
Offset uint32
|
|
}
|
|
|
|
type ImageResourceDataEntry struct {
|
|
RelativeVirtualAddress uint32
|
|
Size uint32
|
|
CodePage uint32
|
|
Reserved uint32
|
|
}
|
|
|
|
const (
|
|
NameIsString = 0x80000000
|
|
OffsetIsDirectory = 0x80000000
|
|
)
|
|
|
|
var runVerbose bool = false
|
|
|
|
func vPrintf(format string, a ...interface{}) {
|
|
if runVerbose {
|
|
fmt.Printf(format, a...)
|
|
}
|
|
}
|
|
|
|
type ResourceDirectoryEntry struct {
|
|
Name string
|
|
Id uint32
|
|
Offset uint32
|
|
IsNamed bool
|
|
IsDirectory bool
|
|
}
|
|
type ResourceDirectory struct {
|
|
Entries []ResourceDirectoryEntry
|
|
}
|
|
|
|
func (rde ResourceDirectoryEntry) String() string {
|
|
var s string
|
|
if rde.IsNamed {
|
|
s = fmt.Sprintf("{Name: %s, offset: 0x%x", rde.Name, rde.Offset)
|
|
} else {
|
|
s = fmt.Sprintf("{ID: %d, offset: 0x%x", rde.Id, rde.Offset)
|
|
}
|
|
if rde.IsDirectory {
|
|
s += " [DIRECTORY]"
|
|
}
|
|
s += "}"
|
|
return s
|
|
}
|
|
|
|
type BINKResourceHeader struct {
|
|
ResourceId uint32
|
|
Size uint32
|
|
OffsetToCurveParams uint32
|
|
Checksum uint32
|
|
Version uint32
|
|
}
|
|
|
|
type BINK struct {
|
|
ResourceId uint32
|
|
Size uint32
|
|
OffsetToCurveParams uint32
|
|
Checksum uint32
|
|
Version uint32
|
|
CurveParamWords uint32
|
|
PKHashBits uint32
|
|
PKScalarBits uint32
|
|
// 2003-only:
|
|
AuthValueBits uint32
|
|
PIDBits uint32
|
|
|
|
// common again
|
|
Curve Curve
|
|
B Point // base point
|
|
K Point // public key point
|
|
|
|
// secret params, only used for passing to generateAndWriteBINK()
|
|
SecretKey *big.Int
|
|
BasePointOrder *big.Int
|
|
}
|
|
|
|
func (bink *BINK) Bytes() []byte {
|
|
buf := make([]byte, bink.Size + 4)
|
|
offs := uint32(0)
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.ResourceId); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.Size); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.OffsetToCurveParams); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.Checksum); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.Version); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.CurveParamWords); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.PKHashBits); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.PKScalarBits); offs += 4
|
|
if bink.Version == 20020420 {
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.AuthValueBits); offs += 4
|
|
binary.LittleEndian.PutUint32(buf[offs:], bink.PIDBits); offs += 4
|
|
}
|
|
|
|
curveParam := bink.Curve.P.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.Curve.A.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.Curve.B.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.B.X.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.B.Y.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.K.X.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
curveParam = bink.K.Y.Bytes()
|
|
reverseByteArray(curveParam)
|
|
copy(buf[offs:], curveParam)
|
|
offs += 4 * bink.CurveParamWords
|
|
|
|
// buf now has all the data. Iterate word-wise to compute the checksum.
|
|
// We cannot seek the buffer, so just extract the byte array and monkey-patch it.
|
|
dw := make([]uint32, bink.Size / 4)
|
|
br := bytes.NewReader(buf[4:])
|
|
binary.Read(br, binary.LittleEndian, &dw)
|
|
checksum := uint32(0)
|
|
for _, n := range dw {
|
|
checksum += n
|
|
}
|
|
bink.Checksum = -checksum
|
|
|
|
binary.LittleEndian.PutUint32(buf[12:], bink.Checksum)
|
|
|
|
return buf
|
|
}
|
|
|
|
func readResourceDirectoryAt(offset uint32, dir *ResourceDirectory, brrsdir *bytes.Reader) {
|
|
(*brrsdir).Seek(int64(offset), io.SeekStart)
|
|
|
|
var irdir ImageResourceDirectory
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, &irdir); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var de ImageResourceDirectoryEntry
|
|
nentries := irdir.NumberOfNamedEntries + irdir.NumberOfIdEntries
|
|
|
|
dir.Entries = make([]ResourceDirectoryEntry, nentries)
|
|
|
|
for i := uint16(0); i < nentries; i++ {
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, &de); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if (de.Offset & OffsetIsDirectory) != 0 {
|
|
de.Offset &= 0x7fffffff
|
|
dir.Entries[i].IsDirectory = true
|
|
} else {
|
|
dir.Entries[i].IsDirectory = false
|
|
}
|
|
dir.Entries[i].Offset = de.Offset
|
|
if (de.NameId & NameIsString) != 0 {
|
|
de.NameId &= 0x7fffffff
|
|
dir.Entries[i].IsNamed = true
|
|
} else {
|
|
dir.Entries[i].IsNamed = false
|
|
}
|
|
dir.Entries[i].Id = de.NameId
|
|
}
|
|
|
|
// grab names now
|
|
for i := uint16(0); i < nentries; i++ {
|
|
if dir.Entries[i].IsNamed {
|
|
(*brrsdir).Seek(int64(dir.Entries[i].Id), io.SeekStart)
|
|
// name is length-prefixed
|
|
var nchars uint16
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, &nchars); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.Grow(int(nchars))
|
|
buf := make([]uint16, nchars)
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
for j := uint16(0); j < nchars; j++ {
|
|
runes := utf16.Decode(buf[j:j+1]) // XXX assumes no sequences occur
|
|
b.WriteRune(runes[0])
|
|
}
|
|
|
|
dir.Entries[i].Name = b.String()
|
|
dir.Entries[i].Id = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
func reverseByteArray(a []byte) {
|
|
for i, j := 0, len(a) - 1; i < j; i, j = i + 1, j - 1 {
|
|
a[i], a[j] = a[j], a[i]
|
|
}
|
|
}
|
|
|
|
func readAndParseBINKAtOffset(offset uint32, brrsdir *bytes.Reader) (bink BINK, err error) {
|
|
(*brrsdir).Seek(int64(offset), io.SeekStart)
|
|
var brh BINKResourceHeader
|
|
err = binary.Read(brrsdir, binary.LittleEndian, &brh)
|
|
if err != nil {
|
|
return bink, fmt.Errorf("cannot read BINK resource header: %v", err)
|
|
}
|
|
|
|
// Verify checksum first; point *after* the resource ID
|
|
(*brrsdir).Seek(int64(offset + 4), io.SeekStart)
|
|
if brh.Size % 4 != 0 || brh.Size > 1024*1024 {
|
|
return bink, errors.New("invalid BINK size (size not divisible by 4 or greater than 1 MiB)")
|
|
}
|
|
rawBINKWords := make([]uint32, brh.Size / 4)
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &rawBINKWords); err != nil {
|
|
return bink, fmt.Errorf("cannot read full BINK: %v", err)
|
|
}
|
|
s := uint32(0)
|
|
for _, n := range rawBINKWords {
|
|
s += n
|
|
}
|
|
if s != 0 {
|
|
return bink, fmt.Errorf("invalid BINK checksum (result: 0x%08x != 0)", s)
|
|
}
|
|
|
|
(*brrsdir).Seek(int64(offset + 4 * 5), io.SeekStart)
|
|
// brrsdir now points back to after the BINKResourceHeader
|
|
|
|
bink.ResourceId = brh.ResourceId
|
|
bink.Size = brh.Size
|
|
bink.OffsetToCurveParams = brh.OffsetToCurveParams
|
|
bink.Checksum = brh.Checksum
|
|
bink.Version = brh.Version
|
|
|
|
switch bink.Version {
|
|
case 19980206:
|
|
params := make([]uint32, 3)
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, ¶ms); err != nil {
|
|
return bink, fmt.Errorf("cannot read BINK params for 19980206 BINK: %v", err)
|
|
}
|
|
bink.CurveParamWords = params[0]
|
|
bink.PKHashBits = params[1]
|
|
bink.PKScalarBits = params[2]
|
|
case 20020420: // blaze it
|
|
params := make([]uint32, 5)
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, ¶ms); err != nil {
|
|
return bink, fmt.Errorf("cannot read BINK params for 20020420 BINK: %v", err)
|
|
}
|
|
bink.CurveParamWords = params[0]
|
|
bink.PKHashBits = params[1]
|
|
bink.PKScalarBits = params[2]
|
|
bink.AuthValueBits = params[3]
|
|
bink.PIDBits = params[4]
|
|
default:
|
|
return bink, fmt.Errorf("unknown BINK version %d", bink.Version)
|
|
}
|
|
|
|
// brrsdir now SHOULD point at the curve params
|
|
newOffset, err := brrsdir.Seek(0, io.SeekCurrent)
|
|
if err != nil {
|
|
return bink, fmt.Errorf("cannot seek to curve params: %v", err)
|
|
}
|
|
|
|
if (uint32(newOffset) - (offset + 4)) / 4 != bink.OffsetToCurveParams {
|
|
return bink, errors.New("not pointing at curve params")
|
|
}
|
|
|
|
// big.Int SetBytes only does big-endian integers and all our integers are
|
|
// little-endian; reverse these after reading.
|
|
|
|
buf := make([]byte, bink.CurveParamWords * 4)
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.Curve.P = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.Curve.P.SetBytes(buf)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.Curve.A = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.Curve.A.SetBytes(buf)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.Curve.B = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.Curve.B.SetBytes(buf)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.B.X = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.B.X.SetBytes(buf)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.B.Y = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.B.Y.SetBytes(buf)
|
|
|
|
bink.B.Z = big.NewInt(1)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.K.X = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.K.X.SetBytes(buf)
|
|
|
|
if err = binary.Read(brrsdir, binary.LittleEndian, &buf); err != nil {
|
|
return bink, err
|
|
}
|
|
bink.K.Y = new(big.Int)
|
|
reverseByteArray(buf)
|
|
bink.K.Y.SetBytes(buf)
|
|
|
|
bink.K.Z = big.NewInt(1)
|
|
|
|
vPrintf("%+v\n", bink)
|
|
|
|
return bink, nil
|
|
}
|
|
|
|
func parseDecoded1998ProductKey(decoded *big.Int, bink BINK) (uint32, bool, error) {
|
|
d := new(big.Int).Set(decoded)
|
|
pidMask := new(big.Int)
|
|
pidMask.SetBit(pidMask, 31, 1).Sub(pidMask, ONE) // (1 << 31) - 1; 31-bit hardcoded
|
|
dc := new(big.Int).Set(d)
|
|
rawpid := uint32(dc.And(dc, pidMask).Int64())
|
|
d.Rsh(d, 31)
|
|
|
|
// Need to keep (e, y) as big ints in case someone's feeding a fucky-wucky BINK
|
|
pkHashBitsMask := new(big.Int)
|
|
pkHashBitsMask.SetBit(pkHashBitsMask, int(bink.PKHashBits), 1).Sub(pkHashBitsMask, ONE)
|
|
dc = new(big.Int).Set(d)
|
|
e := dc.And(dc, pkHashBitsMask)
|
|
d.Rsh(d, uint(bink.PKHashBits))
|
|
|
|
pkScalarBitsMask := new(big.Int)
|
|
pkScalarBitsMask.SetBit(pkScalarBitsMask, int(bink.PKScalarBits), 1).Sub(pkScalarBitsMask, ONE)
|
|
dc = new(big.Int).Set(d)
|
|
y := dc.And(dc, pkScalarBitsMask)
|
|
d.Rsh(d, uint(bink.PKScalarBits))
|
|
|
|
if d.Cmp(ZERO) != 0 {
|
|
return 0, false, fmt.Errorf("bad parse (leftover bits in product key: 0x%x)", d)
|
|
}
|
|
|
|
vPrintf("e = 0x%x\ny = 0x%x\n", e, y)
|
|
|
|
// R = [y]B + [e]K
|
|
yB := bink.Curve.ScalarMult(y, bink.B)
|
|
eK := bink.Curve.ScalarMult(e, bink.K)
|
|
R := bink.Curve.AddPoints(yB, eK)
|
|
bink.Curve.Affinize(&R)
|
|
vPrintf("R = (0x%x, 0x%x)\n", R.X, R.Y)
|
|
// h = H(pid_in_le_bytes, Rx_in_le_bytes, Ry_in_le_bytes)
|
|
buf := make([]byte, 4 + 2 * (bink.CurveParamWords * 4))
|
|
binary.LittleEndian.PutUint32(buf[0:], rawpid)
|
|
R.X.FillBytes(buf[4:4+(bink.CurveParamWords * 4)])
|
|
reverseByteArray(buf[4:4+(bink.CurveParamWords * 4)])
|
|
R.Y.FillBytes(buf[4+(bink.CurveParamWords * 4):4+2*(bink.CurveParamWords * 4)])
|
|
reverseByteArray(buf[4+(bink.CurveParamWords * 4):4+2*(bink.CurveParamWords * 4)])
|
|
vPrintf("buf = 0x%x\n", buf)
|
|
e_b := sha1.Sum(buf)
|
|
|
|
a := make([]byte, len(e_b))
|
|
copy(a, e_b[:])
|
|
reverseByteArray(a)
|
|
e_ := new(big.Int).SetBytes(a)
|
|
e_.Rsh(e_, 4)
|
|
vPrintf("e' = 0x%x\n", e_)
|
|
// h_pkhashbits == e
|
|
// This *seems* to compare from "above" and hard-coded, but I'm not 100% sure
|
|
e_ = e_.And(e_, pkHashBitsMask)
|
|
vPrintf("e' & mask = 0x%x\n", e_)
|
|
|
|
if e.Cmp(e_) != 0 {
|
|
return 0, false, fmt.Errorf("bad signature (0x%x != 0x%x)", e, e_)
|
|
}
|
|
|
|
vPrintf("raw base PID: 0x%08x\n", rawpid)
|
|
isUpgrade := ((rawpid & 1) == 1)
|
|
pid := rawpid >> 1
|
|
return pid, isUpgrade, nil
|
|
}
|
|
|
|
func min(n, m uint) uint {
|
|
if n > m {
|
|
return m
|
|
} else {
|
|
return n
|
|
}
|
|
}
|
|
|
|
// Word-wise read in little-endian, with a bit shift at the end to make it "fit" the into the given amount of bits. Silly because it fits anyway. Was this obfuscation or just incompetence?
|
|
func bitcpycap32(buf []byte, bits uint) (*big.Int) {
|
|
ret := new(big.Int)
|
|
nw := (bits + 31) / 32 // round *up* to nearest amount of DWORDs
|
|
dw := make([]uint32, nw)
|
|
var i int
|
|
for i = 0; i < int(nw); i++ {
|
|
dw[i] = binary.LittleEndian.Uint32(buf[4*i:4*i+4])
|
|
dw[i] >>= (32 - min(bits, 32))
|
|
bits -= 32
|
|
}
|
|
for i = len(dw) - 1; i >= 0; i-- {
|
|
ret.Lsh(ret, 32)
|
|
ret.Or(ret, big.NewInt(int64(dw[i])))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func parseDecoded2002ProductKey(decoded *big.Int, bink BINK) (uint32, bool, error) {
|
|
// The site code bit length is hardcoded to be |0x7ff|
|
|
d := new(big.Int).Set(decoded)
|
|
pidMask := new(big.Int)
|
|
pidMask.SetBit(pidMask, 11, 1).Sub(pidMask, ONE) // (1 << 11) - 1; 11-bit hardcoded
|
|
dc := new(big.Int).Set(d)
|
|
siteCode := uint16(dc.And(dc, pidMask).Int64())
|
|
d.Rsh(d, 11)
|
|
|
|
// Need to keep (e, y) as big ints in case someone's feeding a fucky-wucky BINK. 2003 checks that |e| <= 32 though
|
|
pkHashBitsMask := new(big.Int)
|
|
pkHashBitsMask.SetBit(pkHashBitsMask, int(bink.PKHashBits), 1).Sub(pkHashBitsMask, ONE)
|
|
dc = new(big.Int).Set(d)
|
|
e := dc.And(dc, pkHashBitsMask)
|
|
d.Rsh(d, uint(bink.PKHashBits))
|
|
|
|
pkScalarBitsMask := new(big.Int)
|
|
pkScalarBitsMask.SetBit(pkScalarBitsMask, int(bink.PKScalarBits), 1).Sub(pkScalarBitsMask, ONE)
|
|
dc = new(big.Int).Set(d)
|
|
y := dc.And(dc, pkScalarBitsMask)
|
|
d.Rsh(d, uint(bink.PKScalarBits))
|
|
|
|
// 2003 product keys also have an "auth" value, see https://patents.google.com/patent/US20050036621A1/en
|
|
// We can't validate this part properly because it involves a hash with a large constant only MSFT has. They can use it to check for keygenned product keys though.
|
|
|
|
authValueBitsMask := new(big.Int)
|
|
authValueBitsMask.SetBit(authValueBitsMask, int(bink.AuthValueBits), 1).Sub(authValueBitsMask, ONE)
|
|
dc = new(big.Int).Set(d)
|
|
authValue := dc.And(dc, authValueBitsMask)
|
|
d.Rsh(d, uint(bink.AuthValueBits))
|
|
|
|
if d.Cmp(ZERO) != 0 {
|
|
return 0, false, fmt.Errorf("bad parse (leftover bits in product key: 0x%x)", d)
|
|
}
|
|
|
|
vPrintf("e = 0x%x\ny = 0x%x\n", e, y)
|
|
|
|
hasher := sha1.New()
|
|
hasher.Write([]byte{0x5d})
|
|
vPrintf("siteCode = %x\n", siteCode)
|
|
binary.Write(hasher, binary.LittleEndian, siteCode)
|
|
buf := make([]byte, 4)
|
|
e.FillBytes(buf)
|
|
reverseByteArray(buf)
|
|
vPrintf("e = %x\n", buf)
|
|
hasher.Write(buf)
|
|
buf = make([]byte, 2)
|
|
authValue.FillBytes(buf)
|
|
reverseByteArray(buf)
|
|
vPrintf("authValue = %x\n", buf)
|
|
hasher.Write(buf)
|
|
hasher.Write([]byte{0x00, 0x00})
|
|
digest := hasher.Sum(nil)
|
|
|
|
vPrintf("digest = 0x%x\n", digest)
|
|
|
|
H := bitcpycap32(digest, uint(bink.PKScalarBits))
|
|
|
|
vPrintf("H = 0x%x\n", H)
|
|
|
|
yB := bink.Curve.ScalarMult(y, bink.B)
|
|
HK := bink.Curve.ScalarMult(H, bink.K)
|
|
Q := bink.Curve.AddPoints(yB, HK)
|
|
R := bink.Curve.ScalarMult(y, Q)
|
|
vPrintf("R = %x\n", R)
|
|
bink.Curve.Affinize(&R)
|
|
|
|
vPrintf("R_affine.x = 0x%x\n", R.X)
|
|
vPrintf("R_affine.y = 0x%x\n", R.Y)
|
|
|
|
hasher = sha1.New()
|
|
hasher.Write([]byte{0x79})
|
|
binary.Write(hasher, binary.LittleEndian, siteCode)
|
|
buf = make([]byte, bink.CurveParamWords * 4)
|
|
R.X.FillBytes(buf)
|
|
reverseByteArray(buf)
|
|
hasher.Write(buf)
|
|
R.Y.FillBytes(buf)
|
|
reverseByteArray(buf)
|
|
hasher.Write(buf)
|
|
digest = hasher.Sum(nil)
|
|
|
|
vPrintf("digest = 0x%x\n", digest)
|
|
|
|
entropy := bitcpycap32(digest, uint(bink.PKHashBits + bink.PIDBits))
|
|
|
|
e_ := new(big.Int).Set(entropy)
|
|
e_.And(e_, pkHashBitsMask)
|
|
|
|
if e.Cmp(e_) != 0 {
|
|
return 0, false, fmt.Errorf("bad signature (0x%x != 0x%x)", e, e_)
|
|
}
|
|
|
|
lopid := entropy.Rsh(entropy, uint(bink.PKHashBits))
|
|
|
|
vPrintf("lopid = %b (0x%x)\n", lopid, lopid)
|
|
|
|
// Yes, + 100000, not |. A large lopid can and will spill into the displayed site code
|
|
return uint32(lopid.Int64() + 1000000 * (int64(siteCode) >> 1)), (siteCode & 1) != 0, nil
|
|
}
|
|
|
|
func readRawBINKFromFile(f *os.File, binks *[]BINK) error {
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fi.Size() > 1024*1024 { // arbitrarily restrict to BINKs <= 1MiB
|
|
return fmt.Errorf("possible raw BINK too big (%d bytes)", fi.Size())
|
|
}
|
|
buf := make([]byte, fi.Size())
|
|
if _, err := f.Read(buf); err != nil {
|
|
return err
|
|
}
|
|
bink, err := readAndParseBINKAtOffset(0, bytes.NewReader(buf))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*binks = append(*binks, bink)
|
|
return nil
|
|
}
|
|
|
|
func readBINKsFromFile(path string, binks *[]BINK) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pf, err := pe.NewFile(f)
|
|
if err != nil { // invalid PE, might still be a valid raw BINK
|
|
return readRawBINKFromFile(f, binks)
|
|
}
|
|
var is32bit bool
|
|
var offset, size uint32
|
|
var rsrcVirtualAddress uint32
|
|
switch oh := pf.OptionalHeader.(type) {
|
|
case *pe.OptionalHeader32:
|
|
is32bit = true
|
|
de := oh.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE]
|
|
vPrintf("0x%x: 0x%x\n", de.VirtualAddress, de.Size)
|
|
for _, sec := range pf.Sections {
|
|
if sec.VirtualAddress == de.VirtualAddress {
|
|
vPrintf("found section: %v @ 0x%x (de size: 0x%x, sec size: 0x%x)\n", sec.Name, sec.Offset, de.Size, sec.Size)
|
|
offset = sec.Offset
|
|
size = de.Size
|
|
rsrcVirtualAddress = de.VirtualAddress
|
|
break
|
|
}
|
|
}
|
|
case *pe.OptionalHeader64:
|
|
is32bit = false
|
|
fmt.Println("not 32-bit")
|
|
}
|
|
vPrintf("32-bit: %v, offset: 0x%x, size: 0x%x\n", is32bit, offset, size)
|
|
|
|
rsdir := make([]byte, size)
|
|
if _, err := f.ReadAt(rsdir, int64(offset)); err != nil {
|
|
return err
|
|
}
|
|
brrsdir := bytes.NewReader(rsdir)
|
|
var dir ResourceDirectory
|
|
readResourceDirectoryAt(0, &dir, brrsdir)
|
|
var binkdir ResourceDirectoryEntry
|
|
hasbinkdir := false
|
|
for _, e := range dir.Entries {
|
|
if e.IsNamed && e.Name == "BINK" {
|
|
binkdir = e
|
|
hasbinkdir = true
|
|
break
|
|
}
|
|
}
|
|
if !hasbinkdir {
|
|
return errors.New("cannot find BINK resource directory")
|
|
}
|
|
var idir ResourceDirectory
|
|
readResourceDirectoryAt(binkdir.Offset, &idir, brrsdir)
|
|
|
|
for _, e := range idir.Entries {
|
|
if !e.IsDirectory {
|
|
return errors.New("unexpected non-directory in BINK resource directory")
|
|
}
|
|
var idir ResourceDirectory
|
|
readResourceDirectoryAt(e.Offset, &idir, brrsdir)
|
|
for _, ee := range idir.Entries {
|
|
vPrintf("%v\n", ee)
|
|
if ee.IsDirectory {
|
|
return errors.New("unexpected directory in inner BINK resource directory")
|
|
}
|
|
|
|
var rdata ImageResourceDataEntry
|
|
brrsdir.Seek(int64(ee.Offset), io.SeekStart)
|
|
if err := binary.Read(brrsdir, binary.LittleEndian, &rdata); err != nil {
|
|
return err
|
|
}
|
|
vPrintf("%+v\n", rdata)
|
|
|
|
// Grab the BINK now; rdata has an RVA, so we need to subtract the RVA of .rsrc
|
|
binkOffset := (rdata.RelativeVirtualAddress - rsrcVirtualAddress)
|
|
// absolute position in file: offset + binkOffset
|
|
vPrintf("BINK Offset: 0x%x\n", binkOffset)
|
|
|
|
bink, err := readAndParseBINKAtOffset(binkOffset, brrsdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*binks = append(*binks, bink)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkProductKeyAgainstBINKs(productKey, alphabet string, binks []BINK) (uint32, bool, *BINK, error) {
|
|
biAlphabetLen := big.NewInt(int64(len(alphabet)))
|
|
decoded := new(big.Int)
|
|
for _, c := range productKey {
|
|
if c == '-' {
|
|
continue
|
|
}
|
|
|
|
n := int64(strings.IndexRune(alphabet, c))
|
|
if n == -1 {
|
|
return 0, false, nil, fmt.Errorf("Unknown character %c in product key %s", c, productKey)
|
|
}
|
|
|
|
decoded.Add(big.NewInt(n), decoded.Mul(biAlphabetLen, decoded))
|
|
}
|
|
vPrintf("\ndecoded: 0x%x\n", decoded)
|
|
|
|
for _, bink := range binks {
|
|
vPrintf("trying BINK %+v\n", bink)
|
|
var pid uint32
|
|
var isUpgrade bool
|
|
var err error
|
|
if bink.Version == 19980206 {
|
|
pid, isUpgrade, err = parseDecoded1998ProductKey(decoded, bink)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
} else {
|
|
pid, isUpgrade, err = parseDecoded2002ProductKey(decoded, bink)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
return pid, isUpgrade, &bink, nil
|
|
}
|
|
return 0, false, nil, errors.New("no matching BINK")
|
|
}
|
|
|
|
func makeCheckDigit(n uint32) uint32 {
|
|
s := uint32(0)
|
|
for ; n != 0; n /= 10 {
|
|
s += n % 10
|
|
}
|
|
return 7 - (s % 7)
|
|
}
|
|
|
|
func isRandomizablePID(pid uint32) bool {
|
|
if pid == 460000000 {
|
|
return true
|
|
}
|
|
siteCode := pid / 1000000
|
|
return (siteCode == 270 || siteCode == 335 || (siteCode >= 980 && siteCode <= 983))
|
|
}
|
|
|
|
func generateAndWriteBINK(path string, binkId uint32) {
|
|
bink, err := generateBINK(binkId)
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("cannot generate BINK: %v", err))
|
|
}
|
|
|
|
fmt.Printf("p = %d\n", bink.Curve.P)
|
|
fmt.Printf("B = (%d, %d)\n", bink.B.X, bink.B.Y)
|
|
fmt.Printf("K = (%d, %d)\n", bink.K.X, bink.K.Y)
|
|
fmt.Printf("q = %d\n", bink.BasePointOrder)
|
|
fmt.Printf("k = %d\n", bink.SecretKey)
|
|
|
|
fmt.Printf("\n\n\tSAVE THE k AND q VALUES SOMEWHERE!\n\n\n")
|
|
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("cannot open %s: %v", path, err))
|
|
}
|
|
|
|
_, err = f.Write(bink.Bytes())
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("cannot write %s: %v", path, err))
|
|
}
|
|
|
|
err = f.Close()
|
|
if err != nil {
|
|
log.Fatal(fmt.Errorf("error while closing %s: %v", path, err))
|
|
}
|
|
|
|
fmt.Printf("Wrote this BINK: %+v\n", bink)
|
|
}
|
|
|
|
type pathArray []string
|
|
|
|
func (a *pathArray) String() string {
|
|
var b strings.Builder
|
|
for _, e := range *a {
|
|
b.WriteString(e)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (a *pathArray) Set(v string) error {
|
|
*a = append(*a, strings.TrimSpace(v))
|
|
return nil
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Printf("usage: %s -i DLL/BINK [-i DLL/BINK...] product_key...\n\n", os.Args[0])
|
|
fmt.Printf("example: %s -i xp_sp1.dll -i res0.bink TB32G-8C8RG-TP7RM-TV7VC-CYFDJ WB8RR-Q4R9P-B46B8-9XMFW-BRGXY HWMVT-FB8QC-YTR96-G8VVV-3XBJ7\n", os.Args[0])
|
|
fmt.Printf("\nto generate a new BINK: %s -G -i newkey.bink binkResourceId\n", os.Args[0])
|
|
}
|
|
|
|
func main() {
|
|
var paths pathArray
|
|
var generateBINKMode bool
|
|
flag.Var(&paths, "i", "input file (BINK or DLL)")
|
|
flag.BoolVar(&runVerbose, "v", false, "run (very) verbosely")
|
|
flag.BoolVar(&generateBINKMode, "G", false, "generate new BINK")
|
|
flag.Parse()
|
|
binks := []BINK{}
|
|
|
|
colorReset := "\033[0m"
|
|
colorRed := "\033[31m"
|
|
colorGreen := "\033[32m"
|
|
|
|
if len(paths) == 0 {
|
|
fmt.Printf("no paths with -i given\n")
|
|
usage()
|
|
return
|
|
}
|
|
|
|
if generateBINKMode {
|
|
for i, path := range paths {
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
fmt.Printf("no bink ID given\n")
|
|
usage()
|
|
return
|
|
}
|
|
binkId, err := strconv.Atoi(args[i])
|
|
if err != nil {
|
|
fmt.Printf("%s is not a valid BINK ID: %v\n", args[0], err)
|
|
return
|
|
}
|
|
generateAndWriteBINK(path, uint32(binkId))
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, path := range paths {
|
|
vPrintf("visiting path %s\n", path)
|
|
if err := readBINKsFromFile(path, &binks); err != nil {
|
|
fmt.Printf("warning: cannot read BINK(s) from %s: %v\n", path, err)
|
|
}
|
|
}
|
|
|
|
// parse product key, then validate
|
|
|
|
args := flag.Args()
|
|
for _, productKey := range args {
|
|
productKey = strings.ToUpper(productKey)
|
|
pid, isUpgrade, bink, err := checkProductKeyAgainstBINKs(productKey, "BCDFGHJKMPQRTVWXY2346789", binks)
|
|
if err != nil {
|
|
fmt.Printf("[%s-%s] %s => %v\n", colorRed, colorReset, productKey, err)
|
|
} else {
|
|
var b strings.Builder
|
|
if isUpgrade {
|
|
b.WriteString("upgrade, ")
|
|
}
|
|
if isRandomizablePID(pid) {
|
|
b.WriteString("randomizable, ")
|
|
}
|
|
b.WriteString(fmt.Sprintf("version: %d, BINK ID: 0x%02x", bink.Version, bink.ResourceId))
|
|
|
|
fmt.Printf("[%s+%s] %s => XXXXX-%03d-%06d%d-%02dXXX / XXXXX-OEM-XX%04dX-%05d [%s]\n",
|
|
colorGreen, colorReset,
|
|
productKey,
|
|
pid / 1000000, pid % 1000000, makeCheckDigit(pid % 1000000), bink.ResourceId / 2,
|
|
pid / 100000, pid % 100000,
|
|
b.String())
|
|
}
|
|
}
|
|
}
|
|
|