難読化 (ソフトウェア)

理解しにくいコンピュータコードを作成すること From Wikipedia, the free encyclopedia

ソフトウェアにおける難読化: obfuscation)とは、コンピュータプログラムの動作を変えずに、プログラムコードの内部的なサブルーチン(手続き)の内容・構造・データなどを、人間にとって読み取りにくくなるように改変・加工すること[1]。難読化の対象はソースコードであったり、ソースコードから生成されるマシンコードまたはバイトコードなどの中間表現であったりすることもある。難読化されたコード(obfuscated code)は第三者によるプログラムの解読・解析が困難になる。

その主な理由は、改ざんの防止、リバースエンジニアリングの阻止、セキュリティソフトウェアからの検知回避、またはコードの難読化を解除するというパズルや娯楽としての課題を作成することである。難読化は手動で行うこともできるが、通常は難読化ツールを使用して行われる[2]

概要

おおよそ2つのいずれかの目的のため、プログラマのコーディング、専用アプリケーション、または開発ツールの補助機能によって、ソースコードや実行コードに対して故意にロジックやデータなどの難読化、曖昧化が施される。

  1. コードの目的を隠蔽したり(難解さに基づくセキュリティ英語版を参照)、改竄(タンパリング)やリバースエンジニアリングを阻止したりするため。
  2. コードを解読する者の腕試しのためのパズルや娯楽として。

コード難読化はハードウェアの曖昧化英語版とは本質的に異なっており、後者は電子回路の配置、構造を機能隠蔽を目的として改変することを意味する。

いくつかのプログラミング言語は、他の言語に比べ、その構造や性質により、難読化が容易なものもある[3][4]C言語[5]C++[6][7]Perl[8]はその一例である。

一般に、ソフトウェアの実行にソースコードが必要となるスクリプト言語は解析が容易だが、コンパイラによって機械語を生成する言語は、そのコード変換過程の不可逆性により解析が難しい。機械語は単なる数値の羅列であり、逆アセンブル[注釈 1]することは容易だが、解読には機械語やアセンブラの知識が必要となり、また元のソースコードを復元(逆コンパイル)することは完全にはできないため、コードの意図まで読み取れるとは限らない。

リフレクションのサポートなどの目的で、コンパイル時にシンボル情報をメタデータとして保存する言語や処理系の場合、逆コンパイルによって元のソースコードを復元しやすいため、特にプロプライエタリな商用ソフトウェアでは、難読化によってリバースエンジニアリングを阻止することが多い。通常、プログラミングの際はサブルーチンや変数といったコード上のシンボルには人間にとって分かりやすい名前を付けるが、それらの名前が分かるだけでも解析がかなり容易になるため、そういったシンボルの名前を難読化ツールによって意味のないものに置換するだけでも一定の効果が望める。

手法

難読化の手法は、見た目を変更する表層的なものから、プログラムの意味を保ったまま構造自体を変換する高度なものまで多岐にわたります。

表層的・構文的な手法

主に人間の目から見た際の可読性を下げる手法。

  • 名前の難読化 - 変数や関数に無意味、あるいは誤解を招くような名前を付ける。
  • 要素の混同 - 実際のコードをコメントのように見せかけたり、構文とデータを入り混ぜたりする。
  • 二重コーディング - 空白や改行を調整し、コード全体を詩の形式や特定の図形のように配置する(芸術的効果)[9]
  • その他 - 単純なキーワード置換、自己生成プログラム、高度な圧縮技術の使用など。

セマンティックな手法

プログラムの動作は維持しつつ、内部構造や処理の流れを複雑に変形させるより高度な手法。

  • 制御フローの難読化 - 無関係な計算の結合、関連する計算の分割、処理順序のランダム化、難読化ツールにしか結果が分からない条件式の挿入などにより、処理の流れを追えなくする[10]
  • データ構造の難読化 - 配列の構造を変更したり、クラスの継承関係を再配置したりする[11]
  • 手続き構造の難読化 - 新しい抽象化(関数やメソッドなど)を無意味に挿入したり、既存の構造を変更してコードの全体構造を完全に作り変える。
  • データフローの難読化 - プログラム内を流れるデータの依存関係や追跡を困難にする[12]
  • 以上のような難読化を自己書き換えにより動的に行う、といった手法もある。

以下の例は、単純なソースコードの難読化を示している。どちらのプログラムも同じ出力を表示するが、2番目のバージョンは意図的に理解しにくくされている。

