拾い物のコンパス

まともに書いたメモ

CiscoルータからSCPでIOSを抽出する方法メモ

CiscoのルータからIOSを抜き出すときは,FTP経由での方法をよく聞く.
そこでFTPを試してみたが,上手く行かない.
デバッグする気が起きなかったから,SCPで抜き出してみることにした.
コピー先のWindows上でサーバを立てるのは手間だから,ルータ上でSSHサーバを立てて,SCP接続する.
コマンドメモがうろ覚えだから,上手く行かなかったら訂正してくれると嬉しい.

なにがしたかったか

Cisco機器からIOSのバイナリファイルを抜き出すと,それを使ってGNS3でネットワーク構成を仮想でいじることができる.
というわけでバイナリが欲しかった.

準備

最初はいつも通りにシリアルコンソールで接続する.
加えて,ルータのLANポートのどれかとPCをLANケーブルで接続しておく.
挿したらPCからTelnetでログインしておく.
OSがWindowsならputtyから,putty.zipをダウンロード・適当なところに解凍する. 今回はデスクトップに解凍した前提で進めていく.
dirコマンドで一応自分が抜き出そうとしているIOSのバージョンを確認しておいたほうが良い.
GNS3でサポートしていないバージョンの可能性がある.

ルータ側での設定

CiscoルータからWindowsがインストールされた端末にコピーする.
ただ打ち込んでいくだけ.
大きな流れは以下の通り.

  1. IPを割り振る
  2. ユーザを作成
  3. SSH鍵の作成(今回は1024bit)・有効化
  4. SCPを許可
$ enable
$ configure terminal

$ interface vlan 1
$ ip address 192.168.1.100 255.255.255.0

$ username cisco privilege 15 password cisco
$ line vty 0 4
$ login local
$ hostname R1 # ここは適当で良い
$ ip domain-name r1.com # ここは適当で良い
$ crypto key generate rsa modulus 1024
$ ip ssh version 2
$ ip ssh time-out 120
$ ip ssh authentication-retries 3
$ ip scp server enable

$ aaa new-model
$ aaa session-id unique
$ aaa authentication login default local # 認証にローカルDBを利用
$ aaa authorization exec default local none # ログイン認可方式として,全ての回線にローカルDBを使用して認可する.
$ enable secret cisco

ルータ側の設定はこれで終了.
ポイントはaaa 認証部分.
aaaAuthentication/Authorization/Accountingに関する制御コマンド.
認証方式を全てローカルのDBに設定しないと,認証が上手く行かない.

IOSを抜き出す

ルータ側で

$ dir flash

IOSのファイル名を確認する.
Windowsでプロンプトを開く.

$ cd C:\Users\Desktop\putty # puttyを解凍したフォルダに移動
$ pscp.exe -scp cisco@192.168.1.100:???.bin C:\Users\poppycompass\Desktop\IOS

これでダウンロードできる.
Linuxならscpを打つだけで良い.

失敗談

失敗した事例をメモしておく.

  • SCP: [22 -> 192.168.1.100:?????] send Privilege denied.が出る.

    • 多分,ユーザの権限が十分じゃない.username cisco privilege 15 password ciscoなどで権限を最高にしていないか(show privilegeで確認),aaaを設定していない
  • WinSCPでSCPできない

    • ログインターミナルの関係でエラーが出て上手く行かない.Linuxのターミナル使うか,puttyCUI(pscp.exe)を使おう.
  • スイッチで同じ設定してみたが,上手く行かない

    • 原因不明.知りたい.

おまけ

抜き出してから,IOSのバージョンがGNS3のサポート対象外であることに気づいた.
呆然としつつ,fileコマンドすると ELF 32-bit MSB executable, Sunplus S+core7 RISC, version 1 (SYSV), statically linked, strippedと出て驚いた.
どうやらIOSの正体はELF形式のプログラムのようだ.Wikiにも碌に情報が載っていない.
これはまたマニアックなアーキテクチャを使っているな・・・.

OSがELFってことは,BIOSかそれに準ずるプログラムがELFを実行できるということだろうか.
OSを実行するためのOSでも存在するのかな.
謎だけ残ってしまった.

所感

SCPだとファイアウォールの無効化とかがいらないから,躓きが少なくて良い.

参考

Memo.html[Cisco]IOSイメージの抽出(追記2017/4/3)

Cisco IOS セキュリティ コマンド リファレンス:コマンド D ~ L、Cisco IOS XE Release 3SE(Catalyst 3850 スイッチ) - ip scp server enable [Cisco Catalyst 3850 シリーズ スイッチ] - Cisco

Solved: SCP error - Cisco Community

IOSでSCPを利用する : TEMPEST社長ブログ

AAAとは

pycを読む(スクリプト味)

前回はpycを低レベルな命令に変換して読む方法を説明した.
あれはあれで読めはするが,アセンブリ言語と同様の読みづらさはある.
今回は直接pycpythonスクリプトまで変換する方法を紹介する.
変換するライブラリは複数あるが,ここでは uncompyleを使った.
結論として,精度はいい感じ.pycにソース保護を期待するのはやめましょう.

uncompyle

python byte-code decompiler.
uncompyle2uncompyle6がある.
前者は2系サポート,後者は2・3系両方で使える.

環境

各種バージョン

以下のとおり.

$ uname -a
 Linux poppycompass 4.18.6-arch1-1-ARCH #1 SMP PREEMPT Wed Sep 5 11:54:09 UTC 2018 x86_64 GNU/Linux

$ python2
Python 2.7.15 (default, Jun 27 2018, 13:05:28)
[GCC 8.1.1 20180531] on linux2

Arch Linuxの64ビット版.
つまりただのLinuxpythonは2系.

環境作成

$ virtualenv2 ENV
$ . ENV/bin/activate
$ pip install uncompyle

サンプル

単純なやつ

適当に組んだ.

# sample.py
import string

def add(a, b):
    return a+b

a = "abc"
b = "def"
c = "ghi"

d = a + b + c

sum = 0
for i in range(100):
    sum += 0

add(1,2)

pycを生成して,戻してみる.

$ python2 -m compileall sample.py # pyc生成
Compiling sample.py ...

$ ENV/bin/uncompyle6 ./sample.pyc # uncompyleで変換
# uncompyle6 version 3.2.3
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.15 (default, Jun 27 2018, 13:05:28)
# [GCC 8.1.1 20180531]
# Embedded file name: sample.py
# Compiled at: 2018-09-26 06:49:02
import string

def add(a, b):
    return a + b


a = 'abc'
b = 'def'
c = 'ghi'
d = a + b + c
sum = 0
for i in range(100):
    sum += 0

add(1, 2)
# okay decompiling ./sample.pyc

見た感じ,完璧に戻されている.

まともなやつ

簡単なやつだけじゃつまらないから,Oxyry Python Obfuscatorから取ってきたNQueensで試してみる.
オリジナルコードは以下.

__all__ = []

class NQueens:
    """Generate all valid solutions for the n queens puzzle"""
    
    def __init__(self, size):
        # Store the puzzle (problem) size and the number of valid solutions
        self.__size = size
        self.__solutions = 0
        self.__solve()

    def __solve(self):
        """Solve the n queens puzzle and print the number of solutions"""
        positions = [-1] * self.__size
        self.__put_queen(positions, 0)
        print("Found", self.__solutions, "solutions.")

    def __put_queen(self, positions, target_row):
        """
        Try to place a queen on target_row by checking all N possible cases.
        If a valid place is found the function calls itself trying to place a queen
        on the next row until all N queens are placed on the NxN board.
        """
        # Base (stop) case - all N rows are occupied
        if target_row == self.__size:
            self.__show_full_board(positions)
            self.__solutions += 1
        else:
            # For all N columns positions try to place a queen
            for column in range(self.__size):
                # Reject all invalid positions
                if self.__check_place(positions, target_row, column):
                    positions[target_row] = column
                    self.__put_queen(positions, target_row + 1)


    def __check_place(self, positions, ocuppied_rows, column):
        """
        Check if a given position is under attack from any of
        the previously placed queens (check column and diagonal positions)
        """
        for i in range(ocuppied_rows):
            if positions[i] == column or \
                positions[i] - i == column - ocuppied_rows or \
                positions[i] + i == column + ocuppied_rows:

                return False
        return True

    def __show_full_board(self, positions):
        """Show the full NxN board"""
        for row in range(self.__size):
            line = ""
            for column in range(self.__size):
                if positions[row] == column:
                    line += "Q "
                else:
                    line += ". "
            print(line)
        print("\n")

    def __show_short_board(self, positions):
        """
        Show the queens positions on the board in compressed form,
        each number represent the occupied column position in the corresponding row.
        """
        line = ""
        for i in range(self.__size):
            line += str(positions[i]) + " "
        print(line)

def main():
    """Initialize and solve the n queens puzzle"""
    NQueens(8)

if __name__ == "__main__":
    # execute only if run as a script
    main()

戻した結果

__all__ = []

class NQueens:
    """Generate all valid solutions for the n queens puzzle"""

    def __init__(self, size):
        self.__size = size
        self.__solutions = 0
        self.__solve()

    def __solve(self):
        """Solve the n queens puzzle and print the number of solutions"""
        positions = [
         -1] * self.__size
        self.__put_queen(positions, 0)
        print ('Found', self.__solutions, 'solutions.')

    def __put_queen(self, positions, target_row):
        """
        Try to place a queen on target_row by checking all N possible cases.
        If a valid place is found the function calls itself trying to place a queen
        on the next row until all N queens are placed on the NxN board.
        """
        if target_row == self.__size:
            self.__show_full_board(positions)
            self.__solutions += 1
        else:
            for column in range(self.__size):
                if self.__check_place(positions, target_row, column):
                    positions[target_row] = column
                    self.__put_queen(positions, target_row + 1)

    def __check_place(self, positions, ocuppied_rows, column):
        """
        Check if a given position is under attack from any of
        the previously placed queens (check column and diagonal positions)
        """
        for i in range(ocuppied_rows):
            if positions[i] == column or positions[i] - i == column - ocuppied_rows or positions[i] + i == column + ocuppied_rows:
                return False

        return True

    def __show_full_board(self, positions):
        """Show the full NxN board"""
        for row in range(self.__size):
            line = ''
            for column in range(self.__size):
                if positions[row] == column:
                    line += 'Q '
                else:
                    line += '. '

            print line

        print '\n'

    def __show_short_board(self, positions):
        """
        Show the queens positions on the board in compressed form,
        each number represent the occupied column position in the corresponding row.
        """
        line = ''
        for i in range(self.__size):
            line += str(positions[i]) + ' '

        print line


def main():
    """Initialize and solve the n queens puzzle"""
        for i in range(self.__size):
            line += str(positions[i]) + ' '

        print line


def main():
    NQueens(8)


if __name__ == '__main__':
    main()

やはり,記述はほぼ完璧に戻されている.
面白いのは,"""で付けたコメントアウトは復元でき,#で付けたものは復元できなかったこと.
可読性のためにつける\も消える.そりゃそんな情報残すわけ無いか.

難読化すると・・・?

さっきのやつをOxyry Python Obfuscatorで難読化した結果も試してみる.

""#line:4
__all__ =[]#line:6
class O0000OO0000OOO0OO :#line:8
    ""#line:9
    def __init__ (O00OO0O0O0O00OO00 ,O00OO0OO00OOO00OO ):#line:11
        O00OO0O0O0O00OO00 .__OO00OO0OO00OOO00O =O00OO0OO00OOO00OO #line:13
        O00OO0O0O0O00OO00 .__O00OOO00O0O000000 =0 #line:14
        O00OO0O0O0O00OO00 .__O0OO00000O0O0O0O0 ()#line:15
    def __O0OO00000O0O0O0O0 (OOOOO0O00OO0O0O0O ):#line:17
        ""#line:18
        O0OO0000OO0OO0000 =[-1 ]*OOOOO0O00OO0O0O0O .__OO00OO0OO00OOO00O #line:19
        OOOOO0O00OO0O0O0O .__OO0OOOO0000O0O00O (O0OO0000OO0OO0000 ,0 )#line:20
        print ("Found",OOOOO0O00OO0O0O0O .__O00OOO00O0O000000 ,"solutions.")#line:21
    def __OO0OOOO0000O0O00O (O0OO00O0000O000O0 ,O0OOOOO0OOOOO000O ,OO0OOOO00OO00OOO0 ):#line:23
        ""#line:28
        if OO0OOOO00OO00OOO0 ==O0OO00O0000O000O0 .__OO00OO0OO00OOO00O :#line:30
            O0OO00O0000O000O0 .__OOO0OO0O0000O00OO (O0OOOOO0OOOOO000O )#line:31
            O0OO00O0000O000O0 .__O00OOO00O0O000000 +=1 #line:32
        else :#line:33
            for OO000O0OOOOO0O0OO in range (O0OO00O0000O000O0 .__OO00OO0OO00OOO00O ):#line:35
                if O0OO00O0000O000O0 .__O00O0O00000O0O00O (O0OOOOO0OOOOO000O ,OO0OOOO00OO00OOO0 ,OO000O0OOOOO0O0OO ):#line:37
                    O0OOOOO0OOOOO000O [OO0OOOO00OO00OOO0 ]=OO000O0OOOOO0O0OO #line:38
                    O0OO00O0000O000O0 .__OO0OOOO0000O0O00O (O0OOOOO0OOOOO000O ,OO0OOOO00OO00OOO0 +1 )#line:39
    def __O00O0O00000O0O00O (OO0OOO0OOO0O0OO0O ,O00000O0O00OOOOOO ,O00O0O000000OOOOO ,OOOO00OOO000OO0OO ):#line:42
        ""#line:46
        for OOO000O0OO0O00000 in range (O00O0O000000OOOOO ):#line:47
            if O00000O0O00OOOOOO [OOO000O0OO0O00000 ]==OOOO00OOO000OO0OO or O00000O0O00OOOOOO [OOO000O0OO0O00000 ]-OOO000O0OO0O00000 ==OOOO00OOO000OO0OO -O00O0O000000OOOOO or O00000O0O00OOOOOO [OOO000O0OO0O00000 ]+OOO000O0OO0O00000 ==OOOO00OOO000OO0OO +O00O0O000000OOOOO :#line:50
                return False #line:52
        return True #line:53
    def __OOO0OO0O0000O00OO (OOOO0OOO0OOO0OO00 ,OO0OO0O00OOO00OO0 ):#line:55
        ""#line:56
        for O0OOO0O0O0OO00O0O in range (OOOO0OOO0OOO0OO00 .__OO00OO0OO00OOO00O ):#line:57
            OOO00OO0OOOOOOOOO =""#line:58
            for OO00O0O0OOO0O0000 in range (OOOO0OOO0OOO0OO00 .__OO00OO0OO00OOO00O ):#line:59
                if OO0OO0O00OOO00OO0 [O0OOO0O0O0OO00O0O ]==OO00O0O0OOO0O0000 :#line:60
                    OOO00OO0OOOOOOOOO +="Q "#line:61
                else :#line:62
                    OOO00OO0OOOOOOOOO +=". "#line:63
            print (OOO00OO0OOOOOOOOO )#line:64
        print ("\n")#line:65
    def __OO0O00O0O0OOO0OOO (OO0OO000O00O0OO00 ,O00O00O00OO0O0OOO ):#line:67
        ""#line:71
        OOO000OOO00OO0OOO =""#line:72
        for O00OOO000OO0O00OO in range (OO0OO000O00O0OO00 .__OO00OO0OO00OOO00O ):#line:73
            OOO000OOO00OO0OOO +=str (O00O00O00OO0O0OOO [O00OOO000OO0O00OO ])+" "#line:74
        print (OOO000OOO00OO0OOO )#line:75
def OOO0O0OO0OOO000OO ():#line:77
    ""#line:78
    O0000OO0000OOO0OO (8 )#line:79
if __name__ =="__main__":#line:81
    OOO0O0OO0OOO000OO ()#line:83

戻す

__all__ = []

class O0000OO0000OOO0OO:
    """"""

    def __init__(O00OO0O0O0O00OO00, O00OO0OO00OOO00OO):
        O00OO0O0O0O00OO00.__OO00OO0OO00OOO00O = O00OO0OO00OOO00OO
        O00OO0O0O0O00OO00.__O00OOO00O0O000000 = 0
        O00OO0O0O0O00OO00.__O0OO00000O0O0O0O0()

    def __O0OO00000O0O0O0O0(OOOOO0O00OO0O0O0O):
        """"""
        O0OO0000OO0OO0000 = [
         -1] * OOOOO0O00OO0O0O0O.__OO00OO0OO00OOO00O
        OOOOO0O00OO0O0O0O.__OO0OOOO0000O0O00O(O0OO0000OO0OO0000, 0)
        print ('Found', OOOOO0O00OO0O0O0O.__O00OOO00O0O000000, 'solutions.')

    def __OO0OOOO0000O0O00O(O0OO00O0000O000O0, O0OOOOO0OOOOO000O, OO0OOOO00OO00OOO0):
        """"""
        if OO0OOOO00OO00OOO0 == O0OO00O0000O000O0.__OO00OO0OO00OOO00O:
            O0OO00O0000O000O0.__OOO0OO0O0000O00OO(O0OOOOO0OOOOO000O)
            O0OO00O0000O000O0.__O00OOO00O0O000000 += 1
        else:
            for OO000O0OOOOO0O0OO in range(O0OO00O0000O000O0.__OO00OO0OO00OOO00O):
                if O0OO00O0000O000O0.__O00O0O00000O0O00O(O0OOOOO0OOOOO000O, OO0OOOO00OO00OOO0, OO000O0OOOOO0O0OO):
                    O0OOOOO0OOOOO000O[OO0OOOO00OO00OOO0] = OO000O0OOOOO0O0OO
                    O0OO00O0000O000O0.__OO0OOOO0000O0O00O(O0OOOOO0OOOOO000O, OO0OOOO00OO00OOO0 + 1)

    def __O00O0O00000O0O00O(OO0OOO0OOO0O0OO0O, O00000O0O00OOOOOO, O00O0O000000OOOOO, OOOO00OOO000OO0OO):
        """"""
        for OOO000O0OO0O00000 in range(O00O0O000000OOOOO):
            if O00000O0O00OOOOOO[OOO000O0OO0O00000] == OOOO00OOO000OO0OO or O00000O0O00OOOOOO[OOO000O0OO0O00000] - OOO000O0OO0O00000 ==
OOOO00OOO000OO0OO - O00O0O000000OOOOO or O00000O0O00OOOOOO[OOO000O0OO0O00000] + OOO000O0OO0O00000 == OOOO00OOO000OO0OO + O00O0O000000OOO
OO:
                return False

        return True

    def __OOO0OO0O0000O00OO(OOOO0OOO0OOO0OO00, OO0OO0O00OOO00OO0):
        """"""
        for O0OOO0O0O0OO00O0O in range(OOOO0OOO0OOO0OO00.__OO00OO0OO00OOO00O):
            OOO00OO0OOOOOOOOO = ''
            for OO00O0O0OOO0O0000 in range(OOOO0OOO0OOO0OO00.__OO00OO0OO00OOO00O):
                if OO0OO0O00OOO00OO0[O0OOO0O0O0OO00O0O] == OO00O0O0OOO0O0000:
                    OOO00OO0OOOOOOOOO += 'Q '
                else:
                    OOO00OO0OOOOOOOOO += '. '

            print OOO00OO0OOOOOOOOO

        print '\n'

    def __OO0O00O0O0OOO0OOO(OO0OO000O00O0OO00, O00O00O00OO0O0OOO):
        """"""
        OOO000OOO00OO0OOO = ''
        for O00OOO000OO0O00OO in range(OO0OO000O00O0OO00.__OO00OO0OO00OOO00O):
            OOO000OOO00OO0OOO += str(O00O00O00OO0O0OOO[O00OOO000OO0O00OO]) + ' '

        print OOO000OOO00OO0OOO


def OOO0O0OO0OOO000OO():
    """"""


def OOO0O0OO0OOO000OO():
    """"""
    O0000OO0000OOO0OO(8)


if __name__ == '__main__':
    OOO0O0OO0OOO000OO()

まあ,やっぱりしっかりと戻された.

おわりに

pythonのソース保護としてpycで配布するのが良いという意見があるが,バイトコードにするだけではほぼ無意味なようだ.
本気で保護したいのなら,難読化,言語を変える,クラウドとして提供して一切ソースが手元に渡らないようにするくらいだろうか.

今回紹介したuncompyle以外にも,unpyclibというものもあるようだ.
試してみたらエラーが出て上手く行かなかったから,こちらはまた今度.

リバースエンジニアリング対策にpycをstripする方法はあるのだろうか?

参考

python - Is it possible to decompile a compiled .pyc file into a .py file? - Stack Overflow

GitHub - wibiti/uncompyle2: Python 2.7 decompiler

uncompyle6 · PyPI

Oxyry Python Obfuscator - The most reliable python obfuscator in the world

どうやったらPythonアプリのソースコードを保護できますか? - Quora

Oxyry Python Obfuscator - The most reliable python obfuscator in the world

pycを読む(アセンブリ味)

pycファイルだけが手元にあり,なにが書いてあるか読みたいときがある.
pycのバイトコードは逆アセンブルして読める.
やり方と例文をまとめる.
ただし,これは低レベルな命令になるから可読性は良くない.
直接pythonコードにまで戻す方法があるから,そっちを使ったほうが良い.
厳密な仕様までは調べてないから,間違いはあるかも.指摘歓迎.

環境

各種バージョン

以下のとおり.

$ uname -a
 Linux poppycompass 4.18.6-arch1-1-ARCH #1 SMP PREEMPT Wed Sep 5 11:54:09 UTC 2018 x86_64 GNU/Linux

$ python2
Python 2.7.15 (default, Jun 27 2018, 13:05:28)
[GCC 8.1.1 20180531] on linux2

Arch Linuxの64ビット版.
つまりただのLinuxpythonは2系.

環境作成

環境はローカルに作成した.

$ virtualenv2 ENV
$ . ENV/bin/activate
$ pip install dis

pycを逆アセンブルするスクリプト

以下のスクリプトで変換できる.

# pyc_disasm.py
import dis, marshal
code = open("sample.pyc", "rb").read()[8:]
code = marshal.loads(code)
dis.dis(code)

ここでは,sample.pycというファイルを逆アセンブルする.
sample.pyに変換したいコードを書けばよい. 使い方は簡単.

$ python2 -m compileall sample.py # pyc生成
$ python2 ./pyc_disasm.py

変換一覧

アセンブルしたものはアセンブリ味になっている.
pythonによる記述と逆アセンブルしたものの比較をいくつか挙げておく. 各命令の左にある数値はアドレスのようだ.

ライブラリのインポート

普通にインポート

before

import random

after

    0 LOAD_CONST               0 (-1)
    3 LOAD_CONST               1 (None)
    6 IMPORT_NAME              0 (random)
    9 STORE_NAME               0 (random)

ちょっと書き方を変えたインポート

before

from random import *

after

    0 LOAD_CONST               0 (-1)
    3 LOAD_CONST               1 (('*',))
    6 IMPORT_NAME              0 (random)
    9 IMPORT_STAR

変数定義

before

a = ''

after

        0 LOAD_CONST               0 ('')
        3 STORE_NAME               0 (a)

関数呼び出し

before

def hoge(a,b): 
    return a+b

hoge(1,2)

after
最初の部分が関数定義,後半が呼び出したときの部分.
関数呼び出しは,関数名・引数・CALL・戻り値格納先の順になる

    0 LOAD_CONST               0 (<code object hoge at 0x7fc3dec66b30, file "./sample.py", line 1>)
    3 MAKE_FUNCTION            0
    6 STORE_NAME               0 (hoge)

    9 LOAD_NAME                0 (hoge)
   12 LOAD_CONST               1 (1)
   15 LOAD_CONST               2 (2)
   18 CALL_FUNCTION            2
   21 POP_TOP
   22 LOAD_CONST               3 (None)
   25 RETURN_VALUE

演算

before
2以上の変数による演算.

d = a + b + c

after
a, b, cには,文字列と数値のどちらをいれてもコード自体は変わらない.
直前のLOAD_NAMEで入れる値が変わるだけ.
BINARY_ADDは相手が文字列だと連結,数値だと数値演算を行う.
3つの変数が相手だと,2回BINARY_ADDする.
以下のコードは数値版.

        
        18 LOAD_NAME                0 (a)
        21 LOAD_NAME                1 (b)
        24 BINARY_ADD
        25 LOAD_NAME                2 (c)
        28 BINARY_ADD
        29 STORE_NAME               3 (d)
        32 LOAD_CONST               1 (None)
        35 RETURN_VALUE

文字列連結だと以下の通り
before

allchar = string.ascii_letters + string.punctuation + string.digits # import string後

after

        80 LOAD_NAME                0 (string)
        83 LOAD_ATTR                9 (ascii_letters)
        86 LOAD_NAME                0 (string)
        89 LOAD_ATTR               10 (punctuation)
        92 BINARY_ADD          
        93 LOAD_NAME                0 (string)
        96 LOAD_ATTR               11 (digits)
        99 BINARY_ADD          
       100 STORE_NAME              12 (allchar)

ループ

before

sum = 0
for i in range(100):
    sum += i

after FOR_ITERにジャンプしてループを回しているようだ.

       0 LOAD_CONST               0 (0)
       3 STORE_NAME               0 (sum)

       6 SETUP_LOOP              30 (to 39)
       9 LOAD_NAME                1 (range)
      12 LOAD_CONST               1 (100)
      15 CALL_FUNCTION            1
      18 GET_ITER
 >>   19 FOR_ITER                16 (to 38)
      22 STORE_NAME               2 (i)

      25 LOAD_NAME                0 (sum)
      28 LOAD_NAME                2 (i)
      31 INPLACE_ADD
      32 STORE_NAME               0 (sum)
      35 JUMP_ABSOLUTE           19
 >>   38 POP_BLOCK
 >>   39 LOAD_CONST               2 (None)
      42 RETURN_VALUE

ありがちなコード

before
とあるCTF問題にあった一文.

password = ('').join((choice(allchar) for a in range(randint(60, 60))))

after
繰り返し部はGET_ITERが入るようだ.

    103 LOAD_CONST               6 ('')
    106 LOAD_ATTR               13 (join)
    109 LOAD_CONST               7 (<code object <genexpr> at 0x7f2fb42d9330, file "ransomware.py", line 11>)
    112 MAKE_FUNCTION            0
    115 LOAD_NAME               14 (range)
    118 LOAD_NAME               15 (randint)
    121 LOAD_CONST               8 (60)
    124 LOAD_CONST               8 (60)
    127 CALL_FUNCTION            2
    130 CALL_FUNCTION            1
    133 GET_ITER            
    134 CALL_FUNCTION            1
    137 CALL_FUNCTION            1
    140 STORE_NAME              16 (password)

おわりに

正直,いざpyc読むときにはこんな低レベルなやつを読むのは労力が要る.正にドM.
pythonコードに直に変換する方法がある.
そっちを使ったほうが余程楽.

参考

pycファイルの逆アセンブル - one day in summer

.pycファイルを作成する方法メモ - かせきのうさぎさん

volatilityにSANSのプラグインを追加する方法

volatilityでmimikatzを使いたくなった.
セットアップ方法が見つからなかったから書き残す.

mimikatzとは

Windowsのパスワードなどの各種認証情報をメモリから抜き出すツール.
管理者権限の取得やなりすましにも使える.
CTFではメモリダンプからアカウントの平文パスワードを抜き出すのによく使う.
デフォルトではvolatilityと連携して使うことができない.
SANSがプラグインを書いて公開しているから,それを追加してやれば良い.

やり方

基本はプラグインをコピーするだけ.

$ git clone https://github.com/volatilityfoundation/volati
$ git clone https://github.com/sans-dfir/sift-files       
$ cp sift-files/volatility/* volatility/volatility/plugins

これでmimikatzコマンド自体は使えるよう.しかし・・・

$ python2 volatility/vol.py -f ./memdump.mem mimikatz
Volatility Foundation Volatility Framework 2.6
*** Failed to import volatility.plugins.openioc_scan (ImportError: No module named ioc_writer)
ERROR   : volatility.debug    : You must specify something to do (try -h)

pythonモジュール不足でまだ動かない.
ERROR : volatility.debug : You must specify something to do (try -h)はコマンドが無いときにも出る.わかりにくい.
モジュールについてはvirtualenv2でローカル環境を作成して,整えていく.

$ virtualenv2 ENV && . ENV/bin/activate
$ pip install distorm3 pycrypto lxml ioc_writer colorama construct==2.5.5-reupload

Crypt.HashモジュールのためにpycryptoAttributeError: 'module' object has no attribute 'ULInt32'construct==2.5.5-reuploadで対処.

$ python2 volatility/vol.py -f <mem file> --profile=WinXPSP2x86 mimikatz

これで動く.

参考

error in attribute with construct module · Issue #15 · volatilityfoundation/community · GitHub

python - ImportError: No module named Crypto.Cipher - Stack Overflow

lsass.exe のダンプからユーザーパスワードを抽出 :mimikatz

https://www.iij.ad.jp/dev/tech/techweek/pdf/171108_02.pdf

GitHub - gentilkiwi/mimikatz: A little tool to play with Windows security

Linux系のバックアップ方法メモ

Linuxが動いているサーバのバックアップを取ろうとした時に考えたことのメモ.
差分とか定期的バックアップとかいった頭の良いことは他に書いてあるから,取り扱わない. サクッとできるやつだけ. あくまでメモだから,詳しい方法とかは割愛する.

状況

そのうちバックアップを取ろうと思っていたLinuxがある日突然壊れた.
幸いハードの故障だったからデータは無事だったが,今後を考えて一度バックアップを作ったほうが良いなと思った.

方法

cronで定期的に取ったり,差分だけ取ったりとか色々あるみたいだが,難しいことを考えなければ以下の3つくらいか.

フルバックアップ

物理的な記憶装置に書いてあるデータを全部丸々コピー.
一番確実にデータを取得できる上に,複数パーティション・未使用領域といったデータも一緒に保存できるから,この時点で消してしまっているファイルとかも復旧できるのが強み.
おまけにそのままOSも起動できる(デュアルブートも).
欠点は1TBのバックアップには1TBの記憶装置が必要になること.例え9GBしか使っていなくても,問答無用で1TB必要.
安全にコストはつきものである.
具体的な方法としてはddコマンドかな.
$ dd if=/dev/sda of=backup bs=32M

ファイルシステムごとバックアップ

使っている領域だけをコピーする.
9GBのLinuxなら9GBだけの容量で済む.フルバックアップで悩ましかった問題を解決できる.
当然,未使用領域は保存されないから注意は必要.
フルバックアップの次に検討すると良さそう.

今回調べるまで知らなかったが,Ubuntuextシリーズで,CentOSXFSファイルシステムが違う.
だから,使うツールも前者は$ e2image -ra -p /dev/sda1 /dev/sdb1で後者は$ xfsdump -l0 - /dev/sda1 ./backup.fsみたいになる.
CentOSは古いやつだとxfsdumpではなく,xfs_copyになるようだ.
自分のディストリに合ったやつを選ぶこと.

必要なやつだけまとめる

重要なファイルやディレクトリのみを保存する.
最も簡単かつ玄人向け.
一番容量が小さくなりやすい.
しかし,必要なファイルがわかる人でないと後で困る.
やり方はzip/tar/cpなりで頑張る感じ.
厄介なことに,$ cp -r / /tmp/backup とかやると特殊ファイル関係でエラーが出て時間を溶かすこともある.
シンプルな手法だからといって,作業量も少なくはいかないようだ(経験談).
/etc, /usr, /var, /home, /rootを中心に見ると良さそう.

おわり

とりあえずファイルシステムのバックアップを取ろうかな.

参考

ディスクのクローン - ArchWiki

Obtain file system information | Linux

6.9. ext4 から XFS への移行 - Red Hat Customer Portal

いつの間にかLinuxの/bin関係が/usrへのシンボリックリンクへ変わっていた話

タイトルの通り.気になったから調べた.
調査結果を書き残すが,誰にとって役に立つのかはわからない.

何が起こった

普段はArch Linuxを使っている.
何気なくls / をすると,いつの間にか/bin/usr/binへのシンボリックリンクになっていた.
な・・・ なにを言っているのかわからねーと思うが,おれも何でこうなっているのかわからなかった・・・.
複数のディレクトリがシンボリックリンクになっていて,変わっていたのは以下の通り.

/bin       -> /usr/bin/
/sbin      -> /usr/bin/
/usr/sbin  -> /usr/bin/
/lib       -> /usr/lib/
/lib64     -> /usr/lib/
/usr/lib64 -> /usr/lib/

新手のマルウェアに感染したのかとも思ったが,それにしてはどうにも様子がおかしい.
調べたら,割りと普通のことみたいだった.

調査結果

概要

答えはここにあった.
昔は/usrなどを分けることに意味があったが,最近のLinuxのファイル構造的に必要がなくなってきたらしい.
統合してしまえば,スクリプトに書くパスやらが簡略化されて利点が多いから,統合していこうぜ,ということらしい.

もうちょい詳細

英語の解釈が怪しいから,間違っていたら指摘お願いします.
通常,/bin/sbinディレクトリには,single user mode用のユーティリティが入るようにFilesystem Hierarchy Standardで定義されている.
昔は/usrは必要に応じて後からマウントするやつだから,ディレクトリを分けていた.
しかし,今では起動時にマウントされるディレクトリに含まれている上に,最近の/bin & /sbinのツールの多くは/usrを予めマウントしないと使えないやつが多くなったから,不可分になってしまっているらしい(未確認,本当か?).
だから,現行のOSでは,/bin/usr/binを分けることなく,/にある全てのディレクトリは対応する/usrディレクトリとマージすることにしたらしい.
というわけで,/bin/usr/bin/lib/usr/lib/sbin/usr/binへと言うように統合された.
/sbin/binを統合しちゃったことに対しては,良いのか?,という気はする.1つにするに越したことはないが.

利点

大きな利点は,システムの複雑さを軽減できること.
例えば,ネットワーク共有の観点から言えば,今までは共有時に/usr以下にあるコアコンポーネントを共有できず,マウントする側で用意する必要があった.これがなくなる.
更に,開発時にパスの違いからツール場所をハードコーディングしていたやつを1つにできる.
他には,/usrを複数のゲストOSで読み取り専用で共有できるようになり,ゲストのファイルシステムが数MBまで縮小できる.
以上のような利点があるらしい.

トリビア

初めて今回のマージをしたのは,15年前のOracle Solarisからで,完了したのはSolaris 11らしい.
思ったより歴史が古い.

さいごに

まだArch Linuxでしか確認していないが,Ubuntu 17.10とかでも同じことになっているのだろうか.
気が向いたら確認して見る予定.

参考URL

同じことを考えた人がいたようだ: linux - Why is /bin a symbolic link to /usr/bin? - Unix & Linux Stack Exchange

詳細: TheCaseForTheUsrMerge

Arch Linuxでカーネルパニックが起こった時にやったこと

普段使っている相棒はArch Linuxをインストールしている。
Ubuntuと違い、提供されるパッケージが最新のものだから使いやすい。
しかし今回pacman -Syuを実行したところ、多分kernel panic(freeze?、hang up?、違いがわからない)と呼ばれる現象が起こるようになった。
何が起こったかとどう対処したかを書き残す。
ついでにパッケージ全体を一つ前に戻すスクリプトも書いてみた。

最初に

これを読んでいるということは、それっぽい惨劇が起きたのかもしれない。
そんな人はまず落ち着こう。
テンパってパッケージをやたらと消したり、変更を加えすぎると復旧したときに後が怖い。

問題

普段通りに

$ sudo pacman -Syu

した後、頻繁に画面が凍結して、入力を一切受け付けなくなった。
凍結時の特徴はCapsLockキーのランプやWifiのランプが点滅する。
Ctrl+Alt+F1とかでコンソールに切り替えようとしてもダメ。
1時間に10回(落とす・点ける)以上電源ボタンを触ったのは初めてで壊れやしないかとヒヤヒヤした。
毎回起動時にジャーナル復元メッセージが出たぜ・・・。

パニックトリガー

パニックする引き金になった動作は以下の通り。

  • ネットに繋ぐ(短時間でパニック、特にWifi)
  • 普段通りに使う(プログラムのコンパイル・ファイル操作)
  • ファイルマネージャ(pantheon-files)でUSBをマウント(一発アウト、nautilusはセーフ)

その他起こった事象

$ dmesg
...(snip)...
[drm:i915_gem_idle_work_handler [i915]] *ERROR* Timeout waiting for engines to idle

perf: interrupt took too long (3982 > 3935), lowering kernel.perf_event_max_sample_rate to 50100

Direct firmware load for iwlwifi-6000-6.ucode failed with error -2
...(snip)...

などのエラーが出るようになった。
xmobarも定期的にセグフォした。
かれこれ2年ほどArch Linuxを使っているが、こんな問題だらけなのは初めてだ。

以下を試してみたが、効果はなし。

  • 発生したカーネルメッセージに対してそれぞれ対処
  • ファイルマネージャをアンインストール
  • ウィンドウマネージャ(Xmonad)のダウングレード

最初は入っているソフトがまずいかと思ったが、そうでは無さそうだ。

PC全体が止まること、アップデート以降様々なエラーが出始めたことから、カーネル周りが怪しい感じがしてくる。
案の定,アップデート時に4.13.4-1へと変更があった。

対処

Arch Linuxはこれまでインストールしたパッケージを/var/cache/pacman/pkg以下に全部持っている。
これを利用するといつでも戻すことができる。
問題はカーネル関連だと考えられるため、カーネルを戻した。

$ ls -l /var/cache/pacman/pkg/linux*
-rw-r--r-- 1 root root  62M May 22 14:58 /var/cache/pacman/pkg/linux-4.11.2-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  62M May 29 13:58 /var/cache/pacman/pkg/linux-4.11.3-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  62M Jul  6 02:07 /var/cache/pacman/pkg/linux-4.11.9-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  65M Jul 29 17:55 /var/cache/pacman/pkg/linux-4.12.4-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  65M Aug 11 21:28 /var/cache/pacman/pkg/linux-4.12.5-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  66M Sep 22 04:11 /var/cache/pacman/pkg/linux-4.13.3-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  66M Sep 28 17:15 /var/cache/pacman/pkg/linux-4.13.4-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  57M Oct 27  2015 /var/cache/pacman/pkg/linux-4.2.5-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  58M Mar 15  2016 /var/cache/pacman/pkg/linux-4.5-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  59M Jun  8  2016 /var/cache/pacman/pkg/linux-4.6.2-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  59M Jun 25  2016 /var/cache/pacman/pkg/linux-4.6.3-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  60M Oct  1  2016 /var/cache/pacman/pkg/linux-4.7.6-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  61M Dec  9  2016 /var/cache/pacman/pkg/linux-4.8.12-3-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  61M Dec  9  2016 /var/cache/pacman/pkg/linux-4.8.13-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  61M Feb  7  2017 /var/cache/pacman/pkg/linux-4.9.8-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root 853K Mar  7  2017 /var/cache/pacman/pkg/linux-api-headers-4.10.1-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root 921K Aug 17 19:47 /var/cache/pacman/pkg/linux-api-headers-4.12.7-1-any.pkg.tar.xz
-rw-r--r-- 1 root root 767K Aug  6  2015 /var/cache/pacman/pkg/linux-api-headers-4.1.4-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root 786K Feb 20  2016 /var/cache/pacman/pkg/linux-api-headers-4.4.1-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root 789K May 20  2016 /var/cache/pacman/pkg/linux-api-headers-4.5.5-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root 811K Aug  2  2016 /var/cache/pacman/pkg/linux-api-headers-4.7-1-x86_64.pkg.tar.xz
-rw-r--r-- 1 root root  30M Sep  4  2015 /var/cache/pacman/pkg/linux-firmware-20150904.6ebf5d5-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  35M Jan 14  2016 /var/cache/pacman/pkg/linux-firmware-20160113.40e9ae8-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  36M May 20  2016 /var/cache/pacman/pkg/linux-firmware-20160516.80d463b-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  40M Oct  5  2016 /var/cache/pacman/pkg/linux-firmware-20161005.9c71af9-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  40M Dec 23  2016 /var/cache/pacman/pkg/linux-firmware-20161222.4b9559f-2-any.pkg.tar.xz
-rw-r--r-- 1 root root  42M May  5 15:37 /var/cache/pacman/pkg/linux-firmware-20170422.ade8332-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  45M Jul 14 06:13 /var/cache/pacman/pkg/linux-firmware-20170622.7d2c913-1-any.pkg.tar.xz
-rw-r--r-- 1 root root  46M Sep  8 20:52 /var/cache/pacman/pkg/linux-firmware-20170907.a61ac5c-1-any.pkg.tar.xz

ここからそれっぽいパッケージとカーネル依存のものをダウングレードする。
以下はあくまでこちらの環境の場合だから、自分で調整して欲しい。ファイルの作成日時が鍵となる。
linux-?.??.?-?-x86_64.pkg.tar.xzは間違いなく必要。

$ sudo pacman -U /var/cache/pacman/pkg/util-linux-2.30.1-2-x86_64.pkg.tar.xz /var/cache/pacman/pkg/linux-firmware-20170622.7d2c913-1-any.pkg.tar.xz /var/cache/pacman/pkg/linux-4.12.4-1-x86_64.pkg.tar.xz /var/cache/pacman/pkg/virtualbox-host-modules-arch-5.1.26-3-x86_64.pkg.tar.xz

今のところdmesgに出ていたエラーも消えて、パニックも起こらなくなった。
とりあえずしばらくはこれで使っていこうと思う。

安定した運用への備え

安定して動くバージョンリストを作成する

本当はシステム全体のパッケージをダウングレードしたかったが、その方法はぱっと調べた限り見つからなかった。
downgradeコマンドは,単一パッケージのダウングレードするバージョンを選べるだけ。

というわけで自作してみた。
動作に関しての検証は済んでいないから、利用は自己責任でお願いします。 使った感想とか大歓迎です。

downgrade_all

システムが安定しているときに以下を実行。

$ git clone https://github.com/poppycompass/downgrade_all
$ downgrade_all/save_pkglist.sh

これでpkg(実行した日付).listにパッケージ名のリストが作成される。
一部のパッケージは/var/cache/pacman/pkg以下に無いこともあるから注意。

システムがぶっ壊れたときの復旧は

downgrade_all/recover_pkg.sh <作成したファイル>.list

単調な実装だから、時間がかかる。

同じシステムを一から作成する

インストールしているパッケージのリストをUSBや外部HDDに格納しておくと、OSを一新するときに役立つ。

$ sudo pacman -Qqne > pkg.list
$ sudo pacman -S --needed $(< pkg.list)

所感

リリースされているカーネルの不具合は都市伝説ではなかったようだ。
公式のWikiを見てもあまり情報が無いことに驚いた。
4.13.4-1というバージョンはしばらく忘れられそうにない。

同じ症状に見舞われた人の相棒が元気な姿に戻ってくれればいいなと思う。
数日掛けてのんびり復旧するくらいがちょうど良さそうだ.

参考

Arch Linux の安定化 - ArchWiki