home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

技術/Windows/PE(Portable Executable)フォーマットの実験/02, 再配置情報で遊ぼう! (v1)

技術/Windows/PE(Portable Executable)フォーマットの実験/02, 再配置情報で遊ぼう! (v1)

技術 / Windows / PE(Portable Executable)フォーマットの実験 / 02, 再配置情報で遊ぼう! (v1)
id: 678 所有者: msakamoto-sf    作成日: 2010-06-10 20:31:46
カテゴリ: C言語 Python Windows hacks 

PEファイルの再配置情報を取得、表示してみる。
さらにその発展として、PythonによりDLLをメモリ上にロード、再配置情報とIATを手動で書き換え実行可能にし、実際にエクスポート関数をPythonから呼んでみる。

Pythonは 2.6 を使用。
OSはWindows XP SP3, VCはVC++ 2008 Express Edition, コンパイラとリンカのバージョンは次の通り:

> cl
Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.
> link
Microsoft (R) Incremental Linker Version 9.00.30729.01
Copyright (C) Microsoft Corporation.  All rights reserved.

サンプルコード

今回は、特に再配置情報を操作するPythonスクリプトが巨大化した為、C言語のソースファイルと合わせてzipファイルにまとめた。
以下のURLからダウンロード出来る。
https://www.glamenv-septzen.net//medias/manual_dll_relocation.zip

  • dlltest.c : 再配置情報で遊ぶ為のDLL
  • main.c : dlltest.dll動作確認用の簡単なEXE
  • main1.py : Pythonのctypesモジュールで簡単にdlltest.dllを利用出来ますよ~というサンプル
  • main2.py : ctypesモジュール経由でLoadLibrary() + GetProcAddress()で明示的にdlltest.dllをロードするサンプル
  • show_relocations.py : 再配置情報を表示するPythonスクリプト
  • test_pseudo_load.py : DLLを手動でメモリ上に展開し、再配置情報を更新し、インポート情報を基にIATを更新し、実行可能なメモリイメージとして構築するPythonスクリプト

show_relocations.py, test_pseudo_load.py の解説は後に回し、まずはdlltest.c, main.c, main1/2.py を紹介する。

DLL側:dlltest.c

再配置情報を弄って遊ぶためのDLLで、"/NOENTRY"を指定してCRTのエントリ関数を削り、余計なインポートや再配置情報を省いてみた。
dlltest.c:

#include <windows.h>
 
int __declspec(dllexport) funcA(int a, int b)
{
    MessageBox(GetDesktopWindow(), "funcA", "captionA", MB_OK);
    a = funcC(b);
    return a + b + 1;
}
 
int __declspec(dllexport) funcB(int a, int b)
{
    MessageBox(GetDesktopWindow(), "funcB", "captionB", MB_OK);
    b = funcC(a);
    return a * b + 1;
}
 
int funcC(int c)
{
    return c * 2;
}

GetDesktopWindow(), MessageBox(), funcC()のアドレスが再配置の対象になると予想される。

ビルド:

> cl /LD dlltest.c user32.lib /link /noentry
dumpbinでインポート情報を確認
> dumpbin /imports dlltest.dll
...
    USER32.dll
              10002000 Import Address Table
              10002034 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                  11C GetDesktopWindow
                  1F8 MessageBoxA
dumpbinで再配置情報を確認
> dumpbin /relocations dlltest.dll
...
BASE RELOCATIONS #4
    1000 RVA,       18 SizeOfBlock
       6  HIGHLOW            10003000
       B  HIGHLOW            1000300C
      11  HIGHLOW            10002000
      18  HIGHLOW            10002004
      46  HIGHLOW            10003014
      4B  HIGHLOW            10003020
      51  HIGHLOW            10002000
      58  HIGHLOW            10002004
   # offset type             ファイル上の値
dumpbinで逆アセンブラ結果と付き合わせてみる

再配置情報と付き合わせてみると、見事に各offsetとその値が逆アセンブラ結果と対応していることが分かる。
また、GetDesktopWindow()/MessageBoxA()がそれぞれ

call        dword ptr ds:[10002000h] # GetDesktopWindow()
call        dword ptr ds:[10002004h] # MessageBoxA()

