信息安全公开课收集

MIT计算机系统安全 https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-858-computer-systems-security-fall-2014/

窃听大数据 https://learning.edx.org/course/course-v1:CornellX+ENGRI1280x+3T2017/home

高级加密技术 https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-876j-advanced-topics-in-cryptography-spring-2003/

渗透测试课程集合 https://www.classcentral.com/tag/penetration-testing

社会工程 https://www.udemy.com/course/the-art-of-hacking-humans-intro-to-social-engineering/learn/lecture/9706122#overview https://www.coursera.org/learn/security-awareness-training/home/welcome (感觉这里没有大学公开课有用)

机器学习中的数学 笔记 https://ocw.mit.edu/courses/mathematics/18-657-mathematics-of-machine-learning-fall-2015/lecture-notes/

机器学习笔记 https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-867-machine-learning-fall-2006/lecture-notes/

深度学习介绍 http://introtodeeplearning.com/2020/index.html

大数据和机器学习中的数学 https://ocw.mit.edu/resources/res-ll-005-mathematics-of-big-data-and-machine-learning-january-iap-2020/

linux驱动开发-1 环境搭建

安装qemu

1
apt-get install qemu qemu-system qemu-user

安装交叉编译工具链

1
sudo apt-get install gcc-10-aarch64-linux-gnu

Linux内核网站下载5.10内核源码

1
2
3
4
5
6
7
export ARCH=arm64
export CROSS_COPILE=aarch64-linux-gnu-
make defconfig
# 如果需要可以在此 配置initramfs路径为"/tmp/rootfs/xxx"
# 记得勾选上"Compile the kernel with debug info",以及其他自己调试需要的配置选项
make menuconfig
make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

制作根文件系统

1
2
3
4
5
6
7
mkdir rootfs
git clone https://github.com/mengning/menu.git
cd menu
# 修改Makefile,去掉"find init hello ***"和"qemu -kernel ../linux"语句
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img

安装调试环境

1
sudo apt-get install gdb-multiarch

启动调试环境

1
2
# 在一个窗口输入启动
qemu-system-aarch64 -M virt -cpu cortex-a53 -smp 2 -m 4096M -kernel /tmp/arch/arm64/boot/Image.gz -nographic -append "console=ttyAMA0 init=/linuxrc ignore_loglevel" -initrd /tmp/rootfs/rootfs.img -S -gdb tcp::9000

参数解释

1
2
3
4
5
6
7
8
-smp 核数目
-m 物理内存大小
-kernel 内核压缩镜像位置
-initrd rootfs位置
-nographic 不使用图形界面,不加可能会因为无法启动图形界面而失败
-append cmdline启动参数
-S 在入口处阻塞CPU
-gdb tcp::xxxx 指定通信通道为 本地tcp通道(因为是在同一个机器上),端口号为xxxx,如果不需要指定端口号可以用-s 代替

参考资料

QEMU搭建arm64 Linux调试环境 - 小乐叔叔的文章 - 知乎
https://zhuanlan.zhihu.com/p/345232459

驱动开发入门-1

1. 环境准备

开发环境

使用Win10系统为例

  1. Visiual Studio 2019
  2. Windows SDK 10
  3. Windows Driver Kit (WDK)

都是那种直接安装的

调试环境

驱动是内核模块,所以需要整个Windows系统作为调试环境。由于没有双机调试,只能使用虚拟机网络调试或管道调试。

调试环境找到了这篇文章 https://blog.csdn.net/qq_40277608/article/details/109765965

  1. 基于网络的调试

被调试机器:

1
2
bcdedit /debug on # 设置为调试模式
bcdedit /dbgsettings net hostip:192.168.0.110 port:50010 # 网络调试方式,指定调试机器ip和本机端口,建议49152~65535

这两条指令执行后会显示一个key,保持这个key,下面用到

调试机器:

VS->扩展->Driver->Test->Configure Drivers->Add New Driver

Display Name为设备名字,Network host name为被调试机器IP,下面选择手动配置调试选项以及手动分发驱动文件

