syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

マネージリソースアセンブリの解析

.NETのマネージリソースアセンブリを解析するC#コンソールプログラムのサンプルです(Visual Studio 2012で動作確認)。
非ASCII文字を含む文字列をValueに持つようなエントリー群をXMLファイルに書き出します。

なんでこんなことをしようかと思ったかというと、海外にオフショアしているアプリにおいて、日本語にローカライズされたテキストに誤りがないか調べる仕事(地味!)をする羽目になったんですが、いちいちすべての画面を起動して確認する手間を省くために、ローカライズ用サテライトアセンブリ(DLL)の内部データを解析することで、ある程度まで判断できないだろうか、と考えたからです。マネージアセンブリは文字列を生のUTF-16LEで保持するような仕組みではないらしく、バイナリエディターで直接閲覧することはできないんですが、リフレクションとResourceReaderクラスを使うことで案外あっさり抽出できました。
しかし本当はローカライズ対象言語のネイティブが最初から最後まで翻訳作業を担当するのが当たり前なんですけどね……

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CsResAsmTest1
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        const int extraArgCount = 2;
        if (args.Length < extraArgCount)
        {
          Console.WriteLine("This application requires the following {0} extra argument(s).", extraArgCount);
          Console.WriteLine("Args[0] = Input assembly file path (must already exist)");
          Console.WriteLine("Args[1] = Output directory path (must already exist)");
          return;
        }

        var asciiRegex = new System.Text.RegularExpressions.Regex(@"^[\u0000-\u007E]+$");

        var xmlSettings = new System.Xml.XmlWriterSettings();
        xmlSettings.Indent = true;
        xmlSettings.IndentChars = "\t";
        xmlSettings.Encoding = new UTF8Encoding();

        string strInAsmFilePath = args[0];
        Console.WriteLine("Input assembly file path = \"{0}\"", strInAsmFilePath);
        if (!System.IO.File.Exists(strInAsmFilePath))
        {
          Console.WriteLine("The input assembly file does not exist!!");
          return;
        }
        string strOutDirPath = args[1];
        Console.WriteLine("Output directory path = \"{0}\"", strOutDirPath);
        if (!System.IO.Directory.Exists(strOutDirPath))
        {
          Console.WriteLine("The output directory does not exist!!");
          return;
        }

        string strInAsmFileName = System.IO.Path.GetFileName(strInAsmFilePath);
        string strOutFilePath = System.IO.Path.Combine(strOutDirPath, strInAsmFileName + ".xml");

        // マネージ リソース アセンブリのみに対応。ネイティブ DLL などを解析しようとすると BadImageFormatException がスローされる。
        var resAsm = System.Reflection.Assembly.LoadFrom(strInAsmFilePath);
        var asmName = resAsm.GetName();
        var asmVersion = asmName.Version;
        var resNames = resAsm.GetManifestResourceNames();
        using (var stream = new System.IO.FileStream(strOutFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write))
        {
          using (var xmlWriter = System.Xml.XmlWriter.Create(stream, xmlSettings))
          {
            xmlWriter.WriteStartDocument();
            xmlWriter.WriteStartElement("Assembly");

            xmlWriter.WriteStartAttribute("FileName");
            xmlWriter.WriteString(strInAsmFileName);
            xmlWriter.WriteEndAttribute();
            xmlWriter.WriteStartAttribute("Version");
            xmlWriter.WriteString(asmVersion.ToString());
            xmlWriter.WriteEndAttribute();

            foreach (var resname in resNames)
            {
              xmlWriter.WriteStartElement("ManifestResource");

              xmlWriter.WriteStartAttribute("Name");
              xmlWriter.WriteString(resname);
              xmlWriter.WriteEndAttribute();

              Console.WriteLine("Manifest resource name = {0}", resname);
              // *.jpg, *.ico など、ファイルが直接含まれているものに関しては解析しない。
              if (resname.EndsWith(".resources")) // HACK: 大文字・小文字を区別しないようにするべき?
              {
                using (var fs = resAsm.GetManifestResourceStream(resname))
                {
                  using (var resReader = new System.Resources.ResourceReader(fs))
                  {
                    var dict = resReader.GetEnumerator();
                    while (dict.MoveNext())
                    {
                      // 空文字列や ASCII 文字のみで構成されたテキストは除外する。
                      // 改行のみやタブのみも除外。
                      if (dict.Key is string && dict.Value is string && !String.IsNullOrEmpty(dict.Value as string) && !asciiRegex.IsMatch(dict.Value as string))
                      {
                        xmlWriter.WriteStartElement("Entry");

                        xmlWriter.WriteStartAttribute("Key");
                        xmlWriter.WriteString(dict.Key as string);
                        xmlWriter.WriteEndAttribute();
                        xmlWriter.WriteStartAttribute("Value");
                        xmlWriter.WriteString(dict.Value as string);
                        xmlWriter.WriteEndAttribute();

                        xmlWriter.WriteEndElement();
                      }
                    }
                  }
                }
              }

              xmlWriter.WriteEndElement();
            }
            xmlWriter.WriteEndElement();
            xmlWriter.WriteEndDocument();
          }
        }
        Console.WriteLine("Assembly analysis completed.");
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex);
      }
    }
  }
}

難読化について

ちなみにDotfuscatorみたいな難読化ツールが使われていたりすると、リソースアセンブリでさえも解析(リバースエンジニアリング)はできないかもしれません。Dotfuscatorは.NETプログラムのロジックのみを保護するのか、それともリソースも保護するのかは調べてませんが……

ResourceReaderクラスのドキュメントについて

サンプルを作成するにあたって、MSDNライブラリの下記ヘルプに記載されている解説を参考にしました。
http://msdn.microsoft.com/ja-jp/library/system.resources.resourcereader%28v=vs.110%29.aspx

ちなみに.NET 4における日本語版ヘルプは、2014年9月現在、英語版と比べてコンテンツの情報量に差がありすぎです。有用な情報がまったく翻訳されていません。

http://msdn.microsoft.com/ja-jp/library/system.resources.resourcereader%28v=vs.100%29.aspx
http://msdn.microsoft.com/en-us/library/system.resources.resourcereader%28v=vs.100%29.aspx