という命令になっているが、"10002000h"は丁度IATのアドレスを指すようになっている。PEヘッダー上のIATアドレスはRVAなのでローダが処理してくれるが、実行コード内のIATアドレスについては、再配置情報としてローダに処理して貰う。

> dumpbin /disasm dlltest.dll
...
  ## funcA()
  10001000: 55                 push        ebp
  10001001: 8B EC              mov         ebp,esp
  10001003: 6A 00              push        0
  ## -> "captionA"
  10001005: 68 00 30 00 10     push        10003000h
  ## -> "funcA"
  1000100A: 68 0C 30 00 10     push        1000300Ch
  ## -> GetDesktopWindow()
  1000100F: FF 15 00 20 00 10  call        dword ptr ds:[10002000h]
  10001015: 50                 push        eax
  ## -> MessageBoxA()
  10001016: FF 15 04 20 00 10  call        dword ptr ds:[10002004h]
  1000101C: 8B 45 0C           mov         eax,dword ptr [ebp+0Ch]
  1000101F: 50                 push        eax
  ## -> funcC()
  10001020: E8 5B 00 00 00     call        10001080
  10001025: 83 C4 04           add         esp,4
  10001028: 89 45 08           mov         dword ptr [ebp+8],eax
  1000102B: 8B 4D 0C           mov         ecx,dword ptr [ebp+0Ch]
  1000102E: 8B 55 08           mov         edx,dword ptr [ebp+8]
  10001031: 8D 44 0A 01        lea         eax,[edx+ecx+1]
  10001035: 5D                 pop         ebp
  10001036: C3                 ret
...
  ## funcB()
  10001040: 55                 push        ebp
  10001041: 8B EC              mov         ebp,esp
  10001043: 6A 00              push        0
  ## -> "captionB"
  10001045: 68 14 30 00 10     push        10003014h
  ## -> "funcB"
  1000104A: 68 20 30 00 10     push        10003020h
  ## -> GetDesktopWindow()
  1000104F: FF 15 00 20 00 10  call        dword ptr ds:[10002000h]
  10001055: 50                 push        eax
  ## -> MessageBoxA()
  10001056: FF 15 04 20 00 10  call        dword ptr ds:[10002004h]
  1000105C: 8B 45 08           mov         eax,dword ptr [ebp+8]
  1000105F: 50                 push        eax
  ## -> funcC()
  10001060: E8 1B 00 00 00     call        10001080
  10001065: 83 C4 04           add         esp,4
  10001068: 89 45 0C           mov         dword ptr [ebp+0Ch],eax
  1000106B: 8B 45 08           mov         eax,dword ptr [ebp+8]
  1000106E: 0F AF 45 0C        imul        eax,dword ptr [ebp+0Ch]
  10001072: 83 C0 01           add         eax,1
  10001075: 5D                 pop         ebp
  10001076: C3                 ret
...
  ## funcC()
  10001080: 55                 push        ebp
  10001081: 8B EC              mov         ebp,esp
  10001083: 8B 45 08           mov         eax,dword ptr [ebp+8]
  10001086: D1 E0              shl         eax,1
  10001088: 5D                 pop         ebp
  10001089: C3                 ret

DLLをロードするサンプル:C言語版 main.c

main.c:

#include <stdio.h>
 
int __declspec(dllimport) funcA(int a, int b);
int __declspec(dllimport) funcB(int a, int b);
 
int main(int argc, char *argv[])
{
    printf("funcA(2, 3) = %d\n", funcA(2, 3));
    printf("funcB(2, 3) = %d\n", funcB(2, 3));
    return 0;
}

ビルド・実行:

> cl main.c dlltest.lib
> main.exe
(メッセージボックス表示)
funcA(2, 3) = 10
(メッセージボックス表示)
funcB(2, 3) = 9

DLLをロードするサンプル:Python版 main1.py(ctypesで自動ロード)

main1.py:

import ctypes
dlltest = ctypes.cdll.dlltest
print "funcA(2, 3) = %d" % dlltest.funcA(2, 3)
print "funcB(2, 3) = %d" % dlltest.funcB(2, 3)

実行:

> python main1.py
(メッセージボックス表示)
funcA(2, 3) = 10
(メッセージボックス表示)
funcB(2, 3) = 9

