GitHub avatar

Fox's Blog

Cape Mod: How to Steal Jeb_'s Cape With an RSA Signature Injection

A Fabric mod exploiting a logic flaw in Minecraft's trust system: an RSA signature valid from Mojang but replayed onto the wrong account. Code walkthrough, security implications, and cryptographic lessons.

Cape Mod: how to steal Jeb_'s cape with an RSA signature injection

alt text

What if I told you a valid RSA signature -- but for the wrong account -- is enough to make your friends believe you're wearing Mojang's official cape? Welcome to cape-mod, a Fabric exploit showing how Minecraft trusts a signature while skipping the check that the profile it belongs to is actually yours.

Context: how Minecraft handles skins and capes

In Java Edition, there's a rarely asked question: who is responsible for displaying a player's skin and cape -- the client or the server?

The nuanced answer:

Component Who sends it? Who downloads it?
Skin texture The server sends the signed URL The client downloads from textures.minecraft.net
Cape texture The server sends the signed URL The client downloads from textures.minecraft.net
textures property The server sends the GameProfile from Mojang auth The client verifies the RSA signature

The key point: everything sits in a property called textures on the GameProfile. That property holds:

  • A base64 JSON payload with the texture URLs
  • An RSA signature made with Mojang's private key

The RSA signature wall

Each textures property looks like this when decoded:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

The client verifies the RSA signature against the public key baked into the jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

For remote players (outside local play), the client only accepts skins marked secure -- i.e. with a valid signature:

// SkinManager.createLookup() -- simplified
PlayerSkin optional = ...
    .filter(ps -> !isRemote || ps.secure())  // ← remote players must be secure
    .orElse(defaultSkin);

That check blocks spoofing in theory. But validation stops there, and that's exactly where the attack starts.

The flaw: signature replay

The client checks that the RSA signature is valid. It systematically skips checking that the profileId inside the JSON matches the player's actual UUID.

In other words: a textures property taken from an existing Mojang account (say a Mojang employee's) can be replayed onto any other player. The signature stays valid -- it was genuinely produced by Mojang -- it just comes from a different account.

How to extract a real signature?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) has the Mojang Studios cape. From the Mojang session server:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Response:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

That field's signature was produced by Mojang. It's RSA-2048 SHA-1. It's absolutely valid, even if you replay it onto another UUID -- because Jeb_'s signature stays Jeb_'s signature, and the client skips checking who it was meant for.

The code: how the mod works

The cape-mod mod is tiny -- 65 lines of Java. Here's the core:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Replace the textures property with Jeb_'s
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Steps:

  1. Mixin on Player.getGameProfile() -- the point where the player's profile is returned
  2. Check it's a local server (Integrated Server)
  3. Check it's the host (LAN world)
  4. Replace the textures property with Jeb_'s (hardcoded)
  5. Return a new GameProfile with the injected textures

The GameProfile is therefore forged: it's an artificially built profile that doesn't match the real player. The textures properties are replayed from Jeb_ -- the RSA signature is authentic but applied to the wrong profile. The network packet, on the other hand, is legitimate: the server normally sends the ClientboundPlayerInfoUpdatePacket with that modified profile. It's the profile that's forged; the packet stays legitimate.

When the host's friends join over LAN, they receive the ClientboundPlayerInfoUpdatePacket with the modified profile. The client:

  1. Decodes the base64 payload
  2. Verifies the RSA signature → ✅ valid (it's really Jeb_'s)
  3. Marks the skin as secure=true (since the signature is valid)
  4. Passes the !isRemote || ps.secure() filter → ✅ passes
  5. Downloads and displays Jeb_'s cape

In-game result: the cape on your skin

Here's what it looks like in game. First, front view with Jeb_'s cape displayed on the host:

Cape Mod -- Jeb_ cape shown on the host

You can clearly see the red/white pattern of the official Mojang Studios cape. The render is identical to a real Jeb_ wearing his own cape -- the client downloads exactly the same texture from textures.minecraft.net.

And in immersive view, in a real game:

Cape Mod -- in-game view with visible cape

The cape floats behind the player, waves with movement. Perfectly indistinguishable from an authentic skin with an official cape.

Another angle, in a world with lava and terrain:

Cape Mod -- cape in a natural environment

And a final close-up of real gameplay, where you see the cape in action:

Cape Mod -- cape in classic Minecraft gameplay

For someone joining a LAN without knowing the host uses a mod, the cape looks visually identical to a real Mojang cape. That's precisely the point: the signature is valid, so the client treats it as legitimate.

Why it's a flaw (and why it isn't one)

It's ironic: the exploit works precisely because the signature is valid. This isn't a crypto bypass -- it's worse: a logic flaw in the trust model.

Check Result
RSA signature validity ✅ Valid (signed by Mojang for Jeb_)
Does the profileId in the payload match the host's UUID? ❌ No (Jeb_'s UUID ≠ host's UUID)
Does the client check the match? ❌ No. Only the RSA signature is verified.

Minecraft trusts the signature rather than the identity of whoever carries it. As long as the signature comes from Mojang, the client accepts it. It's like showing a fake passport signed by the government -- the seal is legitimate, even though the passport doesn't belong to you.

Security implications

Scope limited to LAN

The mod only works on an integrated server (LAN). The attacker must:

  • Have a Fabric mod installed
  • Be the host of a LAN world
  • Their friends connect without mods (vanilla)

But the possibilities widen

In theory, with the same technique, you could:

  • Reinject other signed data: heads, illegal enchantments, malicious chat components
  • Combine with a LAN tunnel (NGROK, playit.gg, Radmin VPN) to affect players over the internet
  • Extend to other profile properties that rely on signatures

Why Mojang probably won't patch

This isn't a "vulnerability" in the strict sense -- the signature is valid. Patching it would require Mojang to change the whole authentication model, which is complex. For now, it's an edge case: LAN players are assumed to trust each other.

The philosophical trap

Cape Mod is an excellent proof of concept of a broader truth: you must always check who signed data and for which subject before trusting it.

It's a basic crypto lesson. RSA signs a message; the recipient's identity stays outside the signature. If I hand you a valid Mojang RSA signature, all you know is that Mojang signed something, with no reliable indication of the recipient from the message alone.

That's exactly what happened with SSL/TLS certificates in the 2000s when CAs signed certificates without properly verifying the domain -- the signature was valid, but it applied to the wrong domain.

Conclusion

Cape Mod is something other than a hack in the classic sense -- it's an elegant exploitation of a missing logic check in Minecraft. It shows that:

  1. A valid signature doesn't guarantee the identity of whoever carries it
  2. On LAN, trust is weaker than you'd think
  3. Minecraft's textures properties are essentially injected content -- you must check they match the player carrying them

If you join a LAN world on an "unknown" server (or rather, one whose host runs a suspicious mod), you already have a security problem way before the cape. But it's revealing: Minecraft assumes everyone on a LAN trusts each other. That's true... until the first host abuses that trust.


Resources

3 key points

  1. Valid RSA signatures validate a message, not an identity -- a detail that has cost many systems dearly.
  2. Minecraft skips checking that the player's profile matches the signature it receives -- a logic flaw in validation, with intact cryptography.
  3. On LAN or tunneled, it's all open bar for a mod controlling the integrated server.

Cape Mod : comment voler la cape de Jeb_ avec une injection de signature RSA

Un mod Fabric qui exploite une faille logique dans le système de confiance de Minecraft : une signature RSA valide de Mojang mais replayée sur un mauvais compte. Explication du code, implications de sécurité et leçons cryptographiques.

Cape Mod : comment voler la cape de Jeb_ avec une injection de signature RSA

alt text

Et si je te disais qu'il suffisait d'une signature RSA valide -- mais pour le mauvais compte -- pour faire croire à tes amis que tu portes la cape officielle de Mojang ? Bienvenue dans cape-mod, un exploit Fabric qui montre comment Minecraft fait confiance à une signature tout en omettant de vérifier que le profil auquel elle appartient est effectivement le tien.

Le contexte : comment Minecraft gère les skins et les capes

Dans Java Edition, il y a une question rarement posée : qui est responsable d'afficher le skin et la cape d'un joueur -- le client ou le serveur ?

La réponse est nuancée :

Composant Qui l'envoie ? Qui le télécharge ?
Texture de skin Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Texture de cape Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Propriété textures Le serveur envoie le GameProfile depuis l'auth Mojang Le client vérifie la signature RSA

Le point clé : tout est contenu dans une propriété appelée textures du GameProfile. Cette propriété contient :

  • Un payload JSON en base64 avec les URLs des textures
  • Une signature RSA faite avec la clé privée de Mojang

Le mur de la signature RSA

Chaque propriété textures ressemble à ça quand on la décode :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Le client vérifie la signature RSA contre la clé publique embarquée dans le jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Pour les joueurs distants (hors jeu local), le client accepte uniquement les skins marqués comme secure -- c'est-à-dire avec une signature valide :

// SkinManager.createLookup() -- simplifié
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← les joueurs distants doivent être sécurisés
    .orElse(defaultSkin);

Ce check bloque le spoofing en théorie. Mais la validation s'arrête là, et c'est précisément là que l'attaque commence.

La faille : signature replay

Le client vérifie que la signature RSA est valide. Il omet systématiquement de vérifier que le profileId contenu dans le JSON correspond à l'UUID réel du joueur.

Autrement dit : une propriété textures prise d'un compte Mojang existant (par exemple celui d'un employé Mojang) peut être replay sur n'importe quel autre joueur. La signature reste valide -- elle a été réellement produite par Mojang -- elle vient juste d'un autre compte.

Comment extraire une vraie signature ?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) a la cape Mojang Studios. Depuis le serveur de session Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Réponse :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature de ce champ value a été produite par Mojang. C'est RSA-2048 SHA-1. Elle est absolument valide, même si tu la replays sur un autre UUID -- parce que la signature de Jeb_ reste une signature de Jeb_, et le client omet de contrôler à qui elle était destinée.

Le code : comment le mod fonctionne

Le mod cape-mod est minuscule -- 65 lignes de Java. Voici le cœur :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Remplace la propriété textures par celle de Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Étapes :

  1. Mixin sur Player.getGameProfile() -- le point où le profil du joueur est retourné
  2. Vérifie que c'est un serveur local (Integrated Server)
  3. Vérifie que c'est le host (LAN world)
  4. Remplace la propriété textures par celle de Jeb_ (hardcodée)
  5. Retourne un nouveau GameProfile avec les textures injectées

Le GameProfile est donc forgé : c'est un profil construit artificiellement, qui ne correspond pas au vrai joueur. Les propriétés textures sont replayées depuis Jeb_ -- la signature RSA est authentique mais appliquée au mauvais profil. Le paquet réseau, lui, est légitime : le serveur envoie normalement le ClientboundPlayerInfoUpdatePacket avec ce profil modifié. C'est le profil qui est forgé ; le paquet reste légitime.

Quand les amis du host rejoignent via LAN, ils reçoivent le ClientboundPlayerInfoUpdatePacket avec le profil modifié. Le client :

  1. Décode le payload base64
  2. Vérifie la signature RSA → ✅ valide (c'est réellement celle de Jeb_)
  3. Marque le skin comme secure=true (car la signature est valide)
  4. Passe le filtre !isRemote || ps.secure() → ✅ passe
  5. Télécharge et affiche la cape de Jeb_

Résultat en jeu : la cape sur ton skin

Voici ce que ça donne in-game. D'abord, vue de face avec la cape de Jeb_ affichée sur le host :

Cape Mod -- Jeb_ cape affichée sur le host

On voit clairement le motif rouge/blanc de la cape officielle Mojang Studios. Le rendu reste identique à celui d'un vrai Jeb_ portant sa propre cape -- le client télécharge exactement la même texture depuis textures.minecraft.net.

Et en vue immersive, dans une vraie partie :

Cape Mod -- vue en jeu avec cape visible

La cape flotte derrière le joueur, ondule avec le mouvement. Parfaitement indistinguible d'un skin authentique avec cape officielle.

Autre angle, dans un monde avec lave et terrain :

Cape Mod -- cape dans un environnement naturel

Et une dernière vue rapprochée du gameplay réel, où on voit la cape en action :

Cape Mod -- cape en gameplay classique Minecraft

Pour quelqu'un qui rejoindrait un LAN en ignorant que le host utilise un mod, la cape reste visuellement identique à une vraie cape Mojang. C'est précisément le point : la signature est valide, le client la traite donc comme légitime.

Pourquoi c'est une faille (et pourquoi ce n'en est pas une)

C'est ironique : l'exploit fonctionne précisément parce que la signature est valide. Il s'agit d'autre chose qu'un bypass cryptographique -- c'est pire : une faille logique dans le modèle de confiance.

Check Résultat
Validité de la signature RSA ✅ Valide (signée par Mojang pour Jeb_)
Le profileId dans le payload correspond-il à l'UUID du host ? ❌ Non (Jeb_'s UUID ≠ UUID du host)
Le client vérifie-t-il la correspondance ? ❌ Non. Seule la signature RSA est vérifiée.

Minecraft fait confiance à la signature plutôt qu'à l'identité de celui qui la porte. Tant que la signature vient de Mojang, le client l'accepte. C'est comme montrer un faux passeport signé par le gouvernement -- le sceau est légitime, même si le passeport ne t'appartient pas.

Les implications de sécurité

Portée limitée au LAN

Le mod fonctionne uniquement sur un serveur intégré (LAN). L'attaquant doit :

  • Avoir un mod Fabric installé
  • Être le host d'un monde LAN
  • Ses amis se connectent sans mod (vanilla)

Mais les possibilités s'élargissent

En théorie, avec la même technique, on pourrait :

  • Réinjecter d'autres données signées : heads, enchantements illégaux, composants de chat malveillants
  • Combiner avec un tunnel LAN (NGROK, playit.gg, Radmin VPN) pour affecter des joueurs sur internet
  • Étendre à d'autres propriétés du profil qui dépendent de signatures

Pourquoi Mojang ne va probablement pas patcher

Il s'agit d'autre chose qu'une "vulnérabilité" au sens strict -- la signature est valide. Patcher ça demanderait à Mojang de modifier le modèle d'authentification complet, ce qui est complexe. Pour l'instant, c'est un cas limite : les joueurs LAN sont supposés se faire confiance.

Le piège philosophique

Cape Mod est un excellent proof of concept d'une vérité plus large : tu dois toujours vérifier qui a signé une donnée et pour quel sujet avant de lui faire confiance.

C'est une leçon en cryptographie de base. RSA signe un message ; l'identité du destinataire reste hors de la signature. Si je te donne une signature RSA valide de Mojang, tu sais uniquement que Mojang a signé quelque chose, sans indication fiable sur le destinataire à partir du seul message.

C'est exactement ce qui s'est passé avec les certificats SSL/TLS dans les années 2000 quand les CAs signaient des certificats sans vérifier correctement le domaine -- la signature était valide, mais elle s'appliquait au mauvais domaine.

Conclusion

Cape Mod relève d'autre chose qu'un hack au sens classique -- c'est une exploitation élégante d'un manque de validation logique dans Minecraft. Il montre que :

  1. Une signature valide ne garantit pas l'identité de celui qui la porte
  2. En LAN, la confiance est plus faible qu'on ne le croit
  3. Les propriétés textures de Minecraft sont essentiellement du contenu injecté -- il faut vérifier qu'elles correspondent au joueur qui les porte

Si tu rejoins un monde LAN sur un serveur "inconnu" (ou plutôt, dont le host a un mod suspect), tu as déjà un problème de sécurité bien avant la cape. Mais c'est révélateur : Minecraft suppose que tout le monde sur un LAN se fait confiance. C'est vrai... jusqu'au premier host qui abuse de cette confiance.


Ressources

3 points clés

  1. Les signatures RSA valident un message plutôt qu'une identité -- un détail qui a coûté cher à de nombreux systèmes.
  2. Minecraft omet de vérifier que le profil du joueur correspond à la signature qu'il reçoit -- une faille logique dans la validation, avec une cryptographie intacte.
  3. En LAN ou en tunnel, tout est open bar pour un mod qui contrôle le serveur intégré.

Cape Mod:用 RSA 签名注入偷走 Jeb_ 的披风

一个利用 Minecraft 信任模型逻辑漏洞的 Fabric mod:Mojang 的有效 RSA 签名,被重放到错误的账号上。代码解析、安全影响和密码学教训。

Cape Mod:用 RSA 签名注入偷走 Jeb_ 的披风

alt text

如果我说,只需要一个 RSA 签名,有效,但属于错误的账号,就能让你的朋友相信你穿着 Mojang 官方披风呢?欢迎来到 cape-mod,一个展示 Minecraft 如何信任签名、却忘了验证签名所属 profile 是不是你本人的 exploit。

背景:Minecraft 怎么处理皮肤和披风

Java 版里有个很少被问的问题:到底谁负责显示玩家的皮肤和披风,是客户端还是服务器?

答案有点绕:

组件 谁发的? 谁下载的?
皮肤材质 服务器发签名 URL 客户端从 textures.minecraft.net 下载
披风材质 服务器发签名 URL 客户端从 textures.minecraft.net 下载
textures 属性 服务器从 Mojang 认证发 GameProfile 客户端验 RSA 签名

关键全在 GameProfile 的 textures 属性里。包含:

  • base64 JSON payload,带材质 URL
  • Mojang 私钥做的 RSA 签名

RSA 签名这堵墙

每个 textures 属性解码出来长这样:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

客户端拿 jar 里嵌的公钥(yggdrasil_session_pubkey.der)验 RSA 签名:

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

对远程玩家(非本地游戏),客户端只接受标为 secure 的皮肤,也就是签名有效的:

// SkinManager.createLookup() -- 简化
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← 远程玩家必须 secure
    .orElse(defaultSkin);

这道检查理论上堵死了伪造。但验证到这里就停了,攻击恰恰从这里开始。

漏洞:签名重放

客户端验证 RSA 签名有效。但它从来不验证 JSON 里的 profileId 是不是这个玩家的真实 UUID。

换句话说:从一个真实存在的 Mojang 账号(比如 Mojang 员工号)扒下来的 textures 属性,可以重放到任何别的玩家身上。签名依然有效,它确实是 Mojang 签的,只是签的不是这个号。

怎么搞到真签名?

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)有 Mojang Studios 披风。从 Mojang session 服务器拿:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

返回:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

这个 value 的 signature 是 Mojang 亲手签的。RSA-2048 SHA-1。绝对有效,哪怕你把它重放到别的 UUID 上,因为 Jeb_ 的签名永远是 Jeb_ 的签名,而客户端压根不查它本来发给谁。

代码:mod 怎么工作的

cape-mod 很小,65 行 Java。核心在这里:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // 把 textures 属性换成 Jeb_ 的
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

步骤:

  1. Mixin 到 Player.getGameProfile(),玩家 profile 返回的地方
  2. 确认是本地服务器(Integrated Server)
  3. 确认是房主(LAN 世界)
  4. 把 textures 属性换成硬编码的 Jeb_ 那份
  5. 返回新的 GameProfile

所以 GameProfile 是伪造的:人工拼出来的 profile,对应不上真玩家。textures 属性是从 Jeb_ 那重放来的,RSA 签名真 authentic,但贴错了 profile。网络包本身合法:服务器正常发 ClientboundPlayerInfoUpdatePacket,带的 profile 被改过。是 profile 伪造,包合法。

朋友通过 LAN 进来,收到带改后 profile 的 ClientboundPlayerInfoUpdatePacket,客户端:

  1. 解 base64 payload
  2. 验 RSA 签名 → ✅ 有效(本来就是 Jeb_ 的)
  3. 标成 secure=true(签名有效嘛)
  4. 过 !isRemote || ps.secure() 过滤 → ✅ 通过
  5. 下载显示 Jeb_ 的披风

游戏内效果:披风穿你身上

先看效果。正面,房主身上挂着 Jeb_ 的披风:

Cape Mod -- 房主身上的 Jeb_ 披风

红白 Mojang Studios 官方披风纹样清清楚楚。渲染和真 Jeb_ 穿自己披风一模一样,客户端从 textures.minecraft.net 下的是同一个材质。

