pwnable.tw 之 start ORW 3x17

这里把pwnable.tw上前面的几个题放在一起写篇博客,分别是:

  • start
  • orw
  • 3x17

1 start

1.1 题目

https://pwnable.tw/challenge/#1

1.2 分析

首先,查看文件属性和开启的保护措施:

运行起来试试:

IDA查看二进制文件start:

发现除了_start和_exit没有熟悉的main函数,使用IDA查看伪代码也不是一个正常的函数,原因在于这是一个纯汇编代码。

因此我们需要看懂这段汇编都做了些什么:

1 压了_exit函数的地址

2 清eax,ebx,ecx,edx

3 压字符串,20个字节

4 分别给eax,ebx,ecx,edx赋值(4,1,esp,20),然后int 80h系统调用

5 清ebx,给eax,edx赋值(3,60),然后int 80h系统调用

6 esp加20个字节收回栈空间

7 根据栈上的返回地址(_exit)返回

可以看到有两次系统调用(eax是系统调用号,然后ebx,ecx,edx,esx,edi分别放置系统调用的参数)。查表可知4对应的系统调用是write,3对应的是read。

因此以上4和5可以翻译成:

1
2
write(1,esp,20); // 从栈上读20个字节到标准输出(读内存)
read(0,esp,60); // 从标准输入写60个字节到栈上(写内存)

很明显是个栈溢出,read的60个字节会覆盖到返回地址exit。

1.3 利用

exit被覆盖后,就控制了eip,此时应该让eip指向哪儿才能get shell呢?当然是指向我们构造的一段shellcode,shellcode应该放哪儿呢?如果放在栈上,但此时我们并不知道栈的地址,那么能不能泄露栈地址呢?再看一下汇编代码:

第一次执行完retn之后,esp指向下图位置:

如果此时从08048070处开始执行,就可以将old esp的值打印出来,old esp = esp+4。并且可以继续从此esp指向的位置写0x3C字节,如下图所示,esp往上是第二次的输入,我们可以好好构造这次输入,让下次执行retn时,再一次劫持eip(即ret addr),将ret addr覆盖为shellcode addr即可。

现在重新理一下,oldesp是write系统调用时泄露出来的,因此shellcode addr是old esp+0x14。那么现在需要找一段合适的shellcode来get shell。

http://shell-storm.org/shellcode/找到一段长度合适的shellcode如下:

exp如下:

执行结果如下:

1.4 记录

1.4.1 pwntools的使用

https://pwntools.readthedocs.io/en/stable/about.html

http://brieflyx.me/2015/python-module/pwntools-intro/

pwntools的cyclic:

https://www.cnblogs.com/liuyimin/p/7379985.html

1.4.2 gdb-peda的使用

https://introspelliam.github.io/2017/08/03/pwn/gdb%E7%9A%84%E8%B0%83%E8%AF%95%E4%B8%8E%E4%BD%BF%E7%94%A8/

pwntools + gdb:

http://docs.pwntools.com/en/stable/gdb.html?highlight=gdb#module-pwnlib.gdb

1.4.3 shellcode database

http://shell-storm.org/shellcode/

2 orw

2.1 题目

https://pwnable.tw/challenge/#2

2.2 分析

首先,查看文件属性和开启的保护措施:

运行起来试试:

IDA查看二进制文件orw:

伪代码如下:

【orw_seccomp()是一个设置函数,这里的作用是设置只能使用open,read,write三个系统调用。具体原理参考2.4.2中。】

read从标准输入读取数据放到shellcode地址处,然后转到shellcode处去执行代码。

shellcode地址如下:

view, open subviews, segments查看bss段属性:

【这里显示不可执行,但checksec中RWX属性为has RWX segments且实际shellcode放到bss段中后可执行,这里目前还不知道是为什么???】

2.3 利用

题目只允许使用open,read,write三个系统调用函数,因此通过这三个函数实现打开/home/orw/flag文件,将其读到bss段或者栈中,然后再将bss或栈中的数据写到标准输出(即屏幕上)打印。


payload对应的汇编代码:

执行得到flag:


2.4 记录

2.4.1 64位ubuntu安装32位库

ubuntu 64位版本,安装支持32位程序的二进制库。

sudo dpkg –add-architecture i386

sudo apt-get update

sudo apt-get install zlib1g:i386 libstdc++6:i386 libc6:i386

2.4.2 seccomp和prctl

https://blog.betamao.me/2019/01/23/Linux%E6%B2%99%E7%AE%B1%E4%B9%8Bseccomp/

https://www.jianshu.com/p/62ede45cfb2e

https://veritas501.space/2018/05/05/seccomp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

2.4.3 pwnlib.shellcraft.i386

这次题目中只用到了两个重要的函数。

第一个是将字符串push到栈中,此时esp指向的就是这段字符串。

pwnlib.shellcraft.i386.pushstr(string, append_null=True)

1
2
3
4
5
>>>print shellcraft.i386.pushstr('aaaa').rstrip()
/* push 'aaaa\x00' */
push 1
dec byte ptr [esp]
push 0x61616161

第二个是系统调用,syscall是要调用的函数(eax存放系统调用号),后面紧接着的是各个参数(ebx,ecx,edx等)(参数可以是某个寄存器,如’esp’)。