明確なコード:

#include <stdio.h>

int main(void) {
    int x = 5;
    int y = 7;
    printf("%d\n", x + y);
    return 0;
}

難読化されたコード:

#include <stdio.h>
int main(){int _=5,__=7;printf("%d\n",_-~__-1);}

難読化されたバージョンでは、プログラムの動作を維持したまま、意味のある変数名が削除され、算術式が読みにくい形式に書き換えられている。

娯楽としての難読化

難読化ソースコードの作成や解読は、頭の体操(brain teaser)となる。いくつかコンテストもあり、創作性などを検討し表彰されるコンテストもある。例えば、国際難読化(英単語の正確な訳としては「難読化された」)Cコードコンテスト(IOCCC)、Obfuscated Perl Contest英語版などである。これらのコンテストはプログラムの働きの意外性(見た目と全く異なる働きをするなど)などもコンテストのうちに含まれることも多く、難しさを競うコンテストではないし、単に難しいだけといった作品はまずない。

難読化のパターンには色々あり、単純なキーワード置換、スペースをうまく利用して芸術的な表現を行う、はたまた、自己生成やデータを高圧縮するものなど様々である。

Perlプログラマの中には署名欄英語版のシグネチャに短く難読化されたPerlプログラムを組み込んでいるものもいる。このような署名にはJAPHs(Just another Perl hacker英語版)などがある[13]

