Google Drive File StreamにShellNewがレ○プされるのを回避するバイナリ ハック

# 概要 - シェル I/Fの有り様を決めるのは対話ユーザーであり、アプリケーションでは決してない:rage:

最近 ``Google Drive File Stream`` の機能で、 ``ShellNew`` で ``Google ドキュメント`` や ``Google スプレッド シート`` などが作成出来る様に選択肢が追加される様になった。要らないのレジストリから削除したが、``Google Drive File Stream`` の常駐プロセスが定期的に再作成してしまう。。。

**最終的な判断件はアプリケーションではなく、対話ユーザーであるべきである。**

``関連付け`` しかり、 ``ShellNew`` しかり、 ``SendTo`` しかり。**なぜ後に入れるアプリケーションに使い勝手を変えられなければならない** のか。

Visual Studioで ``.java`` なんか開かない。``.h`` ``.c`` ``.cpp`` ``.cs`` だって、ソース単体を見る程度でIDE (Visual Studio) を起動されても困るし、ちゃんと見たいならそもそもプロジェクトからVisual Studioを立ち上げるって。 ``.bin`` や ``.xml`` など何にでも使うファイルの関連付けを奪い合うのは辞めてくれ。。。。どうせテキスト エディタやバイナリ エディタで十分なんだから。。。Adobe Photoshop CCなんで関係ないのに関連付け持って行きまくるから、インストールしたときの絶望感たるや。。。

 ``SendTo`` や ``ShellNew`` にもみんな突っ込みすぎて、中身が膨れて困る。しかもその殆どが使わない。

私の場合は ``OSのデフォルト ブラウザ`` と ``メイン ブラウザ`` を別で使っているので、 ``Google Drive File Stream`` の場合は、``.gdoc`` ``.gsheet`` ``.gslides`` をファイル システムから操作なんてない。``関連付け`` や ``ShellNew`` はあるだけ無駄なのである。

という事で、``Google Drive File Stream`` のプログラムをハックして、``Google Drive File Stream`` のレ○プから ``ShellNew`` を守ってみた。そんな ``ハック パッチ プログラム`` と ``バイナリ ハック`` の備忘録。

# まとめ
## 環境
### ① 解析に仕様したツール
* テキスト エディタ
   * EmEditor 
   * sakura editor (grep)
* バイナリエディタ
   * BZ (SDIでシンプルで使いやすい)
   * HxD (バイナリでの検索ができる)

### ② ``ハック パッチ  プログラム`` 実行環境
   * cmd (batchfile)
   * WHS (jscript)
   * Powershell (commandlet)

## ゴール
~~無限大の未来へ~~ 無駄のなきShellNewへ

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/739d7f8a-09f7-131b-6990-0f50b4526aa3.png)

## ``ハック パッチ プログラム`` ( ```gdfsfxxxck.bat``` + ```patcher.js``` )
``Google Drive File Stream`` には自動更新機能があり、また実行ファイルのパスにバージョンが含まれており、パスが動的に変化する。そのため、実行中の ``Google Drive File Stream`` のプロセスから対象モジュールを判断するパッチ スクリプトを作成した。定期実行ないしは、 ``ShellNew`` がレ○プされていることに気づいた際に実行することで、幸せになれる。

``ハック パッチ プログラム`` は ``Google Drive File Stream`` の実行ファイルのバイナリの中身を書き換えており、書き換えに成功した場合に以下の様に動作する。

* ``Google Drive File Stream`` のプロセスを強制終了
* 書き換えた ``Google Drive File Stream`` に置き換え
* ゴミの ``ShwllNew`` のエントリの削除
* ``Google Drive File Stream`` の再実行

構成ファイルは

1. gdfsfxxxck.bat
2. patcher.js

の2つ。

```javascript:patcher.js
var shell = new ActiveXObject('WScript.Shell');
var fs = new ActiveXObject('Scripting.FileSystemObject');

/**
 * println.
 * @param obj
 */
function println(obj) {
  WScript.Echo(obj);
}

/**
 * getPatchStr.
 * @param str
 * @return string for use for patchwork.
 */
function getPatchStr(str) {
  var ret = '';
  if(str != null) {
    ret += str;
  }
  ret = ret.replace(new RegExp(' ', 'g'), '');
  ret = ret.toLowerCase();
  return ret;
}

/**
 * loadHexText.
 * @param path
 * @return
 */
function loadHexText(path) {
  var bytes  = null;
  var hex = null;
  (function() {
    var stream = new ActiveXObject("ADODB.Stream");
    stream.Type = 1;
    stream.Open();
    stream.LoadFromFile(path);
    bytes  = stream.Read();
    stream.close();
  })();
  (function() {
    var domdoc = new ActiveXObject("Msxml2.DOMDocument");
    var elm = domdoc.createElement("hex");
    elm.dataType = "bin.hex";
    elm.nodeTypedValue = bytes;
    hex = elm.text;
  })();
  return hex;
}

/**
 * saveHexText.
 * @param path
 * @param str
 */
function saveHexText(path, str) {
  var hex = str;
  var bytes  = null;
  (function() {
    var domdoc = new ActiveXObject("Msxml2.DOMDocument");
    var elm = domdoc.createElement("hex");
    elm.dataType = "bin.hex";
    elm.text = hex;
    bytes = elm.nodeTypedValue;
  })();
  (function() {
    var stream = new ActiveXObject("ADODB.Stream");
    stream.Type = 1;
    stream.Open();
    stream.Write(bytes);
    stream.SaveToFile(path, 2)
    stream.Close()
  })();
}

/**
 * patchwork.
 * 
 * @param txt
 * @param strSrc
 * @param strDst
 * @return
 */
function patchwork(txt, strSrc, strDst) {
  var reg = new RegExp(strSrc, 'ig');

  if(!txt.match(reg)) {
    // not found
    throw new Error('replace string "' + strSrc + '" is not found...');
  } 

  return txt.replace(reg, strDst);
}

/**
 * main entry point.
 *
 * @return returncode 0 or -1.
 */
function main() {
  var args = WScript.Arguments;
  var srcFileName;
  var dstFileName;
  var patchOrg0;
  var patchDst0;

  (function() {
    var lines = [];
    lines.push('//-------------------------------------------------------');
    lines.push('// patcher on WHS.');
    lines.push('//-------------------------------------------------------');
    lines.push('usage:');
    lines.push('  patcher.js [source file] [dist file] [find str] [replace str]');
    lines.push('    source file       source file name or path for patchwork');
    lines.push('    dist file         dist file name or path for patchwork');
    lines.push('    find str(regexp)  string of search for patch.');
    lines.push('                      it trim white space, when use.');
    lines.push('    replace str       string of replace for patch.');
    lines.push('');
    lines.push('');
    lines.push('');
    println(lines.join('\n'));
  })();

  try {
    var patchOrg1;
    var patchDst1;
    var hexStr0, hexStr1;

    srcFileName = args(0);
    dstFileName = args(1);
    patchOrg0 = args(2);
    patchDst0 = args(3);

    patchOrg1 = getPatchStr(patchOrg0);
    patchDst1 = getPatchStr(patchDst0);

    println('');
    println('[init]');
    println('  srcFileName = ' + srcFileName);
    println('  dstFileName = ' + dstFileName);
    println('  patchOrg    = '    + patchOrg0 + ' (' + patchOrg1 + ')');
    println('  patchDst    = '    + patchDst0 + ' (' + patchDst1 + ')');

    println('');
    println('[loadHexText()]');
    hexStr0 = loadHexText(srcFileName);

    println('');
    println('[patchwork()]');
    hexStr1 = patchwork(hexStr0, patchOrg1, patchDst1);

    println('');
    println('[saveHexText()]');
    saveHexText(dstFileName, hexStr1);
    
  } catch(e) {
    throw e;
    //println('');
    //println('appear exception!');
    //println(e);
    //println('name        :' + e.name);
    //println('message     :' + e.message);
    //println('stack       :' + e.stack);
    //println('fileName    :' + e.fileName);
    //println('lineNumber  :' + e.lineNumber);
    //println('number      :' + e.number);
    //println('description :' + e.description);
    //WScript.Quit(-1);
  }
}

main();
```

