レイヤ描画のメインループの捜索する。タイムラインの描画はGDI+で描画されている。
タイムラインの現在時刻のフォーマット文字列("%02d:%02d:%02d.%02d")がヒントになるのでこれで検索する。この関数がレイヤー全体の描画をする関数。
sub_7FF7F78D3760( // タイムラインの現在時間
(__int64)&v196,
(__int64)L"%02d:%02d:%02d.%02d",
(unsigned int)((int)v124 / 60 / 60),
(unsigned int)((int)v124 / 60 % 60),
v170,
v171);
この120行ほど前に目盛りの描画があり、ここも"%02d:%02d:%02d"の文字列フォーマットを使っているので目印になる。
v104 = GdipDrawLine(*(_QWORD *)v103, *v175);// 目盛り描画
...
sub_7FF7F78D3760( // 目盛りの時間のフォーマット
(__int64)&v185,
(__int64)L"%02d:%02d:%02d",
(unsigned int)((int)v107 / 60 / 60),
(unsigned int)((int)v107 / 60 % 60),
v170);
...
MeasureString(v5, &v259, Block);
...
if ( (float)v99 <= (float)((float)(*(float *)&v259 + v102) + 5.0) )
{
DrawString(v5, v109, &v260);
Decompile結果のGdipSetSmoothingMode
の30行ぐらい先にdo-whileループが存在し、これがレイヤのループ。
更に50行ぐらい先にdo-whileループが存在し、これがレイヤ上のオブジェクトを想われる。
レイヤループの初めで呼び出す関数がインデックスを第二引数に取りレイヤを取得する関数。
その次の行がレイヤの高さを取得している。レイヤループ終了から20行前ぐらいのGdipDrawLine
がタイムラインの区切り線の描画。
do // レイヤループ
{
layerObject = (struct_LayerObject *)sub_7FF7F796B470(v17, (unsigned int)layerIndex);
v25 = layerObject->LayerHeight;
...
v68 = GdipDrawLine(*(_QWORD *)v67, *v175);// タイムラインの区切り線
...
}
while ( layerIndex < loopCount ); // レイヤループ終了
タイムラインの区切り線は優良な目印で、本来はGdipDrawLine(GpGraphics,GpPen,float,float,float,float)
だが、floatの引数はDecompilerが認識できない。アセンブリでxmm3
(Y座標)をトレースするとレイヤループ内の描画が理解できる。
後はレイヤループの頭で下記Conditionのブレークポイントを貼るとレイヤオブジェクトのアドレスが分かる。edi
はレイヤループインデックス、rax
はレイヤオブジェクトのアドレス、rax+160
はレイヤのHeightが格納されているアドレス。
msg(
"LAYER_HEIGHT: #%d[0x%X] 0x%X %f\n",
edi,
rax,
rax+0x160,
get_float((rax+0x160))
), 1
上記のブレークポイントで見つけたLayerObject.Height
にWriteのHWブレークを仕込み68.0(最終的なレイヤーのHeightの最小値)を書き込んだ関数を探す。
この関数には優良な目印が存在しない。今のところはF3 0F 10 8F 60 01 00 00
でBinaryPatternSearchをしたところ見つけられた。
関数前半で_RTDynamicCast
で分岐を連発している特徴はある。
200行目ぐらいのループを抜けた少し先にa1->LayerHeight=0
(a1[352]=0
)という部分があり、ここからレイヤのHeightのセット処理が始まる。
10行先ぐらいのfloat比較の分岐がHeightの下限設定と思われる。
__int64 __fastcall CalcLayerHeight(struct_LayerObj *layerObj)
{
... // 200行ぐらい
a1->LayerHeight = 0; // 一旦0初期化
...
if ( *&v24 <= (*(*layerObj->gap0 + 96i64))(layerObj) ) // h < 68 ? 68 : h 的な
{
v25 = (*(double (__fastcall **)(struct_LayerObject *))(*(_QWORD *)layerObj->gap0 + 96i64))(layerObj);
v24 = SLODWORD(v25);
}
layerObj->LayerHeight = v24; // 最終決定
...
}
この最終決定の代入の直前に代入する値を書き換えることでレイヤHeightを強制的に上書きすることができる。 ブレークポイントのConditionに以下のように設定する。(0x42880000=68.0f)
xmm6=0x42880000, 0
LayerObject.Height
のアドレスから、ラベル側の高さ設定関数も捜索できる。
今度はReadのHWブレークポイントをLayerObject.Height
に設定する。
これだけだと右側のタイムラインの高さ計算まで引っかかるのでConditionにrbp
がタイムライン描画の関数の時(Ex.rbp!=0x0000001B75AFB900
)をはじいておく。
引っかかるのは2か所。
まずはラベルの高さに影響を与えない方、こちらは優良な目印はなくF3 0F 58 B0 60 01 00 00 79
(addss xmm6, dword ptr [rax+160h]
,jns *
)で検索すれば一応一か所に絞れる。
もう一か所が本命で、ラベルの高さに影響を与える。こちらも優良な目印はなく、 F3 0F 10 88 60 01 00 00 C7
(movss xmm1, dword ptr [rax+160h]
,mov *
)で検索すれば一応一か所に絞れる。この5行後ぐらいに、ラベルのWindowのサイズを更新するコールバックの呼び出しが存在する。
void __fastcall UpdateTimelineLanel(__int64 a1)
{
... // 240行ぐらい
v37 = (*(*v35 + 152i64))(v35, v44); // こっちは違う(謎)
if ( v40 != *v37 || v41 != v37[1] || v42 != v37[2] || v43 != v37[3] ) // サイズに変更があるかの分岐と思われる
(*(**&_layerObj->gap0[232] + 160i64))(*&_layerObj->gap0[232], &v40); // CallUpdateWindowSize
呼び出されるコールバック関数も優良な目印はなく、F3 0F 11 89 F8 08 00 00 48 FF A0 C0 01 00 00
(movss dword ptr [rcx+8F8h], xmm1
,jmp qword ptr [rax+1C0h]
)で検索すれば一応一か所に絞れる。この関数では更にコールバックを呼び出している。
__int64 __fastcall CallUpdateWindowSize(__int64 *a1, _DWORD *a2)
{
...
return (*(v3 + 448))(); // UpdateWindowSize
}
こちらで呼び出されるコールバック関数は直接User32APIのSrtWindowPos
を呼び出す。(SrtWindowPos
からの捜索でも一応見つけられそう)
void __fastcall UpdateWindowSize(__int64 a1)
{
...
SetWindowPos(*(a1 + 1920), 0i64, v7, v8, v6, cy, 4u);
UpdateWindow(*(a1 + 1920));
...
}
高さ計算+layerObj->Height
へ書き込みを行っているCalcLayerHeight
関数では、レイヤーのインデックスは持っておらずstruct_LayerObj
(layerObj
)が参照できる。
一方で、ラベル側もUpdateTimelineLanel
でstruct_LayerObj
のアドレスを参照できる。
struct_LayerObj struc ; (sizeof=0x380, align=0x8, mappedto_331)
00000000 gap0 db 232 dup(?)
000000E8 windowObj db ? ; struct_WindowObj
000000E9 gap1 db 71 dup(?)
00000130 qword130 dq ?
00000138 int138 dd ?
0000013C gap13C db 36 dup(?)
00000160 LayerHeight dd ?
00000164 gap164 db 532 dup(?)
00000378 byte378 db ?
00000379 byte379 db ?
0000037A db ? ; undefined
0000037B db ? ; undefined
0000037C float37C dd ?
00000380 struct_LayerObj ends
struct_LayerObj
からはstruct_WindowObj
が参照できる。
00000000 struct_WindowObj struc ; (sizeof=0x904, align=0x4, copyof_332)
00000000 gap0 db 880 dup(?)
00000370 pqword370 dq ? ; offset
00000378 gap378 db 1032 dup(?)
00000780 hwnd dq ? ; offset
00000788 gap788 db 204 dup(?)
00000854 oword854 xmmword ?
00000864 gap864 db 68 dup(?)
000008A8 byte8A8 db ?
000008A9 gap8A9 db 75 dup(?)
000008F4 Width dd ?
000008F8 Height dd ?
000008FC X dd ?
00000900 Y dd ?
00000904 struct_WindowObj ends
struct_WindowObj
からはhwnd
が参照できる。hwnd
を使うことでWindowシステムからHookを掛けた場合とで対応が取れる。
例としてCalcLayerHeight
ではhwnd=[[rdi+0xE8]+0x780]
となる。
msg(
"pLayerObj: 0x%X ; pWindowObj: 0x%X ; hwnd: 0x%X ; height: %f \n",
rdi, qword(rdi+0xE8),
qword(qword(rdi+0xE8) + 0x780),
get_float(xmm6)
),
0
struct_LayerObj
にHeightをセットした直後に関数コールを挿入する。
__int64 __fastcall CalcLayerHeight(struct_LayerObj *layerObj)
{
...
layerObj->LayerHeight = v24; // 最終決定
Hook(layerObj); // これを注入する
sub_7FF7F78C7080(v31); // 恐らくdelete的なものと思われる、型はstd::arrayかな?
sub_7FF7F78C7080(v26);
sub_7FF7F78C7080(v36);
return sub_7FF7F78C7080(v41);
...
}
アセンブリは以下のようになっている。
loc_7FF7F79A3890: ; CODE XREF: CalcLayerHeight+4F2↑j
F3 0F 11 B7+ movss dword ptr [rdi+160h], xmm6 ; layerObj->LayerHeight = v24
60 01 00 00
48 8D 4C 24+ lea rcx, [rsp+170h+var_100] ; sub_7FF7F78C7080(v31);
70
E8 DE 37 F2+ call sub_7FF7F78C7080 ; sub_7FF7F78C7080(v31);
FF
90 nop
movss
以降、保存しておかなければならないレジスタは存在しないのでrcx
を引数(struct_LayerObject*
)に、rax
を呼び出すコールバックの指定に利用する。呼び出し用のスタックは元々確保されているので追加で何かする必要はない。
48 8B CF mov rcx, rdi
48 B8 FF FF+ mov rax, 0FFFFFFFFFFFFFFFFh
FF FF FF FF+
FF FF
FF 10 call rax
call rax
FF D0
FF -> Opecode=JMP/CALL
25 -> 11 010 000
11 -> ModR/Mのmod部
010 -> ModR/Mのreg部ではなく、FFに /2 として貸し出し Opecode=CALL r/m64
000 -> ModR/Mのr/m部、modと合わせて Operand=rax(register)
https://www.felixcloutier.com/x86/call
これで最低15Bを消費することになるので、その分の帳尻の合わせなければならない。 後続に4つ連続でfree系の関数が呼び出されているが、このうち3番目以外は実質何もしないので1,2番目の分の命令を使ってしまえる。これで22Bが捻出できる。
auto injecteeAddress = (void*)0x8877665544332211; // movssの次のleaの先頭アドレス
auto hookFunctionPtr (void(*)(void*)) (FUNC)0x1122334455667788;
unsigned char machineCode[22]
{
0x48, 0x8B, 0xCF, // mov rcx, rdi
0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // mov rax, 0FFFFFFFFFFFFFFFFh
0xFF, 0xD0, // call rax
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, // nop x7
};
*(void(*)(void*))(&machineCode[3+2]) = hookFunctionPtr;
DWORD oldProtection;
VirtualProtect(injecteeAddress, _countof(machineCode), PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy(injecteeAddress, machineCode, _countof(machineCode));
https://qiita.com/up-hash/items/8ca41c4038c26a96674a
ちゃんとやるなら、デバッカの仕組みを応用することでできそう。
int3
(0xCC)をinjecteeAddress
に書き込む- 例外が発生し、監視スレッドに制御を移す
int3
を元に戻してrip
を1引き、tf
レジスタを立ててる(SingleStepMode)- 制御を戻す
- SingleStepModeにより1命令実行後に例外が発生し、監視スレッドに制御が移る
tf
を落としてint3
(0xCC)をinjecteeAddress
に書き込む
- https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
- http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/LDP_man-pages/man2/ptrace.2.html
- https://www.ninjastars-net.com/entry/2020/01/27/100000
- https://qiita.com/deta-mamoru/items/89528757351a43e32526
AddVectoredExceptionHandler
を使えば同スレッドで完結できるかも?
FF 15 A1 D4+ call cs:GdipGraphicsClear
2F 00
85 C0 test eax, eax
74 03 jz short loc_7FF6634CE7DE
89 47 08 mov [rdi+8], eax
復帰後はGpStatusを書き込んでいるだけなので、GpStatusは基本Okだし捨てることができる。
unsigned char machineCode[13]
{
0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // mov rax, 0FFFFFFFFFFFFFFFFh
0xFF, 0xD0, // call rax
0x90, // nop x3
};
E8 A6 D1 E4+ call DrawRectangle
FF
90 nop
48 8D 05 B6+ lea rax, off_7FF66385B008
C5 38 00
48 89 85 B0+ mov [rbp+320h+fillInfo], rax
01 00 00
ローカル変数fillInfo
はこの後、使われる前にすぐに別の値で上書きされるためこの処理を捨てて合計20Bを稼ぐ。
DrawRectangle
の中身は単純なので同等のものを作る。
unsigned char machineCode[20]
{
0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // mov rax, 0FFFFFFFFFFFFFFFFh
0xFF, 0xD0, // call rax
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, // nops
};