これは1988年のIOCCCでエントリーされたコードである。作者はイアン・フィリップス(Ian Phillipps[14]、その後トーマス・ボール(Thomas Ball)により解読されている[15]

/*
  LEAST LIKELY TO COMPILE SUCCESSFULLY:
  Ian Phillipps, Cambridge Consultants Ltd., Cambridge, England
*/

#include <stdio.h>
main(t,_,a)
char
*
a;
{
    return!

0<t?
t<3?

main(-79,-13,a+
main(-87,1-_,
main(-86, 0, a+1 )

+a)):

1,
t<_?
main(t+1, _, a )
:3,

main ( -94, -27+t, a )
&&t == 2 ?_
<13 ?

main ( 2, _+1, "%s %d %d\n" )

:9:16:
t<0?
t<-72?
main( _, t,
"@n'+,#'/*{}w+/w#cdnr/+,{}r/*de}+,/*{*+,/w{%+,/w#q#n+,/#{l,+,/n{n+,/+#n+,/#;\
#q#n+,/+k#;*+,/'r :'d*'3,}{w+K w'K:'+}e#';dq#'l q#'+d'K#!/+k#;\
q#'r}eKK#}w'r}eKK{nl]'/#;#q#n'){)#}w'){){nl]'/+#n';d}rw' i;# ){nl]!/n{n#'; \
r{#w'r nc{nl]'/#{l,+'K {rw' iK{;[{nl]'/w#q#\
\
n'wk nw' iwk{KK{nl]!/w{%'l##w#' i; :{nl]'/*{q#'ld;r'}{nlwb!/*de}'c ;;\
{nl'-{}rw]'/+,}##'*}#nc,',#nw]'/+kd'+e}+;\
#'rdq#w! nr'/ ') }+}{rl#'{n' ')# }'+}##(!!/")
:
t<-50?
_==*a ?
putchar(31[a]):

main(-65,_,a+1)
:
main((*a == '/') + t, _, a + 1 )
:

0<t?

main ( 2, 2 , "%s")
:*a=='/'||

main(0,

main(-61,*a, "!ek;dc i@bK'(q)-[w]*%n+r3#l,{}:\nuwloca-O;m .vpbks,fxntdCeghiry")

,a+1);}

このCプログラムはコンパイル後実行するとクリスマスの十二日英語版というクリスマス・キャロルの十二が出力される。ソースコード内に韻文全ての文字列が符号化されている。

同じ年に勝者としてはエントリーされていないが、次の例は空白を利用し芸術性のある表現を行う。実行すると任意長の迷路を生成する[16]

char*M,A,Z,E=40,J[40],T[40];main(C){for(*J=A=scanf(M="%d",&C);
--            E;             J[              E]             =T
[E   ]=  E)   printf("._");  for(;(A-=Z=!Z)  ||  (printf("\n|"
)    ,   A    =              39              ,C             --
)    ;   Z    ||    printf   (M   ))M[Z]=Z[A-(E   =A[J-Z])&&!C
&    A   ==             T[                                  A]
|6<<27<rand()||!C&!Z?J[T[E]=T[A]]=E,J[T[A]=A-Z]=A,"_.":" |"];}

ANSI準拠のCコンパイラでは文字列定数(リテラル)を上書きできないので、"*M"を"M[3]"に変更し、"M="の部分を省略する必要がある[要出典]

オスカル・トレド・グティエレス(Óscar Toledo Gutiérrez)による次の例は、IOCCCの第19回コンテストにて最優秀作に入選(Best of Show entry)したものである。このコードはターミナルディスク・コントローラ英語版を完備した8080エミュレータの実装であり、CP/M-80のブートとCP/Mアプリケーションを実行できる[17]

#include <stdio.h>
           #define n(o,p,e)=y=(z=a(e)%16 p x%16 p o,a(e)p x p o),h(
                                #define s 6[o]
             #define p z=l[d(9)]|l[d(9)+1]<<8,1<(9[o]+=2)||++8[o]
                                #define Q a(7)
           #define w 254>(9[o]-=2)||--8[o],l[d(9)]=z,l[1+d(9)]=z>>8
                               #define O )):((
                  #define b (y&1?~s:s)>>"\6\0\2\7"[y/2]&1?0:(
                               #define S )?(z-=
                    #define a(f)*((7&f)-6?&o[f&7]:&l[d(5)])
                               #define C S 5 S 3
                       #define D(E)x/8!=16+E&198+E*8!=x?
                             #define B(C)fclose((C))
                       #define q (c+=2,0[c-2]|1[c-2]<<8)
                          #define m x=64&x?*c++:a(x),
                         #define A(F)=fopen((F),"rb+")
                    unsigned char o[10],l[78114],*c=l,*k=l
                          #define d(e)o[e]+256*o[e-1]
#define h(l)s=l>>8&1|128&y|!(y&255)*64|16&z|2,y^=y>>4,y^=y<<2,y^=~y>>1,s|=y&4
+64506; e,V,v,u,x,y,z,Z; main(r,U)char**U;{

     { { { } } }       { { { } } }       { { { } } }       { { { } } }
    { { {   } } }     { { {   } } }     { { {   } } }     { { {   } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
    { { {   } } }    { { {     } } }    { { {   } } }    { { {     } } }
      { { ; } }      { { {     } } }      { { ; } }      { { {     } } }
    { { {   } } }    { { {     } } }    { { {   } } }    { { {     } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
   { { {     } } }   { { {     } } }   { { {     } } }   { { {     } } }
    { { {   } } }     { { {   } } }     { { {   } } }     { { {   } } }
     { { { } } }       { { { } } }       { { { } } }       { { { } } }

                                   for(v A((u A((e A((r-2?0:(V A(1[U])),"C")
),system("stty raw -echo min 0"),fread(l,78114,1,e),B(e),"B")),"A")); 118-(x
=*c++); (y=x/8%8,z=(x&199)-4 S 1 S 1 S 186 S 2 S 2 S 3 S 0,r=(y>5)*2+y,z=(x&
207)-1 S 2 S 6 S 2 S 182 S 4)?D(0)D(1)D(2)D(3)D(4)D(5)D(6)D(7)(z=x-2 C C C C
C C C C+129 S 6 S 4 S 6 S 8 S 8 S 6 S 2 S 2 S 12)?x/64-1?((0 O a(y)=a(x) O 9
[o]=a(5),8[o]=a(4) O 237==*c++?((int (*)())(2-*c++?fwrite:fread))(l+*k+1[k]*
256,128,1,(fseek(y=5[k]-1?u:v,((3[k]|4[k]<<8)<<7|2[k])<<7,Q=0),y)):0 O y=a(5
),z=a(4),a(5)=a(3),a(4)=a(2),a(3)=y,a(2)=z O c=l+d(5) O y=l[x=d(9)],z=l[++x]
,x[l]=a(4),l[--x]=a(5),a(5)=y,a(4)=z O 2-*c?Z||read(0,&Z,1),1&*c++?Q=Z,Z=0:(
Q=!!Z):(c++,Q=r=V?fgetc(V):-1,s=s&~1|r<0) O++c,write(1,&7[o],1) O z=c+2-l,w,
c=l+q O p,c=l+z O c=l+q O s^=1 O Q=q[l] O s|=1 O q[l]=Q O Q=~Q O a(5)=l[x=q]
,a(4)=l[++x] O s|=s&16|9<Q%16?Q+=6,16:0,z=s|=1&s|Q>159?Q+=96,1:0,y=Q,h(s<<8)
O l[x=q]=a(5),l[++x]=a(4) O x=Q%2,Q=Q/2+s%2*128,s=s&~1|x O Q=l[d(3)]O x=Q  /
128,Q=Q*2+s%2,s=s&~1|x O l[d(3)]=Q O s=s&~1|1&Q,Q=Q/2|Q<<7 O Q=l[d(1)]O s=~1
&s|Q>>7,Q=Q*2|Q>>7 O l[d(1)]=Q O m y n(0,-,7)y) O m z=0,y=Q|=x,h(y) O m z=0,
y=Q^=x,h(y) O m z=Q*2|2*x,y=Q&=x,h(y) O m Q n(s%2,-,7)y) O m Q n(0,-,7)y)  O
m Q n(s%2,+,7)y) O m Q n(0,+,7)y) O z=r-8?d(r+1):s|Q<<8,w O p,r-8?o[r+1]=z,r
[o]=z>>8:(s=~40&z|2,Q=z>>8) O r[o]--||--o[r-1]O a(5)=z=a(5)+r[o],a(4)=z=a(4)
+o[r-1]+z/256,s=~1&s|z>>8 O ++o[r+1]||r[o]++O o[r+1]=*c++,r[o]=*c++O z=c-l,w
,c=y*8+l O x=q,b z=c-l,w,c=l+x) O x=q,b c=l+x) O b p,c=l+z) O a(y)=*c++O r=y
,x=0,a(r)n(1,-,y)s<<8) O r=y,x=0,a(r)n(1,+,y)s<<8))));
system("stty cooked echo"); B((B((V?B(V):0,u)),v)); }

