1001001

73。CTFのWrite-upや技術的な備忘録を書きとめたいです。

Linuxでコマンド実行する時の処理モデルの話(setsidとか&とかexec)

はじめに

bashシェルスクリプトでコマンドを実行する時、以下のような実行パターンがそれぞれどう違うのか気になって調べたので備忘録です。特にプロセスIDやプロセスグループの違いを確認してます。

command
setsid command
setsid command &
exec command 
exec setsid command

背景

AKSPASSを使って、シェルでsshパスワードログインを自動化する方法を調べていて、以下の記事を読んだ。

expectやsshpassを使わずにシェルでSSHパスワード認証を自動化する #Linux - Qiita

そこでは exec setsid ssh ~~~ のようにしてsshを実行している。

また、以下の記事では、ASKPASS自体がexecute the programしてくれるのでexec不要と説明し、setsid ssh ~~~ としている。

SSH_ASKPASSの使い方について

これらを見て、setsidやexecをつけて実行した時やそれを組み合わせた時の挙動が気になったので検証した。コマンドは全てbashシェルスクリプトで実行している。

各コマンドのざっくりとした動作の違い

chatgptに聞きつつ、動作の違いを整理した。ここでは、プロセスツリー(pid,ppid,pgid)と処理モデル(非同期・同期実行)の観点で整理している。

  1. command
    • 通常の方法でコマンドを実行する。
    • コマンドはシェルスクリプト子プロセスとして実行される。
    • その子プロセスが完了するまで次の処理は実行されない。(同期実行
  2. setsid command
    • setsidコマンドは、新しいセッションとプロセスグループを作成してから指定されたコマンドを実行する。
    • コマンドはシェルスクリプト子プロセスとして実行されるが、プロセスグループは別物となる。
    • 同期実行となり、そのコマンドが終了するまで次の処理には進まない。
  3. setsid command &
    • setsid commandをバックグラウンドで実行する。(&はコマンドをバックグラウンドで実行するためのシェルの制御構造)
    • コマンドはシェルスクリプトとは別プロセスとして起動する。具体的にはinitプロセス(systemdプロセス)を親プロセスとしたプロセスになる。
    • バックグラウンド実行にしているので非同期実行となり、コマンドの終了を待たずに次の処理が実行される。
  4. exec command
    • execコマンドは、現在のプロセスを指定されたコマンドに置き換える。
    • コマンドのプロセスIDはシェルスクリプトのプロセスIDと同一となる。
    • シェルスクリプト以降の処理が書いてあったとしても実行されない
  5. exec setsid command
    • execを使ってsetsid commandを実行した場合、シェルスクリプト自体はsetsid commandに置き換わり、新しいセッションおよびプロセスグループでcommandが実行される。
    • コマンドのプロセスIDはシェルスクリプトのプロセスIDとなるが、その後setsidが実行されるので最終的には別のプロセスとして実行される。
    • シェルスクリプト以降の処理が書いてあったとしても実行されない

それぞれのコマンド実行時のプロセスのPIDを確認する

実際に確認してみた。
実行するコマンドのみ異なる以下のようなシェルスクリプトを5つ用意する。

#!/bin/bash
echo "My PID is $$"
echo "My PPID is $PPID"
PGID=$(ps -o pgid= -p $$)
echo "My PGID is $PGID"

echo "sleep 10"
sleep 10 # ここだけ可変
echo "done."

これを実行しつつ、sleepしている間に別ターミナルでsleepコマンドのpid,ppid,pgidを取得する。

./command.sh
My PID is 19071
My PPID is 17043
My PGID is   19071
sleep 10
done.

ps -o pid,ppid,pgid,cmd -p $(pgrep -o -x "sleep")
    PID    PPID    PGID CMD
  19073   19071   19071 sleep 10

全てのコマンドについてこの方法でシェルスクリプトとコマンドのpidを確認し、関係を整理する。

command

pid ppid pgid
シェルスクリプト 3485 1287 3485
コマンド 3486 3485 3485

コマンドのppidはシェルスクリプトのpidとなり、pgidもシェルスクリプトのpidで共通である。

setsid command

pid ppid pgid
シェルスクリプト 2862 1287 2862
コマンド 2863 2862 2863

コマンドのppidがシェルスクリプトのpidとなっているのは同じだが、pgidは別となっている。
シェルスクリプトでsetsidをつけて実行した場合、子プロセスにはなるがプロセスグループは別となる。

setsid command &

pid ppid pgid
シェルスクリプト 9583 1287 9583
コマンド 9585 1 9585

コマンドのppidは1となり、pgidもシェルスクリプトとは別となっている。
つまり、別プロセスとして実行されている。

exec command

pid ppid pgid
シェルスクリプト 16030 1287 16030
コマンド 16030 1287 16030

元プロセスを置き換えているので、pid,ppid,pgidは全てシェルスクリプトのプロセスと同じになる。

exec setsid command

pid ppid pgid
シェルスクリプト 17484 1287 17484
コマンド 17486 1 17486

おそらくexec直後は一度シェルスクリプトのプロセスを置き換えて実行されるのだが、その後setsidで別プロセスとして実行されるので、pid,pgidは新しくなり、ppidは1となる。

以上、実際にsetsidやexecでプロセスIDやプロセスグループが変化することが確認できた。

余談

setsid command & とexec setsid commandってほぼ一緒じゃない?

どちらも、親プロセスIDは1となるし、プロセスグループもシェルスクリプトのプロセスとは別物になる。
setsid command &はプロセスを置き換えないのでその後に続く処理があれば、commandの実行を待たずその処理に移るという違いはあるが、
setsid command &の後に何も処理がなければexec setsid commandとほぼ同じ動きになる。

chatgptに聞いても概ね同じ回答だった。置き換えるか明示的に終了するかの違いだと。

macにはsetsidがない?

macだとsetsidはなく、nohup使うらしい?(Linux環境でもnohupは使えるだろうけど。)
こっちも同じ動きになるかは未検証。

まとめ

setsid, execをつけたり、組み合わせた時にコマンドのpid,ppid,pgidがどのように変化するのかを確かめた。
プロセスIDとか気にしないのであれば、同期になるか非同期になるかくらいを気にすれば良さそう。

PyInstallerで32bitシステム用のELFを作る

はじめに

64bitアーキテクチャLinux上にて、Dockerで32bit用コンテナを作成し、その上でPyInstallerを使って32bitシステム用ELFを作った際の備忘録です。

背景

Linux環境で動かしたいプログラムをPythonで書いていた。対象環境に必要なライブラリがなくても動くように、PyInstallerとstaticxを使ってonefileに固めて利用するつもりだった。
しかし、対象のLinux環境が32bit環境であることが後から分かった。開発環境は64bit環境である。(なんでこんな環境なのかは割愛します。)
64bit環境において、PyInstallerで32bit用ELFファイルを作ることは可能かつ方法は単純明快で、Python自体がi386向けのものであれば、インストールされるPyInstallerもi386向けとなり、生成されるELFも32bit用となるらしい。

問題

例えば、メインで使っているPython3がPython3.8であれば、あえてPython3.7:i386をインストールし、シンボリックリンクでpython3-64, python3-32などと区別すれば、64bit環境で32bit用Pythonが使えるらしい。
とはいえ、こんなこともなければi386用のPythonなんて滅多に使わないので、これのために環境を汚したくなかった。

環境汚したくないけど新しくVM立てるのは面倒臭い。

解決方針

dockerコンテナ上に一時的にビルドに必要な環境を構築して、終わったらコンテナを消してスッキリ、という方法を取ることにした。
docker searchを使ってi386で検索すると、i386Pythonが使えるイメージが見つかったので、それを使うことにした。

docker search i386                                            
NAME                               DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
balenalib/i386-ubuntu-python       This image is part of the balena.io base ima…   0
(snip)

最終的な解決策

以下のような手順で実行することで、PyInstallerを使って32bit用ELFを作ることができた。手動で1つずつ実行しても同じことができるはず。dockerfile化する方がスマートかもしれない。

#!/bin/bash

container_name='pyinstaller32'
docker rm $container_name # 前のコンテナが残っていたら消す
docker run -dit --name $container_name balenalib/i386-ubuntu-python /bin/bash 

# 1回目は署名キー取得エラーになってしまうため2回実行(解決策わからず)

docker exec $container_name bash -c 'apt update'
docker exec $container_name bash -c 'apt update'


docker exec $container_name bash -c 'apt install -y <必要な依存関係>'

# sconsはstaticxに必要
docker exec $container_name bash -c 'pip install cython wheel scons'
docker exec $container_name bash -c 'pip install <必要な依存関係>'

# pyinstaller+staticxに必要
docker exec $container_name bash -c 'pip install pyinstaller staticx patchelf-wrapper'

# docker側にPythonファイルを渡してELFを生成して、ホスト側に持ってくる
docker cp ./myapp.py $container_name:/tmp
docker exec $container_name bash -c 'cd /tmp && pyinstaller --onefile myapp.py && staticx /tmp/dist/myapp myapp-static'
docker cp $container_name:/tmp/myapp-static ./

docker stop $container_name

少し中身を説明。解消法がわからずに冗長になっている部分などあるため。

コンテナは毎回削除して1から構築し直すようにしているがとても時間がかかるので、何度も実行するなら構築後のイメージを固めておくかいちいちコンテナを消さないようにした方が良さそう。

apt update時に署名キーのエラーが出てしまうが、エラーになるのは1回目だけだったので2回実行するようにした。(冗長だが解消法わからず。)

sconsはstaticxに必要かつ、staticxと一緒のタイミングでinstallしようとするとエラーになるので、前段階でinstallしている。

cythonとwheelは、私が作っていたプログラムで必要になったものなのでなくても問題ないかもしれない。

ホストとコンテナ間のファイルのやりとりはdocker cpを使っている。ホスト側のディレクトリをマウントする方法もありそう。その方がスムーズかもしれない。

これを実行すると、良い感じにonefileで動くELFを作ってくれます。

余談

staticxってなに?

PyInstallerだけだと動的リンクになる。動的リンクされるはずの共有オブジェクトが対象の環境に存在しない場合は動かないので、onefileあれば動くという要件に反する。
それを解消するためにstaticxを利用する。
以下の記事がlddコマンドで動的リンクを確認し、動的な依存関係がなくなる様子を示していてわかりやすい。
【python】Pyinstaller でバイナリ化した実行ファイルにダイナミックリンクしているものがあったので完全に静的なライブラリ化をめざす方法 - ものづくりのブログ

docker-pyinstallerについて

pyinstaller環境をdockerで用意して使いたいという内容を調べていると、docker-pyinstallerというのに行き着いた。日本語の紹介記事もちらほら。

Docker環境のPyInstallerでキレイにExe化する #Python - Qiita

最終コミットが4年前とかなり古いが、2022年11月時点で最新化を試みている人もいた。
docker-pyinstaller を雑に最新にする

32bitELFを作ろうとしているわけではないが、リポジトリを見ると32bit環境用のものもあり有用かと思われたが、やはりメンテナンスされてないのは気がかりなので、これは使わずに自分で試行錯誤してみることにした。

balenalib/i386-ubuntu-pythonってなんなの

docker hubで調べると、Verified Publisherなので問題はなさそう。以下を見ると、IOT向けのイメージを作成して配布しているようなPublisherみたい。
Balena base images - Balena Documentation

まとめ

PyInstaller+staticxは便利。32bitシステム用のELFも作れる。でも別の言語でかけるなら最初からそうした方が良い、きっと。

pymodbus3でエラー

問題

以下のように、collectionsライブラリに関するエラーが出る。

Traceback
(snip)
  File "/<site-packagesのパス>/pymodbus3/utilities.py", line 10, in <module>
    from collections import Callable
ImportError: cannnot import name 'Callable' from 'collections' (/usr/lib/python3.11/collections/__init__.py)

当方のPythonバージョンは3.11系。

原因

3.10系だが、こんな記事があった。
ImportError: cannot import name 'Mapping' from 'collections' using Python 3.10 - Stack Overflow

Python 3.10 以降のcollections側のインターフェースの変更が原因らしい。
対処法としては以下が考えられるとのこと。

  1. Python 3.9 に戻す(ダウングレード)
  2. エラーが出ているライブラリの更新
  3. コードに手動でパッチを当てる

解決策

3つ目のコードに手動でパッチを当てる方法で解決した。先の記事にパッチ内容も載っており、以下のようにすれば良いらしい。

変更前

from collections import Callable

変更後

from collections.abc import Callable

修正箇所が複数に渡る場合は、一括置換してしまうと良い。pymodbus3の場合は以下。

cd /<site-packagesへのパス>/pymodbus3
find ./ -name '*.py' -exec sed -i 's/from collections/from collections.abc/g' {} \;

これで正しく置換できれば、エラーは消える。

余談

とあるOT系の書籍で紹介されていたのでpymodbus3を使ったのだが、unit idを変更する手段がわからなかったので、結局pyModbusTCPという別のライブラリを使った。(collectionsのエラーも出なくなったのでパッチ当てた意味はなかった。)

あと似たような事象だけどちょっと違うパッチを当てている人がいた。その記事は以下。
【python】Code ImportError : cannot import name 'mapping' from 'collections'の解決方法 - 旧帝大生の雑記ブログ

まとめ

Python3.10以降のcollectionsのインターフェース変更によりエラーが出ている場合は、手動でimport文を修正すれば直るかもしれない。

【pypdf】片面ずつスキャンしたPDFを結合して両面スキャンしたかのようにしたい

はじめに

pypdfで2つのPDFファイルを1ページずつ交互に連結する方法と,PythonでのPDF操作のライブラリに関して調べたことの備忘録です.

問題

長辺閉じのA4資料を両面スキャンしたかったのだが、自宅のBrother製プリンターが対応していなかった.仕方ないので片面ずつスキャンした2つのPDFを後から処理して,ページの整合性を保ちつつ,一つのPDFにしたい.
(1枚ずつ手作業で表裏返しながらならできるのですが,紙送りモードでは非対応)

解決方法

pypdfを使って,2つのPDFの各頁を交互にくっつけるスクリプトを書いた.スキャンの順序の関係で,以下のスクリプトでは表PDFの最初1ページ目と,裏PDFの後ろ最後1ページ目から交互に結合している。

詳細は参考に載せているドキュメントのMerging PDF Files辺りを見るのがわかりやすい.

from pypdf import PdfReader, PdfWriter

def main():
    reader1 = PdfReader("omote.pdf") # 表面
    reader2 = PdfReader("ura.pdf") # 裏面
    number_of_pages1 = len(reader1.pages)
    number_of_pages2 = len(reader2.pages)

    # 最後のページが白紙だろうと気にせずスキャンしたので,
    # 表面PDFと裏面PDFの枚数は同じになる.ページ数同じ想定で進むのでそうでなければ念の為止める
    assert number_of_pages1 == number_of_pages2

    writer = PdfWriter() # PdfWriterでmergeやappendができる(appendは最後に追加,mergeはposition指定できる)
    for i in range(number_of_pages1): # 単純に,表面PDFは1枚目から,裏面PDFは最終ページから順に取って並べていく
        # fileobjはpdf名を指定して作成したPdfReaderオブジェクトを指定する
        # pagesは,(start, stop[, step]) で範囲指定するので,今回は1頁ずつ取ってくるようにする
        writer.append(fileobj=reader1, pages=(i, i+1))
        writer.append(fileobj=reader2, pages=(number_of_pages2-i-1, number_of_pages2-i)) 

    with open("test.pdf", "wb") as f:
        writer.write(f)

if __name__ == "__main__":
    main()

Pythonでpdf操作するライブラリって他にあるの?

pypdfはforkがめちゃくちゃいっぱいあって,pypdf2,pypdf3,pypdf4などあるが,pypdfを使っておけば良いらしい.
www.reddit.com

  • pypdf2は良いforkで最近pypdfに統合された
  • pypdf3,4は良くないfork(何が良くないのかは書いていない)
  • 要するにpypdfを使えば良い

といったことが書いてある.
あと,どうやらテキスト操作とか必要ならPyMuPDFというのも選択肢に入ってくるらしい.

以下の記事でも,pypdf使うといいよ〜って書いてあった。
qiita.com

他に解決方法ないの?

VB.NETC#で同じようなことをやっているものがあった。今回環境がMacだったのでPythonの方が楽だと思いpypdfにした。
https://support.mescius.jp/hc/ja/articles/360004021355-複数のPDFを1ページずつ交互に結合する方法

EXT4でフォーマットされた外付けHDDをMacにマウントする

備忘録として残す.

  • 経緯
  • 環境
  • 前提
  • fuse-ext2EXT4でフォーマットされた外付けHDDをMacにマウントする
  • 成功するまでの試行錯誤
    • エラー1: brew caskのエラー
    • エラー2: ext4fuseをbrewで入れようとしてエラー
    • エラー3: ext4fuseでマウントしようとしてInput/Outputエラー
    • エラー4: fuse-ext2のscript.shで75行目で./configureできない等のエラー
  • まとめ
続きを読む

CEHv9の和文の試験に合格した

Certified Ethical Hacker (以下,CEH)という試験を受験して合格しました. せっかくなので感想や勉強法について記載したいと思います.

はじめに

CEHとは

詳細は割愛しますが,

  • クラッカーが使うものと同じ技術やツールについての知識を学ぶ
  • それらを攻撃ではなく防御に活かそうという思想を持つ

という資格です.この思想がCertified Ethical Hacker (認定ホワイトハッカー)という名前の所以です.

CEHはEC-Councilという組織が提供する試験・資格です.受験には最低2年のセキュリティ分野での実務経験が必要になります.しかし,特定の講座を受講し,修了証をもらうことでこの条件をパスできます.(私は今年1年目なのでこの方法を使いました.和文のテキストを使った日本語の講座です.受験費用含め60万かかるので自費だったら厳しかったです...)

試験の概要と問題

試験は,4時間で選択式の問題を125問を解き,正当率が70%を超えれば合格です.30問以上も間違えることができます.(言い方が悪い.) 結構過去問と同じ問題が出ます.ただ今後試験がv9からv10になるので変化する可能性はあります.

また,これは海外の試験ですが,最近和文の試験が追加されました.私はその和文試験を受験しました.ただ,講座や試験は日本語テキスト・和文で受けられるのですが,(現時点では)問題集や参考書に和文のものは存在しないので,模擬試験は英文で行うことになります.

問題の内容としては,7~8割くらいがツールや攻撃手法の問題で,残りは法律やセキュリティマネジメントに関する問題です.(体感です.正確な値ではありません.)

勉強してみた,受験してみた感想

勉強はかなり楽しかったです.EC-Councilが定めるハッキングの段階に沿って,そのハッキングに使う技術やツール,ペンテスト手法を学ぶことができました.

受験の感想ですが,個人的な意見では,選択式の問題しか無い分,旧セキュスペ等より簡単だと感じました.ただ難易度は得意不得意によって判断基準がかなり変わるので何とも言えませんね...

私のスキルセットと勉強法

参考までに私の勉強法とスキルセットを紹介します.ただし,私は講座を受講したことで手に入れた和文のテキストを勉強に使っていましたので,同じく受講した人向けになるかもしれません.

バックグラウンド・スキルセット

  • 情報系の学部・修士
  • 大学時代にCTFチームに所属,基本的な攻撃手法は知っている
  • 現在IT企業の技術開発部で攻撃検証,ネットワーク管理でスイッチやルータのコンフィグ投入など実施

簡単な攻撃手法や,ネットワーク・TCP/IPなどの基本的な知識は元々持っていました.

勉強法

以下,CEH講座受講 (9月頭)から受験(2月2日)までの流れです.

  • 試験5ヶ月前: CEH講座受講.雰囲気を掴む.
  • 試験4ヶ月前: CEH受験エントリを見て情報収集.良さそうなアプリを見つける.
  • 試験4ヶ月前〜1ヶ月前: アプリをコツコツ解く.解けない問題を把握する.
  • 試験3週間前: 良さそうなCEH問題出題サイトを見つける.
  • 試験1週間前: 良しな翻訳に通した問題を解く.テキストをパラ読みして知らないワードを手書きで抽出していく.
  • 受験当日: 午後の試験に備えてお昼ご飯をしっかり食べる
1. アプリで問題を解いてできない範囲を把握

以下のアプリがおすすめ.課金して問題数を増やすとたくさん練習できていい感じです.(僕のiPhoneではやや動作が不安定になることがありましたが,総合的には良アプリでした.)

App名: CEH Pocket Prep、デベロッパ: Pocket Prep, Inc. https://itunes.apple.com/jp/app/ceh-pocket-prep/id896288518?mt=8

f:id:vernalstar:20190204234621p:plain:w200f:id:vernalstar:20190204234547p:plain:w200
出題設定画面と解答画面

最初はやる気が出ない(私だけ?)と思いますので,このアプリで5問ずつくらいからちまちま解いていくと長続きします.自然と多くの問題を解くことができ,どのような問題が解けてどのような問題が苦手なのか把握できて良いです.

また,このアプリのいいところは,過去の模擬テスト実施記録が以下のように現れることです. 1ヶ月前くらいから正答率の少ない模擬試験の問題をとき直すことで,「以前解けなかった問題が解けるようになっているか」「未だに解けない問題はどんな問題か」を簡単に把握することができます.

f:id:vernalstar:20190204234705p:plain:w200
履歴から間違えた問題を確認できる.

2. テキストで試験に出そうでかつ知らないワードや手法を洗い出す

アプリで問題を解くと,どんなツールの問題が多いか,どんな攻撃手法を問われるのかがわかってきます. 例えば,CEHではNessusやnmapの問題が高い確率で出題されます.これは過去問を解けば明らかにわかります.他にもよく出るツールがあったりするので,先に過去問で傾向を把握するのは重要です. その上で,必要そうな知識をテキストからピックアップしていきます. (自分の場合は,手書きの方が頭に入るので手書きでピックアップした上で,抽出したワードを眺めます)

この方法だと,試験に出そうかつ自分が知らない知識を効率よく取り込むことができる(気がする)のでおすすめです.

(オプション) 翻訳に通した問題を解く

英文だとたまにニュアンスを間違えて誤答してしまう問題がありました.そこで試験一週間前は和訳した問題を解いてました.自分で和訳したりGoogle翻訳などでコツコツ和訳するのも良いのですが,さすがにそれは時間がかかるので,なんかこう勝手にやってくれる感じで良しなにやりました.

和文で受ける場合,英文のニュアンスを知る必要はなく,英文を読むよりはサクサク問題を解けるのでかなりスムーズに勉強できました.

上で紹介したウェブの問題サイトは以下です.v8の試験も受けられるので,過去問のカサ増しなどに良いです.

また,以下のようなCEHのテキストのような内容が書いてあるサイトもありました.

受験当日の朝に発見したのできちんと読めてませんが,もし今後受ける方で興味ある方は読んでみると良いかもしれません.

まとめ

CEHの概要,受けた感想,勉強法について書かせていただきました. 情報系の基本的な知識を問う問題が出たり,過去問からの出題もあったりするので,アプリや問題出題サイトで勉強するだけでもかなり解けるようになります.他の方のエントリと合わせて参考になれば幸いです.

PythonのrequestsでTwitter APIを叩く(ユーザ認証編)

PythonTwitter用のライブラリ等を使わずにRequestsでAPIを叩く方法のメモ.OAuth1.0の理解が主な目的です.
今回はユーザ認証を使って特定ユーザのアカウントのタイムラインを取得します.
各処理についてやOauth1.0の署名作成部分についての詳細を追記予定です。

前回はアプリケーション認証によって特定ユーザのアカウントのタイムラインを取得しました.

続きを読む