下一步,在Kernel Mode下设置key以及ip,Bus Parameters是多块网卡时才会使用,要根据PCI规范填入总线号、设备号、功能号

在驱动入口函数添加一个KdBreakPoint(),这个断点只有在Debug版本有用,DbgBreakPoint()同时在Release版本也有用

生成sys文件,调试->附加进程->链接类型=Windows Kernel Mode Debugger,目标选择配置好的,可用进程=kernel

接下来就会出现调试输出界面(实际上就是gdb的调试界面)

重启被调试机器,就可以看到调试信息了

最后把sys文件放在被调试机器中,使用sc start/sc create运行

sc命令文档 https://docs.microsoft.com/zh-cn/windows-server/administration/windows-commands/sc-create

https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/cc742126(v=ws.11)

1
2
sc create HelloDriver binpath="C:\Users\kali\Desktop\HelloDriver.sys" type=kernel start=demand
sc start HelloDriver
  1. 基于管道的调试
1
2
bcdedit /debug on
bcdedit /dbgsettings serial baudrate:115200 debugport:2 # 串口2为调试介质,波特率为115200

XP及更低版本需要更改C:\boot.ini

1
2
3
4
5
[boot loader]
timeout=30
default=multi(0)disk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDWOS="WINDWOS XP Debug" /fastdetect /debug /debugport=com2 /baudrate=11520

关闭虚拟机,在Vmware中新增串口设备,设置命名管道,名字为 \.\pipe\com_2

修改windbg的快捷方式,改为

1
"PATH_TO_WINDBG" -b -k com:pipe,port=\.\pipe\com_2,resets=0

windbg会连接被调试机器

生成驱动,sc create/sc start

  1. 其它

在被调试机器上面安装debugview,并以管理员身份运行,可以查看调试输出

测试示例

VS2019 创建新项目-> Kernel Mode Driver, Empty(KMDF)

新建hello.c,填入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <ntddk.h>

VOID DriverUnload(PDRIVER_OBJECT driver) {
DbgPrint("hello: driver unloading...");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) {
DbgPrint("hello: driver loaded!");
DbgPrint("hello:This is a breakpoint");
KdBreakPoint();
DbgPrint("hello:You step over breakpoint");
driver->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
  • 生成时会有一个漏洞缓解措施,在单个组件中安装就行
  • 以下警告视为错误 项目->自己的项目->配置属性->c/c++->常规->将警告视为错误=否
  • SignTask任务失败 属性->配置属性->Driver Signing-> Sign mode=off
  • 运行时选择调试->附加到进程->类型=Windows kernel mode debugger,目标=sc创建的驱动名,进程=kernel,如果没有kernel就勾选显示所有用户进程

注意:编译位数要和系统位数相同

2. 驱动开发和应用层应用开发的不同

  • 共享的内核空间vs隔离的应用程序
  • 操作系统内核位于高2GB,有接口可以供硬件驱动编程人员调用
  • Windows一般使用系统进程加载内核模块,但是内核代码不一定始终运行在system进程里

数据类型

使用重新定义的数据类型,万一出现问题,重新定义一下就行

  • unsigned long-> ULONG
  • unsigned char-> UCHAR
  • unsigned int-> UINT
  • void->VOID
  • unsigned long*->PULONG
  • unsigned char*->PUCHAR
  • unsigned int*->PUINT
  • void*->PVOID

返回状态

绝大多数内核API都是返回一个状态,也就是一个错误码NTSTATUS,可以使用下面的代码判断

1
2
3
4
5
if(!NT_SUCCESS(status)){ // 处理失败的情况
//...
}else{
//...
}

错误值的意思不总是很明确,可以通过WDK头文件寻找

字符串

由于共享的内核空间,使得很多应用层可以用的API都无法使用

驱动中的字符串用一个结构来容纳

1
2
3
4
5
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MAximumLength;
PWSTR Buffer;
} UNICODE_STRING,*PUNICODE_STRING;

宽字符双字节

打印语句

1
2
UNICODE_STRING str=RTL_CONSTANT_STRING(L"hello driver!");
DbgPrint("%wZ",&str);