沉浸视角,真实对局里:

Cape Mod -- 对局中的披风

披风在身后飘,随移动摆动。和正版官方披风的皮肤完全 indistinguishable。

换个角度,岩浆地形:

Cape Mod -- 自然环境中的披风

再来张正常 gameplay 近景,披风实战:

Cape Mod -- 经典 Minecraft 玩法中的披风

对不知情进 LAN 的人来说,这披风视觉上和真 Mojang 披风没区别。关键就在:签名有效,客户端就当真的处理。

为什么说这是漏洞(又为什么不算)

讽刺的是:exploit 能成立,恰恰因为签名有效。这不是密码学 bypass,比那更糟:是信任模型的逻辑漏洞。

检查项 结果
RSA 签名有效性 ✅ 有效(Mojang 给 Jeb_ 签的)
payload 里 profileId 和房主 UUID 对得上吗? ❌ 对不上(Jeb_ 的 UUID ≠ 房主 UUID)
客户端验对应关系吗? ❌ 不验。只验 RSA 签名。

Minecraft 信任的是签名,不是签名属于谁。只要签名出自 Mojang,客户端就收。这就像拿着政府盖真章的假护照,章是真的,护照不是你的。

安全影响

范围限于 LAN

mod 只在内置服务器(LAN)上工作。攻击者要:

  • 装 Fabric mod
  • 当 LAN 世界房主
  • 朋友用 vanilla 客户端进来

但思路能放大

理论上同一手法可以:

  • 重放别的签名数据:heads、非法附魔、恶意聊天组件
  • 配 LAN 穿透(NGROK、playit.gg、Radmin VPN)打到公网玩家
  • 扩展到 profile 的别的签名属性

Mojang 大概率不会修

这严格说不算"漏洞",签名有效。要修得动整个认证模型,成本巨大。目前算边界情况:LAN 玩家默认互相信任。

哲学陷阱

Cape Mod 是个极好的 proof of concept,讲了一个更大的道理:信任签名数据之前,永远要验谁签的、签给谁。

这是密码学基本课。RSA 签的是消息;收件人身份不在签名里。我给你一个 Mojang 的有效 RSA 签名,你只知道 Mojang 签过什么东西,光看消息看不出签给谁。

2000 年代 SSL/TLS 证书干过一模一样的事,CA 不好好验证域名就签发,签名有效,发错了域名。

结语

Cape Mod 不是传统意义的 hack,是对 Minecraft 缺了一块逻辑校验的优雅利用。说明了:

  1. 有效签名不保证持有人身份,这个坑坑过无数系统
  2. LAN 里信任比你以为的弱
  3. Minecraft 的 textures 属性本质上是注入内容,得验它和穿的人对不对得上

如果你进了陌生人的 LAN 世界(或者说,房主装了可疑 mod),披风之前你早就有更大的安全问题。但它很说明问题:Minecraft 默认 LAN 里人人互信。没错……直到第一个滥用信任的房主出现。


资源

三句话总结

  1. RSA 签名保证的是消息,不是身份,这个细节坑过无数系统。
  2. Minecraft 不验玩家 profile 和收到的签名对不对得上,密码学没坏,校验逻辑缺了一块。
  3. LAN 或穿透下,控制内置服务器的 mod 为所欲为。

ケープMod:Jeb_のケープをRSA署名インジェクションで盗む方法

Mojangの信頼システムの論理欠陥を突くFabric mod。有効なRSA署名なのに間違ったアカウントにリプレイされる。コード解説、セキュリティへの示唆、暗号の教訓。

ケープMod:Jeb_のケープをRSA署名インジェクションで盗む方法

alt text

有効なRSA署名1つで、でも間違ったアカウントのもので、友達にMojang公式ケープを着てると思わせられると言ったらどうする。cape-modへようこそ。署名は信頼するのに、そのプロフィールが本当に自分のものか検証し忘れるMinecraftの仕組みを突くエクスプロイトだ。

背景:Minecraftがスキンとケープをどう扱うか

Java版にはめったに問われない疑問がある。プレイヤーのスキンとケープを表示する責任はクライアントとサーバーのどっちにあるのか。

答えは微妙だ。

部品 誰が送る? 誰が落とす?
スキンテクスチャ サーバーが署名付きURLを送る クライアントがtextures.minecraft.netから落とす
ケープテクスチャ サーバーが署名付きURLを送る クライアントがtextures.minecraft.netから落とす
texturesプロパティ サーバーがMojang認証のGameProfileを送る クライアントがRSA署名を検証する

ポイントは全部GameProfileのtexturesってプロパティに入ってることだ。このプロパティはこうだ。

  • テクスチャURL入りJSONペイロードのbase64
  • Mojang秘密鍵によるRSA署名

RSA署名の壁

各texturesプロパティをデコードするとこう見える。

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

クライアントはRSA署名をjar内蔵の公開鍵(yggdrasil_session_pubkey.der)で検証する。

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

リモートプレイヤー(ローカルゲーム外)には**secure印**のスキンだけ受け入れる。つまり有効な署名付きだ。

// SkinManager.createLookup()(簡略化)
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // リモートプレイヤーは安全であること
    .orElse(defaultSkin);

理論上このチェックがなりすましを止める。でも検証はそこで終わりだ。攻撃が始まるのはまさにそこからだ。

欠陥:署名リプレイ

クライアントはRSA署名が有効か検証する。JSON内のprofileIdがプレイヤーの本当UUIDと一致するかを体系的に見落とす。

つまり、既存Mojangアカウント(Mojang社員とか)から取ったtexturesプロパティを他の任意プレイヤーにリプレイできる。署名は有効なままだ。本当にMojangが作ったものだから。ただ別のアカウントのものってだけだ。

本物の署名をどう抜き出すか

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)はMojang Studiosケープを持ってる。Mojangセッションサーバーからこう取る。

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

返事:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

このvalueフィールドのsignatureはMojang製だ。RSA-2048のSHA-1だ。他UUIDにリプレイしても絶対に有効だ。Jeb_の署名はJeb_の署名のままで、クライアントは誰宛か管理し忘れるからね。

コード:modの仕組み

cape-modはちっぽけだ。Java 65行だ。心臓部はこうだ。

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // texturesプロパティをJeb_のものに置き換える
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

手順:

  1. Player.getGameProfile()へのMixin。プレイヤープロフィールが返される地点だ
  2. ローカルサーバー(統合サーバー)か確認する
  3. ホストか(LANワールド)確認する
  4. texturesプロパティをJeb_のものに置き換える(ハードコード)
  5. 注入済みテクスチャの新しいGameProfileを返す

GameProfileは偽造される。つまり本物のプレイヤーに対応しない人工プロフィールだ。texturesプロパティはJeb_からリプレイされる。RSA署名は本物だけど間違ったプロフィールに適用されてる。ネットワークパケット自体は正規だ。サーバーは普通にこの変更済みプロフィール付きClientboundPlayerInfoUpdatePacketを送る。偽造されてるのはプロフィールで、パケットは正規のままってことだ。

ホストの友達がLAN経由で参加すると、変更済みプロフィール付きClientboundPlayerInfoUpdatePacketを受け取る。クライアントはこうする。

  1. base64ペイロードをデコードする
  2. RSA署名を検証する → 有効(本当にJeb_のものだ)
  3. スキンをsecure=trueに印付けする(署名が有効だから)
  4. フィルタ!isRemote || ps.secure()を通る → 通る
  5. Jeb_のケープをダウンロードして表示する

ゲーム内の結果:スキンにケープが付く

ゲーム内でこう見える。まずホストに表示されたJeb_ケープの正面図だ。

Cape Mod -- ホストに表示されたJeb_ケープ

Mojang Studios公式ケープの赤白模様がはっきり見える。レンダリングは本物のJeb_が自分のケープを着てるのと同一だ。クライアントはtextures.minecraft.netからまったく同じテクスチャを落としてるからね。

没入視点では、本物の対局でこうだ。

Cape Mod -- ケープが見えるゲーム内ビュー

ケープはプレイヤーの後ろではためいて、動きに合わせて揺れる。公式ケープ付き正規スキンと完全に区別不能だ。

別の角度、溶岩と地形のあるワールドでこうだ。

Cape Mod -- 自然環境のケープ

最後に古典的Minecraftゲームプレイ中のクローズアップ。ケープが動いてるのが見える。

Cape Mod -- 古典的ゲームプレイ中のケープ

ホストがmodを使ってると知らずにLAN参加した人には、ケープは本物のMojangケープと見た目が同じだ。まさにポイントはそこだ。署名は有効で、クライアントは正規として扱う。

なぜ欠陥で、なぜ欠陥じゃないのか

皮肉なのは、エクスプロイトがまさに署名が有効だから動くことだ。暗号突破とは別の話だ。もっと悪い。信頼モデルの論理欠陥だ。

チェック 結果
RSA署名の有効性 有効(Jeb_向けにMojang署名)
ペイロード内profileIdはホストUUIDと一致するか 不一致(Jeb_のUUIDはホストのUUIDじゃない)
クライアントは一致を検証するか いいえ。RSA署名だけ検証される。

Minecraftは身分を着る人じゃなく署名を信頼してる。Mojang由来の署名ならクライアントは受け入れる。政府発行の印鑑付き偽パスポートを見せるようなものだ。印鑑は正規だけど、パスポートは持ち主のものじゃない。

セキュリティへの示唆

範囲はLANに限定

modは統合サーバー(LAN)でのみ動く。攻撃者はこうでなきゃいけない。

  • Fabric modを入れてる
  • LANワールドのホストだ
  • 友達はmodなしバニラで参加する

でも可能性は広がる

理論上同じ手口でこうもできる。

  • 他の署名付きデータを再注入する。head、不正エンチャント、悪性チャット部品
  • LANトンネル(NGROK、playit.gg、Radmin VPN)と組み合わせてネット上のプレイヤーに影響させる
  • 署名に依存するプロフィールの他プロパティに拡張する

Mojangがたぶん直さない理由

厳密な意味の「脆弱性」とは別の話だ。署名は有効だからね。直すにはMojangが認証モデル全体を変えなきゃいけなくて複雑だ。今のところ際どいケースだ。LANプレイヤーは互いを信頼する前提だからね。

哲学的な罠

Cape Modは優れた概念実証だ。もっと広い真実を示す。署名したデータと誰宛かを常に検証してから信頼しなきゃいけないってことだ。

基礎暗号の教訓だ。RSAはメッセージに署名する。受取人の身分は署名の外にある。Mojangの有効RSA署名を渡されても分かるのは、Mojangが何かに署名したことだけで、メッセージだけから受取人を確実には示さない。

2000年代にCAがドメインをちゃんと検証せず証明書に署名したSSLやTLS証明書で起きたのとまったく同じだ。署名は有効だったけど、間違ったドメインに適用されてた。

結論

Cape Modは古典的な意味のハックとは別のものだ。Minecraftの論理検証欠落を突く優雅なエクスプロイトだ。示すのはこうだ。

  1. 有効な署名は着る人の身分を保証しない
  2. LANでは信頼は思うより弱い
  3. Minecraftのtexturesプロパティは実質注入コンテンツだ。着るプレイヤーに対応するか検証しなきゃいけない

「知らない」LANワールド(というかホストが怪しいmod入り)に参加するなら、ケープ以前にもっと深刻なセキュリティ問題がある。でも示唆的だ。MinecraftはLAN全員が互いを信頼する前提だ。それは正しい。信頼を悪用する最初のホストまではね。


リソース

要点3つ

  1. RSA署名は身分じゃなくメッセージを検証する。多くのシステムに痛い目を見させた詳細だ。
  2. Minecraftはプレイヤープロフィールが受け取る署名に対応するか検証し忘れる。暗号は無傷なのに検証の論理欠陥だ。
  3. LANやトンネルでは、統合サーバーを握るmodに全部お任せだ。

Cape Mod: RSA 서명 주입으로 Jeb_의 망토 훔치기

Minecraft의 신뢰 시스템에 있는 논리적 결함을 노리는 Fabric 모드. Mojang의 유효한 RSA 서명을 잘못된 계정에 재생했어요. 코드 설명과 보안 함의, 암호학적 교훈.

Cape Mod: RSA 서명 주입으로 Jeb_의 망토 훔치기

alt text

유효한 RSA 서명 하나, 다만 잘못된 계정 앞으로 된 것으로 친구들에게 Mojang 공식 망토를 입은 것처럼 믿게 할 수 있다면 어떨까요? cape-mod에 오신 것을 환영해요. 서명은 믿으면서 그 서명이 속한 프로필이 정말 여러분 것인지 확인하는 것을 빠뜨리는 Minecraft를 보여주는 Fabric 익스플로잇이에요.

배경: Minecraft가 스킨과 망토를 다루는 방법

Java Edition에는 잘 묻지 않는 질문이 있어요. 플레이어의 스킨과 망토를 표시할 책임은 클라이언트와 서버 중 누구에게 있을까요?

답은 미묘해요.

구성 요소 누가 보내나요? 누가 다운로드하나요?
스킨 텍스처 서버가 서명된 URL을 보내요 클라이언트가 textures.minecraft.net에서 다운로드해요
망토 텍스처 서버가 서명된 URL을 보내요 클라이언트가 textures.minecraft.net에서 다운로드해요
textures 속성 서버가 Mojang 인증의 GameProfile을 보내요 클라이언트가 RSA 서명을 검증해요

핵심은 모든 것이 GameProfile의 textures라는 속성에 담긴다는 거예요. 이 속성에 들어있는 것은요.

  • 텍스처 URL이 담긴 base64 JSON 페이로드
  • Mojang 개인 키로 만든 RSA 서명

RSA 서명의 벽

각 textures 속성을 디코딩하면 이렇게 생겼어요.

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

클라이언트는 jar에 내장된 공개 키(yggdrasil_session_pubkey.der)로 RSA 서명을 검증해요.

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

원격 플레이어(로컬 게임 바깥)의 경우 클라이언트는 secure로 표시된 스킨, 즉 서명이 유효한 것만 받아들여요.

// SkinManager.createLookup() -- 단순화
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← 원격 플레이어는 안전해야 해요
    .orElse(defaultSkin);

이 검사는 이론상 스푸핑을 막아요. 하지만 검증은 거기서 멈추고, 바로 그 지점에서 공격이 시작돼요.

결함: 서명 재생

클라이언트는 RSA 서명이 유효한지 검증해요. JSON 안에 든 profileId가 플레이어의 실제 UUID와 일치하는지는 체계적으로 확인하지 않아요.

다시 말하면, 기존 Mojang 계정(예를 들어 Mojang 직원 계정)에서 가져온 textures 속성을 다른 어떤 플레이어에게든 재생할 수 있어요. 서명은 여전히 유효해요. 실제로 Mojang이 만든 것이니까요. 그저 다른 계정에서 온 것뿐이에요.

진짜 서명을 어떻게 꺼낼까요?

Jeb_(UUID 853c80ef-3c37-49fd-aa49-938b674adae6)은 Mojang Studios 망토를 가지고 있어요. Mojang 세션 서버에서요.

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

응답:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

이 value 필드의 signature는 Mojang이 만든 거예요. RSA-2048 SHA-1이에요. 다른 UUID에 재생해도 절대적으로 유효해요. Jeb_의 서명은 여전히 Jeb_의 서명이고, 클라이언트는 그것이 누구를 위한 것이었는지 확인하지 않으니까요.

코드: 모드가 동작하는 방법

cape-mod 모드는 아주 작아요. Java 65줄이에요. 핵심은 이렇습니다.

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // textures 속성을 Jeb_의 것으로 바꿔요
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

단계:

  1. Player.getGameProfile()에 Mixin. 플레이어 프로필이 반환되는 지점이에요
  2. 로컬 서버(Integrated Server)인지 확인
  3. 호스트(LAN 월드)인지 확인
  4. textures 속성을 하드코딩된 Jeb_의 것으로 교체
  5. 주입된 텍스처가 담긴 새 GameProfile 반환

그래서 GameProfile은 위조된 거예요. 진짜 플레이어와 일치하지 않는 인위적으로 만든 프로필이에요. textures 속성은 Jeb_에게서 재생된 거예요. RSA 서명은 진짜지만 잘못된 프로필에 적용됐어요. 네트워크 패킷 자체는 정당해요. 서버는 수정된 프로필이 담긴 ClientboundPlayerInfoUpdatePacket을 정상적으로 보내요. 위조된 것은 프로필이고, 패킷은 정당하게 남아 있어요.

호스트의 친구들이 LAN으로 들어오면 수정된 프로필이 담긴 ClientboundPlayerInfoUpdatePacket을 받아요. 클라이언트는요.

  1. base64 페이로드를 디코딩
  2. RSA 서명 검증 → ✅ 유효 (실제로 Jeb_의 것이에요)
  3. 서명이 유효하니 스킨을 secure=true로 표시
  4. !isRemote || ps.secure() 필터 통과 → ✅ 통과
  5. Jeb_의 망토를 다운로드하고 표시

게임 속 결과: 스킨 위의 망토

게임에서 보이는 모습은 이렇습니다. 먼저 호스트에 표시된 Jeb_ 망토를 정면에서 본 모습이에요.

Cape Mod -- 호스트에 표시된 Jeb_ 망토

공식 Mojang Studios 망토의 빨강과 흰색 무늬가 선명히 보여요. 렌더링은 자신의 망토를 입은 진짜 Jeb_와 똑같아요. 클라이언트는 textures.minecraft.net에서 정확히 같은 텍스처를 다운로드하거든요.

그리고 실제 플레이 장면의 몰입 시점이에요.

Cape Mod -- 망토가 보이는 게임 속 화면

망토가 플레이어 뒤에서 펄럭이고 움직임에 따라 흔들려요. 공식 망토가 있는 진짜 스킨과 완벽히 구분할 수 없어요.

다른 각도, 용암과 지형이 있는 월드에서요.

Cape Mod -- 자연 환경 속 망토

그리고 실제 고전 Minecraft 게임플레이를 가까이서 본 마지막 장면, 망토가 실제로 보이는 모습이에요.

Cape Mod -- 고전 Minecraft 게임플레이 속 망토

호스트가 모드를 쓴다는 것을 모르고 LAN에 들어온 사람에게는, 망토가 진짜 Mojang 망토와 시각적으로 똑같아요. 바로 그 점이 핵심이에요. 서명이 유효해서 클라이언트가 정품으로 처리해요.

왜 결함인지 (그리고 왜 결함이 아닌지)

아이러니하게도 이 익스플로잇은 서명이 유효하기 바로 그 이유로 동작해요. 암호 우회와는 다른 이야기예요. 더 나빠요. 신뢰 모델의 논리적 결함이에요.

검사 결과
RSA 서명 유효성 ✅ 유효 (Jeb_를 위해 Mojang이 서명)
페이로드의 profileId가 호스트 UUID와 일치하나요? ❌ 아니요 (Jeb_의 UUID ≠ 호스트의 UUID)
클라이언트가 일치를 검증하나요? ❌ 아니요. RSA 서명만 검증해요.

Minecraft는 서명을 든 사람의 신원보다 서명을 믿어요. 서명이 Mojang에서 온 한 받아들여요. 정부가 서명한 위조 여권을 보여주는 것과 같아요. 도장은 진짜지만, 여권이 여러분 것은 아니에요.

보안 함의

LAN으로 제한된 범위

모드는 통합 서버(LAN)에서만 동작해요. 공격자는 다음을 갖춰야 해요.

  • 설치된 Fabric 모드
  • LAN 월드의 호스트 자리
  • 친구들은 모드 없이(vanilla) 접속

하지만 가능성은 넓어져요

이론적으로 같은 기법으로 다음을 할 수 있어요.

  • 서명된 다른 데이터 재주입: 머리, 불법 인챈트, 악의적 채팅 컴포넌트
  • LAN 터널(NGROK, playit.gg, Radmin VPN)과 결합해서 인터넷의 플레이어에게 영향
  • 서명에 의존하는 프로필의 다른 속성으로 확장

Mojang이 아마 패치하지 않을 이유

