エンジニア

Mach-Oの中身をバイナリー解析してみた

投稿日:

筆者はWonderplanetでローカライズ化を担当しているエンジニア、Yである。
海外はチートユーザが多いので、チート対策にも気を配っている。
このブログを書くことになった経緯だが、あるちょっとしたプロジェクトのipaを調べている途中、少し気になる事があったためである。
作成されたipaには以下のような文字列が含まれていたためだ。

bash$ strings Any.app/Any | grep "AnyClass"
                 :
__ZZN12AnyNameSpace8AnyClass11getInstanceEvE8instance
__ZZN12AnyNameSpace8AnyClass9AnyMethodEv
__ZZN12AnyNameSpace8AnyClass10AnyMethod2Ev
                 :

ipaはXcodeのReleaseでビルドしており、dSYMは作成しないようにしており、デバッグ情報はDWARFのみだ。
自分の理解の限り、DWARFがdSYMを参照しバックトレースなどのシンボルに変換しているはずなので、実行ファイルにはシンボルがないはずである。
それにこのAnyClassは共有ライブラリのものでもなく、実行ファイルに含まれているClassなのだが。。。

では、デバッグ情報のような謎の名前修飾付きの文字列は一体何であるのか?
一体どこに何の目的で使われているのかますます気になってきたので、調べることにした。

MacOSやiOSの実行ファイルのフォーマットはMach-Oというフォーマットである。
LINUX系はELF(Executable and Linking Format)Windows系はPE(Portable Executable)と呼ばれるようなものだ。
Mach-Oの中身をみるにはotool、nmのようなコマンドがあるので、
まず、AnyApp.ipaを解凍し、nmコマンドを実行してみた。

$> nm --debug-syms Any.app/Any
                 :
0000000101f6e480 D __ZZN12AnyNameSpace8AnyClass11getInstanceEvE8instance
                 :

AnyNameSpace::AnyClass::getInstance()のシンボルとアドレスがマップされている。
ただ、AnyClassの全てではなくgetInstance()のみである。

今度は、stringsコマンドで、実行ファイルの中の文字列を出力してみると
AnyClassに関して多量のクラスやメソッドが出力された。

$> strings Any.app/Any | grep "AnyClass"
                 :
__ZZN12AnyNameSpace8AnyClass11getInstanceEvE8instance
__ZZN12AnyNameSpace8AnyClass9AnyMethodEv
__ZZN12AnyNameSpace8AnyClass10AnyMethod2Ev
                 :

謎の名前修飾付き文字列

これは一体なんだろうか?
AnyNameSpace::AnyClass::AnyMethod()デバッグログで、以下のようなことをしたとすると、あくまで文字列は"AnyNameSpace::AnyClass::AnyMethod()"が残るべきである。

DebugLog("AnyNameSpace::AnyClass::AnyMethod()");

わざわざ、コンパイル後丁寧に名前修飾を付けた形の文字列が表示されるのはおかしい。。。

まずは、この文字列がMach-Oのどのセクションに入っているのかを調べる必要がある。

Mach-Oフォーマット

さて、ここでMach-Oフォーマットに関して簡略に説明をすると。
Mach-Oより古いELFやPEの場合は、以下のようになっている。
|Header|Sections....|

Sectionsはチャンクデータになっていて、以下のようになっている。
|Section ID|size|size分のデータ|
PEの場合は、Section IDが文字列で、.txt .data .resこのような感じだった記憶があり、種類や数も非常に多く、一つのセクションが終わったら何バイトーー512?ーーのAlignを守って次のセクションデータが出てくる形である。

Mach-Oは比較的ELFやPEに比べると新しいので、結構きれいに整理されていて、sectionがカテゴリ化されている。
フォーマットはこんな感じである。
|Header|Load Commands|Data|

Load Commandsのところはチャンクデータになっている。
|command id|command size|
commandの種類はReferenceを残しておくので、参考にしてほしい。

さらに、commadの中には、LC_SEGMENT (0x01), LC_SEGMENT64 (0x19)というのがあって、この中に探しているセクションが入っている。
つまり、
__TEXTセグメントの下に __text、\
_textconst、\_text_stab....のようなチャンクデータが続く形式で、セクションがカテゴリ化されているわけだ。
だったので、otoolを利用して、このipaに含まれるセクションリストを出力して見ることにした。

セクションリストの出力

bash-3.2$ otool -l Any.app/Any
      :
Load command 1
      cmd LC_SEGMENT_64
  cmdsize 1032
  segname __TEXT
   vmaddr 0x0000000100000000
   vmsize 0x0000000001ce8000
  fileoff 0
 filesize 30310400
  maxprot 0x00000005
 initprot 0x00000005
   nsects 12
    flags 0x0
Section
  sectname __text
   segname __TEXT
      addr 0x0000000100006200
      size 0x000000000189e9f8
    offset 25088
     align 2^2 (4)
    reloff 0
    nreloc 0
     flags 0x80000400
 reserved1 0
 reserved2 0
Section
  sectname __stubs
   segname __TEXT
      addr 0x00000001018a4bf8
      size 0x0000000000003144
    offset 25840632
     align 2^2 (4)
    reloff 0
    nreloc 0
     flags 0x80000408
 reserved1 0 (index into indirect symbol table)
 reserved2 12 (size of stubs)
      :

PEとは違い、section情報からすぐにデータが続くのではなく、offsetの方にサイズ分のデータがあるのがわかる。
ということは、先ほど探そうとした文字列がどこのセクションにあるかは、offsetとサイズでdumpをとり、検索すればいいという話だが、セクションが多かったので、実際にmach-oをロードして、文字列を探すプログラムを組むことにした。筆者はまだmach-oフォーマットを実際に触った事がなかったので、ちょうどいい機会だと思った。

更なる解析へ

さて、興味のあるのは、セグメントであるので、command idが0x01か0x19の場合のみである。
command idが0x01か0x19の場合は以下のような構造になる
|command id (0x01 or 0x19)|section size|segname|vmaddr|vmsize|fileoff|filesize|maxprot|initprot|nsects|flags|
そして、nsectsの数分セクションのデータが続く
|sectname|segname|addr|size|offset|align|reloff|nreloc|flags|reserved1|reserved2|reserved3|

ここのoffsetに実際のセグメントのデータが入っている。offsetは何となくヘッダーの次からのオフセットのはず。
これらの構造体は、以下のヘッダーに記載されている。

mach-o/loader.h

                             :
                             :
struct segment_command_64 { /* for 64-bit architectures */
    uint32_t  cmd;            /* LC_SEGMENT_64 */
    uint32_t  cmdsize;        /* includes sizeof section_64 structs */
    char      segname[16];    /* segment name */
    uint64_t  vmaddr;         /* memory address of this segment */
    uint64_t  vmsize;         /* memory size of this segment */
    uint64_t  fileoff;        /* file offset of this segment */
    uint64_t  filesize;       /* amount to map from the file */
    vm_prot_t maxprot;        /* maximum VM protection */
    vm_prot_t initprot;       /* initial VM protection */
    uint32_t  nsects;         /* number of sections in segment */
    uint32_t  flags;          /* flags */
};
                             :
                             :
struct section_64 { /* for 64-bit architectures */
    char     sectname[16];    /* name of this section */
    char     segname[16];     /* segment this section goes in */
    uint64_t addr;            /* memory address of this section */
    uint64_t size;            /* size in bytes of this section */
    uint32_t offset;          /* file offset of this section */
    uint32_t align;           /* section alignment (power of 2) */
    uint32_t reloff;          /* file offset of relocation entries */
    uint32_t nreloc;          /* number of relocation entries */
    uint32_t flags;           /* flags (section type and attributes)*/
    uint32_t reserved1;       /* reserved (for offset or index) */
    uint32_t reserved2;       /* reserved (for count or sizeof) */
    uint32_t reserved3;       /* reserved */
};
                             :
                             :

まずは、stringsで文字列__ZZN12AnyNameSpace8AnyClass9AnyMethodEvがどこにあるのか検索してみた。

bash-3.2$ strings --radix=d Any.app/Any
error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/strings: unknown flag: --radix=d
bash-3.2$

MacOSXに含まれているstringsはoffsetを出力できないようなので、特定の文字列を出力するstringsを適当に作成した。