```bat:gdfsfxxxck.bat
@SETLOCAL
@pushd "%~dp0"

@SET PATCH_CMD=cscript //nologo //E:JScript "%D_CUR%patcher.js"
@SET RPL_BIN_SRC=5C 00 53 00 68 00 65 00 6C 00 6C 00 4E 00 65 00 77 00
@SET RPL_BIN_DST=5C 00 78 00 78 00 78 00 78 00 78 00 78 00 78 00 78 00

@GOTO :l_main

@REM -- --------------------------------------------------------------------------------------------
@REM -- sub_proc
@REM -- --------------------------------------------------------------------------------------------
:sub_proc
  @SET EXE_NAME=%~nx1
  @SET EXE_DIR=%~dp1
  @SET EXE_PATH=%~dpnx1
  @SET BAK_PATH=%~dpnx1.bak
  @SET TMP_PATH=%~dpnx1.tmp
  @ECHO EXE_PATH=%EXE_PATH%
  @ECHO BAK_PATH=%BAK_PATH%
  @ECHO TMP_PATH=%BAK_PATH%

  @REM arg check
  @IF "%~1" == "" (
    ECHO argument is blank... ^(proccess is not found..^)
    GOTO :EOF
  )

  @REM bak check
  @REM @IF EXIST "%BAK_PATH%" (
  @REM   ECHO bak file is exists. ^(already patched.^)
  @REM   GOTO :EOF
  @REM )

  @REM patch
  @ECHO bak file is not found.
  @ECHO start patch work.
  %PATCH_CMD% "%EXE_PATH%" "%TMP_PATH%" "%RPL_BIN_SRC%" "%RPL_BIN_DST%"

  @IF NOT "%ERRORLEVEL%" == "0" (
    ECHO target code is not found...
    GOTO :EOF
  )

  @REM kill
  TASKKILL /F /IM "%EXE_NAME%"

  @REM swap
  MOVE /Y "%EXE_PATH%" "%BAK_PATH%"
  MOVE /Y "%TMP_PATH%" "%EXE_PATH%"

  @REM restart
  @REM "%EXE_PATH%"
  @CALL (
    @SETLOCAL
    CD /D "%EXE_DIR%"
    RUNAS /trustlevel:0x20000 "%EXE_PATH%"
    @ENDLOCAL
  )
@GOTO :EOF

@REM -- --------------------------------------------------------------------------------------------
@REM -- sub_reg
@REM -- --------------------------------------------------------------------------------------------
:sub_reg
  @REM clean reg
  REG DELETE HKEY_CLASSES_ROOT\.gdoc\ShellNew /f
  REG DELETE HKEY_CLASSES_ROOT\.gsheet\ShellNew /f
  REG DELETE HKEY_CLASSES_ROOT\.gslides\ShellNew /f
@GOTO :EOF

@REM -- --------------------------------------------------------------------------------------------
@REM -- l_main
@REM -- --------------------------------------------------------------------------------------------
:l_main
@IF "%1" == "-list" (
  @powershell -Command "wmic process where \"name = 'GoogleDriveFS.exe'\" get ExecutablePath | Select-String "GoogleDriveFS.exe" | Get-Unique"
  @GOTO :l_eof
)
@IF "%1" == "" (
  @FOR /f "usebackq skip=1 tokens=*" %%A IN (`"%0" -list`) DO @(
    @REM @ECHO "%%A"
    CALL :sub_proc "%%A" && CALL :sub_reg
    @REM CALL :sub_reg
    @REM GOTO :l_eof
  )
  @GOTO :l_eof
)

:l_eof
@ENDLOCAL
```

