車輪の再発明

いったい何番煎じだよ!

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版 ―脆弱性攻撃の理論と実際

Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際

まだちょっとしか読めていないんですが、すごく面白いです。 攻撃手法が解説されているだけではなくて、プログラミングとは何かからはじまり、コンピュータの理解が深まる良書だと思います。

というわけで(?)、もはや何番煎じかわからないですが、今日は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()関数の呼び出し元の処理を再開するときに使用します。

バックアップできたら、movrspの値を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>

learip(インストラクションポインタ)に0x9f足したアドレスをrdi(ディスティネーションインデックス)へ書き込んでいます。 詳しくは次回に回しますが、rip+0x9fは"Hello World!"の先頭アドレスで、rdiを使用することで引数としてprintf()へ渡しているようです。

続いてprintf()の実行。これは関数の先頭アドレスをcallするだけなので単純ですね。

戻り値を返す

ソースで言うとreturn 0の部分です。

 64a:    b8 00 00 00 00          mov    eax,0x0

eax(アキュムレータ)は戻り値の格納に使用するレジスタで、このレジスタ0movで書き込んでいます。

スタックをもとに戻す

一通りの処理が終わったので、最初に切り替えたスタックをもとに戻します。

 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、つまり何もしないという命令らしいですが、何のために入っているかよくわからず、、、パディングかな?

次回はgdb(デバッガ)を使用して、レジスタやメモリの値の変化を追っていきます。

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プロトコルについて調べてみました。

ebi216.hateblo.jp

時刻同期の仕組み

f:id:ebi216:20180318013256j:plain

  1. クライアントがNTPサーバにリクエスト①を送信します。また、クライアントはこのときに送信時刻t1を記録しておきます。
  2. NTPサーバがクライアントからのリクエスト①を受信します。また、サーバはこのときに受信時刻t2を記録しておきます。
  3. NTPサーバがクライアントへレスポンス②を返します。レスポンスにはリクエスト受信時刻t2とレスポンスを送信した時刻t3が含まれています。
  4. クライアントがNTPサーバからのレスポンス②を受信します。また、クライアントはこのときに受信時刻t4を記録しておきます。
  5. クライアントはリクエスト送信からレスポンス受信までにかかった時間(t4-t1)からサーバ側の処理時間(t3-t2)を引き2で割ることで、通信の片道にかかった時間を算出します。
  6. クライアントは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)で区切られます。
  • PRIMessageの間には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でマスクをかけて除外しています。(630b00111111

レスポンス送信

    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

まあエスケープくらいわざわざソース読まなくても思いつけって話ですよね、、、