DLLをロードするサンプル:Python版 main2.py(LoadLibrary(), GetProcAddress()で手動ロード)

main2.py:

import ctypes
 
# LoadLibrary(), GetProcAddress()についてはctypes標準機能で自動ロード
 
hModule = ctypes.windll.kernel32.LoadLibraryA('dlltest.dll')
print "hModule = 0x%08X" % hModule
 
addr_funcA = ctypes.windll.kernel32.GetProcAddress(hModule, 'funcA')
print "Address of 'funcA' = 0x%08X" % addr_funcA
 
addr_funcB = ctypes.windll.kernel32.GetProcAddress(hModule, 'funcB')
print "Address of 'funcB' = 0x%08X" % addr_funcB
 
# アドレス値から関数を取得する為の下準備として、prototypeを作成する。
# 今回はcdecl呼び出し規約の関数をインポートするので"CFUNCTYPE"を使う。
# stdcallの場合は"WINFUNCTYPE"を使うことになる。
prototypes = {
    'funcA' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int),
    'funcB' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int),
    }
 
# prototypeとアドレスから、ctypesの関数インスタンスを作成する。
funcA = prototypes['funcA'](addr_funcA)
funcB = prototypes['funcB'](addr_funcB)
 
# 実際に呼んでみる
print "funcA(2, 3) = %d" % funcA(2, 3)
print "funcB(2, 3) = %d" % funcB(2, 3)

実行:

> pythonn main2.py
hModule = 0x10000000
Address of 'funcA' = 0x10001000
Address of 'funcB' = 0x10001040
(メッセージボックス表示)
funcA(2, 3) = 10
(メッセージボックス表示)
funcB(2, 3) = 9

show_relocations.py : 再配置情報を表示するPythonスクリプト

IMAGE_OPTIONAL_HEADERのDataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]はIMAGE_BASE_RELOCATION構造体の配列の先頭アドレスを指しているので、そこから辿ることが出来る。
一つのIMAGE_BASE_RELOCATION構造体の後ろには、実際に書き換えるアドレスの種別とOFFSETを記録したWORD配列が並んでいる。
WORD値の上位4bitが再配置のタイプ、下位12bitがOFFSETとなる。OFFSETはIMAGE_BASE_RELOCATION構造体のVirtualAddressからのOFFSETとなる。

show_relocations.py : ソース全体

show_relocations.py: (ソース巨大化のため省略)

show_relocations.py : 解説

途中までは 日記/2010/06/10/PEファイルのDLL遅延ロード情報を表示するPython と同じで、構造体の定義とメモリ上にヘッダーとセグメントをロードする。

再配置情報の取得は以下のコードから始まる:

# Base Relocations
br_entry = opt_header.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
br_rva = br_entry.VirtualAddress
br_head = image_buf_ptr + br_rva
br_tail = br_head + br_entry.Size
 
if 0 == br_rva:
  print "NO BASE RELOCATIONS"
  quit()

br_tailでわざわざ再配置情報のメモリブロック終端アドレスを保存しているのは、ループ脱出条件に使う為。IMAGE_BASE_RELOCATION構造体には要素数を保持するメンバが無い為、br_tailと比べることでループ脱出を判定している。
EXEなど再配置情報が存在しない場合は、メッセージを表示して終了。

続くコードで、IMAGE_BASE_RELOCATION構造体の配列を順次取得していく:

br_sz = ctypes.sizeof(IMAGE_BASE_RELOCATION)
br_type_sz = ctypes.sizeof(WORD)
offset = br_head
brs = []
while 1:
  br = IMAGE_BASE_RELOCATION()
  ctypes.memmove(
      ctypes.addressof(br),
      offset,
      br_sz)
  br.types = []
  offset = offset + br_sz

次のコードは、IMAGE_BASE_RELOCATION構造体のSizeOfBlockメンバを元に、WORD配列の要素数を逆算している:

 br_types_num = (br.SizeOfBlock - br_sz) / br_type_sz

SizeOfBlockはWORD配列のサイズ + IMAGE_BASE_RELOCATION構造体自身のサイズになっているため、自分自身を引いた後、WORD型のサイズで割れば配列のよう素数が得られる。
以降、要素数を元にWORD配列を取得する:

