えびちゃんの日記

えびちゃん(競プロ)の日記です。

write-up: AlpacaHack Round 8 (Rev)

AlpacaHack Round 8 (Rev) の write-up

AlpacaHack Round 8 (Rev) に参加して、3 問解いて 12/316 位でした。

振り返り

masking tape

とりあえずバイナリを落としてきて実行します。 こういう態度は本当に終わっているんですが、まぁ運営を信じて実行しちゃいます(仮想環境なので一応大丈夫なはず、一応)。

% ./masking-tape 
#> usage: ./masking-tape <input>

% ./masking-tape a
#> wrong

何らかの正しい <input> を寄越せという話っぽさを感じます。

とりあえず r2 (radareorg/radare2) を使って見てみると、strcmp でなにかを比較しているようなので、引数を見てみます*1。

// hook-a.c
#include <stdio.h>
int strcmp(char const* s1, char const* s2) {
    printf("'%s' <=> '%s'\n", s1, s2);
    return 0;
}

これを

% gcc-14 --shared -o hook-a.so hook-a.c

こうして

% LD_PRELOAD=./hook-a.so ./masking-tape a | xxd
#> 00000000: 2708 2303 0313 0313 0301 2331 1311 c803  '.#.......#1....
#> 00000010: c803 1301 c813 1303 1313 1113 2327 203c  ............#' <
#> 00000020: 3d3e 2027 0327 0a27 0240 8008 0808 c8c8  => '.'.'.@......
#> 00000030: 8088 0880 8832 0832 8080 8032 0880 0808  .....2.2...2....
#> 00000040: 4888 80c8 2720 3c3d 3e20 2708 270a 636f  H...' <=> '.'.co
#> 00000050: 6e67 7261 747a 0a                        ngratz.

こう。少し遊んでみます。

% LD_PRELOAD=./hook-a.so ./masking-tape ab | xxd
#> 00000000: 2708 2303 0313 0313 0301 2331 1311 c803  '.#.......#1....
#> 00000010: c803 1301 c813 1303 1313 1113 2327 203c  ............#' <
#> 00000020: 3d3e 2027 0313 270a 2702 4080 0808 08c8  => '..'.'.@.....
#> 00000030: c880 8808 8088 3208 3280 8080 3208 8008  ......2.2...2...
#> 00000040: 0848 8880 c827 203c 3d3e 2027 0827 0a63  .H...' <=> '.'.c
#> 00000050: 6f6e 6772 6174 7a0a                      ongratz.

% LD_PRELOAD=./hook-a.so ./masking-tape Al | xxd
#> 00000000: 2708 2303 0313 0313 0301 2331 1311 c803  '.#.......#1....
#> 00000010: c803 1301 c813 1303 1313 1113 2327 203c  ............#' <
#> 00000020: 3d3e 2027 0823 270a 2702 4080 0808 08c8  => '.#'.'.@.....
#> 00000030: c880 8808 8088 3208 3280 8080 3208 8008  ......2.2...2...
#> 00000040: 0848 8880 c827 203c 3d3e 2027 0240 270a  .H...' <=> '.@'.
#> 00000050: 636f 6e67 7261 747a 0a                   congratz.

1 文字追加するごとに右辺が伸びたり伸びなかったりしそう? なんかのハッシュ的な機構が入ってて、予想するのは大変そう。 とりあえず左辺は 28 bytes なので、28 bytes 程度のフラグが答えになりそう感。

いろいろ試していると、byte ごとに干渉しなさそうなので、とりあえず 1 byte ずつ決めていけばよさそう。なのでそういう solver を書きます。

from pwn import *

target1 = (
    "\x08\x23\x03\x03\x13\x03\x13\x03\x01\x23\x31\x13\x11\xC8"
    "\x03\xC8\x03\x13\x01\xC8\x13\x13\x03\x13\x13\x11\x13\x23"
)
target2 = (
    "\x02\x40\x80\x08\x08\x08\xC8\xC8\x80\x88\x08\x80\x88\x32"
    "\x08\x32\x80\x80\x80\x32\x08\x80\x08\x08\x48\x88\x80\xC8"
)