次は、Just another Perl hacker英語版の例である。

@P=split//,".URRUU\c8R";@d=split//,"\nrekcah xinU / lreP rehtona tsuJ";sub p{
@p{"r$p","u$p"}=(P,P);pipe"r$p","u$p";++$p;($q*=2)+=$f=!fork;map{$P=$P[$f^ord
($p{$_})&6];$p{$_}=/ ^$P/ix?$P:close$_}keys%p}p;p;p;p;p;map{$p{$_}=~/^[P.]/&&
close$_}%p;wait until$?;map{/^r/&&<$_>}%p;$_=$d[$q];sleep rand(2)if/\S/;print

これは"Just another Perl / Unix hacker"という文字列のうち、一塊の文字列を一旦表示し、その後ゆっくり次々と文字列を出力していく[18]

いくつかのPythonを用いた例は、公式のPythonプログラミングFAQなどにある[19][20][21][22]

セキュリティソフトウェア回避のための難読化

XOR暗号化とBase64エンコーディングは、マルウェアアンチウイルスの検出から隠すためによく使用される2つの方法である。どちらも、悪意のあるコードがファイルの形式でどのように見えるかを変更することによって機能し、これによりセキュリティソフトウェアがシグナチャーベースのパターン認識から回避する。

XOR難読化では、攻撃者は秘密鍵を選択し、マルウェアのバイナリにXORビット演算を適用する。これにより、実行可能ファイルがランダムなデータのように変換される。インポートテーブルの関数名は消滅し、PEヘッダは破損し、ファイル全体がその構造を失う。その後、難読化されたペイロードはドロッパーに埋め込まれる。これは、隠されたマルウェアをリソースまたはデータセクションとして含む、一見通常の実行可能ファイルである。ユーザーがドロッパーを実行すると、同じキーを使用して再度XOR操作を実行し、元のマルウェアを再構築し、メモリから直接実行するか、ディスクに書き込んでから実行する[23]

このプロセスにより、アンチウイルスソフトウェアが依存するいくつかの指標が削除される。MZヘッダ(すべてのWindows実行ファイルの先頭を示す2バイトの署名「MZ」)は、XOR操作によって完全に隠される。セキュリティプログラムは、埋め込まれた実行可能ファイルを検索する際、この2バイトの署名を頻繁にスキャンする。Base64エンコーディングは、異なる方法で同様の結果を達成する。すなわち、バイナリデータをASCIIテキストに変換することで、実行可能ファイルがプログラムではなくプレーンテキストのように見えるようになる。

