前言:大概快一个多月没有打过堆风水的题目了,期间或许偶尔碰见那么一两道吧,也都比较简单。国外的比赛经常给一些异构 pwn 或者某些偏于真实的程序,大部分都是篡改指针,然后反复跳转,难点在于读懂程序,最近国内比赛多起来了,开始复现国内的题目发现大部分都是堆风水,然后典型的堆覆盖泄露 libc 然后打 apple2 之类的,感觉长时间不写有点退化了,像湾区杯的 armpwn 也是在写题过程中学到了不少知识。
digtal_bomb:

2.35 版本的 libc,开启了 canary,pie 和 Full RELEO
静态分析:
main:
我们先进入到 main 函数去看,做一个简单的逆向,之后得出主要的菜单在 main->sub_1BD8->sub_19AF,简单看一下运行到这里的条件吧,取一点关键的伪代码(命名经过简单修改):
1 | printf("Enter min (0-500): "); |
那么最终要求 result2 == min && result2 == max,这首先要保证 max == min,
然后令 result1 + 1 == max或者result1 - 1 == min即可。但是 result1 和随机数有关我们无法控制,那就尝试构造与随机数无关的 result1,也就是令 max - min == 1,这样 result2 一定大于 result1,且 result1 == min,触发 min = a1 + 1;,最终就是 min = min++;
综上所述,只要我们依次填入符合 n,n+1,n+1 的数即可进入菜单:

菜单:
1 | void __noreturn sub_19AF() |
看似菜单给了四个选项,实则选择 4 就会报错。接下来就一个一个分析吧:
add:

add 函数当中可以输入 idx 和 size,其中限制 size 大小在 0x10-0x800 之间,之后通过 read 往内存写入数据,但是*(_BYTE *)(*((_QWORD *)&unk_4160 + idx) + v4) = 0;会把最后一个字节写为 0,这就是一个典型的 off-by-null 漏洞。
modify:
在一开始有一个 modify 函数,我们可以修改内容,但是只能修改一次,这种类型似乎在哪里见过:

其余就是很正常的 show 和 delete,show 可以用来泄露数据。
解法一:
泄露 libc_base:
libc_base 的泄露十分繁琐,一切归咎于该题不存在 UAF,导致我们无法对已释放的堆块指针进行泄露。那么我们当前的目标就是利用一个 unsortedbin 去泄露 libc_base,但是不存在 UAF 就要求我们去构造堆重叠,覆盖掉未释放的堆指针,最后利用未释放的堆指针进行 show。
再者考虑到两点:一个是我们只可以对堆内存进行一次的修改,另外是我们堆不存在越界写,这就要求我们必须采取混淆 idx,不断地合并和构造堆块。
构造基本格局:
1 | add(0, 0x410) |

这个时候堆 2 和堆 3 会合并成一个 0x880 的大堆,之后覆盖掉原本 chunk3 的 size 和 prev_size,伪造一个 0x550 大小的 chunk 头,但是保留原堆的 fd 和 bk 指针,然后收回剩余的 bin 内存,但是要注意 idx 的变化:
1 | payload = b"\x00" * 0x430 + p64(0) + p32(0x551) |
清理多余的 fd 和 bk 指针:
1 | delete(3) # 0 |
这里清理原先 chunk0 的 fd 和 bk,防止影响后面的内存布局
伪造第二个堆头:
1 | delete(6) # 6 |
释放 idx 5,6 会构造一个大的合并堆块,这个时候 idx5 和 idx6 还没有被污染,依旧和上述伪造堆头的操作一样,覆盖原先 chunk6 的 prev_size 和 size 然后收回多余的 bin 内存。然后利用 off-by-null 修改低字节的指针
还记得我们上一个伪造的堆头放在哪里了吗?在原先 chunk3 的堆头,这个时候我们把 idx6 指针指向当前 chunk3 ,这个时候的当前 chunk3 的堆头在原先 chunk3 堆头的 addr-0x20 处,注意区分一下:

Unlink:
我们前面伪造了一个在原先 idx3 的 0x560 大小的堆,在改堆+0x550 处还存在一个大小为 0x500 的堆,倘若我们触发 unlink 去将两个堆合并成一个大 unsortedbin ,我们就可以利用 show 函数去泄露 main_arena。
考虑一下:0x560 大小肯定是低地址,是合并堆块的前一部分,我们需要在伪造的 0x500 的堆块再伪造一个 prev_size,保证符合 unlink 的条件:
1 | delete(4) # 4 |

正好伪造到 0x500 的堆的 prev_size 位,且标志位为 0,标记为空闲
触发 unlink 读取 libc:
1 | delete(3) |
delete3 会触发空闲块合并,前面的堆风水正好绕过 unlink 检测触发堆合并构造了一个大小为 0xa50 的大堆块
此时我们就可以去泄露 main_arena,但是当前 idx6 指向原先的 chunk3 的位置,其 data 域在伪造堆块的高地址,距离 0x20,因此我们需要再开辟一个新的 0x20 大小的 chunk8,让 unsortedbin 的 bk 和 fd 往高地址挤 0x20 的大小,这样才可以泄漏到 main_arena:

确实是可以泄露出 libc_base:

泄露 heap_base:
1 | add(9,0x18) |
我们前面利用 add(8,0x18)把整个 unsortedbin 往高地址推了 0x20 个字节,让当前的 chunk6 恰好在 chunk8+0x20 的位置,如果我们再 add(9,0x18),就会导致 idx6 和 idx9 的指针重叠,当我们 delete(6)的时候会发现堆的实际大小是 0x20,就会把 chunk6/9 一同放入 tcachebin

同理此时的 chunk9 的 fd 和 bk 指针是有 safe-linking 保护的,但由于 next_chunk = 0,其 fd 指针实际就是指针原本地址右移 12 位,其实就是 heap_base:

由于 add 函数默认填充一个字节的 a,我们将其去掉即可:
1 | heap_base = (leak_info - 0xa) * 0x10 |
teache poisioning
为了清晰了解当前的对情况,这里直接标注出来:
1 | 0x55a0e6c59290: 0x0000000000000000 0x0000000000000421 # idx2, size=0x420 |
我们可以看到 unsortedbin 虽然是 0xa10 大小的大堆,实际距离 idx4 只有 0x400 的距离,我们申请一个 0x400 大小的堆就可以到 idx4(紧邻但不覆盖),此时我们再申请一个 0x100 的大小的 chunk 就可以和 idx4 完美堆重叠:
1 | add(3, 0x3F0) |
我们之后构造一个投毒,
1 | delete(1) |
由于 idx4 和 idx6 指针指向的地址相同,我们就可以利用 idx6 取修改已经释放的 idx4,类似 UAF
将 idx4 的 fd 指针修改为 stdout,完后重新申请填充 payload 打 houseofapple2 的链条获取 shell:
1 | heap = (heap_base + 0x1050) >> 12 |
总 EXP:
1 | from pwn import* |

中间写的时候绕进去了,晃了很久,这类题目确实该练一练了。
singal:

一个 32 位的 arm 架构,复习一下动态调试的方法:
1 | lsof -i :1234 # 查看当前占用进程 |
启动 qemu-arm:
1 | qemu-arm -g 1234 -L ./ ./cli_patched |
然后 gdb 动调即可:
1 | gdb-multiarch ./cli_patched |

和 mips 一样要先加载,然后就可以了:
1 | set architecture arm |

IDA 静态分析:

IDA 没有给出 main 函数,两个方法找到 main 函数:
- 直接运行程序,查找运行过程中出现的字符串然后找交叉引用:


然后直接就定位到 main 函数了:

- 方法二是笨方法,像这种本来就没多少函数的二进制程序,自己找一找看一眼就能找出来个大概
找到了 main 函数,那就进行程序分析:
分析 main 函数:

1 | int socket(int domain, int type, int protocol); |
创建了一个标准的 IPv4 TCP 套接字,并将返回的文件描述符(sockfd)保存到全局变量 dword_1302C 中
IPv4 会有这么一个结构体:
1 | struct sockaddr_in { |
因此初步操作就是为了改连接 ip 和端口:127.0.0.1:8000
pthread_create 会创建一个新线程,去调用 sub_BAC:
sub_BAC
1 | void __noreturn sub_BAC() |
dword_1302C 正好是一开始创建的 IPV4TCP 协议,向服务器发送 0xA(10)个字节的数据,内容为 "signal\n\0\0",然后可以接受 0x14 个字节的内存
1 | qemu-arm -L ./ ./cli_patched |

1 | nc -lvnp 8000 -s 127.0.0.1 |

它会接收你输入的数据。继续往下看吧:

sub_123C:
这个函数会调用我们刚才输入的内容:

这个函数比较抽象,非常长,但是实际呢,就那么个意思:
1 | sig1: |
回到 main 函数,看一看菜单:
case1:
1 | int __fastcall sub_DF0(int a1) |
代码看起来很长,但是逻辑其实十分简单:
假如 v3 = 1,就会进入:
1 | puts("You are about to run a red light, the surveillance system will take photos to record it!"); |
1 | char v6[4]; // [sp+14h] [bp-50h] BYREF |
read 会先写 0x34 个字节的内容到(void *)(a1 + 1),由于 sprintf 的特性,会把内容先写到 v6,再打印
这就导致这 0x34 的字节在char v6[4];上会存在栈溢出
case2:
1 | ssize_t sub_101C() |
这里控制 sig2 = 5 就可以进入到关键 if:
1 | pthread_mutex_unlock(&stru_13014); |
我们可以输入 nbytes 的值,假如 HIBYTE(nbytes[0])>=LOBYTE(nbytes[0]),就会 read(0, buf, LOBYTE(nbytes[0])),如果我们控制 nbytes 是 0xffffffff,那么我们就可以输入 0xffff 个字节大小的数据,从而进行堆 buf 的栈溢出,修改返回地址写一个 ret2shellcode。
EXP 思路:
过随机数
那么首先我们想要保证,我们可以通过控制 sig2 达到越界读的效果,那么我们就可以利用多次触发 sig2 去泄露 canary 和 libc_base。在触发 sig2 的时候有条件,在 sig1 的判断中有一个或 0 或 1 的随机数,我们尝试去重复申请,不通过就重新尝试:
1 | def set_light(light): |

要求假如我们可以进入 v3,那就 return,假如随机数没有通过,那就输入 sig1 = 2 制造 break 重新生成随机数
泄露 canary:
我们知道 nbytes 是写到栈上的,那么我们就可以利用栈去寻找 canary

如下图,我们发现 s 是写道栈上的,且 s+1 可以作为一个指针,s+1 实际上就是栈上的 var_C 写在 s 之后,存放读入 s 的地址,那么就考虑到(void *)(a1 + 1)可以当作一个栈地址的泄露。


动态调试一下,在 sprintf 处下断点:

发现确实在后面有一个指针,指针地址就是一开始写入数据的地址,并且在指针之后还有 canary,也就是前面看到的栈上 var_8,我们可以利用两次把栈地址和 canary 都泄露出来:

1 | 15:0054 0x407ffcf4 ◂— 0x60b91800 |
然后我们就可以得到从 0089 开始输入内容,00b8 中存在栈地址,00bc 存放着 canary

然后我们接下来就可以去泄露栈地址和 canary:
1 | enter_record() |

很奇妙的是,当填充至 0x2f 的时候,除了最后的换行符会多加一个 0xa 的数据,栈地址指针发生了变化,不再指向填入数据的开头,转而指向了 __libc_start_main。
ret2shellcode:

我们拿到了栈地址和 canary 就可以直接进行栈溢出,打 ret2shellcode 了,这里我们在之前说过
1 | recv_menu() |
解释一下 shellcode
1 | adr r0, binsh // r0 = &"/bin/sh" (execve 第 1 个参数: 可执行文件路径) */ |
EXP:
1 | from pwn import * |

在写题目期间遇到了不少问题,和 ai 交流了一番:呃呃呃
目录下有一个 exp.py,是可以打通的,但是现在有几个问题:
原文件只有cli ld-linux.so.3 libc.so.6 libpthread.so.0
- 我需要打通的是 cli_patched,但是我确定这个 cli_patched 是否 patch 成功
- 如果 patch 成功,就去把这个 cli_patched 打通,如果不成功,告诉我为什么,并修复
- exp.py 虽然在本地可以打通,但是把 libc_base 写死了,我们在打远程环境的时候通常不可以这么做,因此不考虑写死 libc_base 的方法
- 题解给出的是打 ret2shellcode,我希望按照这种方法走
一些有关qemu的知识:qemu起的虚拟环境保护全关。关闭了ASLR:启动的地址是固定的;关闭了NX:shellcode可执行。通过判断远程环境是qemu起的还是实体机起的,如果是qemu一般直接用ret2shellcode,否则泄露+attack。即使所给的二进制文件开了NX和pie,也只是对真机环境有效,qemu中还是没有保护。
上面是很多人总结的信息,说栈地址是可执行的,为什么我的不可执行
还是没有获取到 shell 啊,另外我希望就和博客中一样,既能够打通本地,也可以打通远程
按照打远程的方式打本地,把本地打通,之前不是给过别人的博客文章,别人的思路都有了,为什么不能和他写的类似
STACK_PAGE = 0x40800000 不要这种写死的数据
可以参考这个脚本
那不打 cli_patched 了,去打 cli 即可,依旧要求栈可执行,cli 文件的 0x2f 偏移是正确的
当前脚本不能复用的原因是什么
问几个问题:
- 当前这个二进制文件是否栈可执行
- 如果栈可执行,为什么我给的脚本不能够复用
- 当前在思考什么,问题卡在哪里了
泄露出的 stack 是什么
按照这个解释一下

参考资料:https://blog.csdn.net/z22168/article/details/151622368
说些什么吧!