BioErrorLog Tech Blog

試行錯誤の記録

はじめてのWebスクレイピング | ブログ投稿曜日を分析する

はじめてのWebスクレイピングに挑戦しました。今回は手始めに、自分のブログの曜日ごとの投稿数を分析します。


はじめに

こんにちは、@bioerrorlogです。

インターネットの登場によって、人間と情報の関係はすっかり変化しました。 私が今1秒もかからずにアクセスできる情報は、つい数十年前の資産家が何億円を積み上げても手に入れることのできない情報量でしょう。 これが無料のサービスとして誰でも利用できるのですから、えらいことです1

インターネットがなかった時代、手の届く情報が少なかった時代には、いかに多くの情報を知っているか、が大切でした。 しかし、膨大な情報がそこら中で踊る今では、いかに情報を収集し、抽出し、利用するかが武器となります。


というわけで、まったくの初心者ながら、Webデータ収集に手を出していきます。 どうやらこの分野にはいろいろな呼ばれ方の手法があるようです。 Webスクレイピング、クローリング、データマイニング、テキストマイニング、機械学習...とにかく、どれも有用な情報を引き出そうとする態度は同じでしょう。

今回は第一歩として、Wgetコマンドを用いて自分のこのホームページの情報を取得し、簡単にスクレイピングしてみます。


作業環境

Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work


ブログ投稿曜日を分析する

Wgetコマンドとは

まずは、Wgetコマンドの利用方法をマニュアルから把握します。

$ man wget
~
NAME
       Wget - The non-interactive network downloader.

SYNOPSIS
       wget [option]... [URL]...

DESCRIPTION
       GNU Wget is a free utility for non-interactive download of files from the Web.  It supports HTTP, HTTPS, and FTP protocols, as well as retrieval through HTTP proxies.
~

Wgetは、HTTP, HTTPS, FTPプロトコルなどをサポートするWebダウンローダーのようです。

使い方は単純で、wget [オプション]... [URL]...とします。

それでは、自分のこのホームページ情報を取得するのに必要そうなオプションを揃えていきます。

-r オプション | 再帰的にリンクをダウンロードする

このホームページのURLhttps://www.bioerrorlog.work/を網羅的にダウンロードするには、再帰的にリンクをたどる必要があります。 そこで、再帰検索オプション-rを用います。
マニュアルより、

-r
--recursive
    Turn on recursive retrieving.    The default maximum depth is 5.

デフォルトでは深さ5で、再帰的にリンクを辿ってダウンロードできるようです。

-w オプション | ダウンロード間隔をあける

あまり頻繁にダウンロードを要求するとサーバに負荷がかかってしまうため、ダウンロードに間隔を設けることは重要です。 -wは、ダウンロード間隔を指定することができます。
マニュアルより、

-w seconds
--wait=seconds
    Wait the specified number of seconds between the retrievals.  Use of this option is recommended, as it lightens the server load by making the requests less frequent.  Instead of in
    seconds, the time can be specified in minutes using the "m" suffix, in hours using "h" suffix, or in days using "d" suffix.

デフォルトでは秒でダウンロード間隔を指定でき、mをつけると分間隔で、hをつけると時間間隔で、dをつけると日間隔で指定できるようです。

-l オプション | リンクを辿る深さを指定する

果てしなくリンクを辿り続けることのないよう、-lで深さを指定します。

-l depth
--level=depth
    Specify recursion maximum depth level depth.

-np オプション | 親ディレクトリを辿らない

指定したURLの下位ディレクトリのみを取得するには、-npで親ディレクトリに登ることを禁じる必要があります。

-np
--no-parent
    Do not ever ascend to the parent directory when retrieving recursively.  This is a useful option, since it guarantees that only the files below a certain hierarchy will be downloaded.


必要なオプションはとりあえずこんなところでしょう。

自分のホームページ情報を取得する

それではさっそくこれらオプションを用いて、深さ1、ダウンロード間隔1秒で、このブログのトップページhttps://www.bioerrorlog.work/をWgetでクローリングしてみます。

$ wget -r -w 1 -l 1 -np https://www.bioerrorlog.work/

粛々とクローリングが始まりました。 ダウンロードが終了すると、カレントディレクトリに次のようなディレクトリが作成されました。 ディレクトリを木構造で示すtreeコマンドで確認します。

$ tree
.
└── www.bioerrorlog.work
    ├── about
    ├── archive
    │   ├── 2019
    │   │   ├── 01
    │   │   │   ├── 13
    │   │   │   ├── 15
    │   │   │   ├── 18
    │   │   │   ├── 20
    │   │   │   ├── 22
    │   │   │   ├── 24
    │   │   │   ├── 26
    │   │   │   ├── 28
    │   │   │   └── 31
    │   │   └── 02
    │   │       ├── 02
    │   │       ├── 06
    │   │       ├── 11
    │   │       ├── 16
    │   │       └── 24
    │   └── category
    │       ├── セキュリティ
    │       ├── プログラミング
    │       ├── C
    │       ├── gdb
    │       ├── Linux
    │       ├── Python
    │       ├── radare2
    │       └── その他
    ├── entry
    │   ├── auto-change-resolution
    │   ├── change-resolution
    │   ├── first-c-language-linux
    │   ├── first-python-windows
    │   ├── gdb-default-intel
    │   ├── how-to-man-command
    │   ├── linux-commands
    │   ├── reverse-engineering-ep1
    │   ├── reverse-engineering-ep2-disassemble
    │   ├── reverse-engineering-ep3-access
    │   ├── reverse-engineering-ep4-techniques
    │   ├── reverse-engineering-ep5-radare2
    │   ├── reverse-engineering-ep6-password-crypt
    │   ├── reverse-engineering-ep7-python-keygen
    │   └── what-is-file-permissions
    ├── feed
    ├── index.html
    ├── index.html?page=1547345313
    ├── robots.txt
    └── rss

こいつはなかなか面白いものが見れました。 はてなブログを用いてこのブログを運営しているのですが、改めてサイト構造を見るとなかなか新鮮です。 "category"が”archive"の下に格納されていたことなどは気付きませんでした。
また、下の方にある"index.html"と、"index.html?page=1547345313"も気になります。 何を指しているのでしょうか。 htmlファイルなので、firefoxコマンドで中身を見てみます。

$ firefox index.html index.html?page=1547345313

ブラウザが立ち上がり、html内容が表現されると、まさにどちらもブログトップの内容を表していました。 逆に1547345313という数字が何を指しているのか、よく分かりません。


とりあえず、Webページ情報を取得する方法はおおまかに把握できました。 次は何か情報を抽出してみます。

ブログ投稿曜日を分析する

さて、何の情報を抽出しましょうか。 はじめてなので、簡単そうなものにしたいです。

ブログの投稿日を抽出し、曜日を分析してみるというのはいかがでしょう。 手順としては次のようなものを考えています。

1. ブログ投稿日についてのhtml記述を特定
2. ブログ投稿日についての情報を抽出
3. 曜日を算出・分析

それでは、とにかくやってみます。

投稿日についてのhtml記述を特定する | ブラウザ開発者ツール

まずは、htmlファイルをfirefoxで確認し、どこの部分からブログ投稿日を取得できるかを特定します2

$ firefox index.html

先程のようにfirefoxで開くと、飾り気のないhtmlがブラウザで表示されます。 ページを見ていくと、ブログの投稿日を示している部分が簡単に見つかりました。

しかし、この部分を記述するhtml部分を目視で探し出すのはなかなか困難です。 そこで、この部分を右クリックして"要素を調査 (Inspect Element)"をクリックし、ブラウザの開発者ツールでhtmlの該当部分を特定させます。

<time datetime="2019-02-24" title="2019-02-24">
        <span class="date-year">2019</span><span class="hyphen">-</span><span class="date-month">02</span><span class="hyphen">-</span><span class="date-day">24</span>
</time>

これで、htmlファイルのどの部分にブログ投稿日が記述されているのかが特定できました。

つぎは、正規表現を利用して目的の情報を抽出します。

ブログ投稿日を抽出する | 正規表現

投稿日が記述されている行は

<time datetime="2019-02-24" title="2019-02-24">

となっているので、文字列<time datetime=に着目してgrepコマンドでindex.htmlから検索をかけます3

$ cat index.html | grep '<time datetime='
      <time datetime="2019-02-24" title="2019-02-24">
      <time datetime="2019-02-24" title="2019-02-24">
      <time datetime="2019-02-16" title="2019-02-16">
      <time datetime="2019-02-11" title="2019-02-11">
      <time datetime="2019-02-06" title="2019-02-06">
      <time datetime="2019-02-02" title="2019-02-02">
      <time datetime="2019-01-31" title="2019-01-31">
      <time datetime="2019-01-28" title="2019-01-28">
      <time datetime="2019-01-26" title="2019-01-26">
      <time datetime="2019-01-24" title="2019-01-24">
      <time datetime="2019-01-22" title="2019-01-22">
      <time datetime="2019-01-20" title="2019-01-20">
      <time datetime="2019-01-18" title="2019-01-18">
      <time datetime="2019-01-15" title="2019-01-15">
      <time datetime="2019-01-13" title="2019-01-13">

15行がgrep検索によって抽出されました。 今回情報を取得したトップページには15個のブログが掲載されているので、そのすべての投稿日を取得できた訳です。

しかし、このままのフォーマットでは解析に使えません。 日付部分のみを抽出する必要があります。 そのためにはsedコマンドと正規表現を用いて、必要な部分を切り抜く必要があります。

$ cat index.html | grep '<time datetime=' | sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/'
2019-02-24
2019-02-24
2019-02-16
2019-02-11
2019-02-06
2019-02-02
2019-01-31
2019-01-28
2019-01-26
2019-01-24
2019-01-22
2019-01-20
2019-01-18
2019-01-15
2019-01-13

みごと日付部分が抽出できましたが、sedコマンドに渡した正規表現が暗号のようになっているので、順に説明します。
sedコマンドの部分は、

sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/'

となっています。

まず-Eは、正規表現を記述するためのオプションです。

次の正規表現部分の大枠は、s/. <正規表現> / <文字列> / で、正規表現にマッチした部分が、指定した文字列で置換されます。 つまり今回は、正規表現.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*にマッチする部分を\1に置換する、という操作になります。

ここで面白いのは、\1は正規表現の()内に記述された部分を参照するということです。 正規表現.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*は、頭尾に任意の文字を任意回繰り返す.*を持つため、行全体を示しています。 よって操作としては、()内に記述された内容を切り出す、ということになるわけです。

()内の記述[0-9]{4}-[0-9]{2}-[0-9]{2}は、日付部分を示しています。 2019-02-24のように、0から9の数字[0-9]が、{4}回あるいは{2}回繰り返される塊が、 - で繋がれている文字列を表現しています。

このようにして、目的の情報が抽出できました。

結果をテキストファイル"datetime.txt"に出力し、Pythonでの曜日算出に移ります。

#txtファイルに出力
$ cat index.html | grep '<time datetime=' | sed -E 's/.*([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/' > datetime.txt

日付から曜日投稿数を分析する | Python

最後に、日付情報をPythonで処理し、曜日を算出・分析します。

書いたPythonコードは次のようなものです。 本当はもっときれいなやり方があるのかもしれませんが4、とりあえず曜日の算出という目的は果たせたので良しとします。

目的は、投稿日から曜日を算出し、各曜日の累積投稿数を算出することです。

import sys
import datetime

#引数の取得
args = sys.argv

#引数として渡されたファイルを開いて行ごとに読み込む
datetimes = open(args[1])
dts = datetimes.readlines()

#各曜日累計投稿数を格納する箱を初期化
day_sum = {'Sun':0,'Mon':0,'Tue':0,'Wed':0,'Thu':0,'Fri':0,'Sat':0}

#各日付ごとに処理
for d in dts:
    #日付から曜日を算出
    dt = datetime.date(int(d[0:4]),int(d[5:7]),int(d[8:10]))
    youbi = dt.strftime('%w')

 #曜日に合わせて結果を加算(0→6: 日曜→土曜)
    if youbi=='0':
        day_sum['Sun'] += 1
    elif youbi=='1':
        day_sum['Mon'] += 1
    elif youbi=='2':
        day_sum['Tue'] += 1
    elif youbi=='3':
        day_sum['Wed'] += 1
    elif youbi=='4':
        day_sum['Thu'] += 1
    elif youbi=='5':
        day_sum['Fri'] += 1
    elif youbi=='6':
        day_sum['Sat'] += 1

#結果を出力
print('Sun: %d' % day_sum['Sun'])
print('Mon: %d' % day_sum['Mon'])
print('Tue: %d' % day_sum['Tue'])
print('Wed: %d' % day_sum['Wed'])
print('Thu: %d' % day_sum['Thu'])
print('Fri: %d' % day_sum['Fri'])
print('Sat: %d' % day_sum['Sat'])

処理内容は、コメントに書いたとおりです。
曜日の算出には、datetimeライブラリ5を利用しました。

すこし戸惑ったのは、ループ内の次の処理です。

dt = datetime.date(int(d[0:4]),int(d[5:7]),int(d[8:10]))

datetime.date()には、(<年>, <月>, <日>)の形式で引数を与えなくてはなりませんが、dに格納されるのはstring型の2019-02-24のような日付です。 これを引数として使うために、(int(d[0:4]),int(d[5:7]),int(d[8:10])としました。

それではこのPythonコードに、上述の通り"datetime.txt"として抽出した日付を渡して、コードを実行します。

#datetime.txt
2019-02-24
2019-02-24
2019-02-16
2019-02-11
2019-02-06
2019-02-02
2019-01-31
2019-01-28
2019-01-26
2019-01-24
2019-01-22
2019-01-20
2019-01-18
2019-01-15
2019-01-13
$ python3 datetime_cul.py datetime.txt
Sun: 4
Mon: 2
Tue: 2
Wed: 1
Thu: 2
Fri: 1
Sat: 3

ついに、曜日ごとの記事投稿数を導くことができました。


おわりに

今回は、LinuxコマンドとPythonを用いてWebスクレイピングを行い、自分のブログの曜日ごとの投稿数を分析しました。

正規表現などは今まで全く触れたこともなく、最初はなかなか難解に感じましたが、自分で触ったりサンプルを見たりしているうちに少しずつ慣れてきた気がします。 使いこなすことができれば、非常に強力でしょう。

なお分析結果について見てみると、私は土日に記事を投稿することが多いようです。 たしかに、パソコンをいじれる自由時間が多いのは当然土日ですので、この結果は納得できます。

今回は練習としてとりあえず手を動かしてみましたが、なかなかこれは有効な情報収集の手段になりそうです。 次からは、もっと面白く有益な情報をスクレイピングしていけたらと思います。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

参考


  1. もっとも、いま世間を賑わせているように、このような無料サービスの恩恵は個人情報との引き換えによって成り立っている訳です。

  2. もちろん、実際のWebページにアクセスしても同様の分析ができます。

  3. Linuxコマンドの使い方は別にまとめています:Linuxコマンドまとめ | 使用例とマニュアル - BioErrorLog Tech Blog

  4. お作法的なものもよく分かりませんので、あしからず。

  5. datetime --- 基本的な日付型および時間型 — Python 3.10.6 ドキュメント

Pythonで鍵生成 - Keygen | リバースエンジニアリング入門#7

鍵生成プログラムKeygenをPythonで作成します。 まずはradare2を用いてパスワード認証アルゴリズムを解析し、それを開くことができるKeygenを作成しました。


はじめに

おはよう。@bioerrorlogです。

前回は、ごく簡単な暗号アルゴリズムを用いてパスワード認証プログラム"your_pass_2"を作成しました。

www.bioerrorlog.work

今回は、このバイナリコードを自らリバースエンジニアリングして仕様を解析し、パスワード認証を通過できる鍵を生成する"Keygen"をPythonで作成します。

なお、内容はこのLiveOverflow氏の動画を参考に、自分で少し改変を加えつつ流れを再現するという形になります。

youtu.be


作業環境

Ubuntu18.04.1 LTS を
Windows10の上に、VMwareによって構築した仮想環境で起動しています。
www.bioerrorlog.work


Pythonで鍵生成

radare2でアルゴリズムを解析する

radare2でバイナリコード"your_pass_2"を逆アセンブルし、パスワード認証のアルゴリズムを解析します。 まずは、ビジュアルモードで着目すべき場所を探します。

なお、radare2の使い方については過去記事に詳しく記録しました。

www.bioerrorlog.work

それでは、さっそくradare2のビジュアルモードでmain関数を解析します。

$ r2 your_pass_2
~
> aaa
~
> s sym.main
> VV
Rendering graph...

ビジュアルモードの結果を入念に見ると、一箇所だけループ構造が発生していました(Fig. 1)。 ここを詳しく解析してみます。

f:id:BioErrorLog:20190223232422p:plain
Fig. 1 main関数で見られたループ部分のアセンブラコード構造

forループの特定

ループ構造として、まずはforループを疑ってみます。 これが典型的なforループならば、ループに入る前に0にセットされ、ループのたびに1ずつ増える変数(ループカウンタ" i ")があるはずです。 探してみると、ループ前に0にセットされている変数が2つありました(Fig. 1, ➀ブロック)。

mov dword [local_18h], 0 
mov dword [local_14h], 0

この2つの変数dword [local_18h]dword [local_14h]の行く末を見てみると、dword [local_14h]はループごとに1が加算されているのがわかります(Fig. 1, ➂ブロック)。

add dword [local_14h], 1 

ここからdword [local_14h]がループカウンタ" i "である期待が高まります。

つぎに、dword [local_14h]がループカウンタであれば、forループの終了条件に絡んでいるはずです。 そこでforループの分岐条件を辿ってみると(Fig. 1, ➁)、

mov eax, dword [local_14h]
~
movsxd rbx, eax
~
cmp rbx, rax
jb 0x723

dword [local_14h]eaxrbxと、分岐条件cmp rbx, raxに繋がっていました。

以上のことから、このループ構造はdword [local_14h]をループカウンタ" i "としたforループであると判断しました。

forループの処理を解析する

次に、forループ内の処理(Fig. 1, ➂ブロック)を解析しました。 とても複雑で難解なので、Cで書かれたソースコード1を書き置きます。

#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
        if(argc==2) {
                printf("Checking Your Pass: %s\n", argv[1]);
                int sum = 0;
                for (int i=0; i<strlen(argv[1]); i++) {
                        sum += (int)argv[1][i];
                }
                if(sum==5345) {
                        printf("Hello, Master.\n");
                } else {
                        printf("Access Denied.\n");
                }

        } else {
                printf("Access Denied.\n");
        }
        return 0;
}

このソースコードによると、forループ内では、入力された文字argv[1]を一文字ずつint値として変数sumに加算していることがわかります。 この処理を、radare2で逆アセンブルしたコードから読み解きます。

forループ内(Fig. 1, ➂ブロック)は、

mov rax, qword [s]  
add rax, 8  
mov rdx, qword [rax] 
mov eax, dword [local_14h]
cdqe  
add rax, rdx   
movzx eax, byte [rax]    
movsx eax, al   
add dword [local_18h], eax
add dword [local_14h], 1

となっています。 最初から順に読み解いていきます


まず最初の2行に着目しました。

mov rax, qword [s]  
add rax, 8

レジスタraxに値が格納された後、8が加算されている点に注目します。 これは、raxが配列のアドレスを格納していることを示唆しています。

通常、64 bitコンピュータではメモリが8 byteずつに分割されています。 よって、1行目でraxに何かの配列のアドレスが格納された場合、2つ目の配列要素にアクセスしたいときにはraxに8を加算すればよい、というわけです。

ここで「2つ目の配列要素にアクセス」ということから考えられるのは、入力文字argv[1]でしょう。 argv[]配列は、プログラムの実行時に渡される引数を格納しています。 たとえばこのプログラム"your_pass_2"を、"Wrong-Pass"という文字列を渡して実行した場合、

$ ./your_pass_2 Wrong-Pass

argv[]配列は次のように格納されています。

./your_pass_2 Wrong-Pass
argv[0] argv[1]

よって、上記の2行は次のように解釈できそうです。

アセンブラコード レジスタの解釈
mov rax, qword [s] rax: argv[0]のアドレス
add rax, 8 rax: argv[1]のアドレス


では、次のコードに移ります

mov rdx, qword [rax] 
mov eax, dword [local_14h]
cdqe  
add rax, rdx   

前述の通り、raxargv[1]のアドレスを示しているので、1行目でrdxに格納されたのはargv[1]の中身を指すアドレスでしょう。 通常、先頭が指定されます。

アセンブラコード レジスタの解釈
mov rdx, qword [rax] rdx: argv[1]の中身の先頭を指すアドレス


次の行mov eax, dword [local_14h]では、dword [local_14h]が登場します。 これは、上述したようにforループで1ずつ増えるループカウンタです。 よってループカウンタの値がeaxに格納されるわけです。

その次の行cdqeは、eaxraxに設定する操作です2。 よって最後のadd rax, rdxは、先頭を指すアドレスrdxに、ループカウンタの値raxを足して、raxに格納しています。 ここまでをまとめると、

アセンブラコード レジスタの解釈
mov eax, dword [local_14h] eax: ループカウンタの値
cdqe rax: ループカウンタの値
add rax, rdx rax: 先頭アドレス+ループカウンタ


それでは最後の部分です。

movzx eax, byte [rax]    
movsx eax, al   
add dword [local_18h], eax

まずmovzx eax, byte [rax]で、raxの指し示すアドレスから1バイトを読み込んでいます。 この値はまさに入力文字のASCII値であると考えられます。 例えば、入力文字が"Wrong-Pass"で、ループカウンタが2の場合、

入力文字: W r o n g
ASCII値: 0x57 0x72 0x6F 0x6E 0x67
rax(rdx+ループカウンタ): rdx+1 rdx+2 rdx+3 rdx+4 rdx+5

eaxには"r"のASCII値0x72が格納されるというわけです。

次の行movsx eax, alでは、eaxの下位8ビット部分がeaxに再格納されています。 というのも、aleaxの下位8ビットを示すレジスタだからです3

そして最後には、add dword [local_18h], eaxeaxの値がdword [local_18h]に加算されています。 ここで、先に述べたようにdword [local_18h]は0で初期化された変数です。 つまり、ループごとにdword [local_18h]に入力文字が加算され、最後には入力文字の合計ASCII値の総和が格納されている、ということになります。


かなり長く複雑な解析をしてきました。 ここまでわかったforループの処理をまとめます。

アセンブラコード レジスタの解釈
mov rax, qword [s] rax: argv[0]のアドレス
add rax, 8 rax: argv[1]のアドレス
mov rdx, qword [rax] rdx: argv[1]の中身の先頭を指すアドレス
mov eax, dword [local_14h] eax: ループカウンタの値
cdqe rax: ループカウンタの値
add rax, rdx rax: 先頭アドレス+ループカウンタ
movzx eax, byte [rax] eax: 入力文字のASCII値
movsx eax, al eax: 下位8ビットを再格納
add dword [local_18h], eax dword [local_18h]: 入力文字ASCII値の総和


アクセス可否の判定条件

ここまで、forループ内で入力文字の総和dword [local_18h]が計算されていることを明らかにしました。 つぎは、この処理結果がどのようにしてアクセス可否の判定に使われているかを調べます。

これは簡単に分かりました。 forループ終了後の処理を見ると(Fig. 1, ➃)、次のようになっています。

cmp dword [local_18h], 0x14e1 
jne 0x778

つまり、入力文字の総和dword [local_18h]が0x14e1であるかどうかを判定し、プログラムが分岐しているわけです。

ついに、このプログラムの挙動が明らかになりました。 各入力文字ASCII値の総和が0x14e1であればアクセス承認、そうでなければアクセス拒否です。


Pythonで鍵生成 - Keygen

このパスワード認証プログラムの仕組みがわかったので、これを破るためのKeygenを作成します。 仕様は単純です。 ASCII値の総和が0x14e1となる文字列を自動生成するプログラムを、Pythonで書きます4

import random

#文字列keyの合計ASCII値を算出する関数
def check_key(key):
    char_sum = 0
    for c in key:
        char_sum += ord(c)
    return char_sum

#合計ASCII値が5345(0x14e1)である文字列を無限ループで生成
key = ""
while True:
    key += random.choice("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
    s = check_key(key)
    if s > 5345:
        key = ""
    elif s == 5345:
        print ("Key: {0}".format(key))

さっそくこのPythonプログラム"keygen_your_pass_2.py"実行してみます。

$ python3 keygen_your_pass_2.py 
Key: 3beg-iCw2Xg7OP7E1xkn-HEaxai-KRl43pj5OkJuViZJnIozP9-q-XKOG7v0vWy6
Key: kI_MoMol9yqM6gImi2lyfVhNn6npOo7eRXxelQ4VHh88_BIfb5NtKvsyVHF1
Key: 9qFkJQVe74CTJz5EpF8h6E6bukw1XFYJ7YhndKM58SlymLooMra30RfgUUFUN5kK
Key: PLySl4Mu_eDoOaDQ7BV09K6WGrtOZFLP--pojSBFq8GPxIsgIxX_I0YkNQmBb8f7
Key: ui-IAbPFU2cR20ZMCMYSbyMqbuNpUqNutqwtll-YxeZx83KcI--u-PJF5B3RuAO
Key: ZL6AZgMIlphS0spzgWEsy_sFKusCS_46ahjGm2T7PQBf7h3ifrmytkX89s5z
Key: LfX9lEUp5vGDTVOlo2O7NyJQR_yne6Dd8BBTCQxfBleMmIz8RJPYYa9JuGpfdg
Key: 9yMjf2YKivb84KuhF6iLdvTssguiQne6kVKyjJvAvlV13qskhNgfLOHQBy6
Key: 5tzqUevjCYFoZ1lEnJWoIJJOHD4OMTv3qB5A3W2SBlHlhuXThrqksLV8FuHc-j
Key: _mcHUIbIM5QEAyGQOTqvpY-5TKsVBJEdGuT86fLcP3KrqAUH_fD1s0fR2JNqVS0NL
Key: 6t0pv0F3SbtKXwEjsp47xrfnC9zr2VjtqTOrTEYGcmwoRvpkdBT2bj_T6dO
Key: BjOLf49V_xPvaONmPyiyKoQAP0WYc3PNrq7VD-UQqT8I3otgILvmADOrGo3sDf
Key: nNBYpcTZAXdx8aXA5EUwSgXsKT2lcQpaH7JnuEuvmO2bvvXKNkSdwVPlkIm
Key: YnvH_JbZsDwKmJg7_fA4xnrn-UwM_d_N_G-xQS47wsOI5q12Cwc9hVIx_m3C3D
#以下略

即座に大量の鍵が生成されました。 ちなみに無限ループなので、処理を終了するには"Ctrl+c"を押す必要があります。

それでは、はたしてこれらの鍵でこのプログラム"your_pass_2"のアクセス承認が引き出せるのかを確かめます。

$ ./your_pass_2 A2viBkKm4IU-I_pEGF6-7YKONqAEk1Vl6KIAAg88fHKjFMN2DAqBjBvlwaqib-_xe5
Checking Your Pass: A2viBkKm4IU-I_pEGF6-7YKONqAEk1Vl6KIAAg88fHKjFMN2DAqBjBvlwaqib-_xe5
Hello, Master.

"Hello, Master."
見事、アクセス承認を引き出すことができました。


おわりに

今回は、radare2を用いてパスワード認証アルゴリズムを解析し、それに基づいてPythonで鍵生成プログラムKeygenを作成しました。

以前のようにデバッグ上でレジスタを操作する5のでなく、完全に外側からアクセスを開くのはなかなか趣深いものでした。

今回はソースコードを知っている状態で、かつ極めて簡単な暗号アルゴリズムをリバースしましたが、自分が知らないプログラムの解析にも挑戦してみたいです。 そういうことをするための安全なコミュニティがネット上にはたくさんある6ようなので、今後手を出していけたらと思っています。

[関連記事]

www.bioerrorlog.work

www.bioerrorlog.work

www.bioerrorlog.work


  1. パスワード認証の暗号化 | リバースエンジニアリング入門#6 - 生物系がゼロから始めるTech Blog

  2. Assembly Programming on x86-64 Linux (07)

  3. http://milkpot.sakura.ne.jp/note/x86.htmlより、"eax は rax の下位 32 ビット部分を指す。 また ax は eax の下位 16 ビット部分を指す。 さらに ax は上下 2 つの 8 ビットレジスタ ah, al に分割して使うことができる。"

  4. GitHub - LiveOverflow/liveoverflow_youtube: Material for the YouTube seriesを参考にしています。

  5. レジスタ操作でアクセス承認を引き出す - gdb | リバースエンジニアリング入門#3 - 生物系がゼロから始めるTech Blog

  6. 逆に、禁じられているソフトウェアを解析することは違法ですから、そこの一線は絶対に超えてはなりません。

はじめてのPython | Windows環境構築 - Atom

WindowsでPythonプログラミングを始める環境構築をしたので、ノートを残します。

Pythonを公式ページからダウンロードし、
テキストエディタのAtomをインストールしました。


はじめに

おはよう。@bioerrorlogです。

最近はLinuxで遊んでばかりでしたが、プログラミングはWindowsでも当然できます。
シンプルなプログラミング言語がいいなと思い、Pythonをインストールしました。

Pythonとは、wikipediaによると1

Python is an interpreted, high-level, general-purpose programming language. Created by Guido van Rossum and first released in 1991, Python has a design philosophy that emphasizes code readability, notably using significant whitespace.

1991年に生まれた汎用高級プログラミング言語で、読みやすさを重視した哲学を持っているようです。
MIT2やHarvard3のプログラミング入門講義でも多く取り上げれていることから、初心者にも扱いやすいことが伺えます。

WindowsにはPythonがあらかじめ入っているわけではないので、Pythonをプログラミングするためには環境構築が必要でした。
簡単に記録を残します。


Python - Windows環境構築

Pythonのインストール

Python公式ページ4からPython 3.7.2をダウンロード、インストールしました。

インストール時には、"Add Python 3.7 to PATH"にチェックを入れることを忘れないようにします。
PythonをPATHに通しておくことで、どのディレクトリからもPythonを呼び出すことができるようになるわけです。

ダウンロードからインストールの流れは、Python Japanのページ5に丁寧な記載がありました。

Atomのインストール

Atomは、コードを書くときに使うテキストエディタの一つです。

世の中には、じつに様々なテキストエディタがあるようです。 人によっては執拗なこだわり6を持つ人もいるそうですが、正直私はなんでもいいです。
私が好きでよく動画を見るDevon Crawford氏7がAtomを使っていたのを見て、これにしてみました。

また、Atomはオープンソースなので8、将来的に中身を知りたくなった時にも便利そうです。

さっそくAtom公式ページ9からダウンロードし、インストールしました。

このAtom公式ページはとても美しかったので、期待が膨らみます。

Atomでプログラムを実行する

さっそく、Atomで簡単なコードを書いて実行します。

File > New File
新しいファイルを作成したら、たとえば

print ('Hello, Lain.')

と書いて".py"拡張子で保存してみます。
この".py"拡張子は、Pythonコードにつけられるものです。
コンピュータもこの拡張子を認識することで、コードがPythonで書かれていることを理解します。

保存したら、Ctrl + Shift + B で実行すると、

Hello, Lain.

記念すべき文字列表示の成功です。

ちなみに、拡張子を何もつけずにファイルを保存して実行してみると、

You must select a language in the lower right, or save the file with an appropriate extension.

やはり、然るべき拡張子をつけなさいと怒られてしまいました。

パッケージのアンインストール

PythonとAtomをインストールしましたが、アンインストールする手順も記録しておきます。
Windowsでパッケージをアンインストールするには、

コントロールパネル > プログラムのアンインストール

からアンインストールできます。
一覧から目的のパッケージを選択して”アンインストール”を押せば、アンインストーラーが立ち上がってアンインストールされました。

※しかし、アンインストールについて検索してみると、もっと複雑な手順を踏んでいるものが多くあります。 正直、私はアンインストールの仕組みをあまり理解できていないので、コントロールパネルから簡単にアンインストールするこのやり方が正しいのかわかりません。


おわりに

今回、WindowsでPythonの環境構築をしました。

それにしても、Pythonのコードはとてもシンプルでよいものです。
今回Pythonで書いたものと全く同じ挙動をするコードをCで書くと、

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello, Lain.\n");
    return 0;
}

となります10
Pythonとはえらい違いです。

もちろん、言語というものは恐らく、個性はあれど優劣はないというものでしょう。
それでも、私はなるべく簡素な文章が好ましいと感じる方です。

その点、Pythonからはとても良いお人柄が伝わってきました。