重要的数据结构

  1. 驱动对象:C语言对面向对象的模拟,把设备、驱动、文件看成一个对象
    DRIVER_OBJECT

    用于描述程序提供的功能,填写一组回调函数让Windows函数

  2. 设备对象:可能代表硬盘、管道等,可以接受请求给出响应
    DEVICE_OBJECT

驱动对象生成多个设备对象,而Windows向设备对象发出请求,被驱动对象的分发函数捕获。

  1. 请求:给设备对象发的数据、指令等IRP

    IRP还有几种衍生说法

函数调用

注意编程时查看WDK API,WDK自带的help也可以

后续将会陆续用到常用的函数

驱动开发模型

https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/choosing-a-driver-model

Windows有官方文档,基本上囊括了所有类型的驱动程序

WDK编程特点

  1. 调用源

    • 入口函数
    • 各种分发函数
    • 处理请求完成时的回调函数
    • 其它回调函数
  2. 多线程安全性

    如果总是考虑多线程安全,则会降低效率,所以有以下原则

    • 可能运行于多线程环境的函数必须多线程安全
    • 函数A的所有调用源都是单线程环境,则A也是单线程环境
    • 函数A的其中一个调用源位于多线程环境或多个调用源可并发,则A时多线程
    • 函数A的所有可能运行于多线程环境下的调用路径上,都有序列化的强制措施,则A运行在单线程下
    • 只使用函数内部资源,则是多线程安全
    • 如果对于全局变量的访问强制序列化,则也是多线程安全
  3. 代码中断级

    中断级有两种,Dispatch和Passive,Dispatch级别高,复杂功能要求在Passive级运行

    • 调用路径上无中断级变化,则函数和它的调用源中断级相同
    • 获取自旋锁中断级升高,释放自旋锁中断级下降
  4. PAGE节中的函数不能时Dispatch级,因为可能引发缺页中断,可以使用PAGED_CODE()测试

3. 驱动开发中的字符串和链表

字符串

1
2
3
4
5
typedef struct _UNICODE_STRING{
USHORT Length;
USHORT MAximumLength;
PWSTR Buffer;
} UNICODE_STRING,*PUNICODE_STRING;
1
2
3
4
5
6
7
8
9
10
11
#include <ntstrsafe.h>
NTSTATUS status;
UNICODE_STRING str;
RtlInitUnicodeString(&str,L"My first String"); // 初始化字符串
UNICODE_STRING src=RTL_CONSTANT_STRING(L"My source String"),dst;
WCHAR dst_buf[256]; // 定义缓冲区
RtlInitEmptyUnicodeString(&dst,dst_buf,256*sizeof(WCHAR));// 让dst使用dst_buf作为缓冲区
RtlCopyUnicodeString(&dst,&src); // 字符串拷贝
status=RtlAppendUnicodeString(&dst,L"My second String");// 字符串连接
status=RtlStringCbPrintfW(dst.Buffer,512*sizeof(WCHAR),L"filepath=%wZ,size=%d\r\n",&str,1024); // 相当于swprintf
KdPrint((L"filepath=%wZ,size=%d\r\n",&str,1024));//直接输出,使用双重括号

链表

1
2
3
4
5
6
NTSTATUS status;
UNICODE_STRING dst={0};
dst.Buffer=(PWCHAR)ExAllocPoolWithTag(NonPagedPool,1024,"MyTt");//分配内存空间,NonPagedPool表示不会被交换到硬盘中的内存
dst.Length=0;
dst.MaximumLength=1024;
ExFreePool(dst.Buffer); // 释放内存

ExAllocPoolWithTag和ExFreePool必须成对出现,如果不释放,则永远造成内存泄漏,直到重启计算机

内核开发者定义的数据结构:

