PaluCTF 2024
总结:Reverse只拿了3个一血,排名吊车尾,还是太菜了,其他的不是很会做
Reverse-签到
方法一:
DFS启动,喜提8192种结果,这8192种结果经过原exe加密后都能得到与原encrypted.txt一模一样的内容,其中flag{}格式的有256种,那么接下来只需爆破平台即可 (汗)
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<string.h>
typedef struct TABLE {
bool isvisit;
unsigned char type;
int val;
int MAX_size;
}TABLE,* PTABLE;
typedef struct ENC {
unsigned char s[256];
}ENC;
int n;
PTABLE Init_table(int MAX_size);
void Insert_table(PTABLE table, unsigned char New_type, int New_val);
void Inserts(PTABLE Table);
int Findval(PTABLE Table, unsigned char Nval);
void DFS(PTABLE Table, ENC now, int index, int len, unsigned char* cipher);
void RecoverString(PTABLE Table, unsigned char* new_cipher);
unsigned char cipher[] = "jmdiz61904646906034535196{";
int main()
{
PTABLE Table = Init_table(15);
Inserts(Table);
RecoverString(Table, cipher);
}
PTABLE Init_table(int MAX_size)
{
PTABLE New_table = (PTABLE)malloc(sizeof(TABLE) * MAX_size);
for (int i = 0; i < MAX_size; i++)
{
New_table[i].isvisit = false;
New_table[i].type = NULL;
New_table[i].val = 0;
New_table[i].MAX_size = MAX_size;
}
return New_table;
}
void Insert_table(PTABLE table, unsigned char New_type, int New_val)
{
int index = 0;
int Max_size = table->MAX_size;
while (1)
{
PTABLE PNow = table + index;
if (!PNow->isvisit)
{
PNow->isvisit = true;
PNow->type = New_type;
PNow->val = New_val;
return;
}
else {
index++;
}
if (index >= Max_size)
{
return;
}
}
}
void Inserts(PTABLE Table)
{
Insert_table(Table, 'a', 3);
Insert_table(Table, 'l', 1);
Insert_table(Table, 'f', 4);
Insert_table(Table, 'g', 2);
Insert_table(Table, '{', -1);
Insert_table(Table, '}', -2);
Insert_table(Table, '1', 5);
Insert_table(Table, '2', 7);
Insert_table(Table, '3', -3);
Insert_table(Table, '4', -3);
Insert_table(Table, '5', -3);
Insert_table(Table, '6', -3);
Insert_table(Table, '7', -3);
Insert_table(Table, '8', -3);
Insert_table(Table, '9', -3);
}
int Findval(PTABLE Table, unsigned char Nval)
{
int index = 0;
int Max_size = Table->MAX_size;
while (1)
{
PTABLE PNow = Table + index;
if (PNow->type == Nval) return PNow->val;
else index++;
if (index >= Max_size) return 0;
}
}
void DFS(PTABLE Table, ENC now, int index, int len, unsigned char* cipher)
{
if (index >= len)
{
n++;
printf("%d: ", n);
for (int i = 0; i < len; i++)
{
printf("%c", now.s[i]);
}
printf("\n");
return;
}
unsigned char tmp;
ENC next;
for (int i = 0; i < index; i++)
{
next.s[i] = now.s[i];
}
for (int k = 32; k <= 127; k++)
{
tmp = k + Findval(Table, k);
if (tmp == cipher[index])
{
next.s[index] = k;
DFS(Table, next, index + 1, len, cipher);
}
}
}
void RecoverString(PTABLE Table, unsigned char* new_cipher)
{
n = 0;
int len_str = strlen(new_cipher);
ENC Origin;
memset(Origin.s, 0, len_str);
DFS(Table, Origin, 0, len_str, new_cipher);
}
方法二:PY
茶
IDA打开,可以看到非常人畜无害的加密流程,而熟悉c++逆向的同学都知道,主程序里除了enc函数外都是些没啥卵用的转换函数,不用管,那就让我们点进去康康。
一打开人直接头晕了,半点"茶(TEA)"的样子都看不来,不过看着前面的一堆也只是移位之类的操作,那就不管了,直接定位到关键的encs加密,点进去康康
由于场面太过血腥暴力我就不放encs函数的内部了,总之就是看不太懂,这时直接祭出Findcrypt大法,查找到一个叫做"salsa20"加密的标识,然后跟进去发现了一串base64密文,解出来说是Salsa20升级版
既然是Salsa20的升级版,那就先了解一下这个Salsa20先,百度一搜,嗯?居然是流密码!!!升级版再怎么样也不会改变加密的性质吧,因此可以基本确定是加密是流密码。
经典的流密码有什么?RC4!
那也就意味着排除反调试的情况下,只要保持key等参数不变,那么只需动调取出最后与明文交汇的值便可,就算有反调试也可借助x64dbg强大的ScyllaHide插件直接绕过
事不宜迟,动调,启动!随便输入一个flag,找到关键的异或点,然后取出所有的异或值,然后直接运行到最终的比较函数
然后发现这样一串奇怪的密文,根据我们输入的flag不难发现规律:
如果加密后得到0x12, 0x34 , 0xAB ,0xCD,那么密文则转化为字符串"1234ABCD"。这样我们的密文就应该是0xf5, 0x68, 0xc4 …这样了,接下来就是写一个脚本梭哈。
EXP:
#include<iostream>
using namespace std;
unsigned char key[] =
{
0x93, 0x04, 0xA5, 0xEE, 0x69, 0xAC, 0xA3, 0xA8, 0x0D, 0x45,
0x2E, 0x28, 0x9D, 0x20, 0x06, 0x27, 0xA2, 0xB8, 0xB2, 0xA1,
0x20, 0xF0, 0x7D, 0x98, 0xDB, 0x36, 0x2F, 0x28, 0x76, 0x62,
0x43, 0xF7, 0xED, 0x8F, 0x4D, 0x69, 0xD2, 0xA5, 0xCD, 0x1C,
0x24, 0x81, 0x96, 0x2B, 0xC8, 0x2E, 0x12, 0x34, 0x8B, 0x94,
0x8E, 0x09, 0xBD, 0x28, 0x4F, 0x0D, 0x05, 0xB0, 0xA7, 0xF6,
0x4F, 0x8C, 0xC5, 0x61, 0x93, 0x04, 0xA5, 0xEE, 0x69, 0xAC,
0xA3, 0xA8, 0x0D, 0x45, 0x2E
};
unsigned char enc[]="f568c48912eed6dc520c7164f44b6378e1d0d3e248914fa8847b405a131f";
unsigned char encs[]={0xf5,0x68,0xc4,0x89,0x12,0xee,0xd6,0xdc,0x52,0x0c,0x71,0x64,0xf4,0x4b,0x63,0x78,0xe1,0xd0,0xd3,0xe2,0x48,0x91,0x4f,0xa8,0x84,0x7b,0x40,0x5a,0x13,0x1f};
int main()
{
unsigned char a;
for(int i=0;i<30;i++)
{
a=encs[i]^key[i];
printf("%c",a);
}
}//flag{But_I_Like_ChaCha20_More}
哎哟好像真是个flag,交上去就行了(
我们可以发现flag的内容为ChaCha20,也就是Salsa20的升级版,好家伙,我是真不会。
这里给出出题人的预期exp,一个标准的chacha20算法,可以好好学习一下
#include <iostream>
#include <string>
#include <vector>
#include <bits/stdc++.h>
// 字节序转换
#define U8TO32_LITTLE(p) \
(((uint32_t)((p)[0])) | ((uint32_t)((p)[1]) << 8) | \
((uint32_t)((p)[2]) << 16) | ((uint32_t)((p)[3]) << 24))
// 四分之一轮
void quarter_round(uint32_t *state, int a, int b, int c, int d) {
state[a] += state[b];
state[d] ^= state[a];
state[d] = (state[d] << 16) | (state[d] >> 16);
state[c] += state[d];
state[b] ^= state[c];
state[b] = (state[b] << 12) | (state[b] >> 20);
state[a] += state[b];
state[d] ^= state[a];
state[d] = (state[d] << 8) | (state[d] >> 24);
state[c] += state[d];
state[b] ^= state[c];
state[b] = (state[b] << 7) | (state[b] >> 25);
}
void chacha20_block(uint32_t *state, unsigned char *output) {
uint32_t working_state[16];
for (int i = 0; i < 16; ++i)
working_state[i] = state[i];
for (int i = 0; i < 10; i++) {
// 列轮
quarter_round(working_state, 0, 4, 8, 12);
quarter_round(working_state, 1, 5, 9, 13);
quarter_round(working_state, 2, 6, 10, 14);
quarter_round(working_state, 3, 7, 11, 15);
// 对角轮
quarter_round(working_state, 0, 5, 10, 15);
quarter_round(working_state, 1, 6, 11, 12);
quarter_round(working_state, 2, 7, 8, 13);
quarter_round(working_state, 3, 4, 9, 14);
}
for (int i = 0; i < 16; ++i) {
state[i] += working_state[i];
uint32_t value = state[i];
output[4 * i + 0] = (unsigned char)(value & 0xff);
output[4 * i + 1] = (unsigned char)((value >> 8) & 0xff);
output[4 * i + 2] = (unsigned char)((value >> 16) & 0xff);
output[4 * i + 3] = (unsigned char)((value >> 24) & 0xff);
}
}
std::vector<unsigned char> chacha20_decrypt(const std::vector<unsigned char> &ciphertext, const std::string &key,
const std::string &nonce) {
// 初始化状态矩阵
uint32_t state[17] = {
0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, // 常量
U8TO32_LITTLE(key.data()), U8TO32_LITTLE(key.data() + 4),
U8TO32_LITTLE(key.data() + 8), U8TO32_LITTLE(key.data() + 12),
U8TO32_LITTLE(key.data() + 16), U8TO32_LITTLE(key.data() + 20),
U8TO32_LITTLE(key.data() + 24), U8TO32_LITTLE(key.data() + 28),
0, 0, // 计数器
U8TO32_LITTLE(nonce.data()), U8TO32_LITTLE(nonce.data() + 4),
U8TO32_LITTLE(nonce.data() + 8)
};
std::vector<unsigned char> plaintext;
// 分块解密
for (size_t i = 0; i < ciphertext.size(); i += 64) {
unsigned char block[64];
chacha20_block(state, block);
// 计数器加 1
state[12]++;
// 密钥流异或
for (size_t j = 0; j < 64 && i + j < ciphertext.size(); ++j) {
plaintext.push_back(ciphertext[i + j] ^ block[j]);
}
}
return plaintext;
}
int main() {
std::vector<unsigned char> cipher = {
0xf5, 0x68, 0xc4, 0x89, 0x12, 0xee, 0xd6, 0xdc, 0x52, 0x0c, 0x71, 0x64, 0xf4, 0x4b, 0x63, 0x78, 0xe1, 0xd0, 0xd3, 0xe2, 0x48, 0x91, 0x4f, 0xa8, 0x84, 0x7b, 0x40, 0x5a, 0x13, 0x1f
};
std::string key = "SGludDogSW1wcm92ZSBvZiBTYWxzYTIw";
std::string nonce = "Is_This_TEA?";
std::vector<unsigned char> test = chacha20_decrypt(cipher, key, nonce);
std::string decrypted_message(test.begin(), test.end());
std::cout << "Decrypted message: " << decrypted_message << std::endl;
}
然后给出yuro哥哥的梭哈脚本
from Crypto.Cipher import ChaCha20
enc_data = bytes.fromhex("f568c48912eed6dc520c7164f44b6378e1d0d3e248914fa8847b405a131f")
key = b"SGludDogSW1wcm92ZSBvZiBTYWxzYTIw"
nonce = b"Is_This_"
cc = ChaCha20.new(key=key, nonce=nonce)
print(cc.decrypt(enc_data))
Auth_System
最良心的一道题目了,真-运行就有flag,ida打开,用Graph看,发现左边流程部分没有被识别出来,正常流程走的也是右边,那就patch一下进去康康,把jz改成jnz
好家伙,直接把flag给爆出来了
O2 Optimization
不太懂为啥这题的解题数这么少,可能是大家把他当异架构做了?
DIE康康,发现啥也没识别出来,但清清楚楚地写着架构为AMD64,就是IDA64打开直接一顿猛猛报错,然后也是一副非常诡异的画面
这里根据经验便可猜测是ELF的头被修改了,我们尝试与正确的ELF文件头作比较,最后发现一处关键的修改点。
简单了解一下ELF结构:
圈红处字节代表着版本,0x1为ELF32,0x2为ELF64,而我们的程序架构为AMD64,所以这里应该为0x2,修改然后保存,再用DIE查看发现正常了。
然后ida打开正常,但main()函数无法f5,提示是在0x2356处出了问题,我们定位到次处,点开sub_26C0这个函数,发现并没有关键的加密部分,那就先nop掉
f5成功后,看到又是个C++程序,直接定位到关键enc()加密函数
c++有很多不明所以的东西,靠经验判断是最快的捷径
这里可以直接猜测加密流程为
for(int i=0;i<len;i++)
{
input[i] = (input[i] + key[i % key_len])%0x80;
}
然后密文和key值从这里来,密文的方式与茶那题是一样的
exp
#include<iostream>
using namespace std;
unsigned char encs[]="364d4d5c3e387e00421c597a0a7302144d5b70087e064619567336297d151f56770a7935424f2a780643";
unsigned char enc[]={0x36,0x4d,0x4d,0x5c,0x3e,0x38,0x7e,0x00,0x42,0x1c,0x59,0x7a,0x0a,0x73,0x02,0x14,0x4d,0x5b,0x70,0x08,0x7e,0x06,0x46,0x19,0x56,0x73,0x36,0x29,0x7d,0x15,0x1f,0x56,0x77,0x0a,0x79,0x35,0x42,0x4f,0x2a,0x78,0x06,0x43};
unsigned char key[]="PaluCTF";
int main()
{
/* for(int i=0;i<sizeof(encs)-1;i+=2)
{
printf("0x%c%c,",encs[i],encs[i+1]);
}*/
for(int i=0;i<sizeof(enc);i++)
{
for(int k=32;k<=127;k++)
{
if(((k+key[i%7])%0x80)==enc[i])
{
printf("%c",k);
}
}
//printf("\n");
}
}
//flag{d80a0d76-23af-486e-a0bc-43a463eac552}
PyLu
方法一:摇一个密码✌
方法二:yuro哥哥的z3 solver法
import z3
from Crypto.Util.number import *
def enc(key):
R = bytes_to_long(b"Welcome To PaluCTF!")
MOD = 2**418
R = R ^ ((R - 60) >> 24)
R = R ^ ((R - 60) << 88)
R ^= key
R = (-R * R * 2024) % MOD
R = (R * key) % MOD
return R
res = 0x2E441F765514CCA89173554726494D37E9FBE774B6F807BC5F6E71117530CE3D7DB5F70554C03CD9055F4E42969600904DF1F4DB8
s = z3.Solver()
key = z3.BitVec("key", 418)
s.add(enc(key) == res)
s.check()
m = s.model()
flag = long_to_bytes(m[key].as_long())
tes(m[key].as_long())
print(flag)
帕鲁被病毒感染了
方法一:摇一个misc✌
方法二:PY