context.log_level = "error"


def escape(s):
    return s.replace("'", r"'\''")


flag = ""
for i in range(len(target1)):
    for c in range(ord(" "), ord("~") + 1):
        c = chr(c)
        p = process(
            f"LD_PRELOAD=./hook.-aso ./masking-tape '{escape(flag + c)}'", shell=True
        )
        recv1 = p.recvline()[:-1]
        recv2 = p.recvline()[:-1]
        p.close()
        expected1, actual1 = recv1[: len(target1)], recv1[len(target1) :]
        expected2, actual2 = recv2[: len(target2)], recv2[len(target2) :]
        if (
            len(actual1) == len(actual2) == i + 1
            and expected1[: i + 1] == actual1
            and expected2[: i + 1] == actual2
        ):
            flag += c
            break
    else:
        exit(1)

print(flag)
% python3 solve-a.py
#> Alpaca{********************}

よーぅし

hidden

これもとりあえず実行。

% ./hidden 
#> usage: ./hidden <input>

% ./hidden a
#> wrong

あ〜さっきと同じ感じね。

今回は memcmp で比較しているみたいです? なにやら GDB が main を見つけてくれないみたいで困った。

(gdb) b main
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) n

r2 的には s main とかができたので、何らかのことをして隠されているのでしょうか。とりあえず puts を呼んでいる箇所で止めたりしてみます。

(gdb) b puts
Breakpoint 1 at 0x10b0

(gdb) r a
Starting program: /mnt/hidden a
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, __GI__IO_puts (str=0x555555556020 "wrong") at ./libio/ioputs.c:33
warning: 33 ./libio/ioputs.c: No such file or directory

(gdb) bt
#0  __GI__IO_puts (str=0x555555556020 "wrong") at ./libio/ioputs.c:33
#1  0x0000555555555545 in ?? ()
#2  0x00007ffff7dc23b8 in __libc_start_call_main (main=main@entry=0x5555555553e1, argc=argc@entry=2, argv=argv@entry=0x7fffffffed18) at ../sysdeps/nptl/libc_start_call_main.h:58
#3  0x00007ffff7dc247b in __libc_start_main_impl (main=0x5555555553e1, argc=2, argv=0x7fffffffed18, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffed08)
    at ../csu/libc-start.c:360
#4  0x0000555555555145 in ?? ()

(gdb) x/10i 0x0000555555555545
   0x555555555545:  mov    $0x0,%eax
   0x55555555554a:  mov    -0x18(%rbp),%rdx
   0x55555555554e:  sub    %fs:0x28,%rdx
   0x555555555557:  je     0x55555555555e
   0x555555555559:  call   0x5555555550d0 <__stack_chk_fail@plt>
   0x55555555555e:  mov    -0x8(%rbp),%rbx
   0x555555555562:  leave
   0x555555555563:  ret
   0x555555555564:  endbr64
   0x555555555568:  sub    $0x8,%rsp

なにやら r2 で見た main っぽい命令が見つかったので一旦満足。アドレスの下 1.5 byte も一致していました。

とりあえずまた似たようなことをやってみます。

// hook-b.c
#include <stdio.h>
int memcmp(void const* s1, void const* s2, size_t n) {
    printf("memcmp(%p, %p, %zu)\n", s1, s2, n);
    for (size_t i = 0; i < n; ++i) {
        printf("[%zu]: %#04x %#04x\n", i, *((unsigned char*)s1 + i), *((unsigned char*)s2 + i));
    }
    return 0;
}
% LD_PRELOAD=./hook-b.so ./hidden a
#> memcmp(0x5555555592a0, 0x555555558040, 108)
#> [0]: 0xfc 0xdc
#> [1]: 0xea 0x86
#> [2]: 0x6a 0x1a
#> [3]: 0xfb 0x9a
#> [4]: 0000 0xdd
#> [5]: 0000 0x93
#> [6]: 0000 0x9b
#> [7]: 0000 0x35
#:
#> [104]: 0000 0xb0
#> [105]: 0000 0xa2
#> [106]: 0000 0x99
#> [107]: 0000 0x91
#> congratz