常駐プログラムの強制終了でタスクトレイにアイコンが残留するのはWindowsの糞仕様なのでご容赦願いたい。
(マウスオーバーでイベント起こさせると消える)
# ハック解説
## ①調査
レジストリを書き換えるためには、プログラムのどこかにその文字列を何らかの形で定義している、と考えられる。Resource Hackerやgrepなどで探した結果、 ``GoogleDriveFS.exe`` 内で ``ShellNew`` の ``UTF-16LE`` の文字列を発見した。昨今のWindowsプログラムのUNICODE対応はワイド文字 (USC-2) が主流なので、実行ファイルのバイナリから、UTF-16LEの文字列が出てくるのは妥当と考えられる。

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/a102a45b-ee22-e826-e422-343a7dad8967.png)
![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/d281a413-4f4d-7aaf-96f1-61a9b5d25f11.png)
![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/3d945699-76a7-861d-ac92-0ee2a9758881.png)
![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/b4cb9999-00a9-c204-c477-3a04c25f0148.png)
おそらくこれが、定数フィールドに展開される文字列だろう。

## ②書き換え
``ShellNew``のキーでなければ、とりあえず新規作成のコンテキスト メニューには出てこれないので。``GoogleDriveFS.exe`` の ``ShellNew`` の部分をバイナリ エディタで別の文字に書き換えて、その別の文字列で実行されるかを確認する。**アドレスの問題もあるため、置き換える文字列は同じ文字数である必要がある。**

またこのリテラルはUTF-16LEで格納されているので、UTF-16LEのテキスト ファイルでバイナリ値を確認して、その値を書き込む。

* ``ShellNew`` 8文字 ⇒ 00 53 00 68 00 65 00 6C 00 6C 00 4E 00 65 00 77
* ``xxxxxxxx`` 8文字 ⇒ 00 78 00 78 00 78 00 78 00 78 00 78 00 78 00 78

![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/3fa7d5c5-f393-f77d-14d2-def7905d0852.png)
![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/1d61e370-65e5-7eeb-4370-ef468faebd18.png)
``xxxxxxxx`` でキーが作成される様になった！
(``ShellNew`` は手動で削除した後、再作成されてない！)
![SS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72257/c1961e14-bab2-9f61-e887-673f4cb00678.png)

レジストリには ``xxxxxxxx`` でキーが作成されるだけなので、``新規作成`` のコンテキストに出なくなった！

他にもやり用があるかもしれないが。一応この方法が動作の安定性を確保なども加味した上で最もベターな方法かと思う。

## ③パッチ スクリプト化 ①バイナリ修正 ( ``patcher.js`` )
batファイルではバイナリの走査が出来ないので、WHSで。
VBScriptがアレルギー反応で死んでしまうので、JScriptで記述。
パッチを適用する単品部品としてシンプルに実装する。

* バイナリを16進表記テキストで読み込み (ADODB.Stream + Msxml2.DOMDocument)
* テキスト処理で読み込んだバイナリを置換
* 16進表記テキストからバイナリに書き出し (ADODB.Stream + Msxml2.DOMDocument)
* 第1引数： 入力ファイル
* 第2引数： 出力ファイル
* 第3引数： 置換前文字列 (16進表記テキスト)
* 第4引数： 置換後文字列 (16進表記テキスト)
* 実行例①： ``` > cscript //nologo //E:JScript patcher.js ”GoogleDriveFS.exe” ”GoogleDriveFS.exe.tmp” "5C 00 53 00 68 00 65 00 6C 00 6C 00 4E 00 65 00 77 00" "5C 00 78 00 78 00 78 00 78 00 78 00 78 00 78 00 78 00"```
* 実行例②： ``` > cscript //nologo //E:JScript patcher.js ”GoogleDriveFS.exe” ”GoogleDriveFS.exe.tmp” "5C005300680065006C006C004E0065007700" "5C0078007800780078007800780078007800"```

またコマンド フロント エンドの ``gdfsfxxxck.bat`` では、置換が成功した場合のみ、”GoogleDriveFS.exe” を置換後のものと置き換える様にしている。