엄밀한 의미의 "취약점"과는 다른 이야기예요. 서명은 유효해요. 패치하려면 Mojang이 인증 모델 전체를 바꿔야 해서 복잡해요. 지금은 경계 사례예요. LAN 플레이어는 서로 신뢰한다고 가정하니까요.

철학적 함정

Cape Mod는 더 넓은 진실에 대한 훌륭한 개념 증명이에요. 데이터를 신뢰하기 전에 누가 서명했고 누구를 위해 서명했는지 항상 확인해야 해요.

기본 암호학 교훈이에요. RSA는 메시지에 서명해요. 수신자의 신원은 서명 밖에 있어요. Mojang의 유효한 RSA 서명을 드리면, 여러분이 아는 것은 Mojang이 무언가에 서명했다는 것뿐이고, 메시지 하나만으로는 수신자에 대한 믿을 만한 표시가 없어요.

2000년대에 CA가 도메인을 제대로 확인하지 않고 인증서에 서명했을 때 SSL과 TLS 인증서에 정확히 일어났던 일이에요. 서명은 유효했지만 잘못된 도메인에 적용됐어요.

결론

Cape Mod는 고전적 의미의 핵과는 다른 것이에요. Minecraft의 논리적 검증 부재를 우아하게 파고든 거예요. 보여주는 것은요.

  1. 유효한 서명이 서명을 든 사람의 신원을 보장하지 않아요
  2. LAN에서는 신뢰가 생각보다 약해요
  3. Minecraft의 textures 속성은 본질적으로 주입된 콘텐츠예요. 그것을 든 플레이어와 일치하는지 확인해야 해요

"알 수 없는" 월드(정확히는 호스트가 수상한 모드를 가진)의 LAN 월드에 들어가면, 망토 이전에 이미 보안 문제가 있어요. 하지만 시사하는 바가 있어요. Minecraft는 LAN의 모든 사람이 서로 신뢰한다고 가정해요. 그 가정은 맞아요. 신뢰를 남용하는 첫 호스트가 나오기 전까지는요.


자료

핵심 3가지

  1. RSA 서명은 신원보다 메시지를 검증해요. 많은 시스템에 비싼 대가를 치르게 한 세부 사항이에요.
  2. Minecraft는 플레이어 프로필이 받은 서명과 일치하는지 확인하지 않아요. 암호학은 온전한 채 검증의 논리적 결함이에요.
  3. LAN이나 터널에서는 통합 서버를 장악한 모드에게 모든 것이 열려 있어요.

Cape Mod: RSA imzası enjeksiyonuyla Jeb_'in pelerini nasıl çalınır

Minecraft'ın güven sistemindeki mantıksal bir açıktan yararlanan bir Fabric modu: Mojang'dan geçerli ama yanlış hesapta tekrar oynatılan bir RSA imzası. Kodun açıklaması, güvenlik etkileri ve kriptografik dersler.

Cape Mod: RSA imzası enjeksiyonuyla Jeb_'in pelerini nasıl çalınır

alt text

Sana arkadaşlarına resmi Mojang pelerini taktığına inandırmak için yanlış hesap için geçerli bir RSA imzasının yettiğini söylesem? Minecraftın imzaya güvenip onun gerçekten sana ait profile ait olduğunu doğrulamayı unuttuğunu gösteren bir Fabric exploit olan cape-moda hoş geldin.

Bağlam: Minecraft görünümleri ve pelerinleri nasıl yönetir

Java Editionda nadiren sorulan bir soru vardır: bir oyuncunun görünümünü ve pelerinini göstermekten kim sorumludur: istemci mi sunucu mu?

Yanıt inceliklidir:

Bileşen Kim gönderir? Kim indirir?
Görünüm dokusu Sunucu imzalı URL gönderir İstemci textures.minecraft.net adresinden indirir
Pelerin dokusu Sunucu imzalı URL gönderir İstemci textures.minecraft.net adresinden indirir
textures özelliği Sunucu Mojang kimlik doğrulamadan GameProfile gönderir İstemci RSA imzasını doğrular

Kilit nokta: her şey GameProfileın textures adlı bir özelliğinde bulunur. Bu özellik şunları içerir:

  • Dokuların URLleriyle base64 bir JSON yükü
  • Mojangın özel anahtarıyla yapılmış bir RSA imzası

RSA imzası duvarı

Her textures özelliği kodu çözülünce şuna benzer:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

İstemci RSA imzasını jara gömülü açık anahtara karşı doğrular (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Uzak oyuncular için (yerel oyun dışında) istemci yalnızca güvenli olarak işaretli görünümleri kabul eder: yani geçerli imzalı olanları:

// SkinManager.createLookup() -- basitleştirilmiş
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← uzak oyuncular güvenli olmalı
    .orElse(defaultSkin);

Bu kontrol teoride sahteciliği engeller. Ama doğrulama burada durur ve saldırı tam olarak burada başlar.

Açık: imza tekrarı

İstemci RSA imzasının geçerli olduğunu doğrular. JSON içindeki profileIdnin oyuncunun gerçek UUIDsiyle eşleştiğini doğrulamayı sistematik olarak unutur.

Başka deyişle: mevcut bir Mojang hesabından (örneğin bir Mojang çalışanının hesabı) alınmış bir textures özelliği başka herhangi bir oyuncuda tekrar oynatılabilir. İmza geçerli kalır: gerçekten Mojang tarafından üretilmiştir, sadece başka bir hesaptan gelir.

Gerçek bir imza nasıl çıkarılır?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) Mojang Studios pelerinine sahiptir. Mojang oturum sunucusundan:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Yanıt:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

Bu value alanının signature değeri Mojang tarafından üretilmiştir. RSA-2048 SHA-1dir. Başka bir UUID üzerinde tekrar oynatsan bile kesinlikle geçerlidir: çünkü Jeb_in imzası Jeb_in imzası olarak kalır ve istemci kime yönelik olduğunu kontrol etmeyi unutur.

Kod: mod nasıl çalışır

cape-mod modu miniciktir: 65 satır Java. İşte özü:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // textures özelliğini Jeb_inkiyle değiştir
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Adımlar:

  1. Player.getGameProfile() üzerinde Mixin: oyuncu profilinin döndürüldüğü nokta
  2. Yerel bir sunucu olduğunu doğrular (Integrated Server)
  3. Host olduğunu doğrular (LAN dünyası)
  4. textures özelliğini Jeb_inkiyle değiştirir (sabit kodlanmış)
  5. Enjekte edilmiş dokularla yeni bir GameProfile döndürür

GameProfile bu yüzden sahtedir: gerçek oyuncuya karşılık gelmeyen yapay inşa edilmiş bir profildir. textures özellikleri Jeb_den tekrar oynatılmıştır: RSA imzası gerçektir ama yanlış profile uygulanmıştır. Ağ paketi ise meşrudur: sunucu değiştirilmiş profille ClientboundPlayerInfoUpdatePacketi normal gönderir. Sahte olan profildir; paket meşru kalır.

Hostun arkadaşları LAN üzerinden katıldığında değiştirilmiş profille ClientboundPlayerInfoUpdatePacket alırlar. İstemci:

  1. base64 yükü çözer
  2. RSA imzasını doğrular → ✅ geçerli (gerçekten Jeb_inki)
  3. Görünümü secure=true olarak işaretler (imza geçerli olduğu için)
  4. !isRemote || ps.secure() filtresinden geçer → ✅ geçer
  5. Jeb_in pelerinini indirip gösterir

Oyunda sonuç: görünümünde pelerin

İşte oyun içinde verdiği sonuç. Önce host üzerinde gösterilen Jeb_ peleriniyle önden görünüm:

Cape Mod: host üzerinde gösterilen Jeb_ pelerini

Resmi Mojang Studios pelerininin kırmızı/beyaz deseni açıkça görülüyor. Render gerçek Jeb_in kendi pelerinini takmasıyla aynı kalır: istemci textures.minecraft.net adresinden tam aynı dokuyu indirir.

Ve sürükleyici görünümde, gerçek bir oyunda:

Cape Mod: görünür pelerinle oyun içi görünüm

Pelerin oyuncunun arkasında dalgalanır, hareketle salınır. Resmi pelerinli gerçek bir görünümden mükemmel ayırt edilemez.

Başka bir açı, lavlı ve arazili bir dünyada:

Cape Mod: doğal bir ortamda pelerin

Ve pelerini aksiyonda gördüğümüz klasik Minecraft oynanışına yakından son bir bakış:

Cape Mod: klasik Minecraft oynanışında pelerin

Hostun bir mod kullandığını bilmeden bir LANa katılacak biri için pelerin görsel olarak gerçek bir Mojang peleriniyle aynı kalır. Nokta tam olarak budur: imza geçerlidir, istemci onu meşru sayar.

Neden bir açıktır (ve neden değildir)

İroniktir: exploit tam olarak imza geçerli olduğu için çalışır. Kriptografik bir bypass söz konusu değildir: daha kötüdür, güven modelinde mantıksal bir açık.

Kontrol Sonuç
RSA imzasının geçerliliği ✅ Geçerli (Mojang tarafından Jeb_ için imzalı)
Yükteki profileId hostun UUIDsiyle eşleşiyor mu? ❌ Hayır (Jeb_in UUIDsi ≠ hostun UUIDsi)
İstemci eşleşmeyi doğruluyor mu? ❌ Hayır. Yalnızca RSA imzası doğrulanır.

Minecraft onu taşıyanın kimliğinden çok imzaya güvenir. İmza Mojangdan geldiği sürece istemci kabul eder. Bu devletçe mühürlü sahte bir pasaport göstermek gibidir: ait olmasa bile mühür meşrudur.

Güvenlik etkileri

Kapsam LAN ile sınırlı

Mod yalnızca entegre bir sunucuda (LAN) çalışır. Saldırgan şunları yapmalıdır:

  • Kurulu bir Fabric moduna sahip olmak
  • Bir LAN dünyasının hostu olmak
  • Arkadaşları modsuz (vanilla) bağlanır

Ama olasılıklar genişler

Teoride aynı teknikle şunlar yapılabilir:

  • Diğer imzalı verileri yeniden enjekte etmek: kafalar, yasa dışı büyüler, kötü niyetli sohbet bileşenleri
  • İnternetteki oyuncuları etkilemek için bir LAN tüneliyle (NGROK, playit.gg, Radmin VPN) birleştirmek
  • İmzaya bağlı profilin diğer özelliklerine genişletmek

Mojang muhtemelen yamalamayacak, neden

Katın anlamıyla bir "güvenlik açığı" söz konusu değildir: imza geçerlidir. Bunu yamalamak Mojangın tüm kimlik doğrulama modelini değiştirmesini gerektirir ki bu karmaşıktır. Şimdilik bu bir sınır durumudur: LAN oyuncularının birbirine güvendiği varsayılır.

Felsefi tuzak

Cape Mod daha geniş bir gerçeğin mükemmel bir kavram kanıtıdır: bir veriye güvenmeden önce onu kimin imzaladığını ve hangi konu için olduğunu hep doğrulamalısın.

Bu temel bir kriptografi dersidir. RSA bir mesajı imzalar; alıcının kimliği imzanın dışında kalır. Sana Mojangdan geçerli bir RSA imzası verirsem, yalnızca Mojangın bir şeyi imzaladığını bilirsin, yalnızca mesaja bakarak alıcı hakkında güvenilir belirti olmadan.

2000lerde CAlar alanı doğru doğrulamadan sertifika imzaladığında SSL/TLS sertifikalarında tam olarak olan buydu: imza geçerliydi ama yanlış alana uygulanıyordu.

Sonuç

Cape Mod klasik anlamda bir hackten başka şeydir: Minecrafttaki mantıksal doğrulama eksikliğinin zarif bir suistimalidir. Şunu gösterir:

  1. Geçerli bir imza onu taşıyanın kimliğini garanti etmez
  2. LANda güven sanılandan daha zayıftır
  3. Minecraftın textures özellikleri esasen enjekte edilmiş içeriktir: onları taşıyan oyuncuya karşılık geldikleri doğrulanmalıdır

"bilinmeyen" bir LAN dünyasına katılırsan (daha doğrusu hostunda şüpheli bir mod olan) pelerinden çok önce bir güvenlik sorunun vardır. Ama açıklayıcıdır: Minecraft LANdaki herkesin birbirine güvendiğini varsayar. Bu doğrudur... bu güveni suistimal eden ilk hosta kadar.


Kaynaklar

3 kilit nokta

  1. RSA imzaları kimlikten çok bir mesajı doğrular: birçok sisteme pahalıya mal olmuş bir detay.
  2. Minecraft oyuncu profilinin aldığı imzayla eşleştiğini doğrulamayı unutur: kriptografisi bozulmamış, doğrulamada mantıksal bir açık.
  3. LANda ya da tünelde entegre sunucuyu kontrol eden bir mod için her şey serbesttir.

Cape Mod: come rubare il mantello di Jeb_ con un'iniezione di firma RSA

Un mod Fabric che sfrutta una falla logica nel sistema di fiducia di Minecraft: una firma RSA valida di Mojang ma rigiocata sull'account sbagliato. Spiegazione del codice, implicazioni di sicurezza e lezioni crittografiche.

Cape Mod: come rubare il mantello di Jeb_ con un'iniezione di firma RSA

alt text

E se ti dicessi che bastava una firma RSA valida: ma per l'account sbagliato: per far credere ai tuoi amici che porti il mantello ufficiale di Mojang? Benvenuto in cape-mod, un exploit Fabric che mostra come Minecraft si fida di una firma omettendo di verificare che il profilo a cui appartiene sia davvero il tuo.

Il contesto: come Minecraft gestisce skin e mantelli

In Java Edition, c'è una domanda raramente posta: chi è responsabile di mostrare la skin e il mantello di un giocatore: il client o il server?

La risposta è sfumata:

Componente Chi lo invia? Chi lo scarica?
Texture di skin Il server invia l'URL firmata Il client scarica da textures.minecraft.net
Texture di mantello Il server invia l'URL firmata Il client scarica da textures.minecraft.net
Proprietà textures Il server invia il GameProfile dall'auth Mojang Il client verifica la firma RSA

Il punto chiave: tutto è contenuto in una proprietà chiamata textures del GameProfile. Questa proprietà contiene:

  • Un payload JSON in base64 con gli URL delle texture
  • Una firma RSA fatta con la chiave privata di Mojang

Il muro della firma RSA

Ogni proprietà textures assomiglia a questo quando la decodifichi:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Il client verifica la firma RSA contro la chiave pubblica integrata nel jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
// verifica la firma RSA del valore contro la chiave pubblica
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Per i giocatori remoti (fuori dal gioco locale), il client accetta solo skin segnate come secure: cioè con una firma valida:

// SkinManager.createLookup() -- semplificato
// i giocatori remoti devono essere sicuri
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← i giocatori remoti devono essere sicuri
    .orElse(defaultSkin);

Questo check blocca lo spoofing in teoria. Ma la validazione si ferma lì, ed è proprio lì che inizia l'attacco.

La falla: signature replay

Il client verifica che la firma RSA sia valida. Omette sistematicamente di verificare che il profileId contenuto nel JSON corrisponda all'UUID reale del giocatore.

In altre parole: una proprietà textures presa da un account Mojang esistente (per esempio quello di un impiegato Mojang) può essere rigiocata su qualsiasi altro giocatore. La firma resta valida: è stata davvero prodotta da Mojang: viene solo da un altro account.

Come estrarre una vera firma?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) ha il mantello Mojang Studios. Dal server di sessione Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Risposta:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature di questo campo value è stata prodotta da Mojang. È RSA-2048 SHA-1. È assolutamente valida, anche se la rigiochi su un altro UUID: perché la firma di Jeb_ resta una firma di Jeb_, e il client omette di controllare a chi era destinata.

Il codice: come funziona il mod

Il mod cape-mod è minuscolo: 65 righe di Java. Ecco il cuore:

// inietta le texture firmate di Jeb_ nel profilo dell'host
@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Sostituisce la proprietà textures con quella di Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Passi:

  1. Mixin su Player.getGameProfile(): il punto dove il profilo del giocatore è restituito
  2. Verifica che sia un server locale (Integrated Server)
  3. Verifica che sia l'host (mondo LAN)
  4. Sostituisce la proprietà textures con quella di Jeb_ (hardcodata)
  5. Restituisce un nuovo GameProfile con le texture iniettate

Il GameProfile è quindi forgiato: è un profilo costruito artificialmente, che non corrisponde al vero giocatore. Le proprietà textures sono rigiocate da Jeb_: la firma RSA è autentica ma applicata al profilo sbagliato. Il pacchetto rete, lui, è legittimo: il server invia normalmente il ClientboundPlayerInfoUpdatePacket con questo profilo modificato. È il profilo che è forgiato; il pacchetto resta legittimo.

Quando gli amici dell'host si uniscono via LAN, ricevono il ClientboundPlayerInfoUpdatePacket col profilo modificato. Il client:

  1. Decodifica il payload base64
  2. Verifica la firma RSA: valida (è davvero quella di Jeb_)
  3. Segna la skin come secure=true (perché la firma è valida)
  4. Passa il filtro !isRemote || ps.secure(): passa
  5. Scarica e mostra il mantello di Jeb_

Risultato in gioco: il mantello sulla tua skin

Ecco cosa dà in-game. Prima, vista frontale col mantello di Jeb_ mostrato sull'host:

Cape Mod -- Jeb_ cape affichée sur le host

Si vede chiaramente il motivo rosso/bianco del mantello ufficiale Mojang Studios. Il rendering resta identico a quello di un vero Jeb_ che porta il suo mantello: il client scarica esattamente la stessa texture da textures.minecraft.net.

E in vista immersiva, in una vera partita:

Cape Mod -- vue en jeu avec cape visible

Il mantello fluttua dietro il giocatore, ondeggia col movimento. Perfettamente indistinguibile da una skin autentica con mantello ufficiale.

Altro angolo, in un mondo con lava e terreno:

Cape Mod -- cape dans un environnement naturel

E un'ultima vista ravvicinata del gameplay reale, dove si vede il mantello in azione:

Cape Mod -- cape en gameplay classique Minecraft

Per qualcuno che raggiungesse una LAN ignorando che l'host usa un mod, il mantello resta visivamente identico a un vero mantello Mojang. È proprio il punto: la firma è valida, il client la tratta quindi come legittima.

Perché è una falla (e perché non lo è)

È ironico: l'exploit funziona proprio perché la firma è valida. Si tratta d'altro che un bypass crittografico: è peggio: una falla logica nel modello di fiducia.

Check Risultato
Validità della firma RSA Valida (firmata da Mojang per Jeb_)
Il profileId nel payload corrisponde all'UUID dell'host? No (UUID di Jeb_ diverso da UUID dell'host)
Il client verifica la corrispondenza? No. Solo la firma RSA è verificata.

Minecraft si fida della firma invece che dell'identità di chi la porta. Finché la firma viene da Mojang, il client l'accetta. È come mostrare un passaporto falso firmato dal governo: il sigillo è legittimo, anche se il passaporto non ti appartiene.

Le implicazioni di sicurezza

Portata limitata alla LAN

Il mod funziona solo su un server integrato (LAN). L'attaccante deve:

  • Avere un mod Fabric installato
  • Essere l'host di un mondo LAN
  • I suoi amici si connettono senza mod (vanilla)

Ma le possibilità si allargano

In teoria, con la stessa tecnica, si potrebbe:

  • Re-iniettare altri dati firmati: teste, incantesimi illegali, componenti di chat malevoli
  • Combinare con un tunnel LAN (NGROK, playit.gg, Radmin VPN) per colpire giocatori su internet
  • Estendere ad altre proprietà del profilo che dipendono da firme

Perché Mojang probabilmente non patcherà

Si tratta d'altro che una "vulnerabilità" in senso stretto: la firma è valida. Patcharla richiederebbe a Mojang di modificare il modello di autenticazione completo, il che è complesso. Per ora, è un caso limite: i giocatori LAN sono supposti fidarsi.

La trappola filosofica

Cape Mod è un ottimo proof of concept di una verità più ampia: devi sempre verificare chi ha firmato un dato e per quale soggetto prima di fidarti.

È una lezione di crittografia di base. RSA firma un messaggio; l'identità del destinatario resta fuori dalla firma. Se ti do una firma RSA valida di Mojang, sai solo che Mojang ha firmato qualcosa, senza indicazione affidabile sul destinatario dal solo messaggio.

È esattamente ciò che è successo coi certificati SSL/TLS negli anni 2000 quando le CA firmavano certificati senza verificare correttamente il dominio: la firma era valida, ma si applicava al dominio sbagliato.

Conclusione

Cape Mod è d'altro che un hack in senso classico: è uno sfruttamento elegante di una mancanza di validazione logica in Minecraft. Mostra che:

  1. Una firma valida non garantisce l'identità di chi la porta
  2. In LAN, la fiducia è più debole di quanto si creda
  3. Le proprietà textures di Minecraft sono essenzialmente contenuto iniettato: bisogna verificare che corrispondano al giocatore che le porta

Se ti unisci a un mondo LAN su un server "sconosciuto" (o meglio, il cui host ha un mod sospetto), hai già un problema di sicurezza ben prima del mantello. Ma è rivelatore: Minecraft suppone che tutti su una LAN si fidino. È vero... fino al primo host che abusa di questa fiducia.


Risorse

3 punti chiave

  1. Le firme RSA validano un messaggio invece di un'identità: un dettaglio costato caro a molti sistemi.
  2. Minecraft omette di verificare che il profilo del giocatore corrisponda alla firma che riceve: una falla logica nella validazione, con crittografia intatta.
  3. In LAN o in tunnel, è tutto open bar per un mod che controlla il server integrato.

Cape Mod: wie man Jeb_s Cape mit einer RSA-Signaturinjektion klaut

Ein Fabric-Mod, der eine Logiklücke im Vertrauenssystem von Minecraft ausnutzt: eine gültige RSA-Signatur von Mojang, aber replayt auf ein falsches Konto. Erklärung des Codes, Sicherheitsimplikationen und kryptografische Lehren.

Cape Mod: wie man Jeb_s Cape mit einer RSA-Signaturinjektion klaut

alt text

Was, wenn ich dir sagen würde, dass eine gültige RSA-Signatur reichte, aber für das falsche Konto, um deine Freunde glauben zu lassen, du trägst die offizielle Mojang-Cape? Willkommen bei cape-mod, einem Fabric-Exploit, der zeigt, wie Minecraft einer Signatur vertraut und dabei vergisst zu prüfen, ob das Profil, zu dem sie gehört, wirklich deines ist.

Der Kontext: wie Minecraft Skins und Capes handhabt

In der Java Edition gibt es eine selten gestellte Frage: Wer ist verantwortlich, den Skin und die Cape eines Spielers anzuzeigen, der Client oder der Server ?

Die Antwort ist nuanciert:

Komponente Wer schickt sie? Wer lädt sie herunter?
Skin-Textur Der Server schickt die signierte URL Der Client lädt von textures.minecraft.net herunter
Cape-Textur Der Server schickt die signierte URL Der Client lädt von textures.minecraft.net herunter
textures-Eigenschaft Der Server schickt das GameProfile aus der Mojang-Auth Der Client prüft die RSA-Signatur

Der springende Punkt: Alles steckt in einer Eigenschaft namens textures des GameProfile. Diese Eigenschaft enthält:

  • Einen base64-codierten JSON-Payload mit den Textur-URLs
  • Eine RSA-Signatur mit Mojangs privatem Schlüssel gemacht

Die Mauer der RSA-Signatur

Jede textures-Eigenschaft sieht so aus, wenn man sie dekodiert:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Der Client prüft die RSA-Signatur gegen den im Jar eingebetteten öffentlichen Schlüssel (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Für entfernte Spieler (außerhalb des lokalen Spiels) akzeptiert der Client nur Skins, die als secure markiert sind, also mit gültiger Signatur:

// SkinManager.createLookup() – vereinfacht
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← entfernte Spieler müssen sicher sein
    .orElse(defaultSkin);

Dieser Check blockiert Spoofing in der Theorie. Aber die Validierung hört dort auf, und genau dort beginnt der Angriff.

Die Lücke: Signature Replay

Der Client prüft, dass die RSA-Signatur gültig ist. Er vergisst systematisch zu prüfen, dass die im JSON enthaltene profileId der echten UUID des Spielers entspricht.

Anders gesagt: Eine textures-Eigenschaft von einem existierenden Mojang-Konto (zum Beispiel dem eines Mojang-Mitarbeiters) kann auf jeden anderen Spieler replayt werden. Die Signatur bleibt gültig, sie wurde wirklich von Mojang produziert, sie kommt nur von einem anderen Konto.

Wie extrahiert man eine echte Signatur?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) hat die Mojang-Studios-Cape. Vom Mojang-Session-Server:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Antwort:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

Die signature dieses value-Felds wurde von Mojang produziert. Es ist RSA-2048 SHA-1. Sie ist absolut gültig, selbst wenn du sie auf eine andere UUID replayst, weil Jeb_s Signatur Jeb_s Signatur bleibt, und der Client vergisst zu kontrollieren, für wen sie bestimmt war.

Der Code: wie der Mod funktioniert

Der cape-mod-Mod ist winzig, 65 Zeilen Java. Hier das Herz:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Ersetzt die Eigenschaft Texturen durch die Eigenschaft Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

Schritte:

  1. Mixin auf Player.getGameProfile(), den Punkt, an dem das Spielerprofil zurückgegeben wird
  2. Prüft, dass es ein lokaler Server ist (Integrated Server)
  3. Prüft, dass es der Host ist (LAN-Welt)
  4. Ersetzt die textures-Eigenschaft durch die von Jeb_ (hardcodiert)
  5. Gibt ein neues GameProfile mit den injizierten Texturen zurück

Das GameProfile ist also gefälscht: Es ist ein künstlich gebautes Profil, das nicht zum echten Spieler passt. Die textures-Eigenschaften sind von Jeb_ replayt, die RSA-Signatur ist authentisch, aber auf das falsche Profil angewendet. Das Netzwerkpaket dagegen ist legitim: Der Server schickt normal das ClientboundPlayerInfoUpdatePacket mit diesem modifizierten Profil. Das Profil ist gefälscht; das Paket bleibt legitim.

Wenn die Freunde des Hosts via LAN beitreten, erhalten sie das ClientboundPlayerInfoUpdatePacket mit dem modifizierten Profil. Der Client:

  1. Dekodiert den base64-Payload
  2. Prüft die RSA-Signatur → ✅ gültig (es ist wirklich die von Jeb_)
  3. Markiert den Skin als secure=true (weil die Signatur gültig ist)
  4. Passiert den Filter !isRemote || ps.secure() → ✅ passiert
  5. Lädt Jeb_s Cape herunter und zeigt sie an

Ergebnis im Spiel: die Cape auf deinem Skin

So sieht es in-game aus. Zuerst Frontalansicht mit Jeb_s Cape auf dem Host angezeigt:

Cape Mod -- Jeb_ Cape auf dem Host angezeigt

Man sieht klar das rot/weiße Muster der offiziellen Mojang-Studios-Cape. Das Rendering bleibt identisch zu dem eines echten Jeb_, der seine eigene Cape trägt, der Client lädt exakt dieselbe Textur von textures.minecraft.net herunter.

Und in immersiver Ansicht, in einer echten Partie:

Cape Mod -- vue en jeu avec cape visible

Die Cape flattert hinter dem Spieler, wellt sich mit der Bewegung. Perfekt ununterscheidbar von einem authentischen Skin mit offizieller Cape.

Anderer Winkel, in einer Welt mit Lava und Terrain:

Cape Mod -- cape dans un environnement naturel

Und eine letzte Nahansicht des echten Gameplays, wo man die Cape in Aktion sieht:

Cape Mod -- cape en gameplay classique Minecraft

Für jemanden, der einem LAN beitreten würde, ohne zu wissen, dass der Host einen Mod nutzt, bleibt die Cape visuell identisch zu einer echten Mojang-Cape. Genau das ist der Punkt: Die Signatur ist gültig, der Client behandelt sie also als legitim.

Warum das eine Lücke ist (und warum keine)

Es ist ironisch: Der Exploit funktioniert genau weil die Signatur gültig ist. Es geht um etwas anderes als einen kryptografischen Bypass, es ist schlimmer: Eine Logiklücke im Vertrauensmodell.

Check Ergebnis
Gültigkeit der RSA-Signatur ✅ Gültig (von Mojang für Jeb_ signiert)
Entspricht die profileId im Payload der UUID des Hosts ? ❌ Nein (Jeb_s UUID ≠ Host-UUID)
Prüft der Client die Übereinstimmung ? ❌ Nein. Nur die RSA-Signatur wird geprüft.

Minecraft vertraut der Signatur statt der Identität dessen, der sie trägt. Solange die Signatur von Mojang kommt, akzeptiert der Client sie. Es ist wie einen gefälschten von der Regierung signierten Pass zu zeigen, das Siegel ist legitim, auch wenn der Pass dir nicht gehört.

Die Sicherheitsimplikationen

Auf LAN begrenzte Reichweite

Der Mod funktioniert nur auf einem integrierten Server (LAN). Der Angreifer muss:

  • Einen installierten Fabric-Mod haben
  • Host einer LAN-Welt sein
  • Seine Freunde verbinden sich ohne Mod (vanilla)

Aber die Möglichkeiten erweitern sich

In der Theorie könnte man mit derselben Technik:

  • Andere signierte Daten reinjizieren: Heads, illegale Enchantments, bösartige Chat-Komponenten
  • Mit einem LAN-Tunnel kombinieren (NGROK, playit.gg, Radmin VPN), um Spieler im Internet zu betreffen
  • Auf andere Eigenschaften des Profils ausweiten, die von Signaturen abhängen

Warum Mojang wahrscheinlich nicht patchen wird

Es geht um etwas anderes als eine "Schwachstelle" im strengen Sinn, die Signatur ist gültig. Das zu patchen würde erfordern, dass Mojang das komplette Authentifizierungsmodell ändert, was komplex ist. Vorerst ist es ein Grenzfall: LAN-Spieler sollen einander vertrauen.

Die philosophische Falle

Cape Mod ist ein exzellenter Proof of Concept einer breiteren Wahrheit: Du musst immer prüfen, wer Daten signiert hat und für welches Subjekt, bevor du ihnen vertraust.

Das ist eine Lektion in Basiskryptografie. RSA signiert eine Nachricht; die Identität des Empfängers bleibt außerhalb der Signatur. Wenn ich dir eine gültige RSA-Signatur von Mojang gebe, weißt du nur, dass Mojang etwas signiert hat, ohne verlässliche Angabe zum Empfänger aus der bloßen Nachricht.

Genau das passierte mit SSL/TLS-Zertifikaten in den 2000ern, als CAs Zertifikate signierten, ohne die Domain korrekt zu prüfen, die Signatur war gültig, galt aber für die falsche Domain.

Fazit

Cape Mod ist etwas anderes als ein Hack im klassischen Sinn, es ist eine elegante Ausnutzung fehlender Logikvalidierung in Minecraft. Es zeigt, dass:

  1. Eine gültige Signatur garantiert nicht die Identität dessen, der sie trägt
  2. In LAN ist das Vertrauen schwächer, als man glaubt
  3. Minecrafts textures-Eigenschaften sind im Wesentlichen injizierter Inhalt, man muss prüfen, dass sie zum Spieler passen, der sie trägt

Wenn du einer LAN-Welt auf einem "unbekannten" Server beitrittst (oder eher, dessen Host einen verdächtigen Mod hat), hast du schon ein Sicherheitsproblem lange vor der Cape. Aber es ist aufschlussreich: Minecraft nimmt an, dass sich alle in einem LAN vertrauen. Das stimmt... bis zum ersten Host, der dieses Vertrauen missbraucht.


Ressourcen

3 Kernpunkte

  1. RSA-Signaturen validieren eine Nachricht statt einer Identität, ein Detail, das viele Systeme teuer gekostet hat.
  2. Minecraft vergisst zu prüfen, dass das Spielerprofil der Signatur entspricht, die es empfängt, eine Logiklücke in der Validierung bei intakter Kryptografie.
  3. In LAN oder Tunnel ist alles Open Bar für einen Mod, der den integrierten Server kontrolliert.

Cape Mod : как украсть плащ Jeb_ инъекцией подписи RSA

Мод Fabric, эксплуатирующий логическую уязвимость в системе доверия Minecraft : валидная подпись RSA от Mojang, но переигранная на чужом аккаунте. Объяснение кода, последствия для безопасности и криптографические уроки.

Cape Mod : как украсть плащ Jeb_ инъекцией подписи RSA

alt text

А если я скажу, что достаточно валидной подписи RSA -- но для чужого аккаунта -- чтобы друзья поверили, что ты носишь официальный плащ Mojang? Добро пожаловать в cape-mod, эксплойт Fabric, показывающий, как Minecraft доверяет подписи, забывая проверить, что профиль, к которому она относится, действительно твой.

Контекст : как Minecraft управляет скинами и плащами

В Java Edition есть редко задаваемый вопрос : кто отвечает за отображение скина и плаща игрока -- клиент или сервер?

Ответ нюансированный :

Composant Qui l'envoie ? Qui le télécharge ?
Texture de skin Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Texture de cape Le serveur envoie l'URL signée Le client télécharge depuis textures.minecraft.net
Propriété textures Le serveur envoie le GameProfile depuis l'auth Mojang Le client vérifie la signature RSA

Ключевой момент : всё содержится в свойстве textures объекта GameProfile. Это свойство содержит :

  • JSON-пейлоад в base64 с URL текстур
  • подпись RSA, сделанную приватным ключом Mojang

Стена подписи RSA

Каждое свойство textures выглядит так, когда его декодируешь :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Клиент проверяет подпись RSA по публичному ключу, вшитому в jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Для удалённых игроков (вне локальной игры) клиент принимает только скины, помеченные как secure -- то есть с валидной подписью :

// SkinManager.createLookup() -- simplifié
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← les joueurs distants doivent être sécurisés
    .orElse(defaultSkin);

Эта проверка в теории блокирует спуфинг. Но валидация на этом останавливается, и именно здесь начинается атака.

Уязвимость : переигрывание подписи

Клиент проверяет, что подпись RSA валидна. Он систематически забывает проверить, что profileId внутри JSON соответствует реальному UUID игрока.

Иначе говоря : свойство textures, взятое с существующего аккаунта Mojang (например, аккаунта сотрудника Mojang), может быть переиграно на любом другом игроке. Подпись остаётся валидной -- она реально произведена Mojang -- просто пришла с другого аккаунта.

Как извлечь настоящую подпись?

У Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) есть плащ Mojang Studios. С сессионного сервера Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Ответ :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature этого поля value произведена Mojang. Это RSA-2048 SHA-1. Она абсолютно валидна, даже если переиграть её на другом UUID -- потому что подпись Jeb_ остаётся подписью Jeb_, а клиент забывает проверить, кому она предназначалась.

Код : как работает мод

Мод cape-mod крошечный -- 65 строк Java. Вот сердце :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Remplace la propriété textures par celle de Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Шаги :

  1. Mixin на Player.getGameProfile() -- точка, где возвращается профиль игрока
  2. Проверяет, что это локальный сервер (Integrated Server)
  3. Проверяет, что это хост (LAN-мир)
  4. Заменяет свойство textures на то, что у Jeb_ (захардкожено)
  5. Возвращает новый GameProfile с внедрёнными текстурами

GameProfile таким образом подделан : это искусственно построенный профиль, не соответствующий настоящему игроку. Свойства textures переиграны с Jeb_ -- подпись RSA подлинная, но применена к чужому профилю. Сетевой пакет при этом легитимен : сервер штатно отправляет ClientboundPlayerInfoUpdatePacket с этим изменённым профилем. Подделан профиль ; пакет остаётся легитимным.

Когда друзья хоста заходят через LAN, они получают ClientboundPlayerInfoUpdatePacket с изменённым профилем. Клиент :

  1. Декодирует base64-пейлоад
  2. Проверяет подпись RSA → ✅ валидна (это реально подпись Jeb_)
  3. Помечает скин как secure=true (раз подпись валидна)
  4. Проходит фильтр !isRemote || ps.secure() → ✅ проходит
  5. Скачивает и показывает плащ Jeb_

Результат в игре : плащ на твоём скине

Вот как это выглядит в игре. Сначала вид спереди с плащом Jeb_, показанным на хосте :

Cape Mod -- Jeb_ cape affichée sur le host

Чётко виден красно-белый узор официального плаща Mojang Studios. Рендер остаётся идентичным настоящему Jeb_ в собственном плаще -- клиент скачивает ровно ту же текстуру с textures.minecraft.net.

А в иммерсивном виде, в настоящей партии :

Cape Mod -- vue en jeu avec cape visible

Плащ развевается за игроком, колышется с движением. Совершенно неотличимо от подлинного скина с официальным плащом.

Другой ракурс, в мире с лавой и рельефом :

Cape Mod -- cape dans un environnement naturel

И последний крупный план реального геймплея, где виден плащ в действии :

Cape Mod -- cape en gameplay classique Minecraft

Для того, кто зайдёт в LAN, не зная, что хост использует мод, плащ визуально идентичен настоящему плащу Mojang. В этом и суть : подпись валидна, так что клиент относится к ней как к легитимной.

Почему это уязвимость (и почему нет)

Ирония : эксплойт работает именно потому, что подпись валидна. Речь не о криптографическом обходе -- хуже : о логической уязвимости в модели доверия.

Check Résultat
Validité de la signature RSA ✅ Valide (signée par Mojang pour Jeb_)
Le profileId dans le payload correspond-il à l'UUID du host ? ❌ Non (Jeb_'s UUID ≠ UUID du host)
Le client vérifie-t-il la correspondance ? ❌ Non. Seule la signature RSA est vérifiée.

Minecraft доверяет подписи, а не идентичности того, кто её несёт. Пока подпись от Mojang, клиент её принимает. Это как показать чужой паспорт с государственной печатью -- печать легитимна, даже если паспорт не твой.

Последствия для безопасности

Охват ограничен LAN

Мод работает только на встроенном сервере (LAN). Атакующий должен :

  • Иметь установленный мод Fabric
  • Быть хостом LAN-мира
  • Его друзья подключаются без мода (ванильно)

Но возможности расширяются

В теории той же техникой можно :

  • Реинжектить другие подписанные данные : головы, нелегальные чары, вредоносные компоненты чата
  • Комбинировать с LAN-туннелем (NGROK, playit.gg, Radmin VPN), чтобы затрагивать игроков в интернете
  • Расширить на другие свойства профиля, зависящие от подписей

Почему Mojang, вероятно, не запатчит

Речь не об "уязвимости" в строгом смысле -- подпись валидна. Патч потребовал бы от Mojang менять всю модель аутентификации, а это сложно. Пока это пограничный случай : игрокам в LAN положено доверять друг другу.

Философская ловушка

Cape Mod отличный proof of concept более широкой истины : всегда проверяй, кто подписал данные и для какого субъекта, прежде чем им доверять.

Это урок базовой криптографии. RSA подписывает сообщение ; идентичность получателя остаётся вне подписи. Если я даю тебе валидную подпись RSA от Mojang, ты знаешь лишь, что Mojang подписал что-то, без надёжного указания на получателя из одного сообщения.

Именно это случилось с SSL/TLS-сертификатами в 2000-х, когда CA подписывали сертификаты, толком не проверив домен -- подпись была валидна, но относилась к чужому домену.

Заключение

Cape Mod это не хак в классическом смысле -- это элегантная эксплуатация недостатка логической валидации в Minecraft. Он показывает, что :

  1. Валидная подпись не гарантирует идентичность того, кто её несёт
  2. В LAN доверие слабее, чем кажется
  3. Свойства textures в Minecraft по сути инжектируемый контент -- надо проверять, что они соответствуют игроку, который их несёт