for i in xrange(br_types_num):
    br_type = WORD()
    ctypes.memmove(
        ctypes.addressof(br_type),
        offset,
        br_type_sz)
    offset = offset + br_type_sz
    br.types.append(br_type)
  brs.append(br)
  if offset + 1 > br_tail:
    break

以下は取得出来たIMAGE_BASE_RELOCATION構造体の内容を出力する:

for br in brs:
  print "-----------------"
  print "VirtualAddress = 0x%08X" % br.VirtualAddress
  print "SizeOfBlock = 0x%08X" % br.SizeOfBlock
  print "\t[TYPE],\t[OFFSET],\t[FILE VALUE]"
  for br_type in br.types:
    # 上位4bit
    rel_base_type = (0xF000 & br_type.value) >> 12
    # 下位12bit
    rel_base_offset = 0xFFF & br_type.value
    # 実際にファイルに書き込まれている値を取得
    file_value = DWORD()
    ctypes.memmove(
        ctypes.addressof(file_value),
        image_buf_ptr + br.VirtualAddress + rel_base_offset,
        ctypes.sizeof(DWORD))
    print "\t%s,\t%X h,\t%X" % (
        IMAGE_REL_BAESD_TYPES[rel_base_type], 
        rel_base_offset,
        file_value.value
        )

show_relocations.py : 実行例

> show_relocations.py dlltest.dll
-----------------
VirtualAddress = 0x00001000
SizeOfBlock = 0x00000018
        [TYPE], [OFFSET],       [FILE VALUE]
        HIGHLOW,        6 h,    10003000
        HIGHLOW,        B h,    1000300C
        HIGHLOW,        11 h,   10002000
        HIGHLOW,        18 h,   10002004
        HIGHLOW,        46 h,   10003014
        HIGHLOW,        4B h,   10003020
        HIGHLOW,        51 h,   10002000
        HIGHLOW,        58 h,   10002004

dumpbin:

> dumpbin /relocations dlltest.dll
...
BASE RELOCATIONS #4
    1000 RVA,       18 SizeOfBlock
       6  HIGHLOW            10003000
       B  HIGHLOW            1000300C
      11  HIGHLOW            10002000
      18  HIGHLOW            10002004
      46  HIGHLOW            10003014
      4B  HIGHLOW            10003020
      51  HIGHLOW            10002000
      58  HIGHLOW            10002004

dumpbinと同様に再配置情報を取得出来ている。

test_pseudo_load.py : メモリ上に展開した後、手動で再配置情報とIATを書き換えて実行可能にしてみる

つまり、OSのローダがしている処理を自前で実装してみる実験。
簡単にするため、以下のように機能をそぎ落としている。

  • DLLしかロードしない
  • 序数でエクスポートしているシンボルは無視
  • 序数でインポートしているシンボルも無視
  • インポートで転送しているのが見つかっても無視
  • 事前バインドも無視
  • 遅延ロードは考慮しない

とりあえず今回のdlltest.dllをロードし、funcA(), funcB()を実行するにはこれでも問題ない。

test_pseudo_load.py : ソース全体

test_pseudo_load.py: (ソース巨大化のため省略)

test_pseudo_load.py : 解説

クラス定義に加え、インポート情報/エクスポート情報/再配置情報それぞれの構築とダンプ表示、再配置情報とIATの更新を関数にまとめた。
本体の、実験の要となる処理について見ていく。

まず、ファイルイメージをロードするメモリ領域を VirtualAlloc() を使って読み書き・実行可能なページに確保するようにしている。
これまでの実験ではctypesのcreate_string_buffer()で確保していたが、それだと実行可能なページには確保されない。

# {{{ load to memory
f.seek(0, 0)
file_bytes = f.read() # read all file contents
MEM_COMMIT = 0x1000
PAGE_EXECUTE_READWRITE = 0x40 # WinXP SP2 や Win2k 等では無効かも。MSDNを確認して下さい。
 
image_buf_ptr = ctypes.windll.kernel32.VirtualAlloc(
    None,
    opt_header.SizeOfImage,
    MEM_COMMIT,
    PAGE_EXECUTE_READWRITE
    )
 
