2023/09/20(水) 23:40頃に、PDBファイルやELFファイル中のシンボル名の確認結果を追加しました。その確認中に「x86 PEビルド結果でIDAが正常に関数名を表示できているように見えるものの、そもそも先頭にアンダースコアが付与されている」と気付いたので、本記事タイトル等を修正しました。(以前の記事タイトル: IDAはx86 ELFとx64 PE/ELFで一部レジスタと同一シンボル名を無視するらしい
)
先日開催されたSECCON 2023 Qualsのxuyao問題のバイナリをIDAで解析していると、シンボル情報を含むELFであり、明らかにユーザー定義の関数であるにも関わらず、IDAがとある関数を何故かsub_15B9
とさもシンボル名が無いかのように表示しました。
終了後にTwitter(この表記を使い続けます)でぼやいていると、Arata氏から、Cソースで定義しているes
関数がレジスタ名と衝突しているためIDAはシンボル名として使われなかったのでは、という情報をいただきました。
手元で実験してみたのですが、レジスタの名前と衝突するためIDAではシンボル名esが使われなかったようです。Ghidraやgdbなどではes関数と正しく表示されていました。
— Arata (@arata_nvm) September 18, 2023
確認してみると、少なくともx64 PEの場合やx86/x64 ELFの場合は、eax
等の一部レジスタ名と同一であるシンボル名(=関数名やグローバル変数名)について、IDAは無視しました。なお、x86 PEの場合はビルド結果のPDB中のシンボル名が先頭にアンダースコアの付いた_eax
等のシンボル名になってしまったため、結果は不明です。
- 使用ソフトウェアバージョン等
- 確認結果のまとめ
- 確認用Cソースコード生成Pythonスクリプト
- 例としてeax関数のIDA表示
- 読み込ませるPDBファイルやELFファイル中にeax関数のシンボル情報が存在することの確認
- 感想
使用ソフトウェアバージョン等
以下のソフトウェアを使用しました。
- IDA Version 8.3.230608 Windows x64 (64-bit address size)
- Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.7.2
- gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Free版IDAではx86またはx64の、PE形式またはELF形式のみを扱えるため、その範囲内で確認しました。IDA Pro等ではARM等の別ISAも扱うことができます。別ISAを扱う場合は「無視されるシンボル名」が変化する可能性があります。
PE形式のシンボル情報は、対応するPDBファイルをIDAに読み込ませることで与えました。PE中のDebug Directory等によるPDBファイル以外の方法でシンボル情報を与える方法は、そのようなPEを生成する方法が分からないので未確認です……。なお、ELF形式のシンボル情報はELFそのものに格納されるため、IDAにELFを読み込ませることで自動的に与えられます。
確認結果のまとめ
私はアセンブリ言語やISAにあまり詳しくないため、記述に誤りがあるかもしれません。誤りがあればご指摘いただけると助かります。また、前述した通り、x86 PE形式はPDB中のシンボル名が先頭にアンダースコアをつけた_eax
等のシンボル名になってしまっため、表には含めていません。
- o := IDAがシンボル名の通りに認識して表示する
- x := IDAがシンボル名を無視する
シンボル名 | x86 ELF | x64 PE/ELF | メモ |
---|---|---|---|
al,ah,ax,eax | x | x | |
rax | o | x | x64で追加されたレジスタ |
cl,ch,cx,ecx | x | x | |
rcx | o | x | x64で追加されたレジスタ |
dl,dh,dx,edx | x | x | |
rdx | o | x | x64で追加されたレジスタ |
bl,bh,bx,ebx | x | x | |
rbx | o | x | x64で追加されたレジスタ |
sil,si,esi | x | x | silはx86には無いのでは? |
rsi | o | x | x64で追加されたレジスタ |
dil,di,edi | x | x | dilはx86には無いのでは? |
rdi | o | x | x64で追加されたレジスタ |
spl,sp,esp | x | x | splはx86には無いのでは? |
rsp | o | x | x64で追加されたレジスタ |
bpl,bp,ebp | x | x | bplはx86には無いのでは? |
rbp | o | x | x64で追加されたレジスタ |
ipl | o | o | 存在するか分かりませんが念のため試しました |
ip,eip | x | x | |
rip | o | x | x64で追加されたレジスタ |
cs,ds,es,fs,gs,ss | x | x | セグメントレジスタ系統 |
efl | x | x | EFLAGS系統でeflだけ無視扱いになるのはどういった理由なのか…… |
flg,rfl,flag,flags,eflag,eflags,rflag,rflags | o | o | |
af,cf,df,if,of,pf,sf,tf,zf | x | x | EFLAGSの各種ビット |
iopl,nt,md,rf,vm,ac,vif,vip,id,ai | o | o | ELFAGSの各種ビット |
r8l(r8~r15で同様) | o | o | r8bとは異なりx64でも認識される |
r8b,r8w,r8d(r8~r15で同様) | o | x | x64で追加されたレジスタ |
r8(r8~r15で同様) | x | x | x86には無いのでは? |
r16b,r16l,r16w,r16d,r16 | o | o | 「Advanced Performance Extensions(APX)」で追加されるレジスタの1つ、「未対応」の模様 |
mmx0~mmx15 | o | o | SIMD用、予想に反して認識された |
xmm0~xmm15 | x | x | SIMD用 |
ymm0~ymm15 | x | x | SIMD用 |
st0~st7 | x | x | 浮動小数点数演算用 |
cr0~cr7 | o | o | 制御レジスタ |
dr0~dr7 | o | o | デバッグレジスタ |
r3,a3,pc,lr | o | o | ARM ISAが使うレジスタ名をいくつか確認、流石に認識されました |
なお、「同一かどうか」は大文字小文字を無視して判定されるようです。例えば、eax
, EAX
, Eax
は3個すべて同一の結果になりました。
確認用Cソースコード生成Pythonスクリプト
#!/bin/env python3 import subprocess register_name_list = [ "al", "ah", "ax", "eax", "rax", "cl", "ch", "cx", "ecx", "rcx", "dl", "dh", "dx", "edx", "rdx", "bl", "bh", "bx", "ebx", "rbx", "sil", "si", "esi", "rsi", "dil", "di", "edi", "rdi", "spl", "sp", "esp", "rsp", "bpl", "bp", "ebp", "rbp", "ipl", "ip", "eip", "rip", "cs", "ds", "es", "fs", "gs", "ss", "flg", "efl", "rfl", "flag", "flags", "eflag", "eflags", "rflag", "rflags", # FLAGSレジスタ系 "af", "cf", "df", "if", "of", "pf", "sf", "tf", "zf", "iopl", "nt", "md", "rf", "vm", "ac", "vif", "vip", "id", "ai", "r8b", "r8l", "r8w", "r8d", "r8", "r9b", "r9l", "r9w", "r9d", "r9", "r10b", "r10l", "r10w", "r10d", "r10", "r11b", "r11l", "r11w", "r11d", "r11", "r12b", "r12l", "r12w", "r12d", "r12", "r13b", "r13l", "r13w", "r13d", "r13", "r14b", "r14l", "r14w", "r14d", "r14", "r15b", "r15l", "r15w", "r15d", "r15", "r16b", "r16l", "r16w", "r16d", "r16", # 「Advanced Performance Extensions(APX)」で追加されるレジスタの1つ "mmx0","mmx1","mmx2","mmx3","mmx4","mmx5","mmx6","mmx7","mmx8","mmx9","mmx10","mmx11","mmx12","mmx13","mmx14","mmx15", "xmm0","xmm1","xmm2","xmm3","xmm4","xmm5","xmm6","xmm7","xmm8","xmm9","xmm10","xmm11","xmm12","xmm13","xmm14","xmm15", "ymm0","ymm1","ymm2","ymm3","ymm4","ymm5","ymm6","ymm7","ymm8","ymm9","ymm10","ymm11","ymm12","ymm13","ymm14","ymm15", "st0", "st1", "st2", "st3", "st4", "st5", "st6", "st7", "cr0", "cr1", "cr2", "cr3", "cr4", "cr5", "cr6", "cr7", "dr0", "dr1", "dr2", "dr3", "dr4", "dr5", "dr6", "dr7", "r3", "a3", "pc", "lr", # ARMで使っているらしいレジスタをいくつか ] symbol_name_list = [] for r in register_name_list: if r != "if": # ifはC言語のキーワード symbol_name_list.append(r.lower()) symbol_name_list.append(r.upper()) if not r[1].isdigit(): symbol_name_list.append(r.capitalize()) use_as_function_name = True # if false then use global varianble name # x86 PEでアンダースコアがつくのはマングリングが原因かと疑いましたが、違いました code = "" code += "#include<stdio.h>\n" # code += """#ifdef __cplusplus # extern "C"{ # #endif # """ for sn in symbol_name_list: if use_as_function_name: code += f'void {sn}(void) {{ puts("{sn}"); }}\n' else: code += f'const char {sn}[] = "{sn}";\n' code += "int main(void) {\n" for sn in symbol_name_list: if use_as_function_name: code += f" {sn}();\n" else: code += f" puts({sn});\n" code += " return 0;\n" code += "}\n" # code += """#ifdef __cplusplus # } # #endif # """ with open("test.c", "w") as f: f.write(code) # ELF形式のビルド subprocess.run(["gcc", "-m32", "-o", "test_32.elf", "-no-pie", "-fno-pie", "test.c"]) # PIEが有効だと「__x86_get_pc_thunk_bx」結果からの相対アドレスアクセスをしていて読みづらいので、PIEを無効化します subprocess.run(["gcc", "-m64", "-o", "test_64.elf", "test.c"]) # PE形式は、別途VisualStudioを使い、test.cをx86/x64それぞれでビルドしました
例としてeax
関数のIDA表示
読み込ませるPDBファイルやELFファイル中にeax
関数のシンボル情報が存在することの確認
PDBファイル内容の確認にはmicrosoft/microsoft-pdb: Information from Microsoft about the PDB format. We'll try to keep this up to date. Just trying to help the CLANG/LLVM community get onto Windows.中のcvdump/cvdump.exe
を使いました。ただ「EXE中の特定仮想アドレスにどのシンボル名が紐づいているか」情報をどうすれば得られるのか分かりませんでした。そのため、とりあえずeax
というシンボルがあることを確認しています。
x86 PEに紐づいたPDBファイル中のシンボル確認結果です。eax
というシンボル名がありそうなことが分かります。ただ_eax
という先頭にアンダースコアがついたシンボル名もありそうです。IDAが正常に表示できている理由は_eax
シンボルが存在するために思えます。
C:\>cvdump.exe x86_CppWin32Project.pdb | findstr eax S_PUB32: [0002:0000AC00], Flags: 00000002, _eax S_PUB32: [0003:00000B54], Flags: 00000000, ??_C@_03HMLAHFDL@eax@ (00F264) S_GPROC32: [0002:0000AC00], Cb: 0000004C, Type: 0x1016, eax (000544) S_DEFRANGE_REGISTER: eax (000788) S_DEFRANGE_REGISTER: eax (000988) S_DEFRANGE_REGISTER: eax (000B14) S_DEFRANGE_REGISTER: eax (000C98) S_DEFRANGE_REGISTER: eax (000DA8) S_DEFRANGE_REGISTER: eax (000E70) S_DEFRANGE_REGISTER: eax (000E80) S_DEFRANGE_REGISTER:MayAvailable eax (0013A0) S_DEFRANGE_REGISTER: eax (0015F0) S_DEFRANGE_REGISTER: eax (001600) S_DEFRANGE_REGISTER:MayAvailable eax (001790) S_DEFRANGE_REGISTER: eax (0001B8) S_DEFRANGE_REGISTER: eax (0001D8) S_DEFRANGE_REGISTER:MayAvailable eax (000224) S_DEFRANGE_REGISTER: eax (000234) S_DEFRANGE_REGISTER:MayAvailable eax (00035C) S_DEFRANGE_REGISTER: eax (0006F8) S_DEFRANGE_REGISTER: eax (0009B8) S_DEFRANGE_REGISTER: eax (000A24) S_DEFRANGE_REGISTER: eax (000C3C) S_DEFRANGE_REGISTER: eax (000C50) S_DEFRANGE_REGISTER:MayAvailable eax (000D20) S_DEFRANGE_SUBFIELD_REGISTER: offset at 0004: eax (000D34) S_DEFRANGE_SUBFIELD_REGISTER: offset at 0004:MayAvailable eax (000EB0) S_DEFRANGE_REGISTER: eax (000F1C) S_DEFRANGE_REGISTER: eax (0002A4) S_DEFRANGE_REGISTER: eax (0002D4) S_DEFRANGE_REGISTER:MayAvailable eax (0003A8) S_DEFRANGE_REGISTER: eax (0003D0) S_DEFRANGE_REGISTER:MayAvailable eax (000458) S_DEFRANGE_REGISTER: eax (000468) S_DEFRANGE_REGISTER:MayAvailable eax (000548) S_DEFRANGE_REGISTER: eax (000578) S_DEFRANGE_REGISTER:MayAvailable eax (00060C) S_DEFRANGE_REGISTER: eax (00061C) S_DEFRANGE_REGISTER:MayAvailable eax (0002EC) S_DEFRANGE_REGISTER: eax (000428) S_DEFRANGE_REGISTER: eax (0004E8) S_DEFRANGE_REGISTER: eax (0005E8) S_DEFRANGE_REGISTER: eax S_PROCREF: 0x00000000: ( 3, 0000F264) eax C:\>
x64 PEに紐づいたPDBファイル中のシンボル確認結果です。eax
というシンボル名がありそうなことが分かります。
C:\>cvdump.exe x64_CppWin32Project.pdb | findstr eax Type = 0x1076 Scope = global eax S_PUB32: [0002:00007C20], Flags: 00000002, eax S_PUB32: [0003:00000BD4], Flags: 00000000, ??_C@_03HMLAHFDL@eax@ (0092B0) S_GPROC32: [0002:00007C20], Cb: 00000032, Type: 0x1076, eax (000DCC) S_DEFRANGE_REGISTER: eax (000DDC) S_DEFRANGE_REGISTER:MayAvailable eax (001534) S_DEFRANGE_REGISTER: eax (0016C8) S_DEFRANGE_REGISTER: eax (00037C) S_DEFRANGE_REGISTER: eax (000DD8) S_DEFRANGE_REGISTER: eax S_PROCREF: 0x00000000: ( 1, 000092B0) eax C:\>
x86 ELF中のシンボル確認結果です。仮想アドレス0x08049257
の関数にeax
というシンボル名が紐付けられていそうなことが分かります。
$ readelf -s test_32.elf | grep eax 58: 08049257 25 FUNC GLOBAL DEFAULT 13 eax $
x64 ELF中のシンボル確認結果です。仮想アドレス0x0000000000001233
の関数にeax
というシンボル名が紐付けられていそうなことが分かります。
$ readelf -s test_64.elf | grep eax 58: 0000000000001233 26 FUNC GLOBAL DEFAULT 16 eax $
感想
x86では存在しないはずの、x64で追加されたレジスタへの8-bitアクセス用のレジスタ名称と同一のシンボル名でもIDAは無視する場合があったりして、よく分からない結果になりました。何はともあれ、使用ツールの挙動を知ることは重要ですし、シンボル名があるかどうか等を複数のツールを使って確認するが大事になりそうです。
「重要な関数の名前を省略形にして、無視される種類のレジスタ名と同一の名前にすることで、IDAユーザーだけに対する耐解析として機能させる」ことも一応できるのかなと思いました。