色々な sh
前回の枕に書いた、運動教室の事。初回出席時に運動ノートってのを貰った。毎日の体重、血圧、歩数と歩いた時間を記録するもの。出席時に持参して見せてくださいって説明されてたんだけど、体重に目立った変化無し、血圧は独自に記録してる。歩数も時間も、雨雪槍が降らない限りは、きっちり同じ行動をしてる。よって、見せても、以下同様なんで省略です。
そしたら、保健婦さんが、血圧の記録を見たいって言うので、持って行ったよ。医者から貰った血圧ノートじゃないんですねぇ。平均とか標準偏差まで出してる血圧記録なんて初めてみましたって、びっくりされた。
同時に、標準偏差なんて、統計に詳しいんですかとも聞かれた。そんな事を言って来る人初めてなんで、オイラーもちょっとびっくりです。
同じ条件で測ってるのに、日によってばらつくのは何故? よその人はどうなのって質問を返したら、ありきたりの回答しかなかった。でも、塩分を控えたら、更に低い方へ行くんじゃないですかと、保健婦さんモード。塩抜きに努めて、結果を見せてくださいとも。
降圧剤を飲んでいるんだけど、効果が疑わしい。薬剤師に、この薬を飲んで効いているのはどのぐらいって聞いたら、1日との事。だから毎日服用ね。
その理屈なら、飲まなければ、血圧は上昇するはず。1週間ぐらい飲まなかったけど、統計的に有意な差はなかったぞ。
血圧は、測る度に変動するんで、2回測定して平均値が公式記録になるそうな。定期健診の記録から、2回の差分を参照してみる。
13 9 11 6 18
差が18って大きいな。美人看護婦さんで、最初はどきまきしたけど、3秒で飽きたんかな。まあ、そんなもんだろうね。
一方、 日本高血圧学会が4月改訂予定「高血圧の降圧目標値」とはなのが出てた。
数字が一人歩きしてないか? 朝と夜で値が違うし、運動をすれば大幅に上昇する。普通に考えれば、いかなる場面においても、制限値と言うか推奨値を超えるのは宜しくないと思えるのだが。
そんな事を言ったら、静かに寝てろ、刺激が強いテレビとか、意に沿わないでカッカさせる新聞とかネットなんか見るなが、結論。勿論、山の中に籠って仙人生活をするのが理想であります。
escape from rksh
ひょんな事から知った、制限付きのshellから脱出する事を考える。脱獄しようは、iphoneユーザーの専売特許じゃないからね。
一番分かり易い、cd禁止の制限。chdir(2)を直接アプリ内で使ってしまえば、よもやrkshに感づかれる事はあるまい。
#include <unistd.h> int main(){ chdir("/usr/src"); }
今居る所から、ソースが読み放題の場所へのワープ願望、希望のアプリの第一歩。
ob6$ rksh ob6$ cd /usr/src rksh: cd: restricted shell - can't cd ob6$ cc test.c ob6$ ./a.out rksh: ./a.out: restricted
無事にコンパイル出来たんで、シメシメと思ったら、敵は一枚上手だった。
shell restrict法の第3条に抵触したんだな。ああ、3条ってのは、コマンドは絶対Pathまたは相対Pathで指定できませんって奴ね。
アプリ内でやってる事は検閲出来ないって目論見で、特製の奴を作ったけど、足元すくわれたな。
どういう防御が働いている?
ob64$ grep restricted *.[ch] c_ksh.c: bi_errorf("restricted shell - can't cd"); exec.c: "command -p: restricted"); exec.c: warningf(true, "%s: restricted", cp); exec.c: warningf(true, "%s: restricted", cp); main.c:static int is_restricted(char *name); main.c: int restricted, errexit; main.c: restricted = Flag(FRESTRICTED); main.c: if (is_restricted(argv[0]) || is_restricted(str_val(global("SHELL")))) main.c: restricted = 1; main.c: if (restricted) { main.c:/* Returns true if name refers to a restricted shell */ main.c:is_restricted(char *name) misc.c: { "restricted", 'r', OF_CMDLINE }, sh.h: FRESTRICTED, /* -r: restricted shell */ var.c: errorf("%s: restricted", tvar);
隠密を放って、敵情を探ってこい。隠密って、お庭番とか言うんだったな。
次なる手は、既存のアプリ内から、shellを起動しちゃう技だな。思いつくのは、
ob64$ emacs -f eshell : Welcome to the Emacs shell /tmp/ksh $ cd /usr/src /usr/src $
脱獄成功。まて、こんなんで脱獄なんておこがましいぞ。FreeBSDのjailからbreakしたら、jail breakを認定してあげましょう。 今の場合は精々、入院してる患者が夜こっそり抜け出して、街へ飲みに行くぐらいの感覚ですよ。
ob64$ cd rksh: cd: restricted shell - can't cd ob64$ sh ob64$ cd
身も蓋も無い方法だな。これなら誰でも出来る。
Escape From SHELLcatraz - Breaking Out of Restricted Unix Shells
Restricted Linux Shell Escaping Techniques
皆考える事は一緒だね。
unix v6 の sh
以前globを発見したのは、unix v6 だった。shを見ておかねば。
tglob(c) int c; { if(any(c, "[?*")) gflg = 1; return(c); } execute(t, pf1, pf2) int *t, *pf1, *pf2; : gflg = 0; scan(t, &tglob); if(gflg) { t[DSPR] = "/etc/glob"; execv(t[DSPR], t+DSPR); prs("glob: cannot execute\n"); exit(); }
どうやら、glob用の文字を検出すると、/etc/globに丸投げしちゃう雰囲気だな。実際は走らせてみればいいんだろうけど、ちょっとどうやればいいのか見当が付かないので撤退。
xv6-public の sh
と言う事で、下記の教育用OSを取り寄せる。
ユーザーランドのshを観光するのが目的。みんな目玉のkernelばっかに目がいってるけど、今回は脇役に注目。
sakae@usvr:~/sim/xv6-public$ make .gdbinit sed "s/localhost:1234/localhost:26000/" < .gdbinit.tmpl > .gdbinit sakae@usvr:~/sim/xv6-public$ vi ~/.gdbinit : add-auto-load-safe-path /home/sakae/sim/xv6-public/.gdbinit
gdbから便利に使えるように準備しておきます。
sakae@usvr:~/sim/xv6-public$ make qemu-nox-gdb : *** Now run 'gdb'. qemu-system-i386 -nographic \ -drive file=fs.img,index=1,media=disk,format=raw \ -drive file=xv6.img,index=0,media=disk,format=raw \ -smp 2 -m 512 -S -gdb tcp::26000
取りあえず起動すると、gdbを走らせろとメッセージがでてくる。カーネルを指定して観光の初まりです。
sakae@usvr:~/sim/xv6-public$ gdb -q kernel Reading symbols from kernel...done. + target remote localhost:26000 The target architecture is assumed to be i8086 [f000:fff0] 0xffff0: ljmp $0x3630,$0xf000e05b 0x0000fff0 in ?? () + symbol-file kernel (gdb) c Continuing. ^C
一度カーネルを動かした上で、gdbを呼びだし。
(gdb) symbol-file ./_sh Load new symbol table from "./_sh"? (y or n) y Reading symbols from ./_sh...done. (gdb) b main Breakpoint 1 at 0x0: file sh.c, line 146. (gdb) c Continuing. The target architecture is assumed to be i8086 [ 1b: 0] 0x1b0 <runcmd+64>: call 0xe60 <printf> Thread 1 hit Breakpoint 1, main () at sh.c:146 146 {
shが起動したらgdbに入りたいので、シンボルファイルをsh用に切り替えた上でBPを設定。 そうしておいて、xv6のshの上でshを起動すると、gdbがユーザーランド側で動き出しました。
$ sh ;; Ctrl-a c $ QEMU 2.11.1 monitor - type 'help' for more information (qemu) q
xv6の停止コマンドは用意されていないので、qemuを抜ける事で終了できます。 と、ここまでは復習です。
$ ls | grep echo
こんなコマンドを叩いておいて、runcmdのパイプを認識した所で確認。
(gdb) bt #0 runcmd (cmd=0xbf30) at sh.c:102 #1 0x00000375 in main () at sh.c:168 (gdb) p *(struct execcmd*)pcmd->left $21 = { type = 1, argv = {0x1960 <buf> "ls", 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>}, eargv = {0x1962 <buf+2> "", 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>} (gdb) p *(struct execcmd*)pcmd->right $23 = { type = 1, argv = {0x1965 <buf+5> "grep", 0x196a <buf+10> "echo", 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>}, eargv = {0x1969 <buf+9> "", 0x196e <buf+14> "", 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>, 0x0 <runcmd>} }
確かにパイプを施設してるな。parseしてからrunって流れだな。
ladsh
家の古い本をパラパラしてたら、巻末に参考本の案内が出てた。『プログラミング Linux』なんてのがお勧めらしい。例によって翻訳本。 ladshと言うshellを段階的に開発してみせてくれるそうなので、やってみる。
ladsh[1-4].cまで段階を踏んでるけど、完成度の一番高そうなladsh4.cを動かしてみる。
if (!(newJob.progs[i].pid = fork())) { printf("Child = %d\n", getpid()); signal(SIGTTOU, SIG_DFL);
上記の様にforkした後の子プロセス処理の冒頭に、自pidを報告する様にした。それで走らせてみる。
# cat | grep hot | wc Child = 62160 Child = 85073 Child = 72829 mac hotmot ootoya 1 1 7
catの入力待ちの時に別端末から確認。確かに子供のプロセスが居た。
ob64$ ps a PID TT STAT TIME COMMAND : 2550 p2 S 0:00.02 ./a.out 72829 p2 S+p 0:00.00 cat 62160 p2 S+p 0:00.00 grep hot 85073 p2 S+p 0:00.01 wc
今度は、fg,bgの確認。
# cat | grep lisp | wc Child = 76115 Child = 41638 Child = 83797 ^Z [1] Stopped cat | grep lisp | wc # jobs [1] Stopped cat | grep lisp | wc # fg %1 sbcl clisp 1 1 6
catの待ちの時に、Ctl-zしてbgに追いやる。jobsで確認すると、ちゃんと覚えているね。fgで端末を取り戻し、入力続行。一番難しい部分を含めて、760行で実装されてた。
OpenBSDのgdbを使っている場合、child processへの追跡は出来ないっぽい。
gdbでforkされた子プロセスのデバッグを実施する方法はない。 この対策として、子プロセスの起動の際に、sllepを入れて、sleepしている間に、 attachする方法があるが、現在の最新のgdbなら追いかけることが可能である。 set follow-fork-mode mode プログラムによるforkやvforkの呼び出しに反応するよう、デバッガを設定します。 forkやvforkの呼び出しは、新しいプロセスを生成します。 modeは、以下のいずれかです。 parent forkの後、元のプロセスをデバッグします。子プロセスは、妨げられることなく実行 されます。これがデフォルトです。 child forkの後、新しいプロセスをデバッグします。親プロセスは、妨げられることなく実行 されます。
catch fork なんてのも有るな。
OpenBSDの場合、初期状態では、gdbでattachできない。そこで、やおら
ob64$ doas sysctl kern.global_ptrace=1
を実行しておく事。
ladsh4.cでは、parseCommandの中でglobが呼び出されて、ファイル名の展開が行なわれて、その結果がargvに移される。
89void globLastArgument(struct childProgram * prog, int * argcPtr, 90 int * argcAllocedPtr) { : 107 rc = glob(prog->argv[argc - 1], flags, NULL, &prog->globResult); : 122 } else if (!rc) { 123 argcAlloced += (prog->globResult.gl_pathc - i); 124 prog->argv = realloc(prog->argv, 125 argcAlloced * sizeof(*prog->argv)); 126 memcpy(prog->argv + (argc - 1), 127 prog->globResult.gl_pathv + i, 128 sizeof(*(prog->argv)) * 129 (prog->globResult.gl_pathc - i)); 130 argc += (prog->globResult.gl_pathc - i - 1); 131 }
移す場所を拡張工事して、拡げているね。
# ls /home/sakae/*
こんなのを実行して、どんなファイル名を獲得したか確認。
142 int parseCommand(char ** commandPtr, struct job * job, int * isBg) { : 350 if (*prog->argv[argc]) { 351 argc++; 352 globLastArgument(prog, &argc, &argvAlloced); 353 } B35=> if (!argc) { 355 freeJob(job); 356 return 0; 357 }
(gdb) set print pretty (gdb) p *prog $38 = { pid = 0, argv = 0xd0c26886d00, numRedirections = 0, redirections = 0x0, globResult = { gl_pathc = 7, gl_matchc = 7, gl_offs = 0, gl_flags = 257, gl_pathv = 0xd0c268873c0, gl_statv = 0x0, gl_errfunc = 0x0, gl_closedir = 0x0, gl_readdir = 0x0, gl_opendir = 0x0, gl_lstat = 0x0, gl_stat = 0x0 }, freeGlob = 1, isStopped = 0 }
こちらは、execveに渡せるスタイルになってる。
(gdb) set print array (gdb) p argc $39 = 8 (gdb) p *prog->argv@9 $40 = {0xd0c3c812980 "ls", 0xd0bd4febf20 "/home/sakae/GZ", 0xd0c16be7c30 "/home/sakae/OB", 0xd0c16be7170 "/home/sakae/UDP", 0xd0c34ff0e80 "/home/sakae/base.c", 0xd0c268886e0 "/home/sakae/ladsh1.c", 0xd0c3c812b40 "/home/sakae/ladsh4.c", 0xd0c16be7440 "/home/sakae/src", 0x0}
そして、ls 実行の現場へと向う。見る場所は、runCommandの中。
537 execvp(newJob.progs[i].argv[0], newJob.progs[i].argv); 538 fprintf(stderr, "exec() of %s failed: %s\n", 539 newJob.progs[i].argv[0], 540 strerror(errno));
当りまえだけど、parse時の結果がそのまま使われる。
(gdb) p newJob.progs[i].argv $9 = (char **) 0xf7073f53280 (gdb) p *newJob.progs[i].argv@10 $9 = {0xf6f95916aa0 "ls", 0xf7073f54b30 "/home/sakae/GZ", 0xf7073f54af0 "/home/sakae/OB", 0xf6f959145f0 "/home/sakae/UDP", 0xf7016a25180 "/home/sakae/base.c", 0xf6f95916180 "/home/sakae/ladsh1.c", 0xf7016a25640 "/home/sakae/ladsh4.c", 0xf7073f54900 "/home/sakae/src", 0x0, 0xdfdfdfdfdfdfdfdf <error: Cannot access memory at address 0xdfdfdfdfdfdfdfdf\ >}