これも結局ハッシュめいたものを通して一致すればおめでとう〜という感じっぽい?

% LD_PRELOAD=./hook-b.so ./hidden Alpaca{
#> memcmp(0x5555555592a0, 0x555555558040, 108)
#> [0]: 0xdc 0xdc
#> [1]: 0x86 0x86
#> [2]: 0x1a 0x1a
#> [3]: 0x9a 0x9a
#> [4]: 0xdd 0xdd
#> [5]: 0x93 0x93
#> [6]: 0x9b 0x9b
#> [7]: 0x41 0x35
#> [8]: 0000 0xd3

それっぽさがあるので、それっぽい solver を書きます。

from pwn import *

target = (
    b"\xDC\x86\x1A\x9A\xDD\x93\x9B\x35\xD3\x74\xDA\xEE\xE8\x5A\x3C\xC5"
    b"\x1C\x64\x33\x47\xD2\x3B\x28\xF3\xCC\x5A\x48\x8B\x74\x0C\x4B\x87"
    b"\x38\xD6\x80\x40\x51\xE6\x4A\x27\xA1\x73\x52\x0F\x93\x06\x54\x3D"
    b"\x65\x13\xFB\xC8\x65\xAF\xD2\x67\xB3\x09\xEF\x7D\x23\xA6\x76\xE5"
    b"\x13\x10\x13\xFF\x34\x8D\xAE\xD0\x9C\x2C\x4D\xF3\xA1\xBC\x46\x2F"
    b"\x98\x87\xB6\x57\x1A\xA2\x17\xF1\xF0\xE5\xB0\xBA\x9B\x6D\xB5\xA7"
    b"\xAC\x6A\x5E\xAC\xE8\xF6\x90\xD8\xB0\xA2\x99\x91"
)

context.log_level = "error"


def escape(s):
    return s.replace("'", r"'\''")


flag = ""
for i in range(len(target)):
    for c in range(ord(" "), ord("~") + 1):
        c = chr(c)
        p = process(f"LD_PRELOAD=./hook-b.so ./hidden '{escape(flag + c)}'", shell=True)
        recv = p.recvline()[:-1]
        p.close()
        if target[: len(flag) + 1] == recv[: len(flag) + 1]:
            print(c, end="", flush=True)
            flag += c
            break
    else:
        exit(1)

print()
% python3 solve-b.py
#> Alpaca{**************** ... ***}

よーぅし。

vcipher

とりあえず実行してみます。

% ./vcipher 
#> Input 32-character flag: a
#> Error: Flag must be exactly 32 characters.

% ./vcipher 
#> Input 32-character flag: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#> Input flag: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#> Processing ...
#> Processing ...
#> Processing ...
#> Processing ...
#> Processing ...
#> Processing ...
#> Processing ...
#> Processing ...
#> The flag is incorrect.

お、さっきとは違いますね。

r2 で afl してみると C++ 感があり、ウワーという気持ちになります。

s main v していろいろ見るに、チェックはこのあたりが関係していそうです。

│ │       ┌─> 0x00003a05      8b3c86         mov edi, dword [rsi + rax*4]
│ │       ╎   0x00003a08      393c83         cmp dword [rbx + rax*4], edi
│ │       ╎   0x00003a0b      0f45d1         cmovne edx, ecx
│ │       ╎   0x00003a0e      48ffc0         inc rax
│ │       ╎   0x00003a11      4883f808       cmp rax, 8
│ │       └─< 0x00003a15      75ee           jne 0x3a05

ということで、そのあたりに breakpoint を打ちたいです。

(gdb) b main
Breakpoint 1 at 0x3660

(gdb) r
Starting program: /mnt/vcipher 
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000555555557660 in main ()

(gdb) x/i 0x0000555555557a05
   0x555555557a05 <main+933>: mov    (%rsi,%rax,4),%edi

それっぽさがありますね。

Breakpoint 2, 0x0000555555557a05 in main ()
(gdb) x/8wx $rsi
0x5555555620a0 <_ZL14CORRECT_OUTPUT>: 0x345a7191  0xdcc4950a  0x8ad73f4e  0x6006deee
0x5555555620b0 <_ZL14CORRECT_OUTPUT+16>:  0xb474f6a4  0x9620574d  0x7fba5668  0x45cb397e

(gdb) x/8wx $rbx
0x7fffffffeba8: 0x1558f6b2  0x1ca7b66f  0x03f6762c  0x094537e9
0x7fffffffebb8: 0xf095f7a7  0xf7e4b764  0xfd337721  0xe48238fe

ここの値が同じになるような入力を与えればよさそうな気がします。一応確かめておきましょう。

(gdb) set *0x7fffffffeba8 = 0x345a7191
(gdb) set *0x7fffffffebac = 0xdcc4950a
(gdb) set *0x7fffffffebb0 = 0x8ad73f4e
(gdb) set *0x7fffffffebb4 = 0x6006deee
(gdb) set *0x7fffffffebb8 = 0xb474f6a4
(gdb) set *0x7fffffffebbc = 0x9620574d
(gdb) set *0x7fffffffebc0 = 0x7fba5668
(gdb) set *0x7fffffffebc4 = 0x45cb397e
(gdb) c
Continuing.
The flag is correct!
[Inferior 1 (process 30123) exited normally]

よさそうですね。

というところで、じゃぁどんな感じでここが変わるのかというのを調べていきます。

% gdb -ex 'b *0x0000555555557a05' -ex 'r' -ex 'x/8wx $rbx' ./vcipher <<< xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#:
#> 0x7fffffffeba8:  0x1558f6b2  0x1ca7b66f  0x03f6762c  0x094537e9
#> 0x7fffffffebb8:  0xf095f7a7  0xf7e4b764  0xfd337721  0xe48238fe
#:

% gdb -ex 'b *0x0000555555557a05' -ex 'r' -ex 'x/8wx $rbx' ./vcipher <<< xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxy
#:
#> 0x7fffffffeba8:  0x1558f6b2  0x1ca7b66f  0x03f6762c  0x094537e9
#> 0x7fffffffebb8:  0xf095f7a7  0xf7e4b764  0xfd337721  0xc48238fe
#:

% gdb -ex 'b *0x0000555555557a05' -ex 'r' -ex 'x/8wx $rbx' ./vcipher <<< yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#:
#> 0x7fffffffeba8:  0x1558f692  0x1ca7b66f  0x03f6762c  0x094537e9
#> 0x7fffffffebb8:  0xf095f7a7  0xf7e4b764  0xfd337721  0xe48238fe
#:

% gdb -ex 'b *0x0000555555557a05' -ex 'r' -ex 'x/8wx $rbx' ./vcipher <<< Alpaca{xxxxxxxxxxxxxxxxxxxxxxxx}
#:
#> 0x7fffffffeba8:  0x345a7191  0x1cc4950f  0x03f6762c  0x094537e9
#> 0x7fffffffebb8:  0xf095f7a7  0xf7e4b764  0xfd337721  0x448238fe
#:

ふんふん? とりあえず、さっきの正解と見比べてみます。

0x5555555620a0:  0x345a7191  0xdcc4950a  0x8ad73f4e  0x6006deee
0x5555555620b0: 0xb474f6a4  0x9620574d  0x7fba5668  0x45cb397e

0x7fffffffeba8: 0x345a7191  0x1cc4950f  0x03f6762c  0x094537e9
0x7fffffffebb8: 0xf095f7a7  0xf7e4b764  0xfd337721  0x448238fe

0x7fffffffeba8: 0xoooooooo  0x.oooooo.  0x........  0x........
0x7fffffffebb8: 0x........  0x........  0x........  0xo......o

o でマークした部分は正解のものと一致していそうなので、1 byte ごとに 8 bits ぶん決まりそう?みたいな気持ちになります。 挙動を見た感じだと、3][22][11][00][3 みたいな感じでシフトされていそうな気配があります。

そういえば、入力が 32 bytes で、エンコードされた列も 32 bytes なので、(フラグが複数通りあり得たら嫌なので)全単射になっているんだろうなということを思ってはいました。

ということで結局 1 byte ごとに決める solver を書くのですが、一回の実行にめちゃくちゃ時間がかかるので、めちゃくちゃ時間がかかりそうです。

import sys

from pwn import *

target = [
    0x345A7191,
    0xDCC4950A,
    0x8AD73F4E,
    0x6006DEEE,
    0xB474F6A4,
    0x9620574D,
    0x7FBA5668,
    0x45CB397E,
]


context.log_level = "error"

flag_raw = ["!"] * 32

mask = [0x00000FF0, 0x000FF000, 0x0FF00000, 0xF000000F]
dec = [
    lambda x: x >> 4,
    lambda x: x >> 12,
    lambda x: x >> 20,
    lambda x: (x >> 28) | ((x & 0xF) << 4),
]

i = int(sys.argv[1])
flag_raw[i] = chr(int(sys.argv[2], 16))

target_i = dec[i % 4](target[i // 4] & mask[i % 4])
print(f"target: {target_i:#04x}")
while flag_raw[i] <= "~":
    c = flag_raw[i]
    flag = "".join(flag_raw).replace("'", r"'\''")
    print("current:", flag)
    p = process(
        f"printf '%s\n' '{flag}' | gdb -ex 'b *0x0000555555557a05' -ex 'r' -ex 'x/8wx $rbx' vcipher",
        shell=True,
    )
    p.recvuntil(b"--Type <RET> for more, q to quit, c to continue without paging--")
    recv1 = p.recvline()[:-1]
    recv2 = p.recvline()[:-1]
    p.close()
    words1 = [*map(lambda x: int(x, 16), recv1.decode().split(":")[1][1:].split("\t"))]
    words2 = [*map(lambda x: int(x, 16), recv2.decode().split(":")[1][1:].split("\t"))]
    words = words1 + words2
    print(hex(dec[i % 4](words[i // 4]) & 0xFF))

    if (target[i // 4] & mask[i % 4]) == (words[i // 4] & mask[i % 4]):
        print(c, flush=True)
        break

    flag_raw[i] = chr(ord(flag_raw[i]) + 1)

とりあえずこんな感じで、添字と開始文字を渡して全探索できるコードを書きました。 これを複窓で 20 並列くらいさせれば余裕でしょと思ったのですが、3–4 窓くらいでだいぶ限界み(プロセスの生成がめちゃ遅い)を感じたのでやめました。

% python3 after/solve-c.py 7 41
#> target: 0xad
#> current: !!!!!!!A!!!!!!!!!!!!!!!!!!!!!!!!
#> 0x83
#> current: !!!!!!!B!!!!!!!!!!!!!!!!!!!!!!!!
#> 0x85
#:
#> current: !!!!!!!V!!!!!!!!!!!!!!!!!!!!!!!!
#> 0xad
#> V

Alpaca{V...} ということなので、

Verilog can also be converted to C++.

からエスパーするに V3r1l0g... とかなのかな?と予想したりしました。当たっていたのでウケました。

なにやら上位 4 bits は固まって現れそう?というのと、それっぽい文章になっていそうというのからエスパーして、がちゃがちゃ試しました。 手作業で 40 分くらい(コードを修正しつつ)がんばりながら、フラグは手に入れたので一応満足です。

冷静になると、フラグの長さが既知で、各 byte ごとに並列してできるので、'!' * 32, '"' * 32, ... みたいにして探索すればいいんですよね(ということに、上記を書いてから「まともな解法わからんな〜」と考えながらようやく気づきました)。

from pwn import *

target = [
    0x345A7191,
    0xDCC4950A,
    0x8AD73F4E,
    0x6006DEEE,
    0xB474F6A4,
    0x9620574D,
    0x7FBA5668,
    0x45CB397E,
]


mask = [0x00000FF0, 0x000FF000, 0x0FF00000, 0xF000000F]
dec = [
    lambda x: x >> 4,
    lambda x: x >> 12,
    lambda x: x >> 20,
    lambda x: (x >> 28) | ((x & 0xF) << 4),
]


def get(words, i):
    return dec[i % 4](words[i // 4]) & 0xFF


table = [[0] * 256 for _ in range(32)]

for c in range(0x0, 0x100):
    print(f"current: {c:#04x}")
    p = process("gdb vcipher", shell=True)
    p.sendline(b"b *0x00005555555577c0")
    p.sendline(b"b *0x0000555555557a05")
    p.sendline(b"r")
    p.recvuntil(b"Input 32-character flag: ")
    p.sendline(b"0" * 32)

    p.recvuntil(b"Breakpoint 1,")
    p.recvline()
    p.sendline(b"p $rsp + 0x8")
    sp = int(p.recvline()[41:55].decode(), 16)
    p.sendline(f"x/a {hex(sp)}".encode())
    s = int(p.recvline()[35:49].decode(), 16)

    p.sendline(f"set *(long*){hex(s+0x00)} = {0x0101010101010101 * c}".encode())
    p.sendline(f"set *(long*){hex(s+0x08)} = {0x0101010101010101 * c}".encode())
    p.sendline(f"set *(long*){hex(s+0x10)} = {0x0101010101010101 * c}".encode())
    p.sendline(f"set *(long*){hex(s+0x18)} = {0x0101010101010101 * c}".encode())
    p.sendline(b"c")

    p.recvuntil(b"Breakpoint 2,")
    p.recvline()
    p.sendline(b"x/8wx $rbx")
    recv1 = p.recvline()[:-1]
    recv2 = p.recvline()[:-1]
    p.close()
    words1 = [*map(lambda x: int(x, 16), recv1.decode().split(":")[1][1:].split("\t"))]
    words2 = [*map(lambda x: int(x, 16), recv2.decode().split(":")[1][1:].split("\t"))]
    words = words1 + words2

    for i in range(32):
        table[i][c] = get(words, i)

for i in range(32):
    res = map(lambda x: f"{x:#04x}", table[i])
    print(f'[{i}]: {", ".join(res)}')

入力は適当に与えてしまって、後からデバッガでよい感じの入力を与えたことにすれば、空白文字なりなんなりの制限がある文字列も「与えた」ことにできるんですよね。というので、そういうのを書きました。

どうやら 0x80 以上の byte を与えたときは全単射じゃないっぽそうでしたが、それ未満では下記のような規則になっていそうでした。

0x00: {9,8,b,a,d,c,f,e,1,0,3,2,5,4,7,6}{b,9,f,d,3,1,7,5}
0x01: {7,6,5,4,3,2,1,0,f,e,d,c,b,a,9,8}{f,d,b,9,7,5,3,1}
0x02: {a,b,8,9,e,f,c,d,2,3,0,1,6,7,4,5}{5,7,1,3,d,f,9,b}
0x03: {d,c,f,e,9,8,b,a,5,4,7,6,1,0,3,2}{1,3,5,7,9,b,d,f}
0x04: {9,8,b,a,d,c,f,e,1,0,3,2,5,4,7,6}{6,4,2,0,e,c,a,8}
0x05: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{b,9,f,d,3,1,7,5}
0x06: {3,2,1,0,7,6,5,4,b,a,9,8,f,e,d,c}{a,8,e,c,2,0,6,4}
0x07: {0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f}{1,3,5,7,9,b,d,f}
0x08: {9,8,b,a,d,c,f,e,1,0,3,2,5,4,7,6}{2,0,6,4,a,8,e,c}
0x09: {9,8,b,a,d,c,f,e,1,0,3,2,5,4,7,6}{7,5,3,1,f,d,b,9}
0x0a: {c,d,e,f,8,9,a,b,4,5,6,7,0,1,2,3}{f,d,b,9,7,5,3,1}
0x0b: {3,2,1,0,7,6,5,4,b,a,9,8,f,e,d,c}{0,2,4,6,8,a,c,e}
0x0c: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{e,c,a,8,6,4,2,0}
0x0d: {a,b,8,9,e,f,c,d,2,3,0,1,6,7,4,5}{3,1,7,5,b,9,f,d}
0x0e: {6,7,4,5,2,3,0,1,e,f,c,d,a,b,8,9}{4,6,0,2,c,e,8,a}
0x0f: {6,7,4,5,2,3,0,1,e,f,c,d,a,b,8,9}{0,2,4,6,8,a,c,e}
0x10: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{a,8,e,c,2,0,6,4}
0x11: {a,b,8,9,e,f,c,d,2,3,0,1,6,7,4,5}{f,d,b,9,7,5,3,1}
0x12: {f,e,d,c,b,a,9,8,7,6,5,4,3,2,1,0}{9,b,d,f,1,3,5,7}
0x13: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{f,d,b,9,7,5,3,1}
0x14: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{6,4,2,0,e,c,a,8}
0x15: {b,a,9,8,f,e,d,c,3,2,1,0,7,6,5,4}{b,9,f,d,3,1,7,5}
0x16: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{e,c,a,8,6,4,2,0}
0x17: {b,a,9,8,f,e,d,c,3,2,1,0,7,6,5,4}{f,d,b,9,7,5,3,1}
0x18: {8,9,a,b,c,d,e,f,0,1,2,3,4,5,6,7}{2,0,6,4,a,8,e,c}
0x19: {c,d,e,f,8,9,a,b,4,5,6,7,0,1,2,3}{7,5,3,1,f,d,b,9}
0x1a: {2,3,0,1,6,7,4,5,a,b,8,9,e,f,c,d}{3,1,7,5,b,9,f,d}
0x1b: {e,f,c,d,a,b,8,9,6,7,4,5,2,3,0,1}{f,d,b,9,7,5,3,1}
0x1c: {7,6,5,4,3,2,1,0,f,e,d,c,b,a,9,8}{f,d,b,9,7,5,3,1}
0x1d: {d,c,f,e,9,8,b,a,5,4,7,6,1,0,3,2}{3,1,7,5,b,9,f,d}
0x1e: {b,a,9,8,f,e,d,c,3,2,1,0,7,6,5,4}{8,a,c,e,0,2,4,6}
0x1f: {1,0,3,2,5,4,7,6,9,8,b,a,d,c,f,e}{e,c,a,8,6,4,2,0}

たとえば、0x02: {a,b,8,9,e,f,c,d,2,3,0,1,6,7,4,5}{5,7,1,3,d,f,9,b} は、「添字が [2] の byte は 00 のとき a5 が返ってくる」「01 のとき a7」「07 のとき ab」「08 のとき b5」... のような意味で書いています。45 が返ってくるのは 70 のときで、これは p のことですね。

結局この表はなんですか?(?)ちゃんと †rev† すればわかる感じですか? 特に Verilog まわりのことはよくわかっていません。

こういう表になる前提であれば、そもそも 00 01 02 03 ... 07 08 10 18 20 ... 70 78 の 23 通りだけ試せばよさそうな気がしてきました(実際にはフラグに特殊文字は入らなさそうだからちょっと減りそう)。

所感

rev は、(まだ体系立てた勉強をしていないせいもあるかもですが)なんというか ad hoc っぽい気持ちになるというか、「今回はそういう簡単なエンコードをされていたからできたけど、そうじゃなかったらどうしようもなくない?」みたいな気持ちになります。 (解きようがない問題は出題されない気もしますが?)

と思ったんですが、そもそも自分がやったのは rev ではなくて実験とエスパーな気がしてきました。(r2 や gdb などで多少の assembly を読んでいるとはいえ)decompile しようとしたりせず雰囲気でやっているのが微妙そうです。 あまり長くない時間のコンテストで答えを出す前提だと仕方なさもありそうですが、復習はした方がよさそうだなと思いました。

pwn だと ROP なり ret2whatever なりの概ね体系立った「まぁざっくりこういう方針のことをやるよね」というのがあると思うんですが、rev だとどういう感じなんですかね。デバッガを使いつつ実験しながら脳筋でやるのは正統派ではなさそう(というか正統派でなくてほしい)みたいな気持ちはあります。時には必要ではありそうだとは思いつつですが。

とりあえず、(小さめの整数)/(それなりの整数) を見れたのでうれしい気持ちになりました。

おわり

おわりです。

*1:GDB で見た方が楽という説もあったかも?