# copy headers
ctypes.memmove(
    image_buf_ptr, 
    file_bytes,
    opt_header.SizeOfHeaders
    )

続いて再配置情報を取得・表示する。

base_relocations = build_base_relocations(opt_header, image_buf_ptr)
 
if 0 == len(base_relocations):
  print "NO BASE RELOCATIONS"
  quit()
 
print "-------[ RELOCATION INFO : BEFORE RELOCATION ]-------"
dump_base_relocations(base_relocations, image_buf_ptr)
print "-----------------------------------------------------"
print

その後、ヘッダー上のImageBaseと実際にロードされた先頭アドレスの差分を取得し、再配置情報を更新する。

print "File ImageBase = 0x%08X" % opt_header.ImageBase
print "Real ImageBase = 0x%08X" % image_buf_ptr
diff = image_buf_ptr - opt_header.ImageBase
print "Differencial = 0x%08X" % diff
print
 
relocate_base_relocations(base_relocations, image_buf_ptr, diff)
raw_input('Pausing ... Hit Return for Continue :')

ここまでの実行結果:

> test_pseudo_load.py dlltest.dll
-------[ RELOCATION INFO : BEFORE RELOCATION ]-------
-----------------
VirtualAddress = 0x00001000
SizeOfBlock = 0x00000018
        [TYPE], [OFFSET],       [FILE VALUE]
        HIGHLOW,        6 h,    10003000
        HIGHLOW,        B h,    1000300C
        HIGHLOW,        11 h,   10002000
        HIGHLOW,        18 h,   10002004
        HIGHLOW,        46 h,   10003014
        HIGHLOW,        4B h,   10003020
        HIGHLOW,        51 h,   10002000
        HIGHLOW,        58 h,   10002004
-----------------------------------------------------

File ImageBase = 0x10000000
Real ImageBase = 0x00A00000
Differencial = 0x-F600000

Pausing ... Hit Return for Continue :

ENTERキーを押せば、更新された再配置情報を表示する。

print "-------[ RELOCATION INFO : AFTER RELOCATION ]-------"
dump_base_relocations(base_relocations, image_buf_ptr)
print "----------------------------------------------------"
print

実行結果を見ると、実際にアドレスが調整されたことを確認出来る:

-------[ RELOCATION INFO : AFTER RELOCATION ]-------
-----------------
VirtualAddress = 0x00001000
SizeOfBlock = 0x00000018
        [TYPE], [OFFSET],       [FILE VALUE]
        HIGHLOW,        6 h,    A03000
        HIGHLOW,        B h,    A0300C
        HIGHLOW,        11 h,   A02000
        HIGHLOW,        18 h,   A02004
        HIGHLOW,        46 h,   A03014
        HIGHLOW,        4B h,   A03020
        HIGHLOW,        51 h,   A02000
        HIGHLOW,        58 h,   A02004
----------------------------------------------------

続いてインポート情報をダンプし、表示する。

ids1 = build_import_descriptors(opt_header, image_buf_ptr)
print "-------[ IMPORT INFO : BEFORE IMPORTING ]-------"
dump_import_descriptors(ids1, image_buf_ptr)
print "------------------------------------------------"
print

この時点でのダンプ表示:

-------[ IMPORT INFO : BEFORE IMPORTING ]-------
[USER32.dll]
FirstThunk: 0000204E
        (Original VirtualAddress = 00A02000)
FirstThunk: 00002040
        (Original VirtualAddress = 00A02004)
OriginalFirstThunk: 0000204E
        (Original VirtualAddress = 00A02034)
        Hint: 011C
        Name: GetDesktopWindow
OriginalFirstThunk: 00002040
        (Original VirtualAddress = 00A02038)
        Hint: 01F8
        Name: MessageBoxA
------------------------------------------------

実際にインポート情報を基にアドレス解決を行い、IATを書き換えた後の情報を表示する。

import_dlls(ids1, image_buf_ptr)
raw_input('Pausing ... Hit Return for Continue :')
 
ids2 = build_import_descriptors(opt_header, image_buf_ptr)
print "-------[ IMPORT INFO : AFTER IMPORTING ]-------"
dump_import_descriptors(ids2, image_buf_ptr)
print "-----------------------------------------------"
print

