Skip to content

Instantly share code, notes, and snippets.

@wallstudio
Last active September 23, 2021 03:05
Show Gist options
  • Save wallstudio/b78ce70e015058f7c33e391b0cfd7815 to your computer and use it in GitHub Desktop.
Save wallstudio/b78ce70e015058f7c33e391b0cfd7815 to your computer and use it in GitHub Desktop.

タイムラインのハック

レイヤ描画のメインループの捜索する。タイムラインの描画は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=0a1[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 79addss xmm6, dword ptr [rax+160h],jns *)で検索すれば一応一か所に絞れる。 もう一か所が本命で、ラベルの高さに影響を与える。こちらも優良な目印はなく、 F3 0F 10 88 60 01 00 00 C7movss 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 00movss 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));
  ...
}

ラベルのレイヤWindowとタイムラインのレイヤーの対応付け

高さ計算+layerObj->Heightへ書き込みを行っているCalcLayerHeight関数では、レイヤーのインデックスは持っておらずstruct_LayerObjlayerObj)が参照できる。 一方で、ラベル側もUpdateTimelineLanelstruct_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

CalcLayerHeightにHookする

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

ちゃんとやるなら、デバッカの仕組みを応用することでできそう。

  1. int3(0xCC)をinjecteeAddressに書き込む
  2. 例外が発生し、監視スレッドに制御を移す
  3. int3を元に戻してripを1引き、tfレジスタを立ててる(SingleStepMode)
  4. 制御を戻す
  5. SingleStepModeにより1命令実行後に例外が発生し、監視スレッドに制御が移る
  6. tfを落としてint3(0xCC)をinjecteeAddressに書き込む

AddVectoredExceptionHandlerを使えば同スレッドで完結できるかも?

WIP 強制的にタイムラインの更新をする

wallstudio/RecottePlugin#3

レイヤスキン

タイムライン全体の下地

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
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment