Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How is a Confirmation ID derived from the Installation ID in WPA? #2

Open
CONIGUERO opened this issue Apr 12, 2023 · 3 comments
Open
Labels
question Further information is requested

Comments

@CONIGUERO
Copy link

Would be pretty neat to incorporate a generator for it to get rid of WPA in a "legitimate" way without resorting to patches or third-party XP repacks.

@Endermanch
Copy link
Owner

That's a job for a way different keygen. The algorithm for Confirmation ID generation isn't publicly known and it would require deep research into XP's source code, particularly pidgen / dpcdll. The Microsoft call center still works, so you can generate a valid Confirmation ID via normal means still for free. The only publicly known and confirmed correct information about Installation ID's is on Licenturion. That's how far I am in my research.

https://www.licenturion.com/xp/fully-licensed-wpa.txt

What I can tell you for sure is such a keygen exists and I'm currently trying to acquire it.

@Endermanch Endermanch added the question Further information is requested label Apr 13, 2023
@CONIGUERO
Copy link
Author

The algorithm for Confirmation ID generation isn't publicly known and it would require deep research into XP's source code, particularly pidgen / dpcdll.

Bad news: the "good stuff" for the WPA system was stripped out of the WinXP code "release".
Good news: some kind soul decompiled the libraries and provided a nice zip for the code. Some variables have autogenerated names, but it should be a good starting point.
ds.zip_decompiled_XPSP1_winlogon (1).zip

@Endermanch
Copy link
Owner

Endermanch commented Apr 15, 2023

That doesn't help a ton, but I've figured the algorithm out. Huge props to diamondggg for providing necessary discussions and documents, here's how that works:

The Confirmation ID generation is based on a hyperelliptic curve of genus 2 with a set jacobian (generator):
image

There exists an algorithm that generates valid Confirmation IDs based on Installation IDs. The Installation ID is first unpacked using the Licenturion cheat sheet, then decrypted via a certain scheme. The rest of the job is finding such set of points that would satisfy the curve equation.

It must be noted that cryptographically that scheme is not secure.
The algorithm:

import hashlib

# 226512-274743-842923-777124-961370-722240-570042-517722-757426
installationId = 226512747484292777129613772224570045177275742
installationIdSize = 19 # 17 for XP Gold, 19 for SP1+ (includes 12 bits of sha1(product key))
# all three of following are valid generated
# 013705-060122-603141-961392-086136-909901-494476
confirmationId = 01370060126031496139086139099049447
# 022032-220754-159721-909624-985141-504586-914001
#confirmationId = 02203220751597290962985145045891400
# 137616-847280-708585-827476-874935-313366-790880
#confirmationId = 13761847287085882747874933133679088

def decrypt(encrypted, key):
    size_half = len(encrypted) // 2
    size_half_dwords = size_half - (size_half % 4)
    last = encrypted[size_half*2:]
    encrypted = encrypted[:size_half*2]
    for i in range(4):
        first = encrypted[:size_half]
        second = encrypted[size_half:]
        sha1_result = hashlib.sha1(first + key).digest()
        sha1_result = (sha1_result[:size_half_dwords] +
                       sha1_result[size_half_dwords+4-(size_half%4) : size_half+4-(size_half%4)])
        encrypted = bytes(x^^y for x,y in zip(second, sha1_result)) + first
    return encrypted + last

# unpack&decrypt installationId
iid = int(installationId).to_bytes(installationIdSize, byteorder='little')
iid = decrypt(iid, b'\x6A\xC8\x5E\xD4')
hwid = iid[:8]
productid = int.from_bytes(iid[8:17], byteorder='little')
productkeyhash = iid[17:]
pid1 = productid & ((1 << 17) - 1)
pid2 = (productid >> 17) & ((1 << 10) - 1)
pid3 = (productid >> 27) & ((1 << 25) - 1)
version = (productid >> 52) & 7
pid4 = productid >> 55

assert version == (4 if len(iid) == 17 else 5)

key = hwid + int((pid1 << 41 | pid2 << 58 | pid3 << 17 | pid4) & ((1 << 64) - 1)).to_bytes(8, byteorder='little')
# productkeyhash is not used for validation, it exists just to allow the activation server to reject keygenned pids

# now the math

p = 0x16A6B036D7F2A79
Fp = GF(p)
Fpx.<x> = Fp[]
E = HyperellipticCurve(x^5+0x1400606322B3B04*x^4+0x1400606322B3B04*x^3+0x44197B83892AD0*x^2+0x21840136C85381*x)
J = E.jacobian()

# deserialize divisor
x1 = confirmationId // (p + 1)
x2 = confirmationId % (p + 1)
if x1 <= p:
    # two or less points over GF(p)
    point1 = E.lift_x(Fp(-x1)) if x1 != p else None
    point2 = E.lift_x(Fp(-x2)) if x2 != p else None
    if point1 is not None and point2 is not None:
        # there are 4 variants of how lift_x() could select both y-s
        # we don't distinguish D and -D, but this still leaves 2 variants
        # the chosen one is encoded by order of x1 <=> x2
        lastbit1 = point1[1].lift() & 1
        lastbit2 = point2[1].lift() & 1
        if x2 < x1:
            if lastbit1 == lastbit2:
                point2 = E(point2[0], -point2[1])
        else:
            if lastbit1 != lastbit2:
                point2 = E(point2[0], -point2[1])
    point1 = J(point1) if point1 is not None else J(0)
    point2 = J(point2) if point2 is not None else J(0)
    divisor = point1 + point2
else:
    # a pair of conjugate points over GF(p^2)
    f = (x+x2)*(x+x2)-43*x1*x1 # 43 is the minimal quadratic non-residue in Fp
    Fp2 = GF(p^2)
    point1 = E.lift_x(f.roots(Fp2)[0][0])
    point2 = E(Fp2)(point1[0].conjugate(), point1[1].conjugate())
    divisor = J(Fp2)(point1) + J(Fp2)(point2)
    divisor = J(Fpx(divisor[0]), Fpx(divisor[1])) #return from Fp2 to Fp

d2 = divisor * 0x10001
assert d2[0].degree() == 2
x1 = d2[0][1]/2
x2 = sqrt((x1*x1-d2[0][0])/43)

encrypted = x1.lift() + (x2.lift() - 1) * p
encrypted = int(encrypted).to_bytes(14,byteorder='little')

# end of the math
decrypted = decrypt(encrypted, key)
print(decrypted.hex())
# 0000000000000001000000000000 for the first confirmationId
# 0000000000000002000000000000 for the second confirmationId
# 0000000000000006000000000000 for the last confirmationId
assert decrypted[8:] == b'\0' * 6
assert decrypted[7] <= 0x80
# all zeroes in decrypted[0:7] are okay for the checker
# more precisely: if decrypted[6] == 0, first 6 bytes can be anything
# otherwise, decrypted[0] = length, and decrypted[1:1+length] must match first length bytes of sha1(product key)

All credit belongs to diamondggg.

Repository owner locked as resolved and limited conversation to collaborators Apr 15, 2023
@Endermanch Endermanch reopened this Apr 15, 2023
@Endermanch Endermanch pinned this issue Apr 15, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants