raspberry piのSPIで3つ以上のデバイスを接続する
raspberry piではドライバが公開されているので、簡単にSPIでデバイスを接続できます。 しかしraspberry piでは仕様上、3つ以上のデバイスをSPIで接続することができません。 今回はもっとたくさんのデバイスをSPIで接続したかったので、pythonで頑張ってみました。
なお、以下の内容は次の環境を前提としています。
raspberry pi 3B+ Raspbian 9 python3.7 py-spidev 3.4
方針
SPIではひとつのバスに複数のデバイス(スレーブ)を接続できます。 しかしただ同じバスに接続しただけではどのスレーブと通信したいかがわかりません。 そこで、CS(Chip Selector)を各スレーブに個別にマスターと繋がるように配線します。 初期状態ではすべてのスレーブのCSをHighにしておき、通信したいときは通信対象のスレーブのCSだけLowにします。 raspberry pi ではCSが2つしかないので、同時に2つまでのスレーブしか接続できません。
スレーブを3つ以上接続するためにはCSとして利用できるピンを増やせばいいわけです。 CSは単純に通信中にLowに落とすことができればいいので、wiringpiで操作することにします。
またSPIをpythonで利用するために、py-spidevを利用させていただきました。
コード
#!/usr/bin/python import wiringpi from spidev import SpiDev class SPIMassChips(SpiDev): def __init__(self): super().__init__() def set_cs(self, cs_pins): self.cs_pins = cs_pins wiringpi.wiringPiSetup() for self.__i in self.cs_pins: wiringpi.pinMode(self.__i, 1) wiringpi.digitalWrite(self.__i, 1) def open(self, bus, chip): self.bus = bus self.chip = chip if 0 > self.chip or self.chip > (len(self.cs_pins) - 1): raise ValueError("Invalid chip number.") super().open(self.bus, 0) def close(self): super().close() def xfer2(self, args): for self.__i in self.cs_pins: self.__j = 0 while not wiringpi.digitalRead(self.__i): self.__j = self.__j + 1 if self.__j > 100: raise UserWarning("Device is busy.") wiringpi.digitalWrite(self.cs_pins[self.chip], 0) self.__value = super().xfer2(args) wiringpi.digitalWrite(self.cs_pins[self.chip], 1) return self.__value
見ての通り、py-spidevを継承したクラスになっています。
py-spidevでデータを読み書きする直前直後にwiringpi
でCSに見立てたピンの出力を操作することで3つ以上のスレーブに対応できるようになっています。
実際にSPIでデータを読み書きするメソッドはxfer2()
のみ実装しています。
他のメソッドは私の中で需要がなかったので対応していません。
xfer2()
では一応簡単に排他制御っぽいことやっています。
というのもwitingpi
でCSをLowに落としてから継承元のxfer2()
でデータを読み書きするまでの間に、CSに指定したピンの出力が変更されると意図したスレーブと通信できなくなってしまいます。
そこで、通信開始前にCSに指定されているピンの中にLowになっているものがないか確認し、もしLowになっているものがあればHighになるまで待つようにしました。
また待ち時間は適当なところでタイムアウトするようにしました(ループ回数の100は適当です)。
使い方
本家py-spidevとそんなに大きくは変わりません。
spi = SPIMassChips() # CSとしてスレーブに接続しているピンを設定します。 cs_pins = [21, 22, 23, 24] spi.set_cs(cs_pins) # 本家py-spdevと同じようにバスの番号とスレーブの番号を指定します。 # スレーブの番号は`set_cs()`で渡した配列のインデックスになります(ここでは24番ピンに接続されたスレーブが対象になる)。 spi.open(0, 3) # 本家py-spidevと同様にデータをやり取りできます。 send_data = [0, 0, 0] received_data = xfer2(send_data)
Hello World!を逆アセンブル①
こんな本を買ったので、最近はバイナリの勉強をしていたりします。
Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際
- 作者: Jon Erickson,村上雅章
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/10/22
- メディア: 単行本(ソフトカバー)
- 購入: 9人 クリック: 163回
- この商品を含むブログ (19件) を見る
というわけで(?)、もはや何番煎じかわからないですが、今日はHello Worldのアセンブリを見ていきます。 上記の本だと32bit版で解説されていますが、今回は64bitの環境で試したので、レジスタの名前が違ったりしました。
環境
Ubuntu 16.04を使っています。 gcc(コンパイラ)のバージョンは以下の通り。
$ gcc --version gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
プログラムの作成
どんなC言語の入門書でも必ず最初に出てくるような次のコードをhelloWorld.c
として保存。
#include <stdio.h> int main(){ printf("Hello World!\n"); return 0; }
コンパイルします。
$ gcc helloWorld.c -o helloWorld
出来上がったものを実行すると、標準出力に文字列が返ります。
$ ./helloWorld Hello World!
逆アセンブルしてみる
早速objdump
で逆アセンブルしてみます。
$ objdump -M intel -S helloWorld #中略 000000000000063a <main>: 63a: 55 push rbp 63b: 48 89 e5 mov rbp,rsp 63e: 48 8d 3d 9f 00 00 00 lea rdi,[rip+0x9f] # 6e4 <_IO_stdin_used+0x4> 645: e8 c6 fe ff ff call 510 <puts@plt> 64a: b8 00 00 00 00 mov eax,0x0 64f: 5d pop rbp 650: c3 ret 651: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 658: 00 00 00 65b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] #中略
アセンブリはAT&T記法とIntel記法がありますが、後者の方が見やす気がするのでそちらで表示しています。
-M
オプションで指定します。
スタック領域の切り替え
最初の2行は関数が実行されるときに最初に実行される処理です。
main()
関数を開始するためにスタック領域の切り替えを行います。
63a: 55 push rbp 63b: 48 89 e5 mov rbp,rsp
rbp
(ベースポインタ)がスタックの最古(底)のアドレスで、rsp
(スタックポインタ)が最新(一番上)のアドレスを指しています。
push
は指定したレジスタの値をスタックに積む(保存する)命令で、こうすることで現在(main()
の呼び出し元)のスタックのアドレスをバックアップします。
バックアップしたアドレスはmain()
関数が終了し、main()
関数の呼び出し元の処理を再開するときに使用します。
バックアップできたら、mov
でrsp
の値をrbp
へ書き込みます。
最新のスタックのアドレスがmain()
のスタックのはじめ(底)のアドレスとして使用されるようになるわけです。
printf()
の実行
続いてprintf()
の呼び出しに関する部分です。
63e: 48 8d 3d 9f 00 00 00 lea rdi,[rip+0x9f] # 6e4 <_IO_stdin_used+0x4> 645: e8 c6 fe ff ff call 510 <puts@plt>
lea
でrip
(インストラクションポインタ)に0x9f
足したアドレスをrdi
(ディスティネーションインデックス)へ書き込んでいます。
詳しくは次回に回しますが、rip
+0x9f
は"Hello World!"の先頭アドレスで、rdi
を使用することで引数としてprintf()
へ渡しているようです。
続いてprintf()
の実行。これは関数の先頭アドレスをcall
するだけなので単純ですね。
戻り値を返す
ソースで言うとreturn 0
の部分です。
64a: b8 00 00 00 00 mov eax,0x0
eax
(アキュムレータ)は戻り値の格納に使用するレジスタで、このレジスタに0
をmov
で書き込んでいます。
スタックをもとに戻す
一通りの処理が終わったので、最初に切り替えたスタックをもとに戻します。
64f: 5d pop rbp
main()
の開始時にスタックへ呼び出し元関数のスタックのベースアドレス(底のアドレス)を保存しました。
この処理ではそのアドレスをrbp
へ戻しています。今回はスタックを使用していないので、ありがたみが感じられないですね。
呼び出し元へ戻る
main()
関数の処理がすべて完了したので、呼び出し元へ処理を戻します。
650: c3 ret
何のためか不明な命令
最後の3行は何のためのものかよくわかりませんでした。
651: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 658: 00 00 00 65b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
nop
はNo Operation、つまり何もしないという命令らしいですが、何のために入っているかよくわからず、、、パディングかな?
sqlplusが個人的に使いにくいのでpythonでそれっぽいのを書いてみる
Oracle標準のsqlplusですが、個人的にはちょっと使いにくいのでpyhtonで書いてみました。
もちろんわざわざ作らなくてもその辺にツールはいくらでも転がっているんですが、ダウンロードさせてもらえないんです。いやさせてもらえないことはないけど、使いにくいくらいの理由じゃあねえ、、、。客先常駐あるあるだと思う。
ないなら書いてしまえというわけで、、、。
欲しい機能
- bashみたいにバックスペースで文字を削除したい。
- bashみたいにカーソルキーでカーソルを移動したい。⇒これできないのがすごく面倒。
- bashみたいに↑キーで履歴がでるようにしたい。
- csvでファイルに出力したい。⇒
||','||
とかでできなくはないんだけど、SQLが気持ち悪くなるし、エスケープしてくれないし、、、
環境
- python2.7 ←CentOS7.0でフォルトだから入ってる。
- cx_Oracle 5.3←これだけは別の仕事でどうしても必要だったので入れてもらえた。
使いかた
$ ./oracle_cli.py Help: ? > ? Documented commands (type help <topic>): ======================================== EOF connect export_csv header query ask_fetch exit fetch_rows help quit > > connect admin@localhost:1521/PDB01 Password: Conneted to Oracle successfly. > query SELECT 'hello!' AS HELLO FROM DUAL HELLO ---------- hello! Query done successfly. 1 row(s) selected. >
sqlplusっぽくインタラクティブに操作できる。
helpも一通り書いたので、所見でも困らない、、、はず。
コード
#!/usr/bin/python import cx_Oracle import sys import re import os import csv from getpass import getpass from cmd import Cmd class Oracle_cli(Cmd): prompt = "> " intro = "Help: ?\n" def __init__(self): Cmd.__init__(self) self.isHeader = True self.isConnected = False self.isCsv = False self.isAskContDef = True def do_connect(self, arg): if self.isConnected: print 'Already connecting.\n' return False if not re.match(r'[^/]+/[^@]+@',arg): passwd = getpass('Password: ') arg = arg.replace('@','/'+ passwd +'@') try: self.connection = cx_Oracle.connect(arg) self.cursor = self.connection.cursor() except cx_Oracle.DatabaseError, cx_msg: print 'Faild to connect.' print "%s\n" %cx_msg return False self.isConnected = True print 'Conneted to Oracle successfly.\n' def help_connect(self): print "Connect to oracle." print "Usage: connect UserName[/Password]@HostName:Port/ServiceName\n" def do_query(self, arg): if not self.isConnected: print 'No connection. Execute connect command at first.\n' return False try: self.cursor.execute(arg) except cx_Oracle.DatabaseError, cx_msg: print 'Faild to query.' print "%s\n" %cx_msg return False self.dscr = self.cursor.description if self.dscr: if self.isHeader: self.header = [self.dscr[i][0] for i in xrange(len(self.dscr))] print '\t'.join(self.header) print '----------' if self.isCsv: self.writer.writerow(self.header) self.isCont = True self.isFirstLoop = True self.isAskCont = self.isAskContDef while self.isCont: try: self.rows = self.cursor.fetchmany(1) except cx_Oracle.DatabaseError, cx_msg: print 'Faild to fetch rows.' print "%s\n" %cx_msg return False if not self.rows: break if self.isAskCont and not self.isFirstLoop: while True: self.readed = raw_input('Fetch more %s rows ? ([y]es/[n]o/[d]o not ask anymore) ' %self.cursor.arraysize) if self.readed == 'y': break elif self.readed == 'n': self.isCont = False print '\nQuery done successfly. %s row(s) selected.\n' %str(self.cursor.rowcount -1) return False elif self.readed == 'd': self.isAskCont = False break else: print 'Invalid input.' self.isFirstLoop = False try: self.rows += self.cursor.fetchmany(self.cursor.arraysize -1) except cx_Oracle.DatabaseError, cx_msg: print 'Faild to fetch rows.' print "%s\n" %cx_msg return False for self.row in self.rows: print '\t'.join([str(i) for i in self.row]) if self.isCsv: self.writer.writerows(self.rows) print '\nQuery done successfly. %s row(s) selected.\n' %self.cursor.rowcount else: print 'Query done successfly. %s row(s) affected.\n' %self.cursor.rowcount def help_query(self): print 'Execute query.' print 'Usage: query <SQL>\n' def do_export_csv(self, arg): if arg == 'off': self.isCsv = False print 'Do not export csv.' elif re.match(r'^on +[^ ]+ *$',arg): self.csv = re.sub(r'^on +','',arg) if os.path.exists(self.csv): while True: self.readed = raw_input('%s already exist. Over write ? ([y]es/[n]o) ' %self.csv) if self.readed == 'y': try: self.f = open(self.csv,'w') except: print 'Can not create "%s".\n' %self.csv return False self.writer = csv.writer(self.f,lineterminator='\n') self.isCsv = True print 'Export csv file.\n' return False elif self.readed == 'n': return False else: print 'Invalid input.' else: print 'Invalid argument.\n' def help_export_csv(self): print 'Eport csv file.' print 'Usage: export_csv on </path/of/csv>|off\n' def do_ask_fetch(self, arg): if arg == 'on': self.isAskContDef = True print 'Ask whether to continue fetching every time.\n' elif arg == 'off': self.isAskContDef = False print 'Do not ask whether to continue fetching.\n' else: print 'Invalid argument.\n' def help_ask_fetch(self): print 'Ask whether to continue fetching every time.' print 'Usage: ask_fetch on|off\n' def do_header(self, arg): if arg == 'on': self.isHeader = True print 'Header on.\n' elif arg == 'off': self.isHeader = False print 'Header off.\n' else: print 'Invalid argument.\n' def help_header(self): print 'Show column names as a header.' print 'Usage: header on|off\n' def do_fetch_rows(self, arg): if not self.isConnected: print 'No connection. Execute connect command at first.\n' return False try: self.cursor.arraysize = int(arg) except: print 'Invalid argument.\n' return False print '%s row(s) fetch at a time.\n' %self.cursor.arraysize def help_fetch_rows(self): print 'The number of rows to fetch at a time.' print 'Usage: fetch_rows <integer>\n' def do_exit(self, arg): return True def help_exit(self): print "Exit here.\n" def do_quit(self, arg): return True def help_quit(self): print "Exit here.\n" def do_EOF(self, arg): return True def help_EOF(self): print "Exit here.\n" def emptyline(self): pass if __name__ == '__main__': cli=Oracle_cli() args = sys.argv if args: for i in xrange(len(args)): if args[i] == '-c': i += 1 cli.do_connect(args[i]) if args[i] == '-h': print 'Usage: [-c UserName[/Password]@HostName:Port/ServiceName|-h]' exit() cli.cmdloop()
NTPの仕組みを調べてみた
PowershellでNTPサーバを書きたい衝動に駆られたので、NTPプロトコルについて調べてみました。
時刻同期の仕組み
- クライアントがNTPサーバにリクエスト①を送信します。また、クライアントはこのときに送信時刻t1を記録しておきます。
- NTPサーバがクライアントからのリクエスト①を受信します。また、サーバはこのときに受信時刻t2を記録しておきます。
- NTPサーバがクライアントへレスポンス②を返します。レスポンスにはリクエスト受信時刻t2とレスポンスを送信した時刻t3が含まれています。
- クライアントがNTPサーバからのレスポンス②を受信します。また、クライアントはこのときに受信時刻t4を記録しておきます。
- クライアントはリクエスト送信からレスポンス受信までにかかった時間(t4-t1)からサーバ側の処理時間(t3-t2)を引き2で割ることで、通信の片道にかかった時間を算出します。
- クライアントはt1+"5で算出した時間"とt2を比較することで、NTPサーバと自身のクロックのズレを認識することができます。
つまるところ、NTPサーバの処理はt2とt3をクライアントに返すだけでクライアントより簡単そう。 ただし、時刻同期の精度はt2とt3の時刻をどれだけ正確に計測できるかに左右されるので、オーバーヘッドの大きいPowershellで追及するのは困難。
パケットのフォーマット
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +---+-----+-----+---------------+---------------+---------------+ 0|LI | Ver |Mode | Stratum |PollingInterval| Precision | +---+-----+-----+---------------+---------------+---------------+ 1| RootDelay | +---------------------------------------------------------------+ 2| RootDispersion | +---------------------------------------------------------------+ 3| ReferenceID | +---------------------------------------------------------------+ 4| ReferenceTimestamp | 5| | +---------------------------------------------------------------+ 6| OriginTimestamp | 7| | +---------------------------------------------------------------+ 8| ReceiveTimestamp | 9| | +---------------------------------------------------------------+ 0| TransmitTimestamp | 1| | +---------------------------------------------------------------+
Leap Indicator (LI)
2bitの値でうるう秒があるときはこの辺をいじるらしい。 うるう秒の検証をしたいときに使えるかも。
Version number (Ver)
NTPのバージョンを3bitで表します。3なら0b011
、4なら0b100
ですが、正直違いはよく分かっていません。
Mode
3bitの値でクライアントが送信する際には0b011
、サーバが応答する際には0b100
をセットします。
Peer Clock Stratum (Stratum)
Stratumを8bitで指定します。
Peer Polling Interval (PollingInterval)
クライアントがNTPサーバを見に行く間隔を8bitで指定するようです。
Perr Clock Precision (Precision)
ローカルのクロックの精度を8bitで表します。
Root Deley
Strarum 1までの往復の遅延を32bitで表します。
Root Dispersion
Strarum 1までの誤差を32bitで表す。
Reference ID
時刻参照先サーバのIPアドレス。
Reference Timestamp
ローカルのクロックが最後に時刻同期した時間を64bitで表します。フォーマットは後述。
Orgin Timestamp
クライアントがNTPサーバにリクエストを送信した時刻(上記t1)を64bitで表します。フォーマットは後述。
Receive Timestamp
NTPサーバがクライアントからのリクエストを受信した時刻(上記t2)を64bitでセットします。
Transmit Timestamp
パケットを送信した時刻(上記t3)を64bitでセットします。フォーマットは後述。
64bitの時刻フォーマット
NTPプロトコルでは時刻を1900年1月1日0時0分0秒からの経過秒数を64bit符号なし固定小数点で表現します。上位32bitが整数部、下位32bitが小数点以下となります。 UTCです。
参考サイト
Yoshi's Memo 「NTP (タイム)サーバ のメモ」->「NTP、SNTP のフォーマット」
PowershellでSyslogサーバを書く
アプライアンス製品の検証してるときにちょっとSyslogサーバが欲しくなったので、どこにでもあるPowershellで書きました。 わざわざサーバ立てるほどでもない場合に割と役立ちます。
前提
- 今どきのwindowsならほぼほぼ実行できるPowershell V2で実装。
- TCPは対応してません。
コード
param([int]$port=514,[string]$file) $ErrorActionPreference="stop" $enc=New-Object System.Text.ASCIIEncoding $facility=@("kernel", "user", "mail", "daemon", "auth", "syslog"," lpr", "news", "uucp", "cron", "auth", "ftp", "ntp", "log audit", "log alert", "cron", "local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7") $severity=@("emerg", "alert", "crit", "err", "warning", "notice", "info", "debug") if($file.Length -gt 0){ if(!(Test-Path (Split-Path $file))){ Write-Host "No such path. ($file)" exit } } $client=New-Object System.Net.Sockets.UdpClient($port) Write-Host "Start listening UDP/$port" while($true){ $handler=$client.BeginReceive($null,$null) while(!$handler.IsCompleted){ sleep -Milliseconds 100 } $rcvTime=(Get-Date).ToString("yyyy/MM/dd HH:mm:ss") $remoteEnd=New-Object System.Net.IPEndPoint([System.Net.IPAddress]::Any,$null) $rcvByte=$client.EndReceive($handler,[ref]$remoteEnd) $msg=$enc.GetString($rcvByte,0,$rcvByte.Length) if($msg -match "((?<=^<)[0-9]*)>(.*)"){ $sevCode=[int]$Matches[1]%8 $facCode=([int]$Matches[1]-$sevCode)/8 $log=$rcvTime+" "+$facility[$facCode]+" "+$severity[$sevCode]+" :"+$Matches[2] Write-Host $log if($file.Length -gt 0){ $log |Out-File -Encoding default -Append $file } } }
使い方
Server-Syslog.ps1
終了時はウインドウごと閉じてください。ctrl+c
でもいいんですが、リソースの開放($client.close()
)がうまく実装できてないので、ポートが占有されたままになります。ウインドウを閉じると解放されました。#どなたかいい方法あったら教えてください。
Server-Syslog.ps1 -f C:\example\syslog.log
でファイルに出力できます。フルパスで指定してください。
Server-Syslog.ps1 -p 10514
でポート番号も指定できます。デフォルトは514です。
コード解説
ざっくりいうと、UDP/514で待ち受けしてアスキーに変換し出力しているだけです。 ただ、SyslogはFacilityとSeverityの表現に若干癖があったりします。
if($msg -match "((?<=^<)[0-9]*)>(.*)"){ $sevCode=[int]$Matches[1]%8 $facCode=([int]$Matches[1]-$sevCode)/8
受けとったバイナリをアスキー文字列に変換した後、$msg -match "((?<=^<)[0-9]*)>(.*)"
でPRI(後述)とMessageを取り出しています。
Powershellでは-match
でヒットした文字列が自動変数$Matches
に配列として格納されるので、2~3行目でそれらを加工し、FacilityとSeverityの値を取り出しています。
Syslogフォーマット
例によってRFCを読んだわけではないので間違ってるかもですが、、、
アスキーに直すと以下の感じ。
<PRI>Message
PRI
はFacility×8+Severityで算出される1~3桁の数字が入ります。(Facilityがlocal0、SeverityがwarningならPRIは16×8+4=132)Message
はログの内容でPRI
とは<
(0x3c)と>
(0x3e)で区切られます。PRI
とMessage
の間にはHeader
が入るらしいが、手持ちのデバイスからはそんなフィールドは見つからなかったのでスルー。
PowershellでNTPサーバを書く
NTPの通信をテストしたいのにWindows PCしかない残念な現場があったので、PowershellでNTPサーバを書いてみました。
前提
- 今どきのWindowsなら必ず動くであろうPowershell V2.0で実装。
- NTPサーバが待ち受けするUDP/123ポートはWindows Timeというサービスが使用しているので、スクリプトを実行するときはこいつを止める必要があります。
- 通信の待ち受けは無限ループになっているので終了は×でウインドウを閉じて強制終了。
ctrl+c
でもいいんですが、リスニングしていたポートを解放する処理$client.close()
がうまく実装できなかったので、次回以降起動できません。ウインドウを閉じると解放されるみたいです。- NTPはバージョン3or4です。
- 精度は度外視です。NTPの動作が確認できることを目的に作成しています。
コード
param([switch]$v) function Convert-Datetime2Byte{ param([datetime]$datetime) $arrInt_unixtime=@() $arrByte_unixtime=New-Object System.Byte[] 0 $double_unixtime=($datetime-([datetime]"1900/1/1 9:0:0")).TotalSeconds $arrInt_unixtime+=[System.UInt32]([math]::Truncate($double_unixtime)) $arrInt_unixtime+=[system.uint32]($double_unixtime%1*1000000000) $arrInt_unixtime | ForEach-Object { for($i=3;$i -ge 0;$i--){ $arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i))) } } return $arrByte_unixtime } $ErrorActionPreference="stop" if($v){ $VerbosePreference="continue" }else{ $VerbosePreference="silentlycontinue" } $client=New-Object System.Net.Sockets.UdpClient(123) while($true){ $handler=$client.BeginReceive($null,$null) Write-Verbose "Listen UDP/123..." while(!$handler.IsCompleted){ } $rcvTime=Get-Date $remoteEnd=New-Object System.Net.IPEndPoint([system.net.ipaddress]::None,$null) $rcvByte=$client.EndReceive($handler,[ref]$remoteEnd) switch($rcvByte[0] -band 63){ 35{ Write-Verbose "NTP ver.4 mode client received from $remoteEnd." $responseFlags=36 } 27{ Write-Verbose "NTP ver.3 mode client received from $remoteEnd." $responseFlags=28 } default{ Write-Verbose "Illegal packet or unsupported NTP version received from $remoteEnd." $responseFlags=$null } } if($responseFlags -ne $null){ $sndByte=New-Object System.Byte[] 0 $sndByte+=$responseFlags $sndByte+=@(1,0,0,0,0,0,0,0,0,0,0,0,0,0,0) $sndByte+=Convert-Datetime2Byte (Get-Date) $sndByte+=$rcvByte[($rcvByte.Length-8)..$rcvByte.Length] $sndByte+=Convert-Datetime2Byte $rcvTime $sndByte+=Convert-Datetime2Byte (Get-Date) $client.Send($sndByte,$sndByte.Length,$remoteEnd) >$null Write-Verbose "Sent NTP response to $remoteEnd." } }
使い方
Server-NTP.ps1
-v
オプションでうるさくなります。
終了するときはウインドウごと閉じます。
コード解説
おおまかに言うと、.NETのUDPClientを使ってUDP/123をリッスンし、リクエストが来たら返答する簡単なスクリプトです。
リクエスト待ち受け
$client=New-Object System.Net.Sockets.UdpClient(123) while($true){ $handler=$client.BeginReceive($null,$null) Write-Verbose "Listen UDP/123..." while(!$handler.IsCompleted){ } $rcvTime=Get-Date $remoteEnd=New-Object System.Net.IPEndPoint([system.net.ipaddress]::None,$null) $rcvByte=$client.EndReceive($handler,[ref]$remoteEnd)
UDP/123を非同期でリスニングします。
$true
になったら即座に$rcvTime=Get-Date
でその時刻を記録します。
本当はwhile()
で判定するのではなく、$client.BeginReceive($null,$null)
でコールバックを指定したかったんですが、使い方がいまいちわからなかった。コールバックのほうが精度も高くなる気がする。
パケットを受け取ったらバイト型配列$rcvByte
に格納します。
リクエストの識別
switch($rcvByte[0] -band 63){ 35{ Write-Verbose "NTP ver.4 mode client received from $remoteEnd." $responseFlags=36 } 27{ Write-Verbose "NTP ver.3 mode client received from $remoteEnd." $responseFlags=28 } default{ Write-Verbose "Illegal packet or unsupported NTP version received from $remoteEnd." $responseFlags=$null } }
受信したパケットの先頭1byteにバージョン情報が含まれているので識別します。
上位2bitは不要なので$rcvByte[0] -band 63
でマスクをかけて除外しています。(63
は0b00111111
)
レスポンス送信
if($responseFlags -ne $null){ $sndByte=New-Object System.Byte[] 0 $sndByte+=$responseFlags $sndByte+=@(1,0,0,0,0,0,0,0,0,0,0,0,0,0,0) $sndByte+=Convert-Datetime2Byte (Get-Date) $sndByte+=$rcvByte[($rcvByte.Length-8)..$rcvByte.Length] $sndByte+=Convert-Datetime2Byte $rcvTime $sndByte+=Convert-Datetime2Byte (Get-Date) $client.Send($sndByte,$sndByte.Length,$remoteEnd) >$null Write-Verbose "Sent NTP response to $remoteEnd." }
レスポンスを作成し、送信します。時刻はConvert-Datetime2Byte
でNTPの64bitのフォーマットに変換しています。
Convert-Datetime2Byte
function Convert-Datetime2Byte{ param([datetime]$datetime) $arrInt_unixtime=@() $arrByte_unixtime=New-Object System.Byte[] 0 $double_unixtime=($datetime-([datetime]"1900/1/1 9:0:0")).TotalSeconds $arrInt_unixtime+=[System.UInt32]([math]::Truncate($double_unixtime)) $arrInt_unixtime+=[system.uint32]($double_unixtime%1*1000000000) $arrInt_unixtime | ForEach-Object { for($i=3;$i -ge 0;$i--){ $arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i))) } } return $arrByte_unixtime }
DateTime
型の日時をNTPのフォーマットに変換し、バイト型配列で返します。
NTPではUTCをやり取りするので日本標準時のオフセットを加えて、[datetime]"1900/1/1 9:0:0"
としています。
9~10行目で浮動小数点型の秒数を整数部32bit、小数点以下32bitに分割し、配列に代入しています。
$arrByte_unixtime+=(($_-band (255*([math]::Pow(256,$i))))/([math]::Pow(256,$i)))
は、32bitの数字を8bitごとに分割するために、0b11111111=255
のマスクを上から順々にかけて値を取り出しています。
イケてないところ
どなたか教えてください、、、
- スクリプト終了時に
$client.close()
を実行できていないので、終了後にポートが解放されない。 - パケットの待ち受けが
while()
のループ。$client.BeginReceive($null,$null)
でコールバックを使いたい。
ftpコマンドで記号を含むパスワードを使う話
ftpのログインがはじかれる
同僚が何やらはまっている様子だったので聞いてみると、こんな感じのシェルスクリプトでパスワードにバックスラッシュが含まれているとログインに失敗するんだとか。
#そもそもパスワードにバックスラッシュってのはどうなんだ、、、
#!/bin/bash $FTP_SERVER='192.168.0.1' #FTPサーバのIP $USER='ftpuser' #ユーザー名 $PASS='aaa\bbb' #パスワード $FILE='/foo/bar.tgz' #ダウンロードするファイル ftp -n $FTP_SERVER << EOF user $USER $PASS put $FILE bye EOF
でも、ftpコマンドでuser
を使わないで対話的にバックスラッシュを含むパスワードを指定するとうまくいく。
こんな感じ。
[user@localhost tmp]$ ftp 127.0.0.1 Connected to 127.0.0.1 (127.0.0.1). 220 (vsFTPd 3.0.2) Name (localhost:root): ftpuser 331 Please specify the password. Password: ←aaa\bbbを入力 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp>
困ったときのtcpdump
というわけでtcpdumpしてみると、、、
[user@localhost tmp]$sudo tcpdump -i lo -s 0 -nn -n -X port 21 ~~~中略~~~ 03:06:17.816675 IP 127.0.0.1.34702 > 127.0.0.1.21: Flags [P.], seq 49:62, ack 149, win 342, options [nop,nop,TS val 1087324921 ecr 1087324921], length 13: FTP: PASS aaabbb 0x0000: 4510 0041 4fb6 4000 4006 ecee 7f00 0001 E..AO.@.@....... 0x0010: 7f00 0001 878e 0015 68b8 e838 42c7 b48c ........h..8B... 0x0020: 8018 0156 fe35 0000 0101 080a 40cf 42f9 ...V.5......@.B. 0x0030: 40cf 42f9 5041 5353 2061 6161 6262 620d @.B.PASS.aaabbb. 0x0040: 0a
バックスラッシュがいない!!
ちなみに、対話的に実行した場合は下記。バックスラッシュもいる。
[user@localhost tmp]$sudo tcpdump -i lo -s 0 -nn -n -X port 21 ~~~中略~~~ 03:06:00.160534 IP 127.0.0.1.34702 > 127.0.0.1.21: Flags [P.], seq 15:29, ack 55, win 342, options [nop,nop,TS val 1087307265 ecr 1087301873], length 14: FTP: PASS aaa\bbb 0x0000: 4510 0042 4fb0 4000 4006 ecf3 7f00 0001 E..BO.@.@....... 0x0010: 7f00 0001 878e 0015 68b8 e816 42c7 b42e ........h...B... 0x0020: 8018 0156 fe36 0000 0101 080a 40ce fe01 ...V.6......@... 0x0030: 40ce e8f1 5041 5353 2061 6161 5c62 6262 @...PASS.aaa\bbb 0x0040: 0d0a
ソースを読んでみる
ソースを入手
ソースをyumって、、、
[user@localhost tmp]$ yumdownloader --source ftp Loaded plugins: fastestmirror, kabi, langpacks, versionlock Loading support for Red Hat kernel ABI Enabling updates-source repository Enabling base-source repository Enabling extras-source repository Loading mirror speeds from cached hostfile * base: ftp.iij.ad.jp * extras: ftp.iij.ad.jp * updates: ftp.iij.ad.jp ftp-0.17-67.el7.src.rpm | 96 kB 00:00:01 [user@localhost tmp]$ ll total 96 -rw-rw-r-- 1 user user 98263 Dec 16 2016 ftp-0.17-67.el7.src.rpm [user@localhost tmp]$
展開。
[user@localhost tmp]$ rpm2cpio ftp-0.17-67.el7.src.rpm |cpio -id 279 blocks [user@localhost tmp]$ tar zxf netkit-ftp-0.17.tar.gz [user@localhost tmp]$ cd netkit-ftp-0.17/ftp/ [user@localhost tmp]$ ll total 200 -rw-r--r-- 1 user user 672 Aug 1 1999 Makefile -rw-r--r-- 1 user user 46570 Jul 23 2000 cmds.c -rw-r--r-- 1 user user 1836 Aug 15 1996 cmds.h -rw-r--r-- 1 user user 10376 Sep 29 1999 cmdtab.c -rw-r--r-- 1 user user 4209 Aug 15 1996 domacro.c -rw-r--r-- 1 user user 27271 Jul 31 2000 ftp.1 -rw-r--r-- 1 user user 35673 Dec 14 1999 ftp.c -rw-r--r-- 1 user user 6770 Oct 3 1999 ftp_var.h -rw-r--r-- 1 user user 12836 Oct 2 1999 glob.c -rw-r--r-- 1 user user 1971 Aug 15 1996 glob.h -rw-r--r-- 1 user user 11812 Oct 2 1999 main.c -rw-r--r-- 1 user user 4657 Jul 31 2000 netrc.5 -rw-r--r-- 1 user user 2008 Jul 14 1996 pathnames.h -rw-r--r-- 1 user user 7496 Oct 3 1999 ruserpass.c [user@localhost tmp]$
読む
ftpコマンドのプロンプトへの入力はGNU readlineで読み込まれるようになっている。 GNU readlineはbashみたいにショートカットキーやhistoryを簡単に使えるようになるとのこと。 つまり、bashのようにバックスラッシュはエスケープが必要だった模様。
#main.c 279行目~ static char *get_input_line(char *buf, int buflen) { #ifdef __USE_READLINE__ if (fromatty && !rl_inhibit) { char *lineread = readline("ftp> "); #ここ!! if (!lineread) return NULL; strncpy(buf, lineread, buflen); buf[buflen-1] = 0; if (lineread[0]) add_history(lineread); free(lineread); return buf; } #endif if (fromatty) { printf("ftp> "); fflush(stdout); } return fgets(buf, buflen, stdin); }
ちなみに、user
を使用せず対話的にパスワードを入力すると、下記のようにgetpass(3)が呼ばれる。
こいつは自分でttyを直接読みに行ってくれるようで、エスケープは不要だったというわけ。
#cmds.c 1523行目~ void user(int argc, char *argv[]) { char theacct[80]; int n, aflag = 0; if (argc < 2) (void) another(&argc, &argv, "username"); if (argc < 2 || argc > 4) { printf("usage: %s username [password] [account]\n", argv[0]); code = -1; return; } n = command("USER %s", argv[1]); if (n == CONTINUE) { if (argc < 3 ) argv[2] = getpass("Password: "), argc++; #ここ!! n = command("PASS %s", argv[2]); } if (n == CONTINUE) { if (argc < 4) { printf("Account: "); (void) fflush(stdout); fgets(theacct, sizeof(theacct), stdin); argv[3] = theacct; argc++; } n = command("ACCT %s", argv[3]); aflag++; } if (n != COMPLETE) { fprintf(stdout, "Login failed.\n"); return; } if (!aflag && argc == 4) { (void) command("ACCT %s", argv[3]); } }
エスケープしてあげると、、、
できた。
[user@localhost tmp]$ ftp -n 127.0.0.1 Connected to 127.0.0.1 (127.0.0.1). 220 (vsFTPd 3.0.2) ftp> user ftpuser aaa\\bbb 331 Please specify the password. 230 Login successful. Remote system type is UNIX. Using binary mode to transfer files. ftp>
[user@localhost tmp]$sudo tcpdump -i lo -s 0 -nn -n -X port 21 ~~~中略~~~ 03:48:15.782473 IP 127.0.0.1.34874 > 127.0.0.1.21: Flags [P.], seq 21:35, ack 93, win 342, options [nop,nop,TS val 1089842887 ecr 1089842887], length 14: FTP: PASS aaa\bbb 0x0000: 4510 0042 9281 4000 4006 aa22 7f00 0001 E..B..@.@..".... 0x0010: 7f00 0001 883a 0015 c770 e9ed 5b4e 3615 .....:...p..[N6. 0x0020: 8018 0156 fe36 0000 0101 080a 40f5 aec7 ...V.6......@... 0x0030: 40f5 aec7 5041 5353 2061 6161 5c62 6262 @...PASS.aaa\bbb 0x0040: 0d0a
まあエスケープくらいわざわざソース読まなくても思いつけって話ですよね、、、