## ④パッチ スクリプト化 ②対象モジュールの割り出し
``Google Drive File Stream`` の更新にはひとクセあり。更新後のプロセスを見ると古い版と新しい版が同時稼働している。そのため、うまく **新しい版** に対して処理が行える様に工夫する必要がある。しかも沢山存在するプロセスのリストから、起動している分、愚直に全部というわけにも行かない。Windowsには標準で ``uniq`` コマンドが無いので、``powershell`` の ``Get-Unique`` コマンドレットを使う。更に、``cmd`` から ``powershell`` にパイプで情報が送れないという ~~ナめた~~ 仕様もあり、対象モジュール一連化の処理は ``powershell`` 上で一通りやらせ、標準出力をFOR文で喰う形に。また、FOR文で呼び出せる様に、コマンド フロント エンドの ``gdfsfxxxck.bat`` には2つの動作モードで動く様にして。

* 引数なし ⇒ ダブルクリックなどデフォルト。バイナリ ハックのための基本処理
* 第1引数 ``-list``  ⇒ プロセス一覧の出力

と言う塩梅に。

```bat:(抜粋)&#x20;gdfsfxxxck.bat
@REM -- --------------------------------------------------------------------------------------------
@REM -- l_main
@REM -- --------------------------------------------------------------------------------------------
:l_main
@IF "%1" == "-list" (
  @powershell -Command "wmic process where \"name = 'GoogleDriveFS.exe'\" get ExecutablePath | Select-String "GoogleDriveFS.exe" | Get-Unique"
  @GOTO :l_eof
)
@IF "%1" == "" (
  @FOR /f "usebackq skip=1 tokens=*" %%A IN (`"%0" -list`) DO @(
    CALL :sub_proc "%%A" && CALL :sub_reg
  )
  @GOTO :l_eof
)
:l_eof
```

## ⑤実行
``C:\Program Files`` は管理者権限が必要。 管理者権限のあるシェル、もしくは管理者権限をつけたショートカットなど、工夫して実行する。

自分は ***タスク スケジューラによる定期実行*** と、***スタート メニューのショートカットで簡単実行*** の2つで行っている。


# 後記
正直、Windowsの ``関連付け`` も ``ShellNew`` も、OSがまともなUIを提供しておらず。AppXの登場も相まって、混沌を極めていると思う。 XPレベルのシンプルな ``関連付け`` と ``ShellNew`` に戻してほしい。それ以上は求めていない。そして、``コンテキスト メニュー`` の表示が遅くなりすぎ。一瞬で表示出来ないなら無駄な機能辞めちまえよ。オンライン ストレージのための ``アイコン オーバーレイ`` を始めてからはもっと最悪になってやがる。(ファイル名リネームするときにいつも邪魔されるんだけど、いつになったら直るの？)

正直思うけど、関連付けのエントリの9割は要らねぇだろ・・・。ダブルクリックで開きたいファイル タイプなんてたかが知れてるっての。``ShellNew`` だって ``フォルダ`` と ``テキスト`` と ``ショート カット`` さえあればほぼ問題ない。しいて言えば、Officeのテンプレート機能がクソなので、脱游ゴシックの為に、``Excelブック`` もよく使うけど。まぁそんな程度。

選択肢は多ければ良いというものでもなく。脳が迷う要因を与えるし、ノイズはあると単純にストレスにもなる。

一介の ``アプリケーション`` には ``デスクトップ`` の使い勝手を決める、最終的な決定権など無いこと自覚して、分をわきまえた動作をして欲しい。

だいたい、デフォルト ブラウザに渡したところで``Google Chrome`` とかのプロファイル (アカウント) までは指定出来ないんだから、すでにGoogleパーティ内で微妙なんだって。 だから、Explorerからスプレッドシートを開くのとかって、別段利便が高い訳じゃない。 むしろ煩わしいし、``Google Drive File Stream`` が諸々強要してくる正当性はないんだよね。 それよりも、``Google Chrome`` が ``.gdoc`` ファイル、 ``.gsheet`` ファイル、 ``.gslides`` ファイルのD&Dに対応するほうがベターだと思うんだけどなぁ。。。