目录
这是什么?
进入逆向的世界。以一个 war game 作为开场,后续不断深入。
这个 war game 是 narnia
。链接此处。
Level 0 -> Level 9,难度不断增加。
踏出第一步,Level 0.
根据官网的提示,SSH 连接到服务器。
所有源文件都在 /narnia
目录,并有编译好的二进制。直接运行即可。所需要解锁的密码在 /etc/narnia_pass/
文件夹。比如这是 Level 0,用户是 narnia0
,那么下一级别 narnia1
用户的密码就在 /etc/narnia_pass/narnia1
里。看完本篇,大家就知道整体流程了。
文章越写越长,一段小小的代码,原本不知道会有这么多东西可以挖掘。但是只有挖的够深,才能学到更多。
可以学到什么?
开篇比较简单,但是也有值得学习的地方。几个点拿出来一起学习讨论。
数据类型长度
再后面的小节中会看到源码中对于变量 val
的定义:
long val=0x41414141
了解一下各个 data type
的长度没有坏处。这里可以找到 Wiki 的详细解释。
long
类型的长度,在任何机器上,至少是 32 位,它的表述范围是 [−2,147,483,647, +2,147,483,647]。
Level 0 的设计,就是要我们覆盖 val
这 32 位的地址空间。继续往后看。
管道
管道是再也熟悉不过的概念了,将一个命令的输出,作为另一个命令的输入。例如这样:
那么,如果要传递两个或者两个以上命令的结果作为第二个命令的输入该怎么做?
比如有三个文件,需要一起输出并排序。
应该这样做:
(cat 1.txt; cat 2.txt; cat 3.txt) | sort
格式化输出
作为工具,列在这里。逆向大概率绕不开看源码,所以输出格式化也是源码阅读中非常重要的一部分。
Endianess
我不是专家,所以把这个问题留给 Wiki。Wiki 做出了很好的解释。
虽然是一个常见的话题,但是最为新手的我总会在实践当中忘记这一点,导致最终的 shellcode 无效。列在这里,作为提醒。
x86 系列的 CPU,都是 Little-Endian
,意味写入程序的 shellcode,都应该从最低位开始。例如要写入 0xdeadbeef
到程序,应该使用如下方式:
echo -e '\xef\xbe\xad\xde' | ./program
Setuid
这个漏洞程序的 setuid bit 是设置了的。
Setuid 可以让运行程序的用户暂时获取程序拥有者的权限。也就是说,我以 narnia0 的身份运行 narnia0,因为设置了 setuid,所以我能暂时获得 narnia1 用户的权限,也就可以查看 /etc/narnia_pass/narnia1 文件的内容,获取到 narnia1 用户的密码。
https://en.wikipedia.org/wiki/Setuid
GDB基础
Debuggin with GDB 是学习 GDB 的好文,可以持续学习。
最直观的学习方式,就是用 GDB 一步一步分析程序的运行过程,对比寄存器的变化。
在目标机器上运行
gdb program-name
获取汇编代码:
# 默认反汇编使用 AT&T 语法,有很多的 % 之类的符号,为了阅读更清晰,多采用 intel 语法
set disassembley-flavor intel
# 反汇编 main 函数
disassemble main
这是目标机器上的汇编:
通过分析执行的逻辑,就可以知道栈里面有什么,每个元素有多少空间分配,以及他们的顺序是什么样的。我把重点代码列在下面:
0x0804855b <+0>: push ebp # 【1】ebp 入栈,ESP 当前指向 0x0804855c
0x0804855c <+1>: mov ebp,esp # 将 esp 保存到 ebp
0x0804855e <+3>: push ebx # 【2】ebx 入栈,ESP 当前指向 0x0804855f
0x0804855f <+4>: sub esp,0x18 # 0x18 是十进制 24,这里分配了 24 个字节给所有的局部变量 也就是 val 和 buf,
0x08048562 <+7>: mov DWORD PTR [esp - 0x8],0x41414141 # 将 val 变量移入栈
0x08048569 <+14>: push 0x8048690
...
0x08048573 <+24>: add esp,0x4
0x08048576 <+27>: push 0x80486c3
...
0x08048580 <+37>: add esp,0x4
0x08048583 <+40>: lea eax,[ebp-0x1c]
0x08048586 <+43>: push eax
0x08048587 <+44>: push 0x80486d9
0x08048591 <+54>: add esp,0x8
0x08048594 <+57>: lea eax,[ebp-0x1c]
0x08048597 <+60>: push eax
0x08048598 <+61>: push 0x80486de
...
0x080485a2 <+71>: add esp,0x8
0x080485a5 <+74>: push DWORD PTR [ebp-0x8]
0x080485a8 <+77>: push 0x80486e7
...
0x080485b2 <+87>: add esp,0x8
0x080485b5 <+90>: cmp DWORD PTR [ebp-0x8],0xdeadbeef
单步运行,关注 registers 的情况。
# 打断点
break *0x0804855b
...
# 运行程序
run
# 查看寄存器情况
info registers
# 执行下一行代码
n
以下是每一行代码执行之后 registers 截图。
建议阅读这两篇文章,第一篇对汇编的方法调用的模式做了讲解,看完之后对下面要讲的前4行代码,能有更深的理解。第二篇讲了什么是 Stack Frame,也就是我们分析的栈空间。
第 0 步(程序开始,准备执行第 1 行代码):
ESP 当前指向 0xffffd6dc,EIP 当前指向 0x0804855b。
第 1 步(第 1 行代码执行完毕,准备执行第 2 行代码):
第 1 行代码 push ebp
执行之后。
【上一步:ESP 指向 0xffffd6dc,EIP 指向 0x0804855b。】
ESP 当前指向 0xffffd6d8,EIP 当前指向 0x0804855b。
ESP 向低位移动了 4 个字节(dc - d8),EBP 入栈。
栈内情况:(栈底)EBP。
第 2 步(第 2 行代码执行完毕,准备执行第 3 行代码):
参数列表和局部变量都从 0xffffd6d8 开始,这是上一步 ESP 的位置。
第 2 行代码 mov ebp,esp
执行之后。
【上一步:ESP 指向 0xffffd6d8,EIP 指向 0x0804855b。】
ESP 当前指向 0xffffd6d8,EIP 当前指向 0x0804855e。
ESP 不变,ESP 的值被存入了 EBP(见上图)。
第 3 步(第 3 行代码执行完毕,准备执行第 4 行代码):
第 3 行代码 push ebx
执行之后。
【上一步:ESP 指向 0xffffd6d8,EIP 指向 0x0804855e。】
ESP 当前指向 0xffffd6d4,EIP 当前指向 0x0804855f。
ESP 向低位移动了 4 个字节,EBX 入栈。
栈内情况:(栈底)EBP -> EBX
第 4 步:
第 4 行代码 sub esp,0x18
执行之后。
【上一步:ESP 指向 0xffffd6d4,EIP 指向 0x0804855f。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048562。
ESP 向低位移动了 24 个字节,为局部变量(val,buf)分配了空间。val 有 4 个字节,buf 有 20 个字节。
栈内情况:(栈底)EBP -> EBX
第 5 步:
第 5 行代码 mov DWORD PTR [ebp-0x8],0x41414141
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048562。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048569。
ESP 不变,val 的值写入 ebp - 0x8 (d0) 的位置。注意是写入,不是入栈。所以 val 的位置紧跟在 EBX 之后。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141)
第 6 步:
push 0x8048690
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048569。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x0804856e。
ESP 向低位移动 4 个字节(bc - b8),0x8048690 入栈。
20 个字节的空位就是 buf 的位置。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x8048690
第 7 步:
call 0x80483f0 <puts@plt>
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x0804856e。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x08048573。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x8048690
第 8 步:
add esp,0x4
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x08048573。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048576。
ESP 向高位移动 4 个字节。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x8048690
第 9 步:
push 0x80486c3
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048576。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x0804857b。
ESP 向低位移动 4 个字节。0x80486c3 覆盖掉之前的 0x8048690。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x80486c3
第 10 步:
call 0x80483d0 <printf@plt>
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x0804857b。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x08048580。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x80486c3
第 11 步:
add esp,0x4
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x08048580。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048583。
ESP 向高位移动 4 个字节。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x80486c3
第 12 步:
lea eax,[ebp-0x1c]
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048583。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048586。
ESP 不变。lea 将一个内存地址,放入 EAX。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> 0x80486c3
第 13 步:
push eax
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048586。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x08048587。
ESP 向低位移动 4 个字节。eax入栈,覆盖掉 0x80486c3。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax
第 14 步:
push 0x80486d9
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x08048587。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x0804858c。
ESP 向低位移动 4 个字节。0x80486d9入栈。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486d9
第 15 步:
call 0x8048440 <__isoc99_scanf@plt>
执行之后,我输入了 20 个 A 加上 4 个 B。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x0804858c。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x08048591。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486d9
第 16 步:
add esp,0x8
执行之后。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x08048591。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048594。
ESP 向高位移动 8 个字节。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486d9
第 17 步:
lea eax,[ebp-0x1c]
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048594。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x08048597。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486d9
第 18 步:
push eax
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x08048597。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x08048598。
ESP 向低位移动 4 个字节。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486d9
第 19 步:
push 0x80486de
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x08048598。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x0804859d。
ESP 向低位移动 4 个字节。0x80486de 入栈,覆盖 0x80486d9。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486de
第 20 步:
call 0x80483d0 <printf@plt>
执行之后。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x0804859d。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x080485a2。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486de
第 21 步:
add esp,0x8
执行之后。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x080485a2。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x080485a5。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> eax -> 0x80486de
第 22 步:
push DWORD PTR [ebp-0x8]
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x080485a5。】
ESP 当前指向 0xffffd6b8,EIP 当前指向 0x080485a8。
ESP 向低位移动 4 个字节。ebp - 0x8 位置上的值就是 val 的值,入栈。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> val (0x??????) -> 0x80486de
第 23 步:
push 0x80486e7
执行之后。
【上一步:ESP 指向 0xffffd6b8,EIP 指向 0x080485a8。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x080485ad。
ESP 向低位移动 4 个字节。0x80486e7 入栈,覆盖 0x80486de。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> val (0x??????) -> 0x80486e7
第 24 步:
call 0x80483d0 <printf@plt>
执行之后。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x080485ad。】
ESP 当前指向 0xffffd6b4,EIP 当前指向 0x080485b2。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> val (0x??????) -> 0x80486e7
第 25 步:
add esp,0x8
执行之后。
【上一步:ESP 指向 0xffffd6b4,EIP 指向 0x080485b2。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x080485b5。
ESP 向高位移动 8 个字节。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> val (0x??????) -> 0x80486e7
第 26 步:
cmp DWORD PTR [ebp-0x8],0xdeadbeef
执行之后。
【上一步:ESP 指向 0xffffd6bc,EIP 指向 0x080485b5。】
ESP 当前指向 0xffffd6bc,EIP 当前指向 0x080485bc。
ESP 不变。
栈内情况:(栈底)EBP -> EBX -> 变量 val (0x41414141) -> 20 个字节空位 -> val (0x??????) -> 0x80486e7
到这里,经过这么一些列的操作,这里将 ebp - 0x8 位置上的值和 0xdeadbeef 做比较,再决定后面的流程。
回看第 15 步,调用 scanf
方法接受输入,我输入了 20 个 A 加 4 个 B。
输入完成之后,使用
x/s address
查看内存中的内容。
可以看到 20 个 A 从 bc
位置开始写入。
又可以看到,在 d0
的位置,开始写入了 4 个 B
。
回看第 5 步,val 的值是被写入到 d0 (ebp - 0x8)
的位置,因此,val 的值被覆盖了。
回看第 26 步,取的就是 [ebp - 0x8] 也就是 d0
位置上的值与 0xdeadbeef
进行比较,正是我们可以覆盖的值。
整个分析就结束了。
内存
能看完上面 GDB 的分析,相比对于该程序的栈在内存中的样子应该了解了。
程序在内存中大致如下图:
scanf 方法对于输入的长度没有检测,所以输入 24 个字符之后,val 被覆盖。
如何利用这个漏洞?
源码如下:
#include <stdio.h>
#include <stdlib.h>
int main(){
long val=0x41414141;
char buf[20];
printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
printf("Here is your chance: ");
scanf("%24s",&buf);
printf("buf: %s\n",buf);
printf("val: 0x%08x\n",val);
if(val==0xdeadbeef){
setreuid(geteuid(),geteuid());
system("/bin/sh");
}
else {
printf("WAY OFF!!!!\n");
exit(1);
}
return 0;
}
只要输入的 val
等于 0xdeadbeef
,就会以 system
身份调用 /bin/sh
。那么写入目标值,并将 cat /etc/narnia_pass/narnia1
作为 system
调用的输入。
(python -c 'print "A" * 20 + "\xef\xbe\xad\xde"'; echo 'cat /etc/narnia_pass/narnia1') | ./narnia0
升级成功,获取到 narnia1
的密码,可以登录到 narnia1
,接受下一个挑战。
参考链接
- https://stackoverflow.com/questions/11917708/pipe-multiple-commands-into-a-single-command
- https://en.wikipedia.org/wiki/Endianness
- https://stackoverflow.com/questions/32455684/unix-linux-difference-between-real-user-id-effective-user-id-and-saved-user
- https://www.geeksforgeeks.org/real-effective-and-saved-userid-in-linux/
- https://en.wikipedia.org/wiki/C_data_types
- https://stackoverflow.com/questions/15108932/c-the-x-format-specifier
- https://www.geeksforgeeks.org/signals-c-language/
- https://www.geeksforgeeks.org/signals-c-set-2/
- https://en.wikipedia.org/wiki/C_signal_handling
- https://en.wikipedia.org/wiki/Signal_(IPC)
- http://sourceware.org/gdb/current/onlinedocs/gdb/Machine-Code.html
- https://alvinalexander.com/programming/printf-format-cheat-sheet/
- https://sourceware.org/gdb/current/onlinedocs/gdb/Frames.html
- https://en.wikipedia.org/wiki/Setuid