1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

msysとGnuWin32を併用したらsedでハマった

Posted at

問題

zlib 1.2.11 (tar.gz版)をmsysのbashでビルドしようとし、configureを走らせたらsedが謎のエラーを吐いた。
出たエラーは、

sed.exe: -e 表現 #1, 文字数 1: 未知のコマンドです: 「C」
sed.exe: -e 表現 #1, 文字数 8: アドレスregexが終了していません

の2種類。同様のエラーは他のソフトウェアのconfigureでも見られ、以前から気になっていた。

zlib-configure-error-20180728.png

渡されているコマンドライン引数の調査

エラーメッセージが主張する「未知のコマンド」とはどういうことなのだろうか?
これを調べるため、configure中のsed./xyzに置換することで、以下のコマンドライン引数を出力するプログラムを実行するようにし、実行してみた。
なお、configureの動作への影響を減らすため、標準出力ではなく標準エラー出力に出すようにしている。

xyz.c
# include <stdio.h>

int main(int argc, char* argv[]) {
	int i;
	for (i = 0; i < argc; i++) {
		fprintf(stderr, "argv[%d] = %s\n", i, argv[i]);
	}
	return 0;
}

すると、以下のような出力(冒頭部分)になった。

zlib-sed-command-20180728.png

zlibのconfigure中でこの部分に相当すると考えられるのは、42行目からの

configure(抜粋)
# extract zlib version numbers from zlib.h
VER=`sed -n -e '/VERSION "/s/.*"\(.*\)".*/\1/p' < ${SRCDIR}zlib.h`
VER3=`sed -n -e '/VERSION "/s/.*"\([0-9]*\\.[0-9]*\\.[0-9]*\).*/\1/p' < ${SRCDIR}zlib.h`
VER2=`sed -n -e '/VERSION "/s/.*"\([0-9]*\\.[0-9]*\)\\..*/\1/p' < ${SRCDIR}zlib.h`
VER1=`sed -n -e '/VERSION "/s/.*"\([0-9]*\)\\..*/\1/p' < ${SRCDIR}zlib.h`

であり、なぜか引数にC:/MyApps/MinGW/msys/1.0という余計なプリフィックスがついてしまっていることがわかった。
すなわち、なんとかしてこのプリフィックスが付かない状態の引数をsedに渡せれば、「未知のコマンドです」エラーは潰せそうである。

プリフィックス対策のラッパの作成

余計なプリフィックスによりsedの動作が妨害されるのをさけるため、ラッパを開発した。
まずは小さいケースで試し、sedのかわりに先ほどの引数表示プログラムxyzに引数を渡す。
ラッパを使わないと、このように余計なプリフィックスがついてしまった。

bash-3.1$ xyz "/abc/i def"
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def

C言語での作成(失敗)

wrapper.c
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>

int main(int argc, char* argv[]) {
	static const char* TARGET = "xyz.exe";
	static const char* TRASH = "C:/MyApps/MinGW/msys/1.0";
	const char** args = malloc(sizeof(*args) * (argc + 1));
	int i;
	size_t tlen = strlen(TRASH);
	if (args == NULL) {
		perror("wrapper: malloc");
		return 1;
	}
	args[0] = TARGET;
	for (i = 1; i < argc; i++) {
		args[i] = argv[i];
		if (strncmp(args[i], TRASH, tlen) == 0) args[i] += tlen;
	}
	args[argc] = NULL;
	execvp(args[0], args);
	perror("execvp");
	return 1;
}

渡されたコマンドライン引数中に例のプリフィックスがあれば、削除するという削除するプログラムである。
このプログラムを用いると、以下のような実行結果になった。

bash-3.1$ wrapper "/abc/i def"
bash-3.1$ argv[0] = xyz.exe
argv[1] = /abc/i
argv[2] = def

プリフィックスが渡されるのは回避できているが、スペースを含む引数が別々の引数に分解されてしまっており、別の問題を引き起こすことが予想できる。
execvpではなくCreateProcessでプログラムを起動することも考えたが、引数を適切にエスケープして渡すのはめんどくさそうである。
この方法は諦めた。

Perlでの作成

まずは仕様を確かめるため、以下のスクリプトを実行してみる。

perl_test.pl
# !/usr/bin/perl

use strict;
use warnings;

my @prg = ("xyz");

for (my $i = 0; $i < @ARGV; $i++) {
	printf STDERR "Perl: ARGV[%d] = %s\n", $i, $ARGV[$i];
	push(@prg, $ARGV[$i]);
}

exec { $prg[0] } @prg;
die "exec falied\n";

実行結果は、以下のようになった。

bash-3.1$ perl_test.pl "/abc/i def"
Perl: ARGV[0] = /abc/i def
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def