2020年の機械学習セキュリティ回避コンペティションの調査によると、これらの手法は最新の検出システムをバイパスできることが示された。参加者は、XORエンコーディング、Base64エンコーディング、およびデッドコードの挿入を組み合わせて、3つのコンペティションモデルすべてを回避した。エントロピー(情報の乱雑さ、不確実さ)ベースの検出も失敗し、場合によっては、Base64エンコーディングによって元のマルウェアファイルよりもエントロピーが実際に低下した[23]

これらの手法のシンプルさが、特にそれらを危険なものにしている。XORおよびBase64エンコーディングは、実装に基本的なプログラミングスキルしか必要としないが、高度な機械学習の分類器に対して有効であることが証明された。これにより、セキュリティ研究者は、自動化されたXORキー回復ツールや、実行可能ファイル内の埋め込みリソースのより詳細な分析など、新たな防御に向けて推進されている。

難読化の欠点

せいぜい、難読化は単なる時間稼ぎに過ぎず、プログラムのリバースエンジニアリングが必ずしも不可能とするものではない[24]。実装によってはパフォーマンスの低下を引き起こすほか、依存性の注入(Dependency Injection, DI)などの目的で、参照する対象の名前を文字列で与えるようなリフレクション自己反映計算APIを利用している場合などでも、原理的に難読化によって動かなくなるため、難読化が可能な部分が制限される[25]。また、開発者のビルドプロセスに時間と複雑さが加わり、デバッグが困難になる。

また、難読化は必然的に第三者による安全性や正当性の検証を阻害するため、Webブラウザの拡張機能などのプラットフォームではセキュリティの観点から禁止されつつある[26]

自動化ツール

コードの難読化を実行したりサポートするソフトウェアは、obfuscatorsとよばれるプログラムを例に、多く存在する。これらは学術目的による研究用ツールや趣味、専門家によって作成された商用製品や、オープンソースソフトウェアもある。また逆に難読化を解除するツールも存在する。

商用の難読化ソリューションはソースコードの難読化[27][28]Java[29].NET[30]などのプラットフォーム独立なバイトコードの変換が大部分を占めるが、中にはコンパイルされたバイナリに直接作用するものも存在する。

ユーザーへの通知

AVGアンチウイルスなどの一部のアンチウイルスソフトウェアは、手動で難読化されたコードを含むWebサイトにアクセスしたときにユーザーに警告を出す[37]。難読化の目的の1つは悪意のあるコードを隠すことである可能性があるためである。しかし、ファイルサイズの縮小やセキュリティの向上を目的としてコードの難読化を採用する開発者もいる。一般的なユーザーは、特に信頼できる企業からの無害なコードについて、アンチウイルスソフトウェアが警告を出すことを予期していない場合があるため、そのような機能は実際にユーザーが正当なソフトウェアを使用するのを思いとどまらせる可能性がある。

MozillaGoogleは、それぞれのブラウザ(FirefoxChrome)のアドオンストアにおいて、難読化されたコードを含むブラウザ拡張機能を許可していない[38][39]

難読化とコピーレフト

作者がソースコードを公開することに消極的である場合など、ソースコードを難読化された形式でリリースすることによって、コピーレフトソフトウェアライセンスを回避することが違法であるかどうかについて議論がある。この問題は、GNU General Public Licenseにおいて、「変更を加えるための好ましい形式」を利用可能にすることを要求することによって対処されている[40]。GNUのWebサイトには、「難読化された『ソースコード』は実際のソースコードではなく、ソースコードとしてカウントされない」と記載されている[41]

逆コンパイラ

逆コンパイラとは、実行可能ファイルやライブラリからソースコードをリバースエンジニアリングできるツールである。このプロセスは、暗号技術における従来の「中間者攻撃」に触発されて、マン・イン・ザ・エンド(mite)攻撃と呼ばれることがある。逆コンパイルされたソースコードは読みにくいことが多く、ランダムな関数名や変数名、不正確な変数の型、コンパイラの最適化によって元のソースコードとは異なるロジックが含まれている。

AIモデルの難読化

AIモデルの難読化は、機械学習モデルの内部構造を隠す手法である[42]。難読化によってモデルはブラックボックス化する。これは説明可能なAIとは対照的である。難読化モデルは、トレーニングデータをモデルに入力する前に適用して、ランダムなノイズを追加することもできる。これにより、個々のサンプルやサンプルグループのプロパティに関する機密情報が隠される[43]

関連項目

脚注

外部リンク

Related Articles

Wikiwand AI