bash-3.2$ ./strings Any.app/Any __ZZN12AnyNameSpace8AnyClass9AnyMethodEv
27181180 : __ZZN12AnyNameSpace8AnyClass9AnyMethodEv
bash-3.2$

27181180がoffsetなので、このオフセットがどこに含まれているかを見れば良い。
そこで、プログラムを組んでみた。
mach-o.c

#inclue <sys/types.h>
#include <mach-o/loader.h>
#include <mach-o/nlist.h>
#include <stdio.h>                                                                                                                
#include <sys/stat.h>

typedef struct mach_header_64 MACHO_header_64_t;
typedef struct load_command MACHO_load_command_t;

static MACHO_header_64_t *MACHO_Load(const char *filename,size_t *_size)
{
    struct stat s;
    size_t size;
    FILE *fp;
    void *p; 

    if (stat(filename,&s)!=0)
    {   
        return 0;
    }   
    fp=fopen(filename,"r");
    if (fp==0)
    {   
        printf("file not found!!! %s\n",filename);
        return 0;
    }   
    size=s.st_size;
    if (_size!=0)
    {   
        *_size=size;
    }   
    p=malloc(size+1);
    if (fread((char*)p,size,1,fp)!=1)
    {   
        fclose(fp);
        free(p);
        printf("read error!!! %s\n",filename);
        return 0;
    }   
    fclose(fp);
    return (MACHO_header_64_t*)p;
}

ファイルのロードはこんな感じ、めんどいので一変にロードしておく。
mach-oから先ほど指定したoffsetがどのセクションに入っているかチェックする関数


static void find_segment(const char *filename,int offset,char *segname, char *secname, int64_t* addr)                             
{
    MACHO_header_64_t *mach=MACHO_Load(filename,0);
    MACHO_load_command_t *cmds=(MACHO_load_command_t *)(mach+1);
    int is_found=0;
    const size_t header_size=sizeof(*mach);
    char *p=0;
    offset-=header_size;

    for (int i=0;i<mach->ncmds;++i)
    {
        p=(char*)cmds;
        if (cmds->cmd==LC_SEGMENT_64) /* セグメントの場合のみパース */
        {
            MACHO_segment_command_64_t *seg=(MACHO_segment_command_64_t *)cmds;
            MACHO_section_64_t *sec=(MACHO_section_64_t *)(seg+1);

            for (int j=0;j<seg->nsects;++j) /* セグメントに含まれているセクターのoffsetからどのセクションにいるのかを検索 */
            {
                if (sec[j].offset<offset&&
                        (sec[j].offset+sec[j].size)>offset)
                {
                    *addr=sec[j].addr+(offset-sec[j].offset);
                    strcpy(segname,seg->segname);
                    strcpy(secname,sec[j].sectname);
                    is_found=1;
                    break;
                }
            }
        }
        if (is_found==1)
        {
            break;
        }
        p+=cmds->cmdsize;
        cmds=(MACHO_load_command_t *)p;
    }
    MACHO_UnLoad(mach);
}

これらの関数を使って

int main(int argc,char **argvs)
{
        char seg[128],sec[128];
        int64_t addr=0;
        find_segment(argvs[1],atoi(argvs[2]),seg,sec,&addr);
        printf("%s[%s] addr:%lld\n",seg,sec,addr);
        return 0;
}

これをコンパイルし、実行すると

bash-3.2$ ./mach-o Any.app/Any 27181180
__TEXT[__const] addr:4322148476
bash-3.2$

うん。。。
意外な結果が出た。
実行領域のconst領域である。なぜ、DATA[const]ではないのかもびっくり。
個人的にはデバッグ情報が入っているセクションがあって、そこに入っていることを予想しただけに。。。

TEXT[const] addr:4322148476

メモリ上のアドレスは4322148476(0x1019EC07C)なので、どのセクションでこのメモリアドレスを参照しているのか見たくなった。
なので、この値で4322148476でバイナリを検索していく4byteAlignなので、4バイトずつ検索すれば見つかるはず。