このことから、Perlが起動されるときにはプリフィックスがつかないが、Perlからexecする時にはプリフィックスがついてしまうようである。
そこで、「/で始まらない引数にはプリフィックスがつかないようである」という性質を利用し、/で始まる引数の最初にsedの動作に影響を与えなそうな空白を追加するラッパを作成した。

perl_wrapper.pl
# !/usr/bin/perl

use strict;
use warnings;

my @prg = ("xyz");

for (my $i = 0; $i < @ARGV; $i++) {
	my $arg = $ARGV[$i];
	if (substr($arg, 0, 1) eq "/") { $arg = " " . $arg; }
	push(@prg, $arg);
}

exec { $prg[0] } @prg;
die "exec falied\n";

実行結果は

bash-3.1$ perl_wrapper.pl "/abc/i def"
argv[0] = c:\Temp\xyz.exe
argv[1] =  /abc/i def

となり、うまくいきそうである。

Rubyでの作成

Perlと同様に、まずは仕様を確かめる。

ruby_test.rb
# !ruby

ARGV.each_with_index do |arg, i|
  warn "Ruby: ARGV[#{i}] = #{arg}"
end

exec(*(["xyz"] + ARGV))

(参考:Rubyでコマンドライン引数を取得する方法:ARGV | UX MILK)
実行結果は

bash-3.1$ ruby_test.rb "/abc/i def"
Ruby: ARGV[0] = C:/MyApps/MinGW/msys/1.0/abc/i def
argv[0] = xyz
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def

となり、Perlの時と違ってRubyが起動される時にすでにプリフィックスがついてしまっている。
また、

ruby_test2/rb
# !ruby

exec(*["xyz", "/abc/i def"])

というプログラムを実行すると

bash-3.1$ ruby_test2.rb
argv[0] = xyz
argv[1] = /abc/i def

となり、Rubyからexecする時にはプリフィックスはつかないようであった。

そこで、付けられたプリフィックスを外して起動するラッパを作成した。

ruby_wrapper.rb
# !ruby

trash = 'C:/MyApps/MinGW/msys/1.0'
tlen = trash.length

prg = ['xyz']

ARGV.each do |param_raw|
  param = param_raw
  if param[0, tlen] == trash then param = param[tlen, param.length] end
  prg << param
end

exec(*prg)

実行結果は

bash-3.1$ ruby_wrapper.rb "/abc/i def"
argv[0] = xyz
argv[1] = /abc/i def

となり、うまくいっているようである。

もうひとつのエラー

ラッパーをテスト用のxyzではなくこの環境のsedをフルパスで起動するように設定し、sedにリネームし、このラッパー「sed」があるディレクトリをPATHの先頭に追加した状態でzlibのconfigureを起動した。
(フルパスで指定しないと、本来のsedではなくラッパーを起動しようとしてしまい、うまくいかないことが考えられる)
その結果、「未知のコマンドです」は出なくなったが、「アドレスregexが終了していません」は出てしまい、Makefileが空で正常なビルドができない状態であった。
調べてみると、先頭にスラッシュはないが、スラッシュなどの記号を含む長い引数が、プリフィックスが付くタイミングで途中で切られてしまうようであった。
プリフィックスが付くだけなら消せばいいが、消えてしまった情報を復元するのは不可能であると考えられる。
さて、その対処法は…

合体技だ!

ここで、先ほど調査したPerlとRubyの性質を思い出す。

  • Perlは起動される時は攻撃されないが、execする時に攻撃されてしまう
  • Rubyは起動される時に攻撃されてしまうが、execする時は攻撃されない

(ここで「攻撃」とは、これまで紹介してきたプリフィックスを付けたり途中で切ったりする変換を指す)
ということで、

bash
↓起動 (攻撃されない)
Perl:コマンドライン引数を攻撃に耐えられる形式に変換する
↓起動 (攻撃されるけど対策したので平気)
Ruby:コマンドライン引数をデコードする
↓起動 (攻撃されない)
sed

とすれば、攻撃の影響を避けてsedを起動することができるでしょう。
では、「攻撃に耐えられる形式」とは具体的にどんな形式でしょうか?
スラッシュなどの記号があると攻撃されやすそうな気がしたので、文字を文字コードで16進数に変換してみた。

sed_launcher.pl
# !/usr/bin/perl

use strict;
use warnings;

my $TARGET = "C:\\MyApps\\GnuWin32\\bin\\sed.exe";

my $RUBY_SCRIPT = <<EOS;
exec(*ARGV.map do |param|
  param.each_char.each_slice(2).map { |a| a.join.hex.chr }.join
end)
EOS

my @prg = ("ruby", "-e", $RUBY_SCRIPT, &encode_arg($TARGET));

