プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

IDAは一部レジスタと同一名のシンボルを無視するので注意

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はシンボル名として使われなかったのでは、という情報をいただきました。

確認してみると、少なくともx64 PEの場合やx86/x64 ELFの場合は、eax等の一部レジスタ名と同一であるシンボル名(=関数名やグローバル変数名)について、IDAは無視しました。なお、x86 PEの場合はビルド結果のPDB中のシンボル名が先頭にアンダースコアの付いた_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表示

x86 PEの場合はそもそもシンボル名先頭にアンダースコアが付与されて_eax等になるため、レジスタ名と一致せず、シンボル名の通り表示される

x64 PEの場合はeax関数のシンボル名が無視されてsub_140018C20表示になった

x86 ELFの場合はeax関数のシンボル名が無視されてsub_8049257表示になった

x64 ELFの場合はeax関数のシンボル名が無視されてsub_1233表示になった

読み込ませる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ユーザーだけに対する耐解析として機能させる」ことも一応できるのかなと思いました。