提供: Minecraft Modding Wiki
2013年1月5日 (土) 21:25時点における118.0.113.99 (トーク)による版 (難読化への対抗手段の解説)
移動先: 案内検索

この記事は"ForgeModLoader"を前提MODとしています。

ここでは、coremods用modのASM機能を使用するmod作成方法を紹介します。

概要

  • ASMとは、Java動的ByteCode操作ライブラリの一つです。

通常コンパイル済みのClassファイルの内容は変更できません。
ですが、JavaにClassファイルを読み込む機能ClassLoaderを乗っ取り、ASMにより動的にClassファイルに変更を施すことで
Minecraft.jar内および、mods内のzip等のClassファイルを直接上書きすることなく改変することを可能とします。
本チュートリアルでは、最低限の機能のみの実装のみを紹介するため、ASMライブラリ等の使い方については別途検索ください。

  • ※改変を施す部分のコードは、仮実装でありそのままでは動きません。

ソースコメントにしたがって必要な実装を施して書き換えましょう。

  • ※パフォーマンスへの影響

この機能によってロードされたクラスは、ロードは原則1回しか行われませんし
完了すれば通常どおりコンパイルされたclassファイルとなんら違いはありません。
施した改変内容以上のパフォーマンスへの影響はほぼ無いです。

ソースの解説

作成するソース
TutorialCorePlugin.java:coremods読み込みの基点となります。
TutorialModContainer.java:ModLoaderにおけるmod_XX.classのバージョン情報等のみを格納するものです。
TutorialTransformer.java:Classの改変機能を実装します。
META-INF/MANIFEST.MF:coremod用jarにするために必要です。

TutorialCorePluginクラス作成

    • CorePluginクラスを作成します。

mod_Tutorialソース

package tutorial.asm;
 //tutorial : 独自のパッケージ名を付けられます。
 //主に他の開発者とファイル名の衝突を避けるために利用します(tutorial.abc.asm等)
 //asm : ASM機能を使うクラスを配置する場合の慣例です。解りやすくする以外の意味はありません。

import java.io.File;
import java.util.Map;


import cpw.mods.fml.relauncher.IFMLCallHook;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions;

 //TransformerExclusions.value:coremodsでロードする際に参照されるためパッケージ名と一致させてください。
 //IFMLLoadingPlugin:Coremodsの基礎インタフェース
 //IFMLCallHook:Coremods内で、coremod自身のパス等を取得する等の際に必要となります。
@TransformerExclusions(value={"tutorial.asm"})
public class TutorialCorePlugin implements IFMLLoadingPlugin, IFMLCallHook

    //coremod自身のファイルパスの保存用
	public static File location;
    
    //今回は使用しません
	@Override
	public String[] getLibraryRequestClass()
	{
        return null;
	}

    //Classの改変機能を実装したクラスの完全修飾名を返します。
	@Override
	public String[] getASMTransformerClass()
	{
		return new String[]{
				"tutorial.asm.TutorialTransformer"};
	}

    //coremodの名前やバージョン情報の格納クラスの完全修飾名を返します。
	@Override
	public String getModContainerClass()
	{
		return "tutorial.asm.TutorialModContainer";
	}

    //coremods読み込みの基点クラスの完全修飾名を返します。
	@Override
	public String getSetupClass()
	{
        return "tutorial.asm.TutorialCorePlugin";
	}

    //IFMLCallHookのメソッドです。
    //今回はCoremod自身のjarファイルパスを取得します。
	@Override
	public void injectData(Map<String, Object> data)
	{
		if(data.containsKey("coremodLocation"))
			location = (File) data.get("coremodLocation");
	}

	@Override
	public Void call() throws Exception
	{
		return null;
	}
 }

TutorialModContainerクラス作成

    • ModContainerクラスを作成します。

DummyModContainerクラスを継承して作成します。

package tutorial.asm;

import java.util.Arrays;
import java.util.List;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

