[2024 羊城杯]pic
本文最后更新于31 天前,其中的信息可能已经过时,如有错误可以直接在文章下留言

学期刚开始的羊城杯,拖到现在才复现

题目描述

//gogo,look the flag!

附件在下面链接有,NSSCTF也有,补一下之前的复现。

Release 附件下载 · CTF-Archives/2024-YCB-Undergraduate

看题目描述,应该是用Golang写的

题目附件给了一个可执行程序和一个flag.png

尴尬了,这里复现我找了半天找不到主函数在哪,但是记得当时比赛一下就找到了,好像是搜索flag.png就行了,看了别人的Writeup,才发现IDA 7.7恢复不了Golang的符号表,导致即使我找到了主函数引用的字符串,通过交叉引用也找不到主函数,换成IDA 8.3一下就找到了,好吧,又学到了。之前得知IDA 7.7的反编译插件更全,以为还是IDA 7.7更权威,看来高版本也有高版本的好。

这里给的flag.png格式不对,PNG图片的文件头为89 50 4E 47,文件尾为AE 42 60 82

【CTF杂项】常见文件文件头文件尾格式总结及各类文件头_ctf常见文件头-CSDN博客

题目这里应该是改了PNG文件数据,我们恢复回来应该能得到flag的图片。

主逻辑反编译后的代码如下,变量和函数我自己已经进行了重命名

// main.main
void __fastcall main_main()
{
  __int64 v0; // rax
  int v1; // ecx
  __int64 v2; // rbx
  int v3; // edi
  int v4; // esi
  int v5; // r8d
  int v6; // r9d
  int v7; // r10d
  int v8; // r11d
  __int64 v9; // r14
  int v10; // ecx
  int v11; // r8d
  int v12; // r9d
  int v13; // r10d
  int v14; // r11d
  string *p_string; // rax
  int v16; // ebx
  int v17; // r8d
  int v18; // r9d
  int v19; // r10d
  int v20; // r11d
  int v21; // ecx
  int v22; // r8d
  int v23; // r9d
  int v24; // r10d
  int v25; // r11d
  char *keylen; // rbx
  int v27; // r8d
  int v28; // r9d
  int v29; // r10d
  int v30; // r11d
  int rc4_key; // eax
  int v32; // ecx
  int v33; // r8d
  int v34; // r9d
  int v35; // r10d
  int v36; // r11d
  int v37; // r8d
  int v38; // r9d
  int v39; // r10d
  int v40; // r11d
  __int64 v41; // [rsp-36h] [rbp-C8h]
  __int64 v42; // [rsp-36h] [rbp-C8h]
  __int64 v43; // [rsp-36h] [rbp-C8h]
  __int64 v44; // [rsp-36h] [rbp-C8h]
  __int64 v45; // [rsp-36h] [rbp-C8h]
  __int64 v46; // [rsp-36h] [rbp-C8h]
  __int64 v47; // [rsp-2Eh] [rbp-C0h]
  __int64 v48; // [rsp-26h] [rbp-B8h]
  char v49; // [rsp+Ah] [rbp-88h] BYREF
  __int64 v50; // [rsp+32h] [rbp-60h]
  string *struct_input; // [rsp+3Ah] [rbp-58h]
  __int128 v52; // [rsp+42h] [rbp-50h] BYREF
  _QWORD v53[2]; // [rsp+52h] [rbp-40h] BYREF
  _QWORD v54[2]; // [rsp+62h] [rbp-30h] BYREF
  _QWORD v55[3]; // [rsp+72h] [rbp-20h] BYREF

  while ( &v52 + 8 <= *(v9 + 16) )
    v0 = runtime_morestack_noctxt();
  v55[2] = 0LL;
  if ( main_isDebuggerPresent(v0, v2, v1, v3, v4, v5, v6, v7, v8) )// 
    // 反调试函数
    os_Exit(0, v2, v10, v3, v4, v11);
  v55[0] = &RTYPE_string;
  v55[1] = &off_FCD470;
  fmt_Fprintln(off_FCD9F8, qword_1051D60, v55, 1, 1, &off_FCD470, v12, v13, v14, v41);// 
  // 打印"Welcome to the DASCTF,please input your key:"
  p_string = runtime_newobject(&RTYPE_string);
  struct_input = p_string;
  p_string->ptr = 0LL;
  v54[0] = &RTYPE__ptr_string;
  v54[1] = p_string;
  v16 = qword_1051D58;
  fmt_Fscan(off_FCD9D8, qword_1051D58, v54, 1, 1, v17, v18, v19, v20, v42);
//要求输入
  if ( struct_input->len != 5 )
//检查输入的长度是否为5
    os_Exit(0, v16, v21, 1, 1, v22);
  v53[0] = &RTYPE_string;
  v53[1] = &off_FCD480;
  fmt_Fprintln(off_FCD9F8, qword_1051D60, v53, 1, 1, v22, v23, v24, v25, v43);
  keylen = struct_input->ptr;
  rc4_key = runtime_stringtoslicebyte(&v49, struct_input->ptr, struct_input->len, 1, 1, v27, v28, v29, v30, v44);
//应该是转换成字节串的函数
  v50 = main_NewCipher(rc4_key, keylen, v32, 1, 1, v33, v34, v35, v36, v45);
  os_OpenFile("./flag.png", 10, 0, 0, 1, v37, v38, v39, v40, v46, v47, v48);
//打开flag.png文件
}