1
2
3
4
5
typedef struct _LIST_ENTRY{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY,*PLIST_ENTRY;
InsertHeadList(&list_Head,(PLIST_ENTRY)&my_struct);//插入到链表末尾,非多线程安全

自定义的链表可以把这个结构放在开头

长长整形 LARGE_INTEGER

自旋锁

自旋锁用于保证多线程的安全性

1
2
3
4
5
6
KSPIN_LOCK my_spin_lock;
KIRQL irql;//中断级别
KeInitializeSpinLock(&my_spin_lock);//初始化一个自旋锁
KeAcquireSpinLock(&my_spin_lock,&irql);//提高了中断级别
// your code, single thread executing
KeReleaseSpinLock(&my_spin_lock,&irql);

自旋锁应该是各个线程共享的变量

队列自旋锁可以提高性能,遵守先来先服务原则

1
2
3
4
5
6
KSPIN_LOCK my_queue_spinlock=0;
KeInitializeSpinLock(&my_queue_spinlock);
KLOCK_QUEUE_HANDLE my_lock_queue_handle;
KeAcquireInStackQueuedSpinLock(&my_queue_spinlock,&my_lock_queue_handle);
// your code
KeReleaseInStackQueuedSpinLock(&my_lock_queue_handle);

实例:链表和字符串转换程序

(虽然转换还是不成功,待后面开发填补)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <ntddk.h>
#include <ntstrsafe.h>
#include <windef.h>

typedef struct {
LIST_ENTRY list;
WCHAR c;
UINT len;
}String,*PString;
KSPIN_LOCK spinLock;
KIRQL irql;
VOID printLinkedList(PString head) {
//ExAcquireSpinLock(&spinLock, &irql);
WCHAR dst[1024];
RtlStringCbPrintfW(dst, sizeof(dst), L"hello: LinkedList:");
PString p = head->list.Blink;
UINT len = head->len;
for(UINT i=0;i<len;i++) {
RtlStringCbPrintfW(dst, sizeof(dst), L"%wZ%d,",dst, (UINT)(p->c));
p = p->list.Blink;
}
//KdBreakPoint();
DbgPrint(dst);
//ExReleaseSpinLock(&spinLock, &irql);
}
VOID addToLinkedList(PString head,PString node) {
PString p = ExAllocatePool(NonPagedPool, sizeof(String));
InsertHeadList(head, node);
head->len++;
}
VOID freeLinkedList(PString head) {
PString p = head->list.Blink,pp=head;
UINT len = head->len;
for (UINT i = 0; i < len; i++) {
ExFreePool(pp);
pp = p;
p = pp->list.Blink;
}
}

VOID linkedListToString(PString head,UNICODE_STRING *str) {
WCHAR *buf = ExAllocatePool(NonPagedPool, sizeof(WCHAR)*(head->len));
if (!buf) {
DbgPrint(L"Memory Alloc Failed");
return;
}
PString p = head->list.Blink;
for (UINT i = 0; i < head->len; i++) {
buf[i] = (WCHAR)p->c;
p = p->list.Blink;
}
str->Buffer = buf;
str->Length = head->len;
str->MaximumLength = head->len;
}
VOID stringToLinkedLIst(UNICODE_STRING str,PString head) {

for (UINT i = 0; i < str.Length; i++) {
PString p = ExAllocatePool(NonPagedPool, sizeof(String));
if (!p) {
DbgPrint(L"Memory Alloc Failed");
return;
}
addToLinkedList(head, p);
p->c = str.Buffer[i];
head->len++;
}

}

VOID DriverUnload(PDRIVER_OBJECT driver) {
DbgPrint("hello: driver unloading...");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) {
KeInitializeSpinLock(&spinLock);
UNICODE_STRING str;
String head;
InitializeListHead(&head);
head.len = 0;
RtlInitUnicodeString(&str, L"You are the best!");
DbgPrint("Transfering UNICODE_STRING to LinkList: %wZ", &str);
stringToLinkedLIst(str, &head);
DbgPrint("Transfering LinkList to UNICODE_STRING: ");
//KdBreakPoint();
printLinkedList(&head);
linkedListToString(&head, &str);
DbgPrint(&str);
DbgPrint("Freeing memory");
//freeLinkedList(&head); // 暂时不释放了,似乎链表有检查机制,需要别的释放方法
ExFreePool(str.Buffer);
return STATUS_SUCCESS;
}

参考资料

  • 《Windows内核安全与驱动开发》

LDA隐含狄利克雷分布学习

前置概念

特征值和特征向量

给定一个方阵$\mathbf A$,它的特征向量$\mathbf v$经过$\lambda$线性变换后,新向量与之前的仍在同一直线上,方向或长度可能变化,即

$\mathbf{Av}=\lambda\mathbf{v}$

特征

特征指的是从原始数据提取的单个属性,一般是一个数。原始数据必须转化成一个特征向量才可以进一步分析。它们类似于统计中的自变量。特征向量所属的向量空间称为特征空间。

主题模型

是一种统计模型,如果一篇文章有一个中心思想,那么一些特定的词语会更频繁地出现。但是一篇文章通常包含多个主题,所以应该分析包含哪些主题,每个主题所占比例是多少。

潜在语义索引LSI

不同文章之间使用词语关联起来,得到词语的潜在语义索引。将文本集合表示成一个M*N的矩阵,M是词语总数,N是文档数。(i,j)代表在第j篇文档中i出现的个数。通过对文档向量进行奇异值分解,取前k个最大奇异值及对应的奇异矢量构成一个新矩阵来近似表示原文档向量。新矩阵反映了文档中任意两个词语之间的关联性,消除了词和文档之间语义的模糊度。

多项式分布

贝叶斯定理

$p(A|B)=\frac{p(A)p(B|A)}{p(B)}$

其中$p(A)$是A的先验概率,$P(B)$是边缘概率

贝叶斯估计是将贝叶斯定理推广到连续的概率分布中

先验分布*似然函数=后验分布

表示一个具有多个结果的事件执行k次的概率分布,是二项分布在高维度上的推广,记为$Mult(x)$

$p(x|\beta)=\frac{n!}{\Pi_{i=1}^Kx_i!}\Pi_{i=1}^Kp_i^{x_i}$

狄利克雷分布表示了一组多变量并且连续的概率分布,记为$Dir(\alpha)$

$p(\theta|\alpha)=\frac{\Gamma(\sum_{i=1}^K\alpha_i)}{\Pi_{i=1}^K\Gamma(\alpha_i)}\Pi_{i=1}^K\theta_i^{\alpha_i-1}$

$Dir(\alpha)*Mult(x)=Dir(x+\alpha)$

狄利克雷分布的期望$E(\theta)=\left(\frac{\alpha_1}{\sum_{i=1}^K\alpha_i},\frac{\alpha_2}{\sum_{i=1}^K\alpha_I},…,\frac{\alpha_K}{\sum_{i=1}^K\alpha_i}\right)$

潜在狄利克雷分配

三层贝叶斯模型。

文档中每一个词都是通过主题产生的,一篇文章可能有多个不同的主题。文档集上应分布着关于主题的某个概率分布,每篇文档的主题都满足这个概率分布。而词与词的不同组合,将产生不同的主题,因此每个主题又是关于词的条件概率分布。主题分布是不可见的,但是词语分布是可见的。根据文本向量化的思想,可以通过狄利克雷分布来刻画主题、词语和文档之间的关系,通过对文档中词语的分布情况,来推断文档的主题。

对于一篇文档m,主题分布z是用过多项分布$\theta$来表示并生成的,记做$zMult(\theta)$。分布$\theta$壳解释为通过按文档维度来计算的主题分布情况。这一分部无法直接观测得到,而$\theta$的共轭先验概率分布是超参数为$\alpha$的狄利克雷分布,记为$\thetaDir(\alpha)$,因此需要通过$\alpha$来推断$\theta$

设文档数量为M,主题数为K,共V个词语,$\theta_m$表示第m个文档$d_m$中的主题分布,是K维向量$z_m={z_{m,1},…,z_{m,K}}$

文档$d_m$通过$\theta_m$生成第k个主题记为$z_{m,k}$,$z_{m,k}$下面有$n_{m,k}$个词语,这些词语是通过多项分布$\phiMult$表示并生成的,$wMult(\phi)$。解释为按主题维度来统计词语的分布情况。$\phi$无法直接观测得到,是超参数为$\beta$的狄利克雷分布,记做$\phi~Dir(\beta)$,因此$\phi$通过超参数$\beta$来辅助推断。主题$z_{m,k}$下的词语分布是V维向量,$d_m$可以表示为${w_{m,1},…,w_{m,N}}$,N为词语数。

文档的生成过程就是反复执行选择主题-选择词语的操作,最终生成整个文档的集合。

隐含参数文档主题分布$\theta$和主题词语分布$\phi$是最终概率分布的关键,参数推断有两种方式,变分贝叶斯参数推断方法(使用最大化期望算法EM)和Gibbs采样法。

是词袋模型,不考虑词语顺序。

狄利克雷多项回归DMR

从文本数据本身出发,通过控制先验参数的输入,同时简化模型在采样阶段的复杂性,获得更好的效果和更快的速度。

在对样本利用狄利克雷参数进行建模时,会将狄利克雷先验参数一同作为模型参数进行推断,使得参数通过学习得到。在DMR建模时,相比LDA模型增加了两个参数,分别为文档特征向量$x_d$和主题特征向量$\lambda_t$。假设$x_d$包含了文档中的所有特征,$\lambda_t$包含了各个主题下所有特征的权重值。

首先通过方差为$\sigma^2I$,均值为$0$的正态分布选择一个$\lambda_t$,并从参数为$\beta$的地理克雷分布中选择一个主题t的分布$\phi_t$。设每个主题t的狄利克雷先验参数的计算公式为$\alpha_{dt}=exp(x^T_d\lambda_t$,文档-主题分布$\theta$和主题-词语分布$\phi$两个参数的生产方式与LDA一致。

多元狄利克雷多项回归主题模型MDMR

新闻文档生成过程和传统LDA一致

参考资料

主题模型 https://zh.wikipedia.org/wiki/%E4%B8%BB%E9%A2%98%E6%A8%A1%E5%9E%8B

特征向量 https://zh.wikipedia.org/wiki/%E7%89%B9%E5%BE%81%E5%80%BC%E5%92%8C%E7%89%B9%E5%BE%81%E5%90%91%E9%87%8F

潜在语义索引 https://zh.wikipedia.org/wiki/%E6%BD%9C%E5%9C%A8%E8%AF%AD%E4%B9%89%E7%B4%A2%E5%BC%95

奇异值分解 https://zh.wikipedia.org/wiki/%E5%A5%87%E5%BC%82%E5%80%BC%E5%88%86%E8%A7%A3

隐含狄利克雷分布 https://zh.wikipedia.org/wiki/%E9%9A%90%E5%90%AB%E7%8B%84%E5%88%A9%E5%85%8B%E9%9B%B7%E5%88%86%E5%B8%83

杜增文. 基于狄利克雷回归的微博主题检测模型研究[D].中国科学院大学(中国科学院大学人工智能学院),2020.

瘦子训练

一周7天训练反而会增肌变慢

连续训练两到三天,要彻底休息一天

同一块肌肉不安排连续两天训练

周一周四连躯干,周二周五练四肢,其他时间休息

每2-3月安排恢复周,重量减半或组次减半或完全不练

熬夜是增肌的死对头,皮质醇

可以提早训练时间,少玩手机

每次训练时间延长,频次降低

瘦子吃的笔记

每天每公斤体重摄入1.8g蛋白质,6g碳水化合物

60kg->108g 360g

75kg->135g 450g

食物秤,薄荷营养师APP查看营养含量

每天做饮食记录

安排加餐,早午餐之间、午晚餐加餐,宵夜加餐,都少摄入

早餐要加个水果

正餐两份肉类主菜,三碗米饭

加餐:香蕉,面包,牛奶

练完喝增肌粉MASS GAINER=蛋白粉+碳水,蛋白粉WHEY

胃口是撑大的

简单粗暴:空腹秤体重,争取每周增长

吃够了就不需要蛋白粉

喝各种粉需要多喝水