Если ты заходишь в LAN-мир на "незнакомом" сервере (точнее, чей хост с подозрительным модом), у тебя уже проблема безопасности задолго до плаща. Но это показательно : Minecraft предполагает, что все в LAN доверяют друг другу. Это правда... до первого хоста, злоупотребившего этим доверием.


Ресурсы

3 ключевых пункта

  1. Подписи RSA валидируют сообщение, а не идентичность -- деталь, дорого стоившая многим системам.
  2. Minecraft забывает проверять, что профиль игрока соответствует полученной подписи -- логическая уязвимость в валидации при целой криптографии.
  3. В LAN или туннеле для мода, контролирующего встроенный сервер, всё открытая столовка.

Cape Mod: cómo robar la capa de Jeb_ con una inyección de firma RSA

Un mod Fabric que explota un fallo lógico en el sistema de confianza de Minecraft: una firma RSA válida de Mojang pero repetida en una cuenta equivocada. Explicación del código, implicaciones de seguridad y lecciones criptográficas.

Cape Mod: cómo robar la capa de Jeb_ con una inyección de firma RSA

alt text

¿Y si te dijera que bastaba una firma RSA válida, pero para la cuenta equivocada, para hacer creer a tus amigos que llevas la capa oficial de Mojang? Bienvenido a cape-mod, un exploit Fabric que muestra cómo Minecraft confía en una firma mientras omite comprobar que el perfil al que pertenece es efectivamente el tuyo.

El contexto: cómo gestiona Minecraft skins y capas

En Java Edition hay una pregunta rara vez planteada: ¿quién es responsable de mostrar el skin y la capa de un jugador: el cliente o el servidor?

La respuesta tiene matices:

Componente ¿Quién lo envía? ¿Quién lo descarga?
Textura de skin El servidor envía la URL firmada El cliente descarga desde textures.minecraft.net
Textura de capa El servidor envía la URL firmada El cliente descarga desde textures.minecraft.net
Propiedad textures El servidor envía el GameProfile desde la auth de Mojang El cliente verifica la firma RSA

El punto clave: todo está contenido en una propiedad llamada textures del GameProfile. Esa propiedad contiene:

  • Un payload JSON en base64 con las URLs de las texturas
  • Una firma RSA hecha con la clave privada de Mojang

El muro de la firma RSA

Cada propiedad textures se ve así cuando la decodificas:

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

El cliente verifica la firma RSA contra la clave pública incluida en el jar (yggdrasil_session_pubkey.der):

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Para los jugadores remotos (fuera del juego local), el cliente solo acepta los skins marcados como secure: es decir con una firma válida:

// SkinManager.createLookup() -- simplificado
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← los jugadores remotos tienen que ser seguros
    .orElse(defaultSkin);

Ese check bloquea el spoofing en teoría. Pero la validación se detiene ahí, y es justo ahí donde empieza el ataque.

El fallo: signature replay

El cliente verifica que la firma RSA sea válida. Omite sistemáticamente comprobar que el profileId contenido en el JSON corresponda al UUID real del jugador.

Dicho de otro modo: una propiedad textures tomada de una cuenta Mojang existente (por ejemplo la de un empleado de Mojang) puede repetirse en cualquier otro jugador. La firma sigue siendo válida: la produjo realmente Mojang, solo que viene de otra cuenta.

Cómo extraer una firma real

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) tiene la capa Mojang Studios. Desde el servidor de sesiones de Mojang:

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Respuesta:

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

La signature de ese campo value la produjo Mojang. Es RSA-2048 SHA-1. Es absolutamente válida, aunque la repitas en otro UUID: porque la firma de Jeb_ sigue siendo una firma de Jeb_, y el cliente omite controlar a quién estaba destinada.

El código: cómo funciona el mod

El mod cape-mod es minúsculo: 65 líneas de Java. Aquí el corazón:

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Reemplaza la propiedad textures por la de Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Pasos:

  1. Mixin sobre Player.getGameProfile(): el punto donde se devuelve el perfil del jugador
  2. Comprueba que es un servidor local (Integrated Server)
  3. Comprueba que es el host (mundo LAN)
  4. Reemplaza la propiedad textures por la de Jeb_ (hardcodeada)
  5. Devuelve un nuevo GameProfile con las texturas inyectadas

El GameProfile queda por tanto forjado: es un perfil construido artificialmente, que no corresponde al verdadero jugador. Las propiedades textures están repetidas desde Jeb_: la firma RSA es auténtica pero aplicada al perfil equivocado. El paquete de red, en cambio, es legítimo: el servidor envía normalmente el ClientboundPlayerInfoUpdatePacket con ese perfil modificado. Es el perfil el que está forjado; el paquete sigue siendo legítimo.

Cuando los amigos del host se unen vía LAN, reciben el ClientboundPlayerInfoUpdatePacket con el perfil modificado. El cliente:

  1. Decodifica el payload base64
  2. Verifica la firma RSA → ✅ válida (es realmente la de Jeb_)
  3. Marca el skin como secure=true (porque la firma es válida)
  4. Pasa el filtro !isRemote || ps.secure() → ✅ pasa
  5. Descarga y muestra la capa de Jeb_

Resultado en juego: la capa sobre tu skin

Así se ve in-game. Primero, vista de frente con la capa de Jeb_ mostrada en el host:

Cape Mod: capa de Jeb_ mostrada en el host

Se ve claramente el motivo rojo/blanco de la capa oficial Mojang Studios. El render queda idéntico al de un verdadero Jeb_ llevando su propia capa: el cliente descarga exactamente la misma textura desde textures.minecraft.net.

Y en vista inmersiva, en una partida real:

Cape Mod: vista en juego con capa visible

La capa flota tras el jugador, ondula con el movimiento. Perfectamente indistinguible de un skin auténtico con capa oficial.

Otro ángulo, en un mundo con lava y terreno:

Cape Mod: capa en un entorno natural

Y una última vista cercana del gameplay real, donde se ve la capa en acción:

Cape Mod: capa en gameplay clásico de Minecraft

Para alguien que se uniera a una LAN ignorando que el host usa un mod, la capa sigue siendo visualmente idéntica a una verdadera capa Mojang. Ese es justo el punto: la firma es válida, así que el cliente la trata como legítima.

Por qué es un fallo (y por qué no lo es)

Es irónico: el exploit funciona justamente porque la firma es válida. No se trata de un bypass criptográfico: es peor, un fallo lógico en el modelo de confianza.

Check Resultado
Validez de la firma RSA ✅ Válida (firmada por Mojang para Jeb_)
¿El profileId del payload corresponde al UUID del host? ❌ No (UUID de Jeb_ ≠ UUID del host)
¿El cliente verifica la correspondencia? ❌ No. Solo se verifica la firma RSA.

Minecraft confía en la firma en vez de en la identidad de quien la lleva. Mientras la firma venga de Mojang, el cliente la acepta. Es como mostrar un pasaporte falso firmado por el gobierno: el sello es legítimo, aunque el pasaporte no sea tuyo.

Las implicaciones de seguridad

Alcance limitado a LAN

El mod solo funciona en un servidor integrado (LAN). El atacante tiene que:

  • Tener un mod Fabric instalado
  • Ser el host de un mundo LAN
  • Sus amigos se conectan sin mod (vanilla)

Pero las posibilidades se amplían

En teoría, con la misma técnica, se podría:

  • Reinyectar otros datos firmados: heads, encantamientos ilegales, componentes de chat maliciosos
  • Combinar con un túnel LAN (NGROK, playit.gg, Radmin VPN) para afectar a jugadores en internet
  • Extender a otras propiedades del perfil que dependan de firmas

Por qué Mojang probablemente no lo parcheará

No se trata de una "vulnerabilidad" en sentido estricto: la firma es válida. Parchearlo exigiría a Mojang modificar el modelo de autenticación completo, lo cual es complejo. Por ahora, es un caso límite: se supone que los jugadores LAN confían entre sí.

La trampa filosófica

Cape Mod es un excelente proof of concept de una verdad más amplia: siempre tienes que verificar quién firmó un dato y para qué sujeto antes de confiar en él.

Es una lección de criptografía básica. RSA firma un mensaje; la identidad del destinatario queda fuera de la firma. Si te doy una firma RSA válida de Mojang, solo sabes que Mojang firmó algo, sin indicación fiable sobre el destinatario a partir del solo mensaje.

Es exactamente lo que pasó con los certificados SSL/TLS en los años 2000 cuando las CA firmaban certificados sin verificar correctamente el dominio: la firma era válida, pero se aplicaba al dominio equivocado.

Conclusión

Cape Mod no es un hack en sentido clásico: es una explotación elegante de una falta de validación lógica en Minecraft. Muestra que:

  1. Una firma válida no garantiza la identidad de quien la lleva
  2. En LAN, la confianza es más débil de lo que se cree
  3. Las propiedades textures de Minecraft son esencialmente contenido inyectado: hay que verificar que correspondan al jugador que las lleva

Si te unes a un mundo LAN en un servidor "desconocido" (o más bien, cuyo host tiene un mod sospechoso), ya tienes un problema de seguridad mucho antes que la capa. Pero es revelador: Minecraft supone que todo el mundo en una LAN confía entre sí. Es verdad... hasta el primer host que abusa de esa confianza.


Recursos

3 puntos clave

  1. Las firmas RSA validan un mensaje más que una identidad: un detalle que costó caro a muchos sistemas.
  2. Minecraft omite comprobar que el perfil del jugador corresponde a la firma que recibe: un fallo lógico en la validación, con una criptografía intacta.
  3. En LAN o en túnel, todo es barra libre para un mod que controle el servidor integrado.

Cape Mod : como roubar a capa do Jeb_ com uma injeção de assinatura RSA

Um mod Fabric que explora uma falha lógica no sistema de confiança do Minecraft : uma assinatura RSA válida da Mojang mas repetida na conta errada. Explicação do código, implicações de segurança e lições criptográficas.

Cape Mod : como roubar a capa do Jeb_ com uma injeção de assinatura RSA

alt text

E se eu te dissesse que bastava uma assinatura RSA válida, mas da conta errada, para fazer seus amigos acreditarem que você veste a capa oficial da Mojang ? Bem-vindo ao cape-mod, um exploit Fabric que mostra como o Minecraft confia em uma assinatura sem verificar que o perfil a que ela pertence é realmente o seu.

O contexto : como o Minecraft gerencia skins e capas

Na Java Edition, há uma pergunta raramente feita : quem é responsável por exibir a skin e a capa de um jogador, o cliente ou o servidor ?

A resposta tem nuances :

Componente Quem envia ? Quem baixa ?
Textura da skin O servidor envia a URL assinada O cliente baixa de textures.minecraft.net
Textura da capa O servidor envia a URL assinada O cliente baixa de textures.minecraft.net
Propriedade textures O servidor envia o GameProfile da auth Mojang O cliente verifica a assinatura RSA

O ponto principal : tudo está contido em uma propriedade chamada textures do GameProfile. Essa propriedade contém :

  • Um payload JSON em base64 com as URLs das texturas
  • Uma assinatura RSA feita com a chave privada da Mojang

A muralha da assinatura RSA

Cada propriedade textures é assim quando decodificada :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

O cliente verifica a assinatura RSA contra a chave pública embutida no jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib) : verifica a assinatura RSA contra a chave pública
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Para jogadores remotos (fora do jogo local), o cliente aceita só skins marcadas como secure, ou seja, com assinatura válida :

// SkinManager.createLookup() : jogadores remotos precisam ser seguros
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← jogadores remotos precisam ser seguros
    .orElse(defaultSkin);

Esse check bloqueia spoofing em teoria. Mas a validação para aí, e é exatamente aí que o ataque começa.

A falha : replay de assinatura

O cliente verifica que a assinatura RSA é válida. Ele sistematicamente deixa de verificar que o profileId contido no JSON corresponde ao UUID real do jogador.

Em outras palavras : uma propriedade textures tirada de uma conta Mojang existente (por exemplo a de um funcionário da Mojang) pode ser repetida em qualquer outro jogador. A assinatura continua válida, ela foi realmente produzida pela Mojang, só que veio de outra conta.

Como extrair uma assinatura de verdade ?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) tem a capa Mojang Studios. A partir do servidor de sessão da Mojang :

# Baixa o perfil de sessão do Jeb_ com as propriedades assinadas
curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Resposta :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

A signature desse campo value foi produzida pela Mojang. É RSA-2048 SHA-1. Ela é absolutamente válida, mesmo que você a repita em outro UUID, porque a assinatura do Jeb_ continua sendo uma assinatura do Jeb_, e o cliente deixa de controlar a quem ela era destinada.

O código : como o mod funciona

O mod cape-mod é minúsculo, 65 linhas de Java. Eis o coração :

// Mixin que substitui a propriedade textures do host pela do Jeb_
@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Substitui a propriedade textures pela do Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Etapas :

  1. Mixin em Player.getGameProfile(), o ponto em que o perfil do jogador é retornado
  2. Verifica que é um servidor local (Integrated Server)
  3. Verifica que é o host (mundo LAN)
  4. Substitui a propriedade textures pela do Jeb_ (fixa no código)
  5. Retorna um novo GameProfile com as texturas injetadas

O GameProfile é portanto forjado : é um perfil construído artificialmente, que não corresponde ao jogador real. As propriedades textures são repetidas a partir do Jeb_, a assinatura RSA é autêntica mas aplicada ao perfil errado. O pacote de rede, por sua vez, é legítimo : o servidor envia normalmente o ClientboundPlayerInfoUpdatePacket com esse perfil modificado. É o perfil que é forjado ; o pacote continua legítimo.

Quando os amigos do host entram via LAN, recebem o ClientboundPlayerInfoUpdatePacket com o perfil modificado. O cliente :

  1. Decodifica o payload base64
  2. Verifica a assinatura RSA → ✅ válida (é realmente a do Jeb_)
  3. Marca a skin como secure=true (porque a assinatura é válida)
  4. Passa no filtro !isRemote || ps.secure() → ✅ passa
  5. Baixa e exibe a capa do Jeb_

Resultado no jogo : a capa na sua skin

Eis o que dá in-game. Primeiro, vista de frente com a capa do Jeb_ exibida no host :

Cape Mod -- capa do Jeb_ exibida no host

Dá para ver claramente o padrão vermelho/branco da capa oficial Mojang Studios. A renderização fica idêntica à de um Jeb_ de verdade vestindo a própria capa, o cliente baixa exatamente a mesma textura de textures.minecraft.net.

E em vista imersiva, em uma partida de verdade :

Cape Mod -- vista no jogo com capa visível

A capa flutua atrás do jogador, ondula com o movimento. Perfeitamente indistinguível de uma skin autêntica com capa oficial.

Outro ângulo, em um mundo com lava e terreno :

Cape Mod -- capa em um ambiente natural

E uma última vista de perto do gameplay real, onde dá para ver a capa em ação :

Cape Mod -- capa em gameplay clássico de Minecraft

Para alguém que entrasse em uma LAN sem saber que o host usa um mod, a capa continua visualmente idêntica a uma capa Mojang de verdade. É exatamente o ponto : a assinatura é válida, então o cliente a trata como legítima.

Por que é uma falha (e por que não é)

É irônico : o exploit funciona justamente porque a assinatura é válida. Não se trata de um bypass criptográfico, é pior : uma falha lógica no modelo de confiança.

Check Resultado
Validade da assinatura RSA ✅ Válida (assinada pela Mojang para o Jeb_)
O profileId no payload corresponde ao UUID do host ? ❌ Não (UUID do Jeb_ ≠ UUID do host)
O cliente verifica a correspondência ? ❌ Não. Só a assinatura RSA é verificada.

O Minecraft confia na assinatura em vez da identidade de quem a carrega. Enquanto a assinatura vier da Mojang, o cliente aceita. É como mostrar um passaporte falso assinado pelo governo, o selo é legítimo, mesmo que o passaporte não seja seu.

As implicações de segurança

Alcance limitado à LAN

O mod funciona só em um servidor integrado (LAN). O atacante precisa :

  • Ter um mod Fabric instalado
  • Ser o host de um mundo LAN
  • Os amigos dele se conectam sem mod (vanilla)

Mas as possibilidades se ampliam

Em teoria, com a mesma técnica, daria para :

  • Reinjetar outros dados assinados : heads, encantamentos ilegais, componentes de chat maliciosos
  • Combinar com um túnel LAN (NGROK, playit.gg, Radmin VPN) para afetar jogadores na internet
  • Estender a outras propriedades do perfil que dependem de assinaturas

Por que a Mojang provavelmente não vai corrigir

Não se trata de uma "vulnerabilidade" no sentido estrito, a assinatura é válida. Corrigir isso exigiria a Mojang mudar o modelo de autenticação completo, o que é complexo. Por enquanto, é um caso limite : jogadores em LAN supostamente confiam uns nos outros.

A armadilha filosófica

Cape Mod é um excelente proof of concept de uma verdade mais ampla : você deve sempre verificar quem assinou um dado e para qual sujeito antes de confiar nele.

É uma lição de criptografia básica. RSA assina uma mensagem ; a identidade do destinatário fica fora da assinatura. Se eu te dou uma assinatura RSA válida da Mojang, você só sabe que a Mojang assinou alguma coisa, sem indicação confiável sobre o destinatário a partir só da mensagem.

Foi exatamente o que aconteceu com os certificados SSL/TLS nos anos 2000, quando as CAs assinavam certificados sem verificar direito o domínio, a assinatura era válida, mas se aplicava ao domínio errado.

Conclusão

Cape Mod não é um hack no sentido clássico, é uma exploração elegante de uma falta de validação lógica no Minecraft. Mostra que :

  1. Uma assinatura válida não garante a identidade de quem a carrega
  2. Em LAN, a confiança é mais fraca do que se imagina
  3. As propriedades textures do Minecraft são essencialmente conteúdo injetado, é preciso verificar que correspondem ao jogador que as carrega

Se você entra em um mundo LAN em um servidor "desconhecido" (ou melhor, cujo host tem um mod suspeito), você já tem um problema de segurança bem antes da capa. Mas é revelador : o Minecraft supõe que todo mundo em uma LAN confia uns nos outros. É verdade... até o primeiro host que abusa dessa confiança.


Recursos

3 pontos principais

  1. Assinaturas RSA validam uma mensagem em vez de uma identidade, um detalhe que custou caro a muitos sistemas.
  2. O Minecraft deixa de verificar que o perfil do jogador corresponde à assinatura que recebe, uma falha lógica na validação, com a criptografia intacta.
  3. Em LAN ou em túnel, é festa aberta para um mod que controla o servidor integrado.

Cape Mod : cara mencuri cape Jeb_ dengan injeksi tanda tangan RSA

Mod Fabric yang mengeksploitasi cacat logika dalam sistem kepercayaan Minecraft : tanda tangan RSA valid dari Mojang tapi di replay pada akun yang salah. Penjelasan kode, implikasi keamanan, dan pelajaran kriptografis.

Cape Mod : cara mencuri cape Jeb_ dengan injeksi tanda tangan RSA

alt text

Bagaimana kalau saya bilang bahwa satu tanda tangan RSA valid saja sudah cukup -- tapi untuk akun yang salah -- untuk membuat teman-temanmu percaya kamu memakai cape resmi Mojang? Selamat datang di cape-mod, exploit Fabric yang menunjukkan bagaimana Minecraft memercayai tanda tangan sambil lalai memverifikasi bahwa profil miliknya memang milikmu.

Konteksnya : cara Minecraft mengelola skin dan cape

Di Java Edition, ada pertanyaan yang jarang diajukan : siapa yang bertanggung jawab menampilkan skin dan cape pemain -- klien atau server?

Jawabannya bernuansa :

Komponen Siapa yang mengirim? Siapa yang mengunduh?
Tekstur skin Server mengirim URL bertanda tangan Klien mengunduh dari textures.minecraft.net
Tekstur cape Server mengirim URL bertanda tangan Klien mengunduh dari textures.minecraft.net
Properti textures Server mengirim GameProfile dari auth Mojang Klien memverifikasi tanda tangan RSA

Poin kunci : semuanya terkandung dalam properti bernama textures dari GameProfile. Properti ini berisi :

  • Payload JSON dalam base64 berisi URL tekstur
  • Tanda tangan RSA yang dibuat dengan kunci privat Mojang