import cpw.mods.fml.common.DummyModContainer;
import cpw.mods.fml.common.LoadController;
import cpw.mods.fml.common.ModMetadata;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.versioning.ArtifactVersion;
public class TutorialModContainer extends DummyModContainer
{
	public TutorialModContainer()
    {
        super(new ModMetadata());
        getMetadata();
    }

	@Override
	public List<ArtifactVersion> getDependencies()
	{
		return super.getDependencies();
	}

	@Override
	public ModMetadata getMetadata()
	{
		//modの名前や、他のModと区別するための一意なID情報などを指定します。
		ModMetadata meta = super.getMetadata();

        meta.modId       = "transformertutorial";
        meta.name        = "TransformerTutorial";
        meta.version     = "1.0.0";
        meta.authorList  = Arrays.asList("Author");
        meta.description = "";
        meta.url         = "";
        meta.credits     = "";

        return meta;
	}

	@Override
	public boolean registerBus(EventBus bus, LoadController controller)
	{
		bus.register(this);
		return true;
	}

	@Subscribe
    public void init(FMLInitializationEvent event)
    {
    }
}

TutorialTransformerクラス作成

    • Transformerクラスを作成します。

IClassTransformerインタフェースを実装します。

package flammpfeil.everybodysnametag.asm;

import java.util.List;

import java.io.InputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

import cpw.mods.fml.relauncher.FMLRelauncher;
import cpw.mods.fml.relauncher.IClassTransformer;

//Opcodes : インプリメントすると、ASMによるバイトコード定数にアクセスするのに便利です。
public class TutorialTransformer implements IClassTransformer , Opcodes
{

    //クラスがロードされる際に呼び出されるメソッドです。
    @Override
    public byte[] transform(String name, byte[] bytes)
    {
        try
        {
          //改変対象のクラスの完全修飾名です。
          //後述でMinecraft.jar内の難読化されるファイルを対象とする場合の簡易な取得方法を紹介します。
          String targetClassName = "net.minecraft.src.target";
          
          	//FMLRelauncher.side() : Client/Serverどちらか一方のを対象とする場合の判定などに使用できます。
          	//今回は"CLIENT"と比較し、Client側のファイルを対象としている例です。
          	
            //name : 現在ロードされようとしているクラス名が格納されています。
            if(FMLRelauncher.side().equals("CLIENT") && name.equals(targetClassName))
            {

              //--------------------------------------------------------------
              //クラスファイル丸ごと差し替える場合
              //--------------------------------------------------------------
              
              /*
              
              ZipFile zf = null;
              InputStream zi = null;
              try{
               zf = new ZipFile(TutorialCorePlugin.location);
               
               //差し替え後のファイルです。coremodのjar内のパスを指定します。
               ZipEntry ze = zf.getEntry("target.class");
              
               if(ze != null){
                zi = zf.getInputStream(ze);
                bytes = new byte[(int) ze.getSize()];
                zi.read(bytes);
               }
              }finally{
               if(zi != null)
                zi.close();
               if(zf != null)
                zf.close();
              }
              
              */
              
              //--------------------------------------------------------------
              //ASMを使用し、既存のクラスファイルに改変を施す場合。
              //今回のサンプルでは下記の想定で記述しています。
              //EntityLiving.classのdoRenderLivingの先頭に
              //tutorial/test.classのpassTestRender(EntityLiving,double,double,double)メソッドの呼び出しを追加する
              //--------------------------------------------------------------
              
              /*
                ClassNode cnode = new ClassNode();
                ClassReader reader = new ClassReader(bytes); //ASMで、bytesに格納されたクラスファイルを解析します。
                reader.accept(cnode, 0);

                MethodNode mnode = null;
                //改変対象のメソッド名です
                String targetMethodName = "doRenderLiving" ;
                //改変対象メソッドの戻り値型および、引数型をあらわします
                String targetMethoddesc = "(Lnet/minecraft/entity/EntityLiving;DDDFF)V";

                //対象のメソッドを検索取得します。
                for(MethodNode curMnode : (List<MethodNode>) cnode.methods)
                    if(targetMethodName.equals(curMnode.name) && targetMethoddesc.equals(curMnode.desc)){
                        mnode = curMnode;
                        break;
                    }
                    
                if(mnode != null){
                    InsnList overrideList = new InsnList();
                    
                    //メソッドコールを、バイトコードであらわした例です。
                    overrideList.add(new VarInsnNode(ALOAD, 1));
                    overrideList.add(new VarInsnNode(DLOAD, 2));
                    overrideList.add(new VarInsnNode(DLOAD, 4));
                    overrideList.add(new VarInsnNode(DLOAD, 6));
                    overrideList.add(new MethodInsnNode(INVOKESTATIC, "tutorial/test", "passTestRender", "(LEntityLiving;DDD)V"));

                    //mnode.instructions.get(1)で、対象のメソッドの先頭を取得
                    //mnode.instructions.insertで、指定した位置にバイトコードを挿入します。
                    mnode.instructions.insert(mnode.instructions.get(1), overrideList);

                    //改変したクラスファイルをバイト列に書き出します
                    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES|ClassWriter.COMPUTE_MAXS);
                    cnode.accept(cw);
                    bytes = cw.toByteArray();
                }
                */
            }
        }
        catch(Exception e)
        {
            throw new RuntimeException("failed : TutorialTransformer loading",e);
        }

        return bytes;
    }
}