for (my $i = 0; $i < @ARGV; $i++) {
	push(@prg, &encode_arg($ARGV[$i]));
}

exec { $prg[0] } @prg;
die "exec failed\n";

sub encode_arg {
	my @data = unpack("C*", $_[0]);
	my $ret = "";
	for (my $i = 0; $i < @data; $i++) {
		$ret .= sprintf("%02X", $data[$i]);
	}
	return $ret;
}

(参考:Rubyで文字列をn文字ごとに区切って配列に格納する - ぬいぐるみライフ?)
このスクリプトを先ほどのラッパーと同様にPATHの先頭のディレクトリにsedという名前で置いてzlibのconfigureを実行すると、無事エラーが出ず終了した。
続いてmakeを行った結果、共有ライブラリのビルドでcannot find -lcというエラーが出てしまったが、libz.aは生成されており、それを用いてアプリケーションをビルドすることもできた。

考察

そもそも、どうしてPerlとRubyで攻撃のタイミングに差が出たのか。
それは、一般に「msys内のプログラムからmsys外のプログラムを実行する時に変換をかける」からであると考えられる。
変換内容から察するに、そもそもこのプリフィックスを付ける動作は本来「攻撃」ではなく、互換性のために「ルートディレクトリ」からの絶対パスを実際のファイルシステム上のパスに変換する処理であると考えられる。それが、スラッシュで始まるsedの変換コマンドに対しても誤爆してしまったのだろう。
(ただし、長い引数が切られる仕様については、明らかに情報を破壊しており、これでは説明が付かない)

この環境におけるPerlとRubyの実体を調べると、

bash-3.1$ where perl
c:\MyApps\Perl64\bin\perl.exe
C:\MyApps\MinGW\msys\1.0\bin\perl.exe
bash-3.1$ where ruby
c:\MyApps\ruby-2.2.2-x64-mingw32\bin\ruby.exe

となった。すなわち、Perlはmsys内のものとmsys外のものがあり、Rubyはmsys外のみである。
bashからmsys内のPerlを呼び出す時は変換がかからず、msys内のPerlからmsys外のRuby、sed、テストプログラムを呼び出す時は変換がかかるということだろう、また、msys外のプログラムからmsys外のプログラムを実行するときも、当然変換はかからないと考えられる。

それでは、PATHを見るとmsys外のPerlの方がmsys内のPerlより先に来ているのに、なぜmsys内のPerlが使われたか。
それは、bashからPerlスクリプトを直接実行したため、スクリプト先頭の#!/usr/bin/perlによりmsys内のPerlが選択されたからであると考えられる。
実際、perlコマンドを用いてテストスクリプトを実行するとPerlが呼び出される時に変換がかかり、/usr/bin/perlコマンドを用いてテストスクリプトを実行するとPerlが呼び出される時には変換がかからないことが確認できた。

bash-3.1$ perl ./perl_test.pl "/abc/i def"
Perl: ARGV[0] = C:/MyApps/MinGW/msys/1.0/abc/i def
bash-3.1$ argv[0] = xyz
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i
argv[2] = def

bash-3.1$ /usr/bin/perl ./perl_test.pl "/abc/i def"
Perl: ARGV[0] = /abc/i def
argv[0] = c:\Temp\xyz.exe
argv[1] = C:/MyApps/MinGW/msys/1.0/abc/i def

Rubyについてはmsys外のものしかなく、#!/usr/bin/rubyと書くと/usr/bin/ruby^M: bad interpreter: No such file or directoryというエラーになったため、先頭は#!rubyとした。

ついでに、msys内のsedを明示的に実行すると、ラッパを使わなくても「未知のコマンドです」エラーが出ずに正常に処理がされることが確認できた。

bash-3.1$ where sed
c:\MyApps\GnuWin32\bin\sed.exe
c:\MyApps\w32tex_20150125\bin\sed.exe
C:\MyApps\MinGW\msys\1.0\bin\sed.exe
bash-3.1$ echo abc | sed "/abc/i def"
sed.exe: -e 表現 #1, 文字数 1: 未知のコマンドです: 「C」
bash-3.1$ echo abc | /c/MyApps/MinGW/msys/1.0/bin/sed "/abc/i def"
def
abc

結論

/から始まる仮想パスをmsys外のプログラムに渡す時にファイルシステム上の実際のパスに変換する機能がsedの変換指示に対し誤爆し、ついでになぜか記号が入った長い引数に対し途中で切る破壊工作が行われる問題に対し、msys内のPerlとmsys外のRubyを用いて引数を破壊工作されないと考えられる英数字だけの形式にエンコードしてmsys外に出ることで対処を行った。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?