Tembok tanda tangan RSA

Setiap properti textures tampak seperti ini setelah didekode :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Klien memverifikasi tanda tangan RSA terhadap kunci publik yang tertanam di jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Untuk pemain jarak jauh (di luar permainan lokal), klien hanya menerima skin yang ditandai secure -- yaitu dengan tanda tangan valid :

// SkinManager.createLookup() -- disederhanakan
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← pemain jarak jauh harus aman
    .orElse(defaultSkin);

Cek ini memblokir spoofing secara teori. Tapi validasi berhenti di situ, dan tepat di sanalah serangan dimulai.

Cacatnya : replay tanda tangan

Klien memverifikasi bahwa tanda tangan RSA valid. Ia secara sistematis lalai memverifikasi bahwa profileId di dalam JSON cocok dengan UUID nyata pemain.

Dengan kata lain : properti textures yang diambil dari akun Mojang yang sudah ada (misalnya milik karyawan Mojang) bisa di replay pada pemain lain mana pun. Tanda tangannya tetap valid -- ia memang benar-benar diproduksi oleh Mojang -- hanya saja berasal dari akun lain.

Cara mengekstrak tanda tangan asli?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) punya cape Mojang Studios. Dari server sesi Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Respons :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature dari kolom value ini diproduksi oleh Mojang. Ini RSA-2048 SHA-1. Ia mutlak valid, bahkan kalau kamu replay pada UUID lain -- karena tanda tangan Jeb_ tetap tanda tangan Jeb_, dan klien lalai mengontrol untuk siapa ia ditujukan.

Kodenya : cara mod bekerja

Mod cape-mod mungil -- 65 baris Java. Berikut intinya :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Mengganti properti textures dengan milik Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Langkahnya :

  1. Mixin pada Player.getGameProfile() -- titik di mana profil pemain dikembalikan
  2. Memeriksa bahwa ini server lokal (Integrated Server)
  3. Memeriksa bahwa ini host (LAN world)
  4. Mengganti properti textures dengan milik Jeb_ (hardcoded)
  5. Mengembalikan GameProfile baru dengan tekstur yang disuntikkan

Jadi GameProfile itu dipalsukan : profil yang dibangun artifisial, yang tidak cocok dengan pemain sebenarnya. Properti textures di replay dari Jeb_ -- tanda tangan RSA otentik tapi diterapkan pada profil yang salah. Paket jaringannya sah : server mengirim normal ClientboundPlayerInfoUpdatePacket dengan profil yang diubah ini. Profilnyalah yang dipalsukan ; paketnya tetap sah.

Ketika teman-teman host bergabung lewat LAN, mereka menerima ClientboundPlayerInfoUpdatePacket dengan profil yang diubah. Klien :

  1. Mendekode payload base64
  2. Memverifikasi tanda tangan RSA → ✅ valid (memang milik Jeb_)
  3. Menandai skin sebagai secure=true (karena tanda tangannya valid)
  4. Lolos filter !isRemote || ps.secure() → ✅ lolos
  5. Mengunduh dan menampilkan cape Jeb_

Hasil dalam permainan : cape pada skinmu

Begini tampilannya in-game. Pertama, tampak depan dengan cape Jeb_ ditampilkan pada host :

Cape Mod -- cape Jeb_ ditampilkan pada host

Motif merah/putih cape resmi Mojang Studios terlihat jelas. Rendernya tetap identik dengan Jeb_ asli yang memakai capenya sendiri -- klien mengunduh tepat tekstur yang sama dari textures.minecraft.net.

Dan dalam tampilan imersif, dalam permainan sungguhan :

Cape Mod -- tampak dalam permainan dengan cape terlihat

Cape berkibar di belakang pemain, bergelombang mengikuti gerakan. Sempurna tidak bisa dibedakan dari skin otentik dengan cape resmi.

Sudut lain, di dunia dengan lava dan medan :

Cape Mod -- cape di lingkungan alami

Dan satu tampilan dekat terakhir dari gameplay nyata, di mana cape terlihat beraksi :

Cape Mod -- cape dalam gameplay Minecraft klasik

Bagi seseorang yang bergabung ke LAN tanpa tahu host memakai mod, cape tetap terlihat identik dengan cape Mojang asli. Tepat itulah poinnya : tanda tangannya valid, jadi klien memperlakukannya sebagai sah.

Kenapa ini cacat (dan kenapa bukan)

Ironisnya : exploit berfungsi tepat karena tanda tangannya valid. Ini soal lain daripada bypass kriptografis -- lebih buruk : cacat logika dalam model kepercayaan.

Cek Hasil
Validitas tanda tangan RSA ✅ Valid (ditandatangani oleh Mojang untuk Jeb_)
Apakah profileId dalam payload cocok dengan UUID host? ❌ Tidak (UUID Jeb_ ≠ UUID host)
Apakah klien memverifikasi kecocokannya? ❌ Tidak. Hanya tanda tangan RSA yang diverifikasi.

Minecraft memercayai tanda tangan alih-alih identitas pembawanya. Selama tanda tangan berasal dari Mojang, klien menerimanya. Seperti menunjukkan paspor palsu yang dicap pemerintah -- capnya sah, meski paspor bukan milikmu.

Implikasi keamanan

Cakupan terbatas pada LAN

Mod hanya berfungsi pada server terintegrasi (LAN). Penyerang harus :

  • Memasang mod Fabric
  • Menjadi host dunia LAN
  • Teman-temannya terhubung tanpa mod (vanilla)

Tapi kemungkinannya meluas

Secara teori, dengan teknik yang sama, orang bisa :

  • Menyuntik ulang data bertanda tangan lain : heads, enchantment ilegal, komponen chat berbahaya
  • Menggabung dengan tunnel LAN (NGROK, playit.gg, Radmin VPN) untuk memengaruhi pemain di internet
  • Memperluas ke properti lain dari profil yang bergantung pada tanda tangan

Kenapa Mojang mungkin tidak akan menambal

Ini soal lain daripada "kerentanan" dalam arti sempit -- tanda tangannya valid. Menambalnya menuntut Mojang mengubah model autentikasi lengkap, yang rumit. Untuk sekarang, ini kasus batas : pemain LAN diasumsikan saling percaya.

Jebakan filosofis

Cape Mod adalah proof of concept yang bagus untuk kebenaran yang lebih luas : kamu harus selalu memverifikasi siapa menandatangani sebuah data dan untuk subjek apa sebelum memercayainya.

Ini pelajaran kriptografi dasar. RSA menandatangani sebuah pesan ; identitas penerima tetap di luar tanda tangan. Kalau saya memberimu tanda tangan RSA valid dari Mojang, kamu hanya tahu Mojang menandatangani sesuatu, tanpa indikasi andal tentang penerima hanya dari pesannya.

Tepat inilah yang terjadi dengan sertifikat SSL/TLS di tahun 2000-an ketika CA menandatangani sertifikat tanpa memverifikasi domain dengan benar -- tanda tangannya valid, tapi diterapkan pada domain yang salah.

Kesimpulan

Cape Mod soal lain daripada hack dalam arti klasik -- ini eksploitasi elegan atas kurangnya validasi logika di Minecraft. Ia menunjukkan bahwa :

  1. Tanda tangan valid tidak menjamin identitas pembawanya
  2. Di LAN, kepercayaan lebih lemah daripada yang dikira
  3. Properti textures Minecraft pada dasarnya konten yang disuntikkan -- harus diverifikasi bahwa mereka cocok dengan pemain yang membawanya

Kalau kamu bergabung ke dunia LAN di server "tidak dikenal" (atau lebih tepatnya, yang hostnya punya mod mencurigakan), kamu sudah punya masalah keamanan jauh sebelum cape. Tapi ini revelator : Minecraft mengasumsikan semua orang di LAN saling percaya. Itu benar... sampai host pertama menyalahgunakan kepercayaan itu.


Sumber daya

3 poin kunci

  1. Tanda tangan RSA memvalidasi pesan alih-alih identitas -- detail yang merugikan banyak sistem.
  2. Minecraft lalai memverifikasi bahwa profil pemain cocok dengan tanda tangan yang diterimanya -- cacat logika dalam validasi, dengan kriptografi yang utuh.
  3. Di LAN atau tunnel, semuanya open bar untuk mod yang mengendalikan server terintegrasi.

Cape Mod : RSA हस्ताक्षर इंजेक्शन से Jeb_ की केप चुराना

एक Fabric mod जो Minecraft के विश्वास तंत्र में तार्किक खामी का शोषण करता है : Mojang का वैध RSA हस्ताक्षर लेकिन गलत खाते पर फिर से चलाया गया। कोड की व्याख्या, सुरक्षा निहितार्थ और क्रिप्टोग्राफिक सबक।

Cape Mod : RSA हस्ताक्षर इंजेक्शन से Jeb_ की केप चुराना

alt text

और अगर मैं तुम्हें कहूँ कि वैध RSA हस्ताक्षर ही काफी था, लेकिन गलत खाते के लिए, ताकि तुम्हारे दोस्त मानें कि तुम आधिकारिक Mojang केप पहनते हो ? cape-mod में स्वागत है, एक Fabric exploit जो दिखाता है कि Minecraft हस्ताक्षर पर भरोसा करते हुए यह जाँचना भूल जाता है कि जिस प्रोफाइल का वह है वह सच में तुम्हारा है।

संदर्भ : Minecraft स्किन और केप कैसे प्रबंधित करता है

Java Edition में, एक सवाल शायद ही पूछा जाता है : खिलाड़ी की स्किन और केप दिखाने की जिम्मेदारी किसकी है, क्लाइंट की या सर्वर की ?

उत्तर सूक्ष्म है :

घटक कौन भेजता है ? कौन डाउनलोड करता है ?
स्किन टेक्सचर सर्वर हस्ताक्षरित URL भेजता है क्लाइंट textures.minecraft.net से डाउनलोड करता है
केप टेक्सचर सर्वर हस्ताक्षरित URL भेजता है क्लाइंट textures.minecraft.net से डाउनलोड करता है
textures गुण सर्वर Mojang प्रमाणीकरण से GameProfile भेजता है क्लाइंट RSA हस्ताक्षर जाँचता है

मुख्य बिंदु : सब कुछ GameProfile के textures नामक गुण में समाहित है। इस गुण में है :

  • टेक्सचर URL के साथ base64 में JSON पेलोड
  • Mojang की निजी कुंजी से बना RSA हस्ताक्षर

RSA हस्ताक्षर की दीवार

हर textures गुण ऐसा दिखता है जब उसे डिकोड किया जाता है :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

क्लाइंट jar में रखी सार्वजनिक कुंजी (yggdrasil_session_pubkey.der) के खिलाफ RSA हस्ताक्षर जाँचता है :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

दूरस्थ खिलाड़ियों के लिए (स्थानीय खेल के बाहर), क्लाइंट सिर्फ secure के रूप में चिह्नित स्किन स्वीकार करता है, यानी वैध हस्ताक्षर वाली :

// SkinManager.createLookup() , सरलीकृत
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← दूरस्थ खिलाड़ी सुरक्षित होने चाहिए
    .orElse(defaultSkin);

यह जाँच सिद्धांत में spoofing रोकती है। लेकिन सत्यापन वहीं रुकता है, और हमला ठीक वहीं शुरू होता है।

खामी : हस्ताक्षर replay

क्लाइंट जाँचता है कि RSA हस्ताक्षर वैध है। वह व्यवस्थित रूप से यह जाँचना भूल जाता है कि JSON में समाहित profileId खिलाड़ी के असली UUID से मेल खाता है।

दूसरे शब्दों में : मौजूदा Mojang खाते (उदाहरण के लिए किसी Mojang कर्मचारी का) से ली गई textures गुण किसी भी अन्य खिलाड़ी पर replay की जा सकती है। हस्ताक्षर वैध रहता है, यह सच में Mojang द्वारा बनाया गया था, यह बस दूसरे खाते से आया है।

असली हस्ताक्षर कैसे निकालें ?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) के पास Mojang Studios केप है। Mojang सत्र सर्वर से :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

उत्तर :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

इस value फ़ील्ड का signature Mojang द्वारा बनाया गया था। यह RSA-2048 SHA-1 है। यह बिल्कुल वैध है, भले ही तुम इसे दूसरे UUID पर replay करो, क्योंकि Jeb_ का हस्ताक्षर Jeb_ का हस्ताक्षर रहता है, और क्लाइंट यह नियंत्रित करना भूल जाता है कि यह किसके लिए था।

कोड : mod कैसे काम करता है

cape-mod mod छोटा है, 65 पंक्तियों का Java। यहाँ हृदय है :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // textures गुण को Jeb_ वाले से बदलता है
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

चरण :

  1. Player.getGameProfile() पर Mixin, वह बिंदु जहाँ खिलाड़ी का प्रोफाइल लौटाया जाता है
  2. जाँचता है कि यह स्थानीय सर्वर है (Integrated Server)
  3. जाँचता है कि यह host है (LAN world)
  4. textures गुण को Jeb_ वाले से बदलता है (hardcoded)
  5. इंजेक्ट किए टेक्सचर के साथ नया GameProfile लौटाता है

इसलिए GameProfile जाली है : यह कृत्रिम रूप से बनाया गया प्रोफाइल है, जो असली खिलाड़ी से मेल नहीं खाता। textures गुण Jeb_ से replay किए गए हैं, RSA हस्ताक्षर प्रामाणिक है लेकिन गलत प्रोफाइल पर लगाया गया। नेटवर्क पैकेट, अपनी ओर से, वैध है : सर्वर सामान्य रूप से संशोधित प्रोफाइल के साथ ClientboundPlayerInfoUpdatePacket भेजता है। प्रोफाइल जाली है ; पैकेट वैध रहता है।

जब host के दोस्त LAN से जुड़ते हैं, तो उन्हें संशोधित प्रोफाइल के साथ ClientboundPlayerInfoUpdatePacket मिलता है। क्लाइंट :

  1. base64 पेलोड डिकोड करता है
  2. RSA हस्ताक्षर जाँचता है → ✅ वैध (यह सच में Jeb_ वाला है)
  3. स्किन को secure=true चिह्नित करता है (क्योंकि हस्ताक्षर वैध है)
  4. !isRemote || ps.secure() फ़िल्टर पास करता है → ✅ पास
  5. Jeb_ की केप डाउनलोड और प्रदर्शित करता है

खेल में परिणाम : तुम्हारी स्किन पर केप

यह in-game ऐसा दिखता है। पहले, host पर प्रदर्शित Jeb_ केप के साथ सामने का दृश्य :

Cape Mod -- host पर प्रदर्शित Jeb_ केप

आधिकारिक Mojang Studios केप का लाल/सफेद पैटर्न स्पष्ट दिखता है। रेंडर बिल्कुल वैसा रहता है जैसे अपनी केप पहने असली Jeb_ का, क्लाइंट textures.minecraft.net से बिल्कुल वही टेक्सचर डाउनलोड करता है।

और immersive दृश्य में, असली खेल में :

Cape Mod -- केप दिखती हुई खेल दृश्य

केप खिलाड़ी के पीछे तैरती है, गति के साथ लहराती है। प्रामाणिक आधिकारिक केप वाली स्किन से पूरी तरह अप्रभेद्य।

दूसरा कोण, लावा और भूभाग वाली दुनिया में :

Cape Mod -- प्राकृतिक वातावरण में केप

और असली गेमप्ले का आखिरी close दृश्य, जहाँ केप क्रिया में दिखती है :

Cape Mod -- क्लासिक Minecraft गेमप्ले में केप

किसी के लिए जो LAN से जुड़े host mod इस्तेमाल करता है यह जाने बिना, केप दृष्टिगत रूप से असली Mojang केप जैसी रहती है। यही ठीक बिंदु है : हस्ताक्षर वैध है, इसलिए क्लाइंट इसे वैध मानता है।

यह खामी क्यों है (और क्यों नहीं है)

यह विडंबना है : exploit ठीक इसलिए काम करता है क्योंकि हस्ताक्षर वैध है। यह क्रिप्टोग्राफिक bypass से कुछ और है, यह बदतर है : विश्वास मॉडल में तार्किक खामी।

जाँच परिणाम
RSA हस्ताक्षर वैधता ✅ वैध (Jeb_ के लिए Mojang द्वारा हस्ताक्षरित)
पेलोड में profileId host के UUID से मेल खाता है ? ❌ नहीं (Jeb_ का UUID ≠ host का UUID)
क्या क्लाइंट मेल जाँचता है ? ❌ नहीं। सिर्फ RSA हस्ताक्षर जाँचा जाता है।

Minecraft उसे पहनने वाले की पहचान के बजाय हस्ताक्षर पर भरोसा करता है। जब तक हस्ताक्षर Mojang से आता है, क्लाइंट स्वीकार करता है। यह सरकार द्वारा हस्ताक्षरित नकली पासपोर्ट दिखाने जैसा है, मुहर वैध है, भले ही पासपोर्ट तुम्हारा न हो।

सुरक्षा निहितार्थ

LAN तक सीमित दायरा

mod सिर्फ एकीकृत सर्वर (LAN) पर काम करता है। हमलावर को चाहिए :

  • स्थापित Fabric mod
  • LAN दुनिया का host होना
  • उसके दोस्त बिना mod (vanilla) जुड़ते हैं

लेकिन संभावनाएँ बढ़ती हैं

सिद्धांत में, उसी तकनीक से, किया जा सकता है :

  • अन्य हस्ताक्षरित डेटा फिर से डालना : heads, अवैध enchantment, दुर्भावनापूर्ण चैट घटक
  • LAN सुरंग (NGROK, playit.gg, Radmin VPN) के साथ जोड़ना ताकि इंटरनेट पर खिलाड़ी प्रभावित हों
  • प्रोफाइल के अन्य गुणों तक बढ़ाना जो हस्ताक्षरों पर निर्भर हैं

Mojang शायद पैच क्यों नहीं करेगा

यह कड़े अर्थ में "भेद्यता" से कुछ और है, हस्ताक्षर वैध है। इसे पैच करने के लिए Mojang को पूरा प्रमाणीकरण मॉडल बदलना होगा, जो जटिल है। फिलहाल, यह सीमांत मामला है : LAN खिलाड़ियों को एक दूसरे पर भरोसा करना माना जाता है।

दार्शनिक जाल

Cape Mod व्यापक सत्य का उत्कृष्ट proof of concept है : तुम्हें हमेशा जाँचना चाहिए कि डेटा किसने हस्ताक्षरित किया और किस विषय के लिए, उस पर भरोसा करने से पहले।

यह बुनियादी क्रिप्टोग्राफी सबक है। RSA संदेश हस्ताक्षर करता है ; प्राप्तकर्ता की पहचान हस्ताक्षर के बाहर रहती है। अगर मैं तुम्हें Mojang का वैध RSA हस्ताक्षर दूँ, तो तुम सिर्फ जानते हो कि Mojang ने कुछ हस्ताक्षरित किया, सिर्फ संदेश से प्राप्तकर्ता का कोई विश्वसनीय संकेत नहीं।

2000 के दशक में SSL/TLS प्रमाणपत्रों के साथ ठीक यही हुआ था जब CA डोमेन सही से जाँचे बिना प्रमाणपत्र हस्ताक्षर करते थे, हस्ताक्षर वैध था, लेकिन वह गलत डोमेन पर लागू होता था।

निष्कर्ष

Cape Mod क्लासिक अर्थ में hack से कुछ और है, यह Minecraft में तार्किक सत्यापन कमी का सुरुचिपूर्ण शोषण है। यह दिखाता है कि :

  1. वैध हस्ताक्षर उसे पहनने वाले की पहचान गारंटी नहीं करता
  2. LAN में, विश्वास सोचे से कमजोर है
  3. Minecraft के textures गुण अनिवार्य रूप से इंजेक्ट की गई सामग्री हैं, जाँचना चाहिए कि वे उन्हें पहनने वाले खिलाड़ी से मेल खाते हैं

