概要 - シェル I/Fの有り様を決めるのは対話ユーザーであり、アプリケーションでは決してない
最近 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へ
ハック パッチ プログラム
( 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
の再実行
構成ファイルは
- gdfsfxxxck.bat
- patcher.js
の2つ。
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();
@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の文字列が出てくるのは妥当と考えられる。
②書き換え
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
xxxxxxxx
でキーが作成される様になった!
(ShellNew
は手動で削除した後、再作成されてない!)
レジストリには 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
⇒ プロセス一覧の出力
と言う塩梅に。
@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に対応するほうがベターだと思うんだけどなぁ。。。