因为题目保留了符号表,所以分析起来并不是特别的难,这里动态调试一直跟踪就好了,很好发现struct_input 是一个结构体,由输入和输入长度组成,这一点Golang比C的反编译代码明显好多。

然后main_NewCipher就是个RC4加密,代码如下

// main.NewCipher
_DWORD *__golang main_NewCipher(
        __int64 key,
        __int64 keylen,
        __int64 a3,
        __int64 a4,
        __int64 a5,
        __int64 a6,
        __int64 a7,
        __int64 a8,
        __int64 a9)
{
  __int64 v9; // r14
  _DWORD *sbox; // rax
  __int64 i; // rcx
  __int64 i_0; // rbx
  int j; // esi
  int tmp; // r9d
  void *retaddr; // [rsp+8h] [rbp+0h] BYREF
  __int64 v16; // [rsp+10h] [rbp+8h]
  __int64 keycopy; // [rsp+10h] [rbp+8h]
  __int64 v19; // [rsp+20h] [rbp+18h]

  while ( &retaddr <= *(v9 + 16) )
  {
    keycopy = key;
    v19 = a3;
    runtime_morestack_noctxt(key, keylen, a3, a4, a5, a6, a7, a8, a9);
    key = keycopy;
    a3 = v19;
  }
  if ( keylen && keylen <= 256 )
  {
    v16 = key;
    sbox = runtime_newobject(&RTYPE_main_Cipher);
    for ( i = 0LL; i < 256; ++i )
      sbox[i] = i;     // S盒初始化
    i_0 = 0LL;
    j = 0;
    while ( i_0 < 256 )
    {
      tmp = sbox[i_0];
      if ( i_0 % keylen >= keylen )
        runtime_panicIndex(i_0 % keylen);
      j += tmp + *(v16 + i_0 % keylen);
      sbox[i_0] = sbox[j];
      sbox[j] = tmp;      // 打乱S盒
      ++i_0;
    }
  }
  else
  {
    runtime_convT64(keylen, keylen, a3, a4, a5, a6, a7, a8, a9);
    return 0LL;
  }
  return sbox;
}

这里的变量我也进行了重命名,看起来就很明显是RC4加密了,但是这里并不完整,还有生成密钥流并与明文异或的代码,这里并没有

但是后续打开了flag.png文件之后就没有伪代码了,这里不知道为什么,这是一个值得探讨的问题,是出题人故意的还是怎样,在这打一个问号 ?

所以这里就要求我们不得不去看汇编了,这里既然打开了flag.png文件,那么后续应该就会读取flag.png的数据,我们就动态调试追踪其数据放在了哪里

flag.png的数据如下

输入12345,后续阅读汇编,发现数据存储在rsi寄存器当中,如图

后续的汇编指令对flag.png处理的逻辑如下,rcx寄存器存储flag.png数据的大小,这里的指令将flag.png的所有数据与我们输入的第二位,即RC4密钥的第二位进行异或运算。

最后再进行数据处理的汇编指令如图

剩下的RC4加密的代码也包括在这里了,flag.png的数据与RC4加密算法产生的密钥流进行异或,这里最终还多异或了一个0x11,在撕这里的汇编的时候,学到了取模256的时候,通过r10寄存器r10d到r10b来实现。

那这里解题的思路就只有爆破RC4加密算法的密钥,也就是我们的输入,爆破直到flag.png的文件头匹配PNG的文件头。

爆破脚本如下

import itertools
from tqdm import tqdm

# 初始化 S 数组,使用 RC4 密钥调度算法(KSA)
def initialize(key):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + ord(key[i % len(key)])) % 256
        S[i], S[j] = S[j], S[i]
    return S

# 生成密钥流,使用伪随机生成算法(PRGA)
def generate_key_stream(S, length):
    i = j = 0
    key_stream = []
    for _ in range(length):
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]
        key_stream.append(S[(S[i] + S[j]) % 256])
    return key_stream

# 解密函数,通过 RC4 密钥流与输入数据进行异或运算
def decrypt(data, key):
    S = initialize(key)
    key_stream = generate_key_stream(S, len(data))
    flag = [(data[i] ^ key_stream[i] ^ 0x11 ^ ord(key[1])) for i in range(len(data))]
    if flag == [0x89,0x50,0x4E,0x47]:  # 检查是否匹配 PNG 文件头标识符
        return key

# 生成所有可能的 5 字符 RC4 密钥
def generate_rc4_key():
    chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    return (''.join(key_tuple) for key_tuple in itertools.product(chars, repeat=5))

# 主程序,尝试每一个密钥进行解密
data = [0x85, 0x43, 0x72, 0x78]
for key in tqdm(generate_rc4_key()):
    decrypted_key = decrypt(data, key)
    if decrypted_key:
        print(f"Found key: {decrypted_key}")
        break
#Found key: 0173d

来自博客🤗回来啦,就知道你舍不得~

打开可执行程序输入密钥即可得到图片

所以flag就是DASCTF{good_y0u_get_the_ffffflag!}

当然这里的博客爆破的时候是把数字放在前面,所以得到答案就会很快,但是如果爆破的时候把字母放在爆破表前面,就要爆破很久,建议还是用C语言,我还看到有用多线程爆破的,有空学习一下,不知道是不是效率会更高。

顺便把有爆破脚本的文章都放下面,下次学习的时候就不用找了

2024 羊城杯个人部分Writeup – lrhtony 的小站

2024羊城杯-re – CoolBreeze ‘s blog.

2024年羊城杯粤港澳大湾区网络安全大赛WP-Reverse篇

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