अगर तुम "अज्ञात" दुनिया (या बल्कि, जिसका host संदिग्ध mod रखता है) में LAN दुनिया से जुड़ते हो, तो केप से बहुत पहले तुम्हें सुरक्षा समस्या है। लेकिन यह खुलासा करता है : Minecraft मानता है कि LAN पर सब एक दूसरे पर भरोसा करते हैं। यह सच है... पहले host तक जो इस विश्वास का दुरुपयोग करता है।


संसाधन

3 मुख्य बिंदु

  1. RSA हस्ताक्षर पहचान के बजाय संदेश मान्य करते हैं, एक विवरण जो कई तंत्रों को महँगा पड़ा।
  2. Minecraft यह जाँचना भूल जाता है कि खिलाड़ी का प्रोफाइल उसे मिले हस्ताक्षर से मेल खाता है, सत्यापन में तार्किक खामी, क्रिप्टोग्राफी अक्षुण्ण।
  3. LAN या सुरंग में, mod के लिए सब खुला है जो एकीकृत सर्वर नियंत्रित करता है।

Cape Mod : كيف تسرق cape الخاص بـJeb_ بحقن توقيع RSA

مود Fabric يستغل ثغرة منطقية في نظام ثقة Minecraft : توقيع RSA صالح من Mojang لكن مُعاد تشغيله على حساب خطأ. شرح الكود والآثار الأمنية والدروس التشفيرية.

Cape Mod : كيف تسرق cape الخاص بـJeb_ بحقن توقيع RSA

alt text

وماذا لو قلت لك أنه يكفي توقيع RSA صالح، لكن للحساب الخطأ، لتوهم أصدقاءك أنك تحمل cape الرسمية لـMojang ؟ مرحبا بك في cape-mod، استغلال Fabric يوضح كيف تثق Minecraft بتوقيع بينما تغفل التحقق من أن الملف الذي ينتمي له هو فعلا ملفك.

السياق : كيف تدير Minecraft الـskins والـcapes

في Java Edition، هناك سؤال نادر الطرح : من المسؤول عن عرض skin وcape لاعب، العميل أم الخادم ؟

الجواب دقيق :

المكوّن من يرسله ؟ من ينزّله ؟
قوام skin الخادم يرسل الرابط الموقَّع العميل ينزّل من textures.minecraft.net
قوام cape الخادم يرسل الرابط الموقَّع العميل ينزّل من textures.minecraft.net
خاصية textures الخادم يرسل GameProfile من مصادقة Mojang العميل يتحقق من توقيع RSA

النقطة الأساسية : كل شيء محتوى في خاصية تسمى textures من GameProfile. تحوي هذه الخاصية :

  • حمولة JSON بترميز base64 مع روابط القوام
  • توقيع RSA مصنوع بمفتاح Mojang الخاص

جدار توقيع RSA

تبدو كل خاصية textures هكذا عند فك ترميزها :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

يتحقق العميل من توقيع RSA مقابل المفتاح العام المضمَّن في jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

بالنسبة للاعبين البعيدين (خارج اللعب المحلي)، يقبل العميل فقط skins الموسومة secure، أي ذات توقيع صالح :

// SkinManager.createLookup()، مبسطة
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← اللاعبون البعيدون يجب أن يكونوا آمنين
    .orElse(defaultSkin);

يحجب هذا الفحص التزوير نظريا. لكن التحقق يتوقف هنا، وهنا بالضبط يبدأ الهجوم.

الثغرة : إعادة تشغيل توقيع

يتحقق العميل أن توقيع RSA صالح. ويغفل منهجيا التحقق من أن profileId المحتوى في JSON يطابق UUID الحقيقي للاعب.

بعبارة أخرى : خاصية textures مأخوذة من حساب Mojang موجود (مثلا حساب موظف Mojang) يمكن إعادة تشغيلها على أي لاعب آخر. يبقى التوقيع صالحا، فقد أنتجته Mojang فعلا، لكنه قادم من حساب آخر.

كيف تُستخرَج توقيع حقيقي ؟

يملك Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) cape الخاصة بـMojang Studios. من خادم جلسات Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

الرد :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

أنتجت Mojang signature حقل value هذا. إنه RSA-2048 SHA-1. إنه صالح تماما، حتى لو أعدت تشغيله على UUID آخر، لأن توقيع Jeb_ يبقى توقيع Jeb_، ويغفل العميل التحقق لمن كان مقصودا.

الكود : كيف يعمل المود

مود cape-mod ضئيل، 65 سطرا من Java. إليكم القلب :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // يستبدل خاصية textures بتلك الخاصة بـJeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

الخطوات :

  1. Mixin على Player.getGameProfile()، النقطة التي يُعاد فيها ملف اللاعب
  2. يتحقق أنه خادم محلي (Integrated Server)
  3. يتحقق أنه المضيف (عالم LAN)
  4. يستبدل خاصية textures بتلك الخاصة بـJeb_ (مُرمَّزة صلبا)
  5. يعيد GameProfile جديدا مع القوام المحقونة

إذن GameProfile مزوَّر : ملف مبني اصطناعيا، لا يطابق اللاعب الحقيقي. خاصيتا textures مُعاد تشغيلهما من Jeb_، فتوقيع RSA أصيل لكن مطبق على الملف الخطأ. أما حزمة الشبكة فشرعية : يرسل الخادم عاديا ClientboundPlayerInfoUpdatePacket مع هذا الملف المعدل. الملف هو المزوَّر ؛ والحزمة تبقى شرعية.

عندما ينضم أصدقاء المضيف عبر LAN، يستلمون ClientboundPlayerInfoUpdatePacket مع الملف المعدل. العميل :

  1. يفك ترميز حمولة base64
  2. يتحقق من توقيع RSA ← ✅ صالح (إنه فعلا توقيع Jeb_)
  3. يسم الـskin كـsecure=true (لأن التوقيع صالح)
  4. يجتاز المرشح !isRemote || ps.secure() ← ✅ يجتاز
  5. ينزّل ويعرض cape الخاص بـJeb_

النتيجة في اللعبة : cape على skin خاصتك

إليكم ما يعطيه في اللعبة. أولا، منظر أمامي مع cape الخاص بـJeb_ معروضة على المضيف :

Cape Mod -- Jeb_ cape affichée sur le host

نرى بوضوح نقش cape الرسمية الحمراء/البيضاء الخاصة بـMojang Studios. يبقى العرض مطابقا لعرض Jeb_ حقيقي يحمل cape خاصته، فالعميل ينزّل نفس القوام بالضبط من textures.minecraft.net.

ومنظر غامر، في مباراة حقيقية :

Cape Mod -- vue en jeu avec cape visible

تطفو cape خلف اللاعب، وتتموج مع الحركة. غير قابلة للتمييز تماما عن skin أصيل مع cape رسمية.

زاوية أخرى، في عالم مع حمم وتضاريس :

Cape Mod -- cape dans un environnement naturel

ونظرة أخيرة مقربة للعب الحقيقي، حيث نرى cape أثناء العمل :

Cape Mod -- cape en gameplay classique Minecraft

لشخص ينضم إلى LAN جاهلا أن المضيف يستخدم مودا، تبقى cape مطابقة بصريا لـcape حقيقية من Mojang. وهذه هي النقطة بالضبط : التوقيع صالح، فيعاملها العميل كشرعية.

لماذا هي ثغرة (ولماذا ليست واحدة)

من المفارقة : يعمل الاستغلال لأن التوقيع صالح بالضبط. إنه شيء آخر غير تجاوز تشفيري، بل أسوأ : ثغرة منطقية في نموذج الثقة.

الفحص النتيجة
صلاحية توقيع RSA ✅ صالح (موقَّع من Mojang لـJeb_)
هل profileId في الحمولة يطابق UUID المضيف ؟ ❌ لا (UUID الخاص بـJeb_ ≠ UUID المضيف)
هل يتحقق العميل من التطابق ؟ ❌ لا. وحده توقيع RSA يُتحقَّق.

تثق Minecraft بالتوقيع بدلا من هوية حامله. ما دام التوقيع من Mojang، يقبله العميل. كإظهار جواز مزيف موقَّع من الحكومة، فالختم شرعي، حتى لو لم يكن الجواز لك.

الآثار الأمنية

نطاق محدود بـLAN

يعمل المود فقط على خادم مدمج (LAN). يجب على المهاجم :

  • امتلاك مود Fabric مثبت
  • أن يكون مضيف عالم LAN
  • أصدقاؤه يتصلون دون مود (vanilla)

لكن الاحتمالات تتسع

نظريا، بنفس التقنية، يمكن :

  • إعادة حقن بيانات موقعة أخرى : رؤوس وتعاويذ غير قانونية ومكونات دردشة خبيثة
  • الدمج مع نفق LAN (NGROK وplayit.gg وRadmin VPN) للتأثير على لاعبين على الإنترنت
  • التوسع لخصائص أخرى من الملف تعتمد على توقيعات

لماذا لن تصحح Mojang غالبا

إنه شيء آخر غير "ثغرة" بالمعنى الدقيق، فالتوقيع صالح. يتطلب تصحيح ذلك من Mojang تعديل نموذج المصادقة الكامل، وهذا معقد. حاليا، إنها حالة حدية : يُفترَض أن يثق لاعبو LAN ببعضهم.

الفخ الفلسفي

Cape Mod إثبات مفهوم ممتاز لحقيقة أوسع : يجب دائما التحقق من وقّع بيانة ولأي موضوع قبل الوثوق بها.

إنه درس في التشفير الأساسي. يوقّع RSA رسالة ؛ وتبقى هوية المتلقي خارج التوقيع. إذا أعطيتك توقيع RSA صالحا من Mojang، تعرف فقط أن Mojang وقّعت شيئا ما، دون دلالة موثوقة على المتلقي من الرسالة وحدها.

وهذا بالضبط ما حدث مع شهادات SSL/TLS في سنوات 2000 عندما كانت CAs توقّع شهادات دون التحقق صحيحا من النطاق، فكان التوقيع صالحا، لكنه ينطبق على النطاق الخطأ.

الخلاصة

Cape Mod شيء آخر غير اختراق بالمعنى التقليدي، بل استغلال أنيق لنقص تحقق منطقي في Minecraft. يوضح أن :

  1. توقيع صالح لا يضمن هوية حامله
  2. في LAN، الثقة أضعف مما يُظن
  3. خصائص textures في Minecraft هي أساسا محتوى محقون، يجب التحقق من مطابقتها للاعب الذي يحملها

إذا انضممت لعالم LAN على خادم "مجهول" (أو بالأحرى، مضيفه لديه مود مشبوه)، فلديك فعلا مشكلة أمنية قبل cape بكثير. لكنه كاشف : تفترض Minecraft أن الجميع في LAN يثق ببعضه. وهذا صحيح... حتى أول مضيف يسيء استخدام هذه الثقة.


الموارد

3 نقاط أساسية

  1. توقيعات RSA تصادق على رسالة بدلا من هوية، تفصيل كلف الكثير للعديد من الأنظمة.
  2. تغفل Minecraft التحقق من مطابقة ملف اللاعب للتوقيع الذي يستلمه، ثغرة منطقية في التحقق، مع تشفير سليم.
  3. في LAN أو نفق، كل شيء مباح لمود يتحكم بالخادم المدمج.

Cape Mod : cách đánh cắp cape của Jeb_ bằng tiêm chữ ký RSA

Một mod Fabric khai thác lỗ hổng logic trong hệ thống tin cậy của Minecraft : chữ ký RSA hợp lệ của Mojang nhưng replay sang tài khoản sai. Giải thích mã, hàm ý bảo mật và bài học mật mã.

Cape Mod : cách đánh cắp cape của Jeb_ bằng tiêm chữ ký RSA

alt text

Và nếu tôi nói với bạn rằng chỉ cần một chữ ký RSA hợp lệ -- nhưng cho tài khoản sai -- là đủ khiến bạn bè tin rằng bạn đang khoác cape chính thức của Mojang? Chào mừng tới cape-mod, một exploit Fabric cho thấy Minecraft tin vào chữ ký thế nào trong khi quên kiểm tra rằng hồ sơ nó thuộc về có thực sự là của bạn hay không.

Bối cảnh : Minecraft quản lý skin và cape thế nào

Trong Java Edition, có một câu hỏi hiếm khi được đặt ra : ai chịu trách nhiệm hiển thị skin và cape của người chơi -- client hay server?

Câu trả lời có sắc thái :

Thành phần Ai gửi? Ai tải?
Texture skin Server gửi URL đã ký Client tải từ textures.minecraft.net
Texture cape Server gửi URL đã ký Client tải từ textures.minecraft.net
Thuộc tính textures Server gửi GameProfile từ auth Mojang Client kiểm tra chữ ký RSA

Điểm mấu chốt : mọi thứ nằm trong một thuộc tính gọi là textures của GameProfile. Thuộc tính này chứa :

  • Một payload JSON base64 với các URL texture
  • Một chữ ký RSA làm bằng khóa riêng của Mojang

Bức tường chữ ký RSA

Mỗi thuộc tính textures trông như sau khi giải mã :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

Client kiểm tra chữ ký RSA với khóa công khai nhúng trong jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

Với người chơi từ xa (ngoài game cục bộ), client chỉ chấp nhận skin đánh dấu secure -- tức có chữ ký hợp lệ :

// SkinManager.createLookup() -- rút gọn
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← người chơi từ xa phải được bảo mật
    .orElse(defaultSkin);

Bước kiểm tra này chặn spoofing về lý thuyết. Nhưng việc xác thực dừng ở đó, và chính xác ở đó cuộc tấn công bắt đầu.

Lỗ hổng : replay chữ ký

Client kiểm tra rằng chữ ký RSA hợp lệ. Nó luôn quên kiểm tra rằng profileId chứa trong JSON khớp với UUID thật của người chơi.

Nói cách khác : một thuộc tính textures lấy từ một tài khoản Mojang hiện có (ví dụ của một nhân viên Mojang) có thể replay lên bất kỳ người chơi nào khác. Chữ ký vẫn hợp lệ -- nó thực sự do Mojang tạo ra -- chỉ là nó tới từ tài khoản khác.

Cách trích một chữ ký thật?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) có cape Mojang Studios. Từ máy chủ phiên Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

Phản hồi :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature của trường value này do Mojang tạo ra. Đó là RSA-2048 SHA-1. Nó tuyệt đối hợp lệ, dù bạn replay nó sang UUID khác -- vì chữ ký của Jeb_ vẫn là chữ ký của Jeb_, và client quên kiểm soát nó vốn dành cho ai.

Mã nguồn : mod hoạt động thế nào

Mod cape-mod tí hon -- 65 dòng Java. Đây là cốt lõi :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";

    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;",
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // Thay thuộc tính textures bằng của Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(),
                                           new PropertyMap(b.build())));
    }
}

Các bước :

  1. Mixin lên Player.getGameProfile() -- điểm nơi hồ sơ người chơi được trả về
  2. Kiểm tra đây là máy chủ cục bộ (Integrated Server)
  3. Kiểm tra đây là host (LAN world)
  4. Thay thuộc tính textures bằng của Jeb_ (ghi cứng)
  5. Trả về một GameProfile mới với texture đã tiêm

GameProfile vì vậy được rèn : đó là hồ sơ dựng nhân tạo, không khớp người chơi thật. Các thuộc tính textures được replay từ Jeb_ -- chữ ký RSA xác thực nhưng áp sai hồ sơ. Gói mạng thì hợp lệ : server gửi bình thường ClientboundPlayerInfoUpdatePacket với hồ sơ đã sửa này. Chính hồ sơ bị rèn ; gói vẫn hợp lệ.

Khi bạn bè của host tham gia qua LAN, họ nhận ClientboundPlayerInfoUpdatePacket với hồ sơ đã sửa. Client :

  1. Giải mã payload base64
  2. Kiểm tra chữ ký RSA → ✅ hợp lệ (đúng là của Jeb_)
  3. Đánh dấu skin là secure=true (vì chữ ký hợp lệ)
  4. Vượt bộ lọc !isRemote || ps.secure() → ✅ qua
  5. Tải và hiển thị cape của Jeb_

Kết quả trong game : cape trên skin của bạn

Đây là những gì xảy ra in-game. Trước hết, nhìn chính diện với cape của Jeb_ hiển thị trên host :

Cape Mod -- cape Jeb_ hiển thị trên host

Ta thấy rõ họa tiết đỏ/trắng của cape chính thức Mojang Studios. Render vẫn giống hệt một Jeb_ thật khoác cape của chính mình -- client tải đúng cùng texture từ textures.minecraft.net.

Và ở góc nhìn đắm chìm, trong một ván thật :

Cape Mod -- nhìn trong game với cape hiện rõ

Cape bay sau lưng người chơi, uốn theo chuyển động. Hoàn toàn không phân biệt được với skin xác thực có cape chính thức.

Góc khác, trong thế giới có dung nham và địa hình :

Cape Mod -- cape trong môi trường tự nhiên

Và một góc cận cuối từ gameplay thật, nơi ta thấy cape đang hoạt động :

Cape Mod -- cape trong gameplay Minecraft cổ điển

Với ai tham gia LAN mà không biết host dùng mod, cape vẫn trông giống hệt cape Mojang thật. Đó chính xác là điểm mấu chốt : chữ ký hợp lệ, nên client xử lý nó như hợp lệ.

Vì sao đây là lỗ hổng (và vì sao không hẳn)

Thật trớ trêu : exploit hoạt động chính xác vì chữ ký hợp lệ. Đây không phải bypass mật mã -- mà tệ hơn : một lỗ hổng logic trong mô hình tin cậy.

Kiểm tra Kết quả
Tính hợp lệ của chữ ký RSA ✅ Hợp lệ (Mojang ký cho Jeb_)
profileId trong payload có khớp UUID của host? ❌ Không (UUID của Jeb_ ≠ UUID của host)
Client có kiểm tra sự khớp? ❌ Không. Chỉ chữ ký RSA được kiểm tra.

Minecraft tin vào chữ ký thay vì danh tính người mang nó. Miễn chữ ký tới từ Mojang, client chấp nhận. Giống như trình hộ chiếu giả có con dấu chính phủ -- con dấu hợp lệ, dù hộ chiếu không phải của bạn.

Hàm ý bảo mật

Phạm vi giới hạn ở LAN

Mod chỉ hoạt động trên máy chủ tích hợp (LAN). Kẻ tấn công phải :

  • Cài mod Fabric
  • Là host của thế giới LAN
  • Bạn bè kết nối không mod (vanilla)

Nhưng khả năng mở rộng

Về lý thuyết, với cùng kỹ thuật, có thể :

  • Tiêm lại dữ liệu có chữ ký khác : head, enchant bất hợp pháp, thành phần chat độc hại
  • Kết hợp với tunnel LAN (NGROK, playit.gg, Radmin VPN) để ảnh hưởng người chơi trên internet
  • Mở rộng sang thuộc tính khác của hồ sơ phụ thuộc chữ ký

Vì sao Mojang có thể sẽ không vá

Đây không phải "lỗ hổng" theo nghĩa chặt -- chữ ký hợp lệ. Vá nó đòi Mojang sửa toàn bộ mô hình xác thực, việc này phức tạp. Hiện tại, đây là trường hợp biên : người chơi LAN được cho là tin nhau.

Cái bẫy triết học

Cape Mod là một proof of concept xuất sắc cho một sự thật rộng hơn : bạn luôn phải kiểm tra ai đã ký một dữ liệu và cho chủ thể nào trước khi tin nó.

Đó là bài học mật mã cơ bản. RSA ký một thông điệp ; danh tính người nhận vẫn nằm ngoài chữ ký. Nếu tôi đưa bạn một chữ ký RSA hợp lệ của Mojang, bạn chỉ biết Mojang đã ký cái gì đó, mà không có chỉ dẫn đáng tin về người nhận chỉ từ thông điệp.

Đó chính xác là chuyện đã xảy ra với chứng chỉ SSL/TLS những năm 2000 khi các CA ký chứng chỉ mà không kiểm tra đúng tên miền -- chữ ký hợp lệ, nhưng áp sai tên miền.

Kết luận

Cape Mod không phải hack theo nghĩa cổ điển -- đó là khai thác thanh lịch một thiếu sót xác thực logic trong Minecraft. Nó cho thấy :

  1. Chữ ký hợp lệ không đảm bảo danh tính người mang nó
  2. Trong LAN, mức tin cậy thấp hơn ta tưởng
  3. Các thuộc tính textures của Minecraft về cơ bản là nội dung được tiêm -- phải kiểm tra chúng khớp với người chơi mang chúng

Nếu bạn tham gia thế giới LAN trên máy chủ "không quen" (hay đúng hơn, host có mod đáng ngờ), bạn đã có vấn đề bảo mật từ lâu trước cái cape. Nhưng điều này đáng suy ngẫm : Minecraft cho rằng mọi người trên LAN tin nhau. Điều đó đúng... cho tới host đầu tiên lạm dụng niềm tin đó.


Tài nguyên

3 điểm chính

  1. Chữ ký RSA xác thực một thông điệp chứ không phải danh tính -- chi tiết đã khiến nhiều hệ thống trả giá đắt.
  2. Minecraft quên kiểm tra hồ sơ người chơi có khớp chữ ký nó nhận hay không -- lỗ hổng logic trong xác thực, với mật mã còn nguyên.
  3. Trong LAN hay tunnel, mọi thứ mở toang cho một mod kiểm soát máy chủ tích hợp.

Cape Mod : วิธีขโมยเคปของ Jeb_ ด้วยการฉีดลายเซ็น RSA

ม็อด Fabric ที่ใช้ประโยชน์จากช่องโหว่เชิงตรรกะในระบบความเชื่อถือของ Minecraft : ลายเซ็น RSA ที่ถูกต้องของ Mojang แต่ถูกนำมาใช้ซ้ำกับบัญชีผิด คำอธิบายโค้ด, ผลกระทบด้านความปลอดภัย และบทเรียนด้านการเข้ารหัส

Cape Mod : วิธีขโมยเคปของ Jeb_ ด้วยการฉีดลายเซ็น RSA

alt text จะเกิดอะไรขึ้นถ้าบอกว่า แค่มีลายเซ็น RSA ที่ถูกต้อง -- แต่สำหรับบัญชีผิด -- ก็ทำให้เพื่อนคุณเชื่อว่าคุณสวมเคปทางการของ Mojang ได้แล้ว ยินดีต้อนรับสู่ cape-mod เอ็กซ์พลอยต์ Fabric ที่แสดงให้เห็นว่า Minecraft เชื่อถือลายเซ็นโดยไม่ตรวจสอบว่าโปรไฟล์ที่เป็นเจ้าของลายเซ็นนั้นเป็นของคุณจริงหรือไม่

บริบท : Minecraft จัดการสกินและเคปอย่างไร

ใน Java Edition มีคำถามที่เราไม่ค่อยได้ถามกัน : ใครเป็นผู้รับผิดชอบในการแสดงสกินและเคปของผู้เล่น -- ไคลเอ็นต์หรือเซิร์ฟเวอร์?

คำตอบมีรายละเอียดปลีกย่อย :

ส่วนประกอบ ใครส่ง? ใครดาวน์โหลด?
Texture สกิน เซิร์ฟเวอร์ส่ง URL ที่เซ็นชื่อแล้ว ไคลเอ็นต์ดาวน์โหลดจาก textures.minecraft.net
Texture เคป เซิร์ฟเวอร์ส่ง URL ที่เซ็นชื่อแล้ว ไคลเอ็นต์ดาวน์โหลดจาก textures.minecraft.net
พร็อพเพอร์ตี้ textures เซิร์ฟเวอร์ส่ง GameProfile จากการยืนยันตัวตนของ Mojang ไคลเอ็นต์ตรวจสอบลายเซ็น RSA

จุดสำคัญ : ทุกอย่างอยู่ในพร็อพเพอร์ตี้ที่ชื่อ textures ของ GameProfile พร็อพเพอร์ตี้นี้ประกอบด้วย :

  • Payload JSON ในรูปแบบ base64 ที่มี URL ของ texture ต่าง ๆ
  • ลายเซ็น RSA ที่สร้างด้วยคีย์ส่วนตัวของ Mojang

กำแพงลายเซ็น RSA

พร็อพเพอร์ตี้ textures แต่ละอันเมื่อถอดรหัสจะมีลักษณะแบบนี้ :

{
  "timestamp": 1783666316269,
  "profileId": "d90b68bc81724329a047f1186dcd4336",
  "profileName": "akronman1",
  "signatureRequired": true,
  "textures": {
    "SKIN": {
      "url": "http://textures.minecraft.net/texture/3e6defcb7de5a0e05c75525c6cd46e4b9b416b92e0cf4baa1e0a9e212a887f3f7"
    },
    "CAPE": {
      "url": "http://textures.minecraft.net/texture/70efffaf86fe5bc089608d3cb297d3e276b9eb7a8f9f2fe6659c23a2d8b18edf"
    }
  }
}

ไคลเอ็นต์ตรวจสอบลายเซ็น RSA เทียบกับคีย์สาธารณะที่ฝังอยู่ใน jar (yggdrasil_session_pubkey.der) :

// Property.java (authlib)
public boolean isSignatureValid(PublicKey publicKey) {
    Signature sig = Signature.getInstance("SHA1withRSA");
    sig.initVerify(publicKey);
    sig.update(this.value.getBytes());
    return sig.verify(Base64.decodeBase64(this.signature));
}

สำหรับผู้เล่นระยะไกล (ไม่ใช่ในเครื่อง) ไคลเอ็นต์จะยอมรับเฉพาะสกินที่ถูกทำเครื่องหมายว่า secure -- นั่นคือมีลายเซ็นที่ถูกต้อง :

// SkinManager.createLookup() -- แบบย่อ
PlayerSkin skin = optional
    .filter(ps -> !isRemote || ps.secure())  // ← ผู้เล่นระยะไกลต้องมีความปลอดภัย
    .orElse(defaultSkin);

การตรวจสอบนี้ป้องกันการปลอมแปลงในทางทฤษฎี แต่นี่คือจุดที่เริ่มน่าสนใจ

ช่องโหว่ : การนำลายเซ็นมาใช้ซ้ำ (signature replay)

ไคลเอ็นต์ตรวจสอบว่าลายเซ็น RSA ถูกต้อง แต่มันไม่เคยตรวจสอบว่า profileId ที่อยู่ใน JSON ตรงกับ UUID จริงของผู้เล่น

กล่าวอีกนัยหนึ่ง : พร็อพเพอร์ตี้ textures ที่นำมาจากบัญชี Mojang ที่มีอยู่จริง (เช่นของพนักงาน Mojang) สามารถนำมาเล่นซ้ำกับผู้เล่นคนอื่นได้ ลายเซ็นยังคงถูกต้อง -- มันถูกสร้างขึ้นโดย Mojang จริง ๆ -- แค่มาจากอีกบัญชีหนึ่ง

จะแยกลายเซ็นจริงออกมาได้อย่างไร?

Jeb_ (UUID 853c80ef-3c37-49fd-aa49-938b674adae6) มีเคป Mojang Studios จากเซิร์ฟเวอร์เซสชันของ Mojang :

curl -s "https://sessionserver.mojang.com/session/minecraft/profile/853c80ef-3c37-49fd-aa49-938b674adae6?unsigned=false"

ผลลัพธ์ :

{
  "id": "853c80ef-3c37-49fd-aa49-938b674adae6",
  "name": "jeb_",
  "properties": [
    {
      "name": "textures",
      "value": "ewogICJ0aW1lc3RhbXAiIDogMTc4MzYxOTcyNjAxMSwKICAicHJvZmlsZUlkIiA6ICI4NTNjODBl...",
      "signature": "RgIPF4d/iTDWJV..."
    }
  ]
}

signature ของฟิลด์ value นี้ถูกสร้างขึ้นโดย Mojang เป็นลายเซ็น RSA-2048 SHA-1 ที่ถูกต้องอย่างสมบูรณ์ แม้คุณจะนำไปเล่นซ้ำกับ UUID อื่น -- เพราะลายเซ็นของ Jeb_ ก็ยังคงเป็นลายเซ็นของ Jeb_ และไคลเอ็นต์ไม่เคยตรวจสอบว่ามันควรจะเป็นของคุณ

โค้ด : ม็อดทำงานอย่างไร

ม็อด cape-mod มีขนาดเล็กมาก -- แค่ 65 บรรทัดของ Java นี่คือหัวใจสำคัญ :

@Mixin(Player.class)
public class ServerPlayerMixin {
    private static final String TEXTURES_VALUE =
        "ewogICJ0aW1lc3RhbXAiIDogMTc4MzY2NjMxNjI2OSwKICAicHJvZmlsZUlkIiA6ICJkOTBi...";
    
    private static final String TEXTURES_SIGNATURE =
        "oxoAfZRLVNSfXYFMNbDKZ9XxrTHmz/k2yxzOxksXY3f6aDhY3gCyFCCtDreEWI7fpG9...";

    @Inject(method = "getGameProfile()Lcom/mojang/authlib/GameProfile;", 
            at = @At("RETURN"), cancellable = true)
    private void injectCape(CallbackInfoReturnable<GameProfile> cir) {
        Player self = (Player) (Object) this;
        if (!(self instanceof ServerPlayer serverPlayer)) return;
        MinecraftServer server = ((ServerPlayerAccessor) serverPlayer).getServer();
        if (!(server instanceof IntegratedServer)) return;

        GameProfile host = server.getSingleplayerProfile();
        GameProfile original = cir.getReturnValue();
        if (host == null || !host.name().equals(original.name())) return;

        // แทนที่พร็อพเพอร์ตี้ textures ด้วยของ Jeb_
        ImmutableMultimap.Builder<String, Property> b = ImmutableMultimap.builder();
        for (Property p : original.properties().values()) {
            if (!p.name().equals("textures")) {
                b.put(p.name(), p);
            }
        }
        b.put("textures", new Property("textures", TEXTURES_VALUE, TEXTURES_SIGNATURE));
        cir.setReturnValue(new GameProfile(original.id(), original.name(), 
                                           new PropertyMap(b.build())));
    }
}

ขั้นตอน :

  1. Mixin บน Player.getGameProfile() -- จุดที่โปรไฟล์ของผู้เล่นถูกส่งกลับ
  2. ตรวจสอบว่าเป็นเซิร์ฟเวอร์ภายในเครื่อง (Integrated Server)
  3. ตรวจสอบว่าเป็นโฮสต์ (โลก LAN)
  4. แทนที่ พร็อพเพอร์ตี้ textures ด้วยของ Jeb_ (แบบ hardcode)
  5. ส่งคืน GameProfile ใหม่ที่มี texture ที่ถูกฉีดเข้าไป

GameProfile จึงถูกปลอมแปลงขึ้นมา : เป็นโปรไฟล์ที่สร้างขึ้นโดยไม่ได้ตรงกับผู้เล่นจริง พร็อพเพอร์ตี้ textures ถูกนำมาเล่นซ้ำจาก Jeb_ -- ลายเซ็น RSA เป็นของแท้ แต่ถูกใช้กับโปรไฟล์ผิด แพ็กเก็ตเครือข่ายนั้นถูกต้องตามกฎหมาย : เซิร์ฟเวอร์ส่ง ClientboundPlayerInfoUpdatePacket ตามปกติกับโปรไฟล์ที่ถูกแก้ไขนี้ สิ่งที่ถูกปลอมคือโปรไฟล์ ไม่ใช่แพ็กเก็ต

เมื่อเพื่อนของโฮสต์เข้าร่วมผ่าน LAN พวกเขาจะได้รับ ClientboundPlayerInfoUpdatePacket พร้อมกับโปรไฟล์ที่ถูกแก้ไข ไคลเอ็นต์ :

  1. ถอดรหัส payload base64
  2. ตรวจสอบลายเซ็น RSA → ✅ ถูกต้อง (เป็นของ Jeb_ จริง ๆ)
  3. ทำเครื่องหมายสกินว่า secure=true (เพราะลายเซ็นถูกต้อง)
  4. ผ่านตัวกรอง !isRemote || ps.secure() → ✅ ผ่าน
  5. ดาวน์โหลดและแสดงเคปของ Jeb_

ผลลัพธ์ในเกม : เคปบนสกินของคุณ

นี่คือสิ่งที่เห็นในเกม เริ่มจากมุมมองด้านหน้าพร้อมเคปของ Jeb_ ที่แสดงบนโฮสต์ :

Cape Mod -- เคป Jeb_ แสดงบนโฮสต์

เราจะเห็นลวดลายสีแดง/ขาวของเคปทางการ Mojang Studios อย่างชัดเจน ไม่มีความแตกต่างจาก Jeb_ ตัวจริงที่สวมเคปของตัวเอง -- ไคลเอ็นต์ดาวน์โหลด texture เดียวกันจาก textures.minecraft.net

และในมุมมองแบบดื่มด่ำ ในเกมจริง :

Cape Mod -- มุมมองในเกมพร้อมเคปที่มองเห็น

เคปปลิวไปด้านหลังผู้เล่น ขยับตามการเคลื่อนไหว แยกไม่ออกจากสกินจริงที่มีเคปทางการ

อีกมุม ในโลกที่มีลาวาและภูมิประเทศ :

Cape Mod -- เคปในสภาพแวดล้อมธรรมชาติ

และมุมใกล้ชิดสุดท้ายของการเล่นจริง ที่เห็นเคปในขณะเล่น :

Cape Mod -- เคปในการเล่น Minecraft ทั่วไป

สำหรับคนที่เข้าร่วม LAN โดยไม่รู้ว่าโฮสต์ใช้ม็อดอยู่ ไม่มีทางแยกแยะได้เลยว่านี่คือเคป Mojang ปลอม นี่คือประเด็น : ลายเซ็นถูกต้อง ไคลเอ็นต์ไม่มีเหตุผลที่จะสงสัย

ทำไมนี่ถึงเป็นช่องโหว่ (และทำไมถึงไม่ใช่)

เป็นเรื่องน่าขัน : เอ็กซ์พลอยต์ทำงานได้เพราะลายเซ็นถูกต้อง ไม่มีการบายพาสการเข้ารหัสลับใด ๆ ที่นี่ -- ที่แย่กว่านั้นคือมันเป็นช่องโหว่เชิงตรรกะ ในโมเดลความเชื่อถือ

การตรวจสอบ ผลลัพธ์
ความถูกต้องของลายเซ็น RSA ✅ ถูกต้อง (เซ็นโดย Mojang สำหรับ Jeb_)
profileId ใน payload ตรงกับ UUID ของโฮสต์หรือไม่? ❌ ไม่ (UUID ของ Jeb_ ≠ UUID ของโฮสต์)
ไคลเอ็นต์ตรวจสอบความสอดคล้องนี้หรือไม่? ❌ ไม่ เฉพาะลายเซ็น RSA เท่านั้นที่ถูกตรวจสอบ

Minecraft เชื่อถือลายเซ็น ไม่ใช่ตัวตนของผู้ที่ถือลายเซ็นนั้น ตราบใดที่ลายเซ็นมาจาก Mojang ไคลเอ็นต์ก็ยอมรับ มันเหมือนกับการแสดงพาสปอร์ตปลอมที่เซ็นโดยรัฐบาล -- ตราประทับถูกต้อง แม้พาสปอร์ตจะไม่ใช่ของคุณ

ผลกระทบด้านความปลอดภัย

ขอบเขตจำกัดที่ LAN

ม็อดทำงานได้เฉพาะบนเซิร์ฟเวอร์แบบบูรณาการ (LAN) ผู้โจมตีต้อง :

  • มีม็อด Fabric ติดตั้งอยู่
  • เป็นโฮสต์ของโลก LAN
  • เพื่อน ๆ เชื่อมต่อโดยไม่มีม็อด (vanilla)

แต่ความเป็นไปได้ขยายวงกว้างขึ้น

ในทางเทคนิคแล้ว ด้วยเทคนิคเดียวกัน สามารถ :

  • นำข้อมูลที่เซ็นแล้วกลับมาฉีดใหม่ : หัว, เอ็นแชนต์ที่ผิดกฎหมาย, ส่วนประกอบแชทที่เป็นอันตราย
  • รวมกับอุโมงค์ LAN (NGROK, playit.gg, Radmin VPN) เพื่อส่งผลต่อผู้เล่นบนอินเทอร์เน็ต
  • ขยายไปยังพร็อพเพอร์ตี้อื่น ๆ ของโปรไฟล์ที่ขึ้นอยู่กับลายเซ็น

ทำไม Mojang อาจจะไม่แพตช์

ไม่มี "ช่องโหว่" ในความหมายที่เคร่งครัด -- ลายเซ็นถูกต้อง การแพตช์สิ่งนี้จะต้องการให้ Mojang เปลี่ยนโมเดลการยืนยันตัวตนทั้งหมด ซึ่งเป็นเรื่องซับซ้อน ในตอนนี้มันเป็นกรณีขอบ : ผู้เล่น LAN ถูกสันนิษฐานว่าไว้ใจซึ่งกันและกัน

กับดักเชิงปรัชญา

Cape Mod เป็นการพิสูจน์แนวคิดที่ยอดเยี่ยมของความจริงที่กว้างกว่า : คุณไม่ควรเชื่อถือลายเซ็นโดยไม่ตรวจสอบว่าใครเป็นคนเซ็นและเกี่ยวกับอะไร

นี่คือบทเรียนพื้นฐานด้านการเข้ารหัส RSA เซ็นข้อความ ไม่ใช่ตัวตน ถ้าฉันให้ลายเซ็น RSA ที่ถูกต้องของ Mojang แก่คุณ คุณก็รู้ว่า Mojang เซ็นบางอย่าง คุณไม่รู้ว่าสำหรับใคร และคุณไม่สามารถสันนิษฐานได้เพียงแค่มองข้อความ

นี่คือสิ่งที่เกิดขึ้นกับใบรับรอง SSL/TLS ในช่วงปี 2000 เมื่อ CA รับรองอะไรก็ได้ -- ลายเซ็นถูกต้อง แต่มันใช้กับโดเมนผิด

บทสรุป

Cape Mod ไม่ใช่แฮ็กในความหมายคลาสสิก -- มันเป็นการใช้ประโยชน์อย่างสวยงามจากการขาดการตรวจสอบเชิงตรรกะใน Minecraft มันแสดงให้เห็นว่า :

  1. ลายเซ็นที่ถูกต้องไม่ได้รับประกันตัวตนของผู้ถือมัน
  2. ใน LAN ความเชื่อถือนั้นอ่อนแอกว่าที่เราคิด
  3. พร็อพเพอร์ตี้ textures ของ Minecraft โดยเนื้อแท้แล้วคือเนื้อหาที่ถูกฉีด -- จำเป็นต้องตรวจสอบว่ามันตรงกับผู้เล่นที่สวมใส่หรือไม่

ถ้าคุณเข้าร่วมโลก LAN บนเซิร์ฟเวอร์ "ที่ไม่รู้จัก" (หรือโฮสต์ที่มีม็อดน่าสงสัย) คุณมีปัญหาด้านความปลอดภัยตั้งแต่ก่อนเคปอยู่แล้ว แต่มันเป็นอาการ : Minecraft สมมติว่าทุกคนบน LAN ไว้ใจซึ่งกันและกัน ซึ่งเป็นจริง... จนกว่าจะไม่เป็น


แหล่งข้อมูล

3 ประเด็นสำคัญ

  1. ลายเซ็น RSA รับรองข้อความ ไม่ใช่ตัวตน -- รายละเอียดที่ทำให้หลายระบบต้องเสียหาย
  2. Minecraft ไม่ตรวจสอบว่าโปรไฟล์ผู้เล่นตรงกับลายเซ็นที่ได้รับ -- ช่องโหว่เชิงตรรกะ ไม่ใช่เชิงการเข้ารหัส
  3. ใน LAN หรือในอุโมงค์ ทุกอย่างเปิดกว้างสำหรับม็อดที่ควบคุมเซิร์ฟเวอร์แบบบูรณาการ

Related Articles