META-INF/MANIFEST.MFファイルの作成

META-INF/MANIFEST.MFファイルを作成します。 TutorialCorePluginのクラス名を指定します。

Manifest-Version: 1.0
FMLCorePlugin: tutorial.asm.TutorialCorePlugin

jarパッケージにまとめる

前述で作成したclassを形式にまとめます。 coremods用のmodではjar形式でなければなりません。

今回の例では下記のようなファイル構成となります。

  • tutorial.jar
    • META-INF
      • MANIFEST.MF
    • tutorial
      • asm
        • TutorialCorePlugin.class
        • TutorialModContainer.class
        • TutorialTransformer.class
    • target.class (丸ごと差し替え例用)

通常のmodのように、zipで圧縮し、jarに拡張子を変更すれば完成です。

動作確認

coremod形式のmodは、基本的にはEclipseでのデバッグでは読み込みが行われません。 機能の実装部分については、元ファイルを直接改変するなどしましょう。

coremodとしての動作確認は、実際の動作環境へ放り込んで行うことになります。

難読化への対抗手段の解説

  • 準備

改変したいClassを通常通り書き換え。recompile>reobfuscateしてしまいます。

  • 丸ごと差し替える場合

reobfに出力されたクラスファイル名を見てTutorialTransformerを適宜書き換えましょう。

※置き変えてしまうので、同一のクラスを置き買えるものとは競合してしまいます。

  • ASMによる改変を行う場合

reobfに出力されたクラスファイルと、改変前のクラスファイルを
JBVD等の適当なByteCodeViewerで開き、2つを比較して改変された部分を特定します。
それを元にASMのコードに置き変えTutorialTransformerを適宜かきかえましょう。

サンプルのようなメソッドのフック処理程度であれば
Eclipse等のByteCodeOutlineプラグインなどで、改変部分のコードを直接ASM形式のコードに変換して
難読化される部分を書きかえる程度で、そのまま使える場合もあります。

※ByteCode操作のばあい、同一クラスにに複数の改変を施すことができるため、競合させずにそれぞれの改変を施すこともできます。

  • 雑記

メソッドコールの追加は今回の例のとおりですが
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが
ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。)


自分のコメントを追加
Minecraft Modding Wikiはすべてのコメントを歓迎します。匿名で投稿したくない場合は、アカウント作成またはログインしてください。無料です。