pwnlib.shellcraft.i386.linux.syscall(syscall=None, arg0=None, arg1=None, arg2=None, arg3=None, arg4=None, arg5=None)

1
2
3
4
5
6
7
8
9
10
11
>>> print pwnlib.shellcraft.i386.linux.syscall('SYS_execve', 1, 'esp', 2, 0).rstrip()
/* call execve(1, 'esp', 2, 0) */
push SYS_execve /* 0xb */
pop eax
push 1
pop ebx
mov ecx, esp
push 2
pop edx
xor esi, esi
int 0x80

参考别人的writeup,发现简洁写法(因为提前用context设置了目标环境):

3 3x17

3.1 题目

https://pwnable.tw/challenge/#32

3.2 分析

查看文件类型和开启的保护机制:

是去了符号表的,且静态链接。因此IDA的F5基本上没用了,只能纯看汇编。先执行看看:

属于addr和data,这里猜测会是将data写到addr上。 后续查看汇编代码,确实是这样。

IDA打开二进制文件,只有start函数:

不知道以上各地址和寄存器的值代表什么内容,因此自己写了个printf(“hello world!\n”)的小程序,使用IDA打开,找到start对应的汇编,如下:

因此3x17的start可解析为下图:

64位汇编参数传递规则如下:

因此

__libc_start_main(mian[sub_401B6D], argc, ubp_av, init [loc_4028D0], fini[sub_402960], rtld_fini)

main函数代码如下:

sub_40EE70具体做了什么,看汇编代码太复杂,因此通过gdb调试看结果,使用gdb在401BED处下断点。

断点处信息如下:

执行完该函数之后,返回值会存放在RAX中。刚刚输入的是12345,返回值为0x3039,即12345的十六进制,因此该函数就是将输入的十进制数转换为一个十六进制地址。

init是执行main函数之前会执行的,而fini是main执行完后执行的函数。

因此考虑用任意地址写去覆盖fini的执行流程。如下是fini的汇编代码,在4B40F0处分别调用两个函数,且调用顺序是先fini_array[1]后fini_array[0]。那么只要将数组中的地址覆盖为我们想要的地址,就可以控制程序去执行了。

3.3 利用

获得以上信息之后,我们需要考虑,应当让fini_array的两个函数地址分别被覆盖为什么,才能达到利用的目的。利用就是get shell。在程序中使用strings搜索“/bin/sh”无果,因此一步完成get shell是不可能的,需要寻找其他方法。

main函数中可以实现任意地址写,如果将fini_array[1]的地址指向main,那么似乎就可以继续任意地址写。查看main函数,发现byte_4B9330为1时才能进入任意地址写操作,而我们第二次进入该函数时byte_4B9330已经为2了。怎么办呢?但是看看前面的int8,这是一个8位无符号整形,因此不用加多久,就整数溢出又变成1了。

那么怎么让main一直被调用呢?剩下的fini_array[0]就派上用场了。


把fini_array[1]和fini_array[0]分别覆盖为main和调用array的fini函数,就可以实现如下循环。

这样就可以不限次数的任意地址写了。但是往哪里写,写什么内容呢?因为没有可写可执行段,因此直接把shellcode布置到内存空间中跳转执行是不可能的。那么就只能考虑ROP了,但不知道栈的位置,也没法去布置栈空间实现ROP。不过RIP是我们可以控制的,因此只要存在某一刻rsp会被泄露出来,那么只要在这一刻之前把对应地址空间布置好,那么就可以不断地ret然后把ROP链串起来啦。

回到fini函数中,rbp原本的值被暂时存放在栈中,这里以rbp做临时寄存器,存放了fini_array的起始地址,此时rbp=0x4B40F0。

如果call指令能跳转到leave; ret; 这样的指令去,那么就可以控制rsp的地址了。如下是main函数中一条合适的指令:

1
2
3
4
5
6
7
8
rbp = 0x4B40F0

leave:
mov rsp,rbp rsp = 0x4B40F0, rbp = 0x4B40F0
pop rbp rsp = 0x4B40F8, rbp = fini_array[0]

ret:
pop rip rsp = 0x4B4100, rip = fini_array[1]

这里必须让fini_array[1]为main,fini_array[0]为0x401C4B。这样rip被控制再去执行一次main函数,利用最后的ret,使rip从0x4B4100处执行,这里是我们提前布置好的空间。

那么接下来的工作就是怎么布置0x4B4100以上的空间,通过ROP的方式获取shell。

一条简单的获取shell的命令:

32位系统上通过int 80进行系统调用,64位系统上通过syscall指令实现。根据以上代码,需要控制rax为59(execve的系统调用号,0x3B),rdi为字符串“/bin/sh\x00”的地址,rsi为0,rdx为0。因此rsp指向位置的ROP链应如此布置:

1
2
3
4
5
6
7
8
9
pop_rax
0x3B
pop rdi
addr_of_bin_sh
pop rsi
0
pop rdx
0
syscall

最后,字符串“/bin/sh\x00”随便找一块可写的空间写上去就行。

寻找gadget:

写exp:

执行获得flag:

3.4 记录

3.4.1 64位汇编参数传递

http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/stack-intro-zh/