実行結果を見ると、FirstThunk(=IAT)の内容が実際のアドレスに変更されていることを確認出来る:

Pausing ... Hit Return for Continue :(ENTERキーを押す)
-------[ IMPORT INFO : AFTER IMPORTING ]-------
[USER32.dll]
FirstThunk: 77D0D1D2
        (Original VirtualAddress = 00A02000)
FirstThunk: 77D307EA
        (Original VirtualAddress = 00A02004)
OriginalFirstThunk: 0000204E
        (Original VirtualAddress = 00A02034)
        Hint: 011C
        Name: GetDesktopWindow
OriginalFirstThunk: 00002040
        (Original VirtualAddress = 00A02038)
        Hint: 01F8
        Name: MessageBoxA
-----------------------------------------------

最後にロードしたDLL自身のエクスポート情報をダンプ表示する:

export_directory = build_export_directory(
    opt_header, section_headers, image_buf_ptr)
 
print "-------[ EXPORT INFO ]-------"
dump_export_directory(export_directory)
print "-----------------------------"
print
raw_input('O.K, Here we go ... Hit Return for Continue :')

実際のダンプ表示:

-------[ EXPORT INFO ]-------
>>> IMAGE_EXPORT_DIRECTORY <<<
Characteristics = 0000
TimeDateStamp = 4C108036
MajorVersion = 00
MinorVersion = 00
Name = dlltest.dll [RVA=20AC]
Base = 0001
NumberOfFunctions = 0002
NumberOfNames = 0002
AddressOfFunctions RVA = 2098
AddressOfNames RVA = 20A0
AddressOfNameOrdinals RVA = 20A8
-----------------
Functions:
        Function[0] RVA = 00001000
        Function[1] RVA = 00001040
-----------------
Names:
        Name[0] = funcA
        Name[1] = funcB
-----------------
Ordinals:
        Ordinal[0] = 0
        Ordinal[1] = 1
-----------------------------

O.K, Here we go ... Hit Return for Continue :

この時点で、手動で展開したメモリイメージの実行準備が整った。
funcA, funcBのアドレスをエクスポート情報から取得してみる:

addr_funcA = image_buf_ptr + get_export_address(export_directory, 'funcA')
print "funcA = %08X" % addr_funcA
addr_funcB = image_buf_ptr + get_export_address(export_directory, 'funcB')
print "funcB = %08X" % addr_funcB

get_export_address()は次のようになっており、エクスポート名→序数→アドレスへの変換を行う:

def get_export_address(d, name):
 
  # 名前 → 序数テーブル上のインデックス番号
  i = 0
  found = False
  for v in d._Names_:
    if v == name:
      found = True
      break
    i = i + 1
 
  if not found:
    return 0
 
  # 序数テーブル上のインデックス番号 → 序数
  o = d._Ordinals_[i]
 
  # 序数 = EAT上のインデックス番号 → 関数アドレス
  return d._Functions_[o]

以降は main2.py と同様にCFUNCTYPEでプロトタイプを用意し、取得したアドレス値を使ってctypesの関数オブジェクトを作成し、呼ぶ。

prototypes = {
    'funcA' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int),
    'funcB' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int),
    }
 
funcA = prototypes['funcA'](addr_funcA)
funcB = prototypes['funcB'](addr_funcB)
 
print "funcA(2, 3) = %d" % funcA(2, 3)
print "funcB(2, 3) = %d" % funcB(2, 3)

実行結果:

O.K, Here we go ... Hit Return for Continue : (ENTERを押す)
funcA = 00A01000
funcB = 00A01040
(メッセージボックス表示)
funcA(2, 3) = 10
(メッセージボックス表示)
funcB(2, 3) = 9

以上、PythonスクリプトによるDLLの手動ロードと実行を簡単なサンプルで確認出来た。

参考

PEフォーマットの各情報を取得・表示するこれまでの実験:



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-07-13 12:19:18
md5:9817f51e02307d5d1d31731d357ad29e
sha1:760f3b2ce6f64de3a300a0761651c30f7520a5bd
コメント
コメントを投稿するにはログインして下さい。