void find_addr(const char *filename,char *segname,char *secname,int64_t addr,int64_t *_offset,int64_t *_addr)                     
{
    size_t size;
    MACHO_header_64_t *mach=MACHO_Load(filename,&size);
    MACHO_load_command_t *cmds=(MACHO_load_command_t *)(mach+1);
    int is_found=0;
    char *p=0;
    int *p_int=(int*)cmds;
    int cnt=size/4;
    int64_t offset=0;

    for (int i=0;i<cnt;++i)
    {   
        int64_t *p_int64=(int64_t*)p_int;
        if (*p_int64==addr)
        {   
            offset=i*4;
            *_offset=offset;
            break;
        }
        ++p_int;
    }
    offset-=sizeof(*mach);

    for (int i=0;i<mach->ncmds;++i)
    {
        p=(char*)cmds;
        if (cmds->cmd==LC_SEGMENT_64)
        {
            MACHO_segment_command_64_t *seg=(MACHO_segment_command_64_t *)cmds;
            MACHO_section_64_t *sec=(MACHO_section_64_t *)(seg+1);

            for (int j=0;j<seg->nsects;++j)
            {
                if (sec[j].offset<offset&&
                    (sec[j].offset+sec[j].size)>offset)
                {   
                    *_addr=sec[j].addr+(offset-sec[j].offset);
                    strcpy(segname,seg->segname);
                    strcpy(secname,sec[j].sectname);
                    is_found=1;
                    break;
                }   
            }   
        }   
        if (is_found==1)
        {   
            break;
        }   
        p+=cmds->cmdsize;
        cmds=(MACHO_load_command_t *)p;
    }   

    MACHO_UnLoad(mach);
}

main関数に追加

int main(int argc,char **argvs)
{
        char seg[128],sec[128];
        int64_t addr=0;
        if (argc==4&&strcmp(argvs[2],"-p")==0) /* 追加したところ */
        {
            int64_t offset,addr=0;
            find_addr(argvs[1],seg,sec,atoll(argvs[3]),&offset,&addr);
            printf("%s[%s] offset:%lld addr:%lld\n",seg,sec,offset,addr);
            retur 0;
        }
        find_segment(argvs[1],atoi(argvs[2]),seg,sec,&addr);
        printf("%s[%s] addr:%lld\n",seg,sec,addr);
        return 0;
    return 0;
}

実行結果

bash-3.2$ ./mach-o Any.app/Any -p 4322148476
__DATA[__const] offset:31817288 addr:4326784552

なるほど。
DATAのconstセクションで参照している。
前後をdumpしてみると。

bash-3.2$ ./filedump Any.app/Any 31817288 64 16
------------ dump() 64 bytes ------------ 
00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|
------------------------------------------------
7c|c0|9e|01|01|00|00|00|00|00|00|00|00|00|00|00|   |...............
a8|7e|e5|01|01|00|00|00|0c|2d|f3|00|01|00|00|00|   .~.......-......
10|2d|f3|00|01|00|00|00|14|2d|f3|00|01|00|00|00|   .-.......-......
4c|2d|f3|00|01|00|00|00|64|2d|f3|00|01|00|00|00|   L-......d-......

dump結果、
0x1019EC07Cが入っているのが見える(リトルエンディアンなので順番が逆に見えるが。。。)

7c|c0|9e|01|01|00|00|00|00|00|00|00|00|00|00|00|

その後、続くデータには64ビットの数字が続いている。
アドレスからすれば、
0x01019EC07C,0, 0x0101e57ea8,0x0100f32d0c....

これらは何か?DWARFはどこのセクションに?

これらは一体、何だろうか。。。
ここは置いておいて、DWARF (Debug With Attributed Record Format )について調べることにした。
目的は、このDWARFがどのセクションに配置され、これらの文字列を参照しているかどうかをみるためである。
ELFやPEのようなフォーマットは.debuginfoのような分かりやすいセクションに配置されていた。
だが、Mach-Oにはそのようなセクションが見当たらないので、今度はDWARFがどこに入っているのかを探していく。

DWARFに関しては次回に。。。

To be continued.

採用情報

ワンダープラネットでは、一緒に働く仲間を幅広い職種で募集しております。

-エンジニア

© WonderPlanet Inc.