workspace/posts/
Blog
技术文章与安全实践记录。
- 两百行代码搓一个原始神经网络
这个文章主要还是因为这周研究了挺久的AI相关的东西,然后休闲时刷视频发现了这个
We’re Building Computers Wrong
闲的没事尝试复现了一下其中的第一个demo,这也是是一个很简单但很有意思的demo 如果你闲的蛋疼可以考虑看一下视频,就能大致知到原理了,当然你也可以考虑跳过,我在下面简述一下
另外完整的代码我已经放在 github 了,点一下我头像下面的连接就可以找到
基本原理
如果你看了视频,这一步可以跳过
1958年,Frank Rosenblatt提出了一种模仿神经发射信号方式的机器 Mark I 感知机,Mark I 的输入层是一个 20x20 的感光单元矩阵,共400个光电探测器,用于"看"图像,并将光学信号转化为电信号,神经元之间连接的"权重"被编码在电位器中 回到神经细胞(这里不是严格生物意义上的神经细胞)上,一个神经细胞可以有两种激活状态,激活或者不激活,而神经细胞之间可以有连接,如果一个神经细胞连接数个神经细胞,这个细胞的激活状态由其他神经细胞决定,这些神经细胞的激活与不激活的状态通过连接传递给这个神经细胞,不同的神经细胞之间的连接强度不同,部分连接可以起到刺激作用,部分为抑制作用,当整合后的信号超过某个阈值,这个神经细胞就会激活
我们可以把这里的连接强度视作一种权重,不同神经细胞的输入乘以对应的权重的和是否能够达到某个阈值决定下一个神经细胞的状态,因此我们可以很简单的模拟出神经细胞的运作方式,复现一下 Mark I 感知机 有可能你看的一头雾水,直接上实战演示
搓一个原始神经网络
这里的原理并不是特别复杂,因此你甚至可以使用c语言搓一个出来,所以我们就直接掏出巨大的c语言,直接做一个简单的demo使得可以分辨方形和圆形
基本处理
为了方便接下来的代码编写,我们先搓一个common.h文件
宏
#ifndef COMMON_H #define COMMON_H #include <stdio.h> #include <stdlib.h> #include <string.h> #define WIDTH 20 // 图形宽度 #define HEIGHT 20 // 图形高度 #define BIAS 20.0f // 分类阈值神经元输入
首先我们需要一个
20*20的"感光单元矩阵",当然我们使用一个20*20char数组就行了,数组的每个索引处可以是一个数,这里我们为了方便,只采用0和1,这20*20的每个神经元的权重我们同样采用一个数组保存,但是这个没办法缩减,使用一个float数组typedef unsigned char Image[HEIGHT][WIDTH]; typedef float Weights[HEIGHT][WIDTH];图像的保存与加载
这里为了简单方便的表示,我们就不使用.png .jpg之类的复杂的图片,我们使用一个.bin文件,每个字节代表一个像素,每个图片20*20=400个字节
static inline int load_image(const char *filepath, Image img) { FILE *f = fopen(filepath, "rb"); if (!f) return -1; size_t n = fread(img, sizeof(unsigned char), HEIGHT * WIDTH, f); fclose(f); return (n == HEIGHT * WIDTH) ? 0 : -1; } static inline void save_weights(const Weights w, const char *path) { FILE *f = fopen(path, "wb"); if (!f) { fprintf(stderr, "ERROR: cannot write weights to %s\n", path); exit(1); } fwrite(w, sizeof(float), HEIGHT * WIDTH, f); fclose(f); }连接权重的加载
同样,我们可以给将权重保存到.bin文件,并从此加载
- Ret2dlresolve
CTF
理论部分
首先最好确认你学习过ELF文件结构相关的内容,当然如果没学过这里也会简单的讲解一些 本文主要讲靠pwntools的Ret2dlresolvePayload工具一把梭所需payload,这比手搓payload轮椅太多了,手搓payload咕到下回了,毕竟国道上的大运只管开就好了,但旁边的小轿车考虑的可就多了
这里来复习一下动态链接和延迟绑定
图片中展示了当我们call一个函数时的大致流程(虽然缺少了部分内容,但是还是很直观的),如果我们首次 call 一个函数,首先是PLT[n]被执行,jmp 到 GOT 表中 对应的条目,但此时该GOT条目里存的还不是该函数的真实地址,而是 该函数 plt表 中下一条指令的地址,然后此时会push reloc_arg,它是在重定位表 .rel.plt 中,用来定位函数相关信息的一个偏移量,接下来跳转到 PLT 的起始部分,通常称为 PLT[0],然后PLT[0] 会先 push 一个 link_map 结构体的地址(通过 GOT[1] 获得),然后 jmp 到 _dl_runtime_resolve 函数(其地址存储在 GOT[2] 中)解析和填充GOT[n]为真实的函数地址,这个过程中我们只需要在可控的内存中伪造_dl_runtime_resolve的参数,也就是link_map 和 reloc_arg
demo
32位 非 Full RELRO
示例demo
#include <stdio.h> #include <unistd.h> static void vuln(void) { char buf[64]; puts("Input:"); read(0, buf, 0x200); } int main(void) { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); vuln(); return 0; }使用以下命令编译
- 栈迁移 - Pivoting
CTF
栈迁移
之前答应某人写一篇文章讲栈迁移,这里附带一些栈相关的知识,尽可能全面的讲述栈和栈迁移,结合例题应用 文章讨论的内容都建立在
x86-64 linux上栈相关寄存器
rsp
- 指向当前栈顶,决定 CPU 从哪里取返回地址与 ROP 数据
rbp
- 指向当前函数栈帧基址,用作稳定的栈内数据定位与栈迁移跳板
栈相关的指令
这里只讲一些需要特别注意的指令,add sub一类明显的就略过了
push
push是一个压栈指令,可以将寄存器立即数或内存中的数据压入栈中,这条指令的具体操作包括
- rsp - 8
- 将指定数据写在rsp指向的内存处 需要注意是先rsp减小,再写入数据,当压入立即数时会先符号扩展至8字节,再压入
pop
pop与push相反,用于弹栈,可以将rsp处的值弹出,他的流程大致为
- 弹出rsp处的值
- rsp + 8 需要注意pop事实上只移动了rsp,原本rsp处弹栈的数据并不会被清理
call
事实上call是一条隐式的push,当我们通过call调用某个地址时,实际做的是
- push rip
- jmp addr 它会将call的下一条指令的地址压,并jmp到刚刚call的地址
ret
和call相反,ret则是一条隐式的pop,当执行ret时
- pop rip
- jmp rip 会将之前call压栈的返回地址pop到rip,并跳转
leave
leave则是栈迁移的一条关键指令,主要作用是销毁栈 执行的时候
- mov rsp, rbp
- pop rbp 会将rsp移动到rbp处,并将当前位置的地址pop到rbp完成对rbp的移动
如何迁移
对于迁移栈,我们的目的是将栈移动到一个更加可控的位置,或利用有限的溢出大小实现更长的ROP链,也可以通过多次栈迁移布置特定的数据结构栈风水等
gadget
对于gadget的选取,一般有两种选择
pop rbp retn leave retn这些gadget可以帮助我们控制rbp进而间接控制rsp
迁移目标位置的选择
可以通过gdb调试寻找一些可读可写的区域
pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA Start End Perm Size Offset File (set vmmap-prefer-relpaths on) 0x400000 0x401000 r--p 1000 0 pwn 0x401000 0x402000 r-xp 1000 1000 pwn 0x402000 0x403000 r--p 1000 2000 pwn 0x403000 0x404000 r--p 1000 2000 pwn 0x404000 0x405000 rw-p 1000 3000 pwn 0x405000 0x426000 rw-p 21000 0 [heap] 0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7c28000 0x7ffff7db0000 r-xp 188000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7db0000 0x7ffff7dff000 r--p 4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7dff000 0x7ffff7e03000 r--p 4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7e03000 0x7ffff7e05000 rw-p 2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6 0x7ffff7e05000 0x7ffff7e12000 rw-p d000 0 [anon_7ffff7e05] 0x7ffff7fae000 0x7ffff7fb1000 rw-p 3000 0 [anon_7ffff7fae] 0x7ffff7fbd000 0x7ffff7fbf000 rw-p 2000 0 [anon_7ffff7fbd] 0x7ffff7fbf000 0x7ffff7fc3000 r--p 4000 0 [vvar] 0x7ffff7fc3000 0x7ffff7fc5000 r-xp 2000 0 [vdso] 0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7fc6000 0x7ffff7ff1000 r-xp 2b000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]以这里为例,
0x404000 0x405000 rw-p 1000 3000 pwn就是一个很不错的选择
Terminal
C0nvR3 Lab terminal ready. Type help for commands.