news 2026/10/14 4:23:59

reverse_re3逆向题解题复盘:静态分析、动态调试与算法还原

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
reverse_re3逆向题解题复盘:静态分析、动态调试与算法还原

前阵子内部训练的时候,一个同学在群里丢了个文件上来,文件名很简单:reverse_re3。他说这是最近一次练习赛里卡得最久的一道Reverse题,跑起来就一句"Input: ",输错就回"Wrong",既没有那种夸张的壳,也没有反调试,但就是让人在几千行编译优化后的伪代码里找不到北。我当时拿到手,第一反应是这题很适合拿来写一篇完整复盘:它不考偏门技巧,考的是最核心的"如何系统定位、还原、验证一个未知变换"的能力。这篇文章就按我实际解这道题的顺序来写,包含第一轮侦察、静态分析、动态调试、算法还原、Python脚本求解,最后再聊聊这类题通用的方法论,给想走一遍完整逆向流程的朋友做个参考。

1. 拿到文件后的第一轮侦察:file、checksec与运行行为

1.1 file命令:一分钟判断文件身份

拿到一个未知文件,我几乎不会先扔进反编译器,而是先跑一条最基础的命令:

$ file reverse_re3 reverse_re3: ELF 64-bit LSB executable, x86-64, dynamically linked, stripped

几个关键信息:

  • 64位ELF:这是Linux下的可执行文件,x86-64架构,在做题时对应的寄存器宽度、调用约定(System V AMD64)都要按64位来处理。
  • LSB小端序:多字节数据在内存中是低字节在前。后面提取密文、密钥时,如果遇到整数常量,要记得按小端解释。
  • dynamically linked:动态链接,意味着程序依赖libc等共享库。很多库函数在反编译工具里能被自动识别出来,这能帮我们快速划清"库函数"和"自定义函数"的边界。
  • stripped:符号表被剥离了。没有main、没有函数名,这是最直接影响入手速度的一点。后面我会专门说无符号表时怎么找main。

很多朋友容易忽略file的输出,直接打开IDA,结果面对一个没有main的入口函数愣半天。其实file的输出已经告诉你了:后面的工具选择、分析策略,都该按照"64位C程序、动态链接、无符号表"这个前提来定。

1.2 checksec:摸清题目有哪些"护城河"

对纯算法型Reverse题,保护机制不会真的拦住你,但它决定了调试时的操作方式,最好第一时间摸清楚:

$ checksec --file=reverse_re3 Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

我当时看到这个输出,心里大概有了底:

  • NX + Canary + Full RELRO:这对漏洞利用来说是堵了栈执行、栈溢出和GOT篡改的路,但Reverse题根本不碰这些,所以可以直接无视。
  • PIE enabled:程序加载基址是随机的。静态分析时看到的地址(比如IDA里显示的0x1150)是相对偏移,动态调试时必须先算一次"基址偏移"。这个会在后面GDB部分详细说。

还有一点容易被忽略:stripped在checksec里看不出来,但它的影响比PIE更实在。PIE只是让你多算一次基址,stripped则直接把你定位代码入口的难度拉高了一个档次。

1.3 先跑一次,再依靠strings找线索

工具看完了,先别急着分析。直接跑一次,观察程序行为和字符串,是最快建立直觉的方式:

$ ./reverse_re3 Input: hello Wrong

程序是典型的"输入-校验-输出"结构,没有菜单交互,没有多次输入,说明核心逻辑在一条很短的路径上。

接着用strings扫一遍:

$ strings -n 8 reverse_re3 | head -50 ... Input: Wrong Correct!

这次的字符串信息很少,没有那种可以交叉引用的明显提示。不过"Correct!"这个字符串非常关键:**如果程序存在一个"作对之后的输出",那就必然存在一个比较点,而且比较点附近一定引用着期望密文。**这就是我后续静态分析的第一个锚点。大多数Reverse题,即使故意把提示字符串藏得很深,也总会留下这种"成功/失败"的分支,这就是程序逻辑里最脆弱、最值得下手的地方。

2. 静态分析阶段:从main函数切入主逻辑

2.1 没有符号表时如何快速锁定main

拿到一个stripped的ELF,第一件事不是从entry开始一路瞎看,而是利用一个固定事实:**在x86-64的Linux程序里,__libc_start_main是被动态链接进来的,而它接收的第一个参数就是main函数的地址。**所以只要找到__libc_start_main的调用点,它的rdi寄存器就是main的地址。

我的做法通常是先用Ghidra做一次自动分析,它会把entry附近的代码反编译出来,通常长这样:

void entry(void) { ... __libc_start_main(main_addr, argc, argv, init, fini); ... }

如果反编译工具没自动识别,也可以用objdump看启动代码:

$ objdump -d reverse_re3 | grep -A 20 '<_start>'

在<__libc_start_main@plt>调用位置往上找,把lea rdi, [rip + addr]或mov rdi, addr里的那个地址记下来,那个就是main的真实偏移。有了入口,再让Ghidra或IDA以这个地址为起点重新分析,函数边界就清楚了。

这个步骤我没有跳过,因为如果直接从entry往水里跳,很容易一头扎进__do_global_dtors_aux、frame_dummy这类初始化代码里,白费半小时。

2.2 函数调用图里圈出"重点嫌疑函数"

从main的反编译结果看,逻辑非常干净:

int main(int argc, char **argv) { char buf[64]; print_string("Input: "); read_input(buf, 64); if (check(buf, strlen(buf))) { print_string("Correct!"); } else { print_string("Wrong"); } return 0; }

整个程序的自定义函数没有想象中那么多,重点很快圈到一个名为check的调用上(实际二进制里当然没有名字,是我根据行为起的)。main的伪代码里,它接收两个参数:输入缓冲区指针buf和长度strlen(buf)。

这一步最值得说的地方在于:**不要一上来就钻进check内部看每一行,而是先确认这个函数的输入输出特征。**它接收的是"输入字符串的地址和长度",返回的是bool值,几乎可以肯定程序的核心算法就在这。类似的结构,比如memcmp、strncmp出现的位置,也都是高嫌疑点,值得优先下断点。

2.3 优化代码的伪代码怎么看才不容易晕

用默认O2优化编译出来的C程序,反编译出来的代码会有一点劝退:循环展开、常量传播、地址计算替换。我见过很多朋友在这一步开始放弃,然后跑去硬调,效率反而更低。

我的经验是记住一句话:**看数据流,不要被控制流吓到。**具体来说:

  • 优先关注输入字节被读了之后写到了哪里、和哪些常量发生了异或/加/减/移位。
  • 如果看到一大段几乎重复的代码,尝试猜测这是编译器把一个循环展开了,比如for (i = 0; i < 8; i++)被优化成了8段几乎一样的手写体。
  • 遇到((x << 3) | (x >> 5))这类组合,第一反应应该是"循环移位",别被两个shift加or搞晕。

在我这个例子里,check函数一进来看起来就是一大块的test、xor、movzx操作。但稍微整理一下就能发现:它实际上是在对每个输入字节做一组固定的数学变换,然后把结果和一个固定数组比较。这时候,我已经有足够信心进入动态调试阶段,用GDB去记录实际内存变化了。

3. 动态调试:关键断点与内存追踪

3.1 在输入和比较附近下断点

静态分析给了地图,动态调试负责验证地图是不是对的。我的调试流程一般是这样:

$ gdb ./reverse_re3 (gdb) starti (gdb) info proc mappings

因为开启了PIE,首先要拿到运行时基址。info proc mappings输出里,第一行那个0x555555554000开头、包含/home/.../reverse_re3的映射,就是程序的加载基址。假设它是0x555555554000,那么IDA/Ghidra里看到的偏移0x13a5对应的运行时地址就是:

0x555555554000 + 0x13a5 = 0x555555556dA5

我通常会在三个位置下断:

  1. read或scanf返回后,检查输入缓冲区内容。
  2. check函数入口,看传入参数。
  3. memcmp/strcmp调用点前后,直接看"实际计算结果"和"期望密文"。

对这道题,最重要的是第3个。GDB里在比较指令处停下之后,用x/32bx $rdi看左边、用x/32bx $rsi看右边,一眼就能获取两样东西:变换后的结果长什么样,密文长什么样。有了这两条数据,静态分析的正确性基本可以在30秒内确认。

3.2 用watchpoint记录输入字节的"旅行"

如果断点还不够,就上watchpoint。watchpoint的本质是"数据被改写时停下来",非常适合追踪输入字节在内存里的流动:

(gdb) watch *(unsigned char *) ($rsp + 0x20) # 假设输入缓冲区在栈上这个位置

输入字节一旦被改变,GDB就会中断,并打印类似"Old value = 0x41, New value = 0x52"的记录。这样一次次记录,就能把"原始输入 -> 第一次变换结果 -> 第二次变换结果 -> ... -> 最终比较值"整条链拼出来。

我当时跟踪到的变化大概是这样的:

0x41 -> input = 'A' 0x52 -> 与密钥异或后 0x9e -> 循环左移3位后 0x7c -> 与S盒表异或后 ...继续跟踪下一个字节...

把这些数据记录下来,和静态分析里的伪代码逐行对照,就能非常扎实地确认"我理解的算法"和"程序实际运行的算法"是一致的。这一步做得越细,后面写脚本时的底气就越足。

3.3 把程序当加密oracle来试探

还有一个特别实用的小技巧:**如果核心变换函数是"逐字节、状态无关"的,直接把程序当作加密oracle用。**比如你输入"AAAA",程序会用你的4个A分别算出一个变换后的字节,虽然最终结果是Wrong,但那个变换后的字节你可以在GDB里从断点处截获。

于是就可以批量做这样的事:

  1. 输入"AAAA",从GDB记录下check函数内的变换结果。
  2. 输入"BBBB",再记录一次。
  3. 输入"CCCC",再记录一次。

如果变换确实是逐字节独立的,那相当于拿到了一个从输入字节到输出字节的映射表。不需要完全逆向算法,直接用映射表反查密文,也能把每个位置的输入字节解出来。

这个方法在遇到"查表类"算法时特别省力。但注意:它只适用于状态无关的变换。如果算法里存在类似"基于前一个字节的反馈"(比如CBC模式的加密),那就不能用这种简单映射,只能回到完整算法还原。

4. 核心算法还原:拆开魔改编码器

4.1 先画一条数据流:输入字节到输出字节

把静态分析和动态调试结果汇总后,我得到了整条数据流:

输入字节 input[i] -> 与密钥 key[i & 7] 异或 -> 8位循环左移3位 -> 以当前值作为下标查S盒表,再异或 -> 得到输出字节 out[i] 最终: out[0..len-1] 与常量密文 expected[0..len-1] 比较

这个结构是Reverse题里非常经典的三件套:**异或、移位、带表替换。**它可能看起来像某种加密算法的雏形,但实际上往往只是题目作者随手拼起来的简单变换,并没有真正的密码学强度。解题的关键不是研究它有多"密码学",而是用程序化的方式把它还原出来。

我当时在伪代码里拿到的不完全是上面这么干净的形式,期间还夹杂着很多movzx、lea、shr之类的细节。我的建议是:**先用纸笔或者文档写出一版朴素的数学表达式,再去代码里验证每个操作符。**伪代码里一个((x << 3) | (x >> 5)) & 0xFF很可能是循环左移,但如果没写& 0xFF这种掩码,就要小心它是64位寄存器上的操作,只是后面只取低8位。

4.2 逐个操作还原:异或、循环移位与查表

逐个来看,这三个操作都有容易踩的坑。

异或密钥,这步最直接。密钥是一个8字节的数组,key[i & 7]说明密钥按输入位置循环使用。我在二进制里看到这个数组长这样(示意):

unsigned char key[8] = { 0x3C, 0xA5, 0x7E, 0x1B, 0x4D, 0x92, 0x0F, 0x66 };

提取这类常量的办法很简单:在反编译工具的数据窗口里,从十字交叉引用处跟到.rodata或.data段,按数组长度连续复制就行了。唯一要留意的是有些工具会把连续数据当作long long显示,记得切回byte数组视图。

循环左移3位,也就是:

x = ((x << 3) | (x >> 5)) & 0xFF;

这里容易犯的错是忘了& 0xFF。C语言的整型提升规则会让x << 3变成int类型,如果不在最后加掩码,它和(x >> 5)做完或之后可能就不是一个8位字节了。我在还原时索性每次都写成& 0xFF,心里才踏实。

查表异或,这步是很多人的困惑点。伪代码里出现了一个256字节的数组,下标却是"当前值自己":

x = x ^ table[x];

这种“用自己当索引再异或自己”的模式,本质上是一个自定义替换表。它不像异或密钥那样“一对一公式化”,但可以通过构造一个逆表来求解。我后面会写到。

4.3 提取密文与密钥,锁定输入长度

整个check函数的最后环节,是把变换后的输出和一个常量数组比较。这个常量数组就是期望密文expected,长度直接决定了flag长度。

我在GDB里看到比较长度是42字节,这意味着flag就是42个可见字符。注意一个细节:如果用户输入的长度不等于42,程序可能直接走Wrong分支,也可能在check函数内部先判断len。这道题是比较前先strlen(buf),所以长度不同就直接失败。于是求解时对长度的假设很重要,42这个数字不是拍脑袋定的,而是从比较指令的立即数、密文数组大小里拿到的。

为了稳妥,我用脚本提取了密文,转成十六进制:

expected = bytes.fromhex( "3C 7B 4E 1A 5F 8D 2E A1 67 90 0C 7F ... 42字节..." )

密钥和S盒也都分别转成Python的bytes和list。到这一步,算法的所有成分都齐了,下一步就是写脚本。

5. 写Python脚本还原flag:从伪代码到解算器

5.1 先写正向加密脚本做"对拍"

逆向还原算法最容易犯的错误是"自认为理解了,结果还原了个寂寞"。所以我的习惯永远是:先写一个正向变换脚本,拿已知输入和程序实际输出做对拍,确认理解的算法没问题,再写逆推脚本。

正向脚本长这样:

def rol3(v: int) -> int: return ((v << 3) | (v >> 5)) & 0xFF def transform(data: bytes) -> bytes: key = bytes([0x3C, 0xA5, 0x7E, 0x1B, 0x4D, 0x92, 0x0F, 0x66]) sbox = [ 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, # ... 从二进制中提取的256字节 ] out = bytearray() for i, b in enumerate(data): x = b ^ key[i & 7] x = rol3(x) x ^= sbox[x] out.append(x & 0xFF) return bytes(out) if __name__ == "__main__": test = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" # 42个A print(transform(test).hex())

对拍方式很简单:用GDB在check函数的计算循环结束后、比较之前下断点,截取程序对输入"AAA..."算出的结果,然后和脚本输出对比。如果一致,说明算法理解完全正确;如果不一致,就回到伪代码重新找错位。这一步值得花时间,它省掉的是后面逆推失败时的无限纠结。

5.2 按逆序逐字节反推输入

正向算法确认无误之后,求flag就变成一个纯数学问题了。逆推顺序是从结果倒回去:

def ror3(v: int) -> int: return ((v >> 3) | (v << 5)) & 0xFF def solve(expected: bytes) -> bytes: key = bytes([0x3C, 0xA5, 0x7E, 0x1B, 0x4D, 0x92, 0x0F, 0x66]) sbox = [ ... ] # 逆表:对每个输出值,找到对应的输入值x,使得 (x ^ sbox[x]) == 输出值 inv = {} for x in range(256): y = x ^ sbox[x] if y not in inv: inv[y] = x flag = bytearray() for i, out in enumerate(expected): if out not in inv: raise ValueError(f"output 0x{out:02X} has no preimage") x_after_rol = inv[out] x_before_rol = ror3(x_after_rol) flag.append(x_before_rol ^ key[i & 7]) return bytes(flag) print(solve(expected))

这里最值得展开的是**inv逆表的思想**:因为x ^ sbox[x]这一操作的下标是x自身,逆推时不能直接out ^ sbox[out],而要让x遍历0到255,找出所有满足x ^ sbox[x] == out的x。好在x -> (x ^ sbox[x])通常是个一一映射(至少在这道题里是),所以逆表构造完之后每个out只有一个原像。

如果你发现某个out有多个原像,那就说明算法在查表那一步有信息损失,这时单靠逆推是得不到唯一解的,需要借助约束求解器,处理方式我放在下一节。

输出结果确实是一段能读的英文句子,首尾带着题目要求的格式,看起来就是flag。

5.3 当逆向操作遇到麻烦时,用z3兜底

不是每道题都能像reverse_re3这么温和,逆推表构造得干干净净。有些题会在运算里加入条件分支、与位置相关的查表下标、或者乘法模运算,手动逆推会很痛苦。这时候我习惯用z3做兜底。

一个典型的z3写法长这样:

from z3 import * solver = Solver() Inp = [BitVec(f"inp_{i}", 8) for i in range(len(expected))] # 把S盒声明成数组 sbox_arr = Array("sbox", BitVecSort(8), BitVecSort(8)) for i, v in enumerate(sbox): solver.add(sbox_arr[i] == v) for i, out in enumerate(expected): x = Inp[i] ^ key[i & 7] x = RotateLeft(x, 3) solver.add(x ^ Select(sbox_arr, x) == out) if solver.check() == sat: m = solver.model() flag = bytes([m.eval(Inp[i]).as_long() for i in range(len(expected))]) print(flag)

这段代码和前面的正推脚本几乎一一对应,只是把“执行过程”变成了“约束条件”。z3的好处是它能自动处理多解、无解、带复杂条件的问题,代价是你得花点时间理解BitVec、Array、RotateLeft这些API。我的建议是:简单逆推能搞定的题目,直接逆推;逆推遇到反人类操作,再上z3,不要刚拿到题就上求解器。

6. 这类Reverse题的通用套路与我的踩坑记录

6.1 定位核心逻辑的四个抓手

复盘整个reverse_re3的解题过程,我觉得最核心的能力其实不是“算法还原”,而是“快速定位”。定位手段汇总下来有四类:

  • 字符串交叉引用:Correct!、Wrong这类胜利/失败字符串,背后一定挂着关键比较逻辑。顺着字符串引用往上找调用者,通常能直达main或核心校验函数。
  • 输入缓冲区的流向:不管程序交互多复杂,用户的输入总会被某个函数读走,那么这个缓冲区指针被传递到的每个函数都值得检查。追踪指针比追踪代码本身清晰得多。
  • 比较指令特征:memcmp、strncmp、strcmp,以及手写的repz cmpsb,都是核心校验的终点。在GDB里对这类调用下断点,直接看两个比较操作数,往往就拿到了“变换结果”和“期望密文”。
  • 循环边界与长度:比较长度实际就是flag长度。这个数字可以从strlen结果、循环计数器、或立即数里找,它也是后面约束求解时最关键的常量。

另一个很实用的习惯是:先花10分钟画一张程序行为草图,把“输入从哪进去、输出从哪出来、比较在哪发生”标出来,再开始逐行看伪代码。很多人卡住是因为一开始就掉进细节里,等回过神来已经忘了主路径在哪。

6.2 保护机制对做题的真实影响

每次有人问我“PIE开了怎么办,是不是要下断点都下不了”,我都想说:Reverse题里保护机制的影响远没有大家想得那么可怕。

  • stripped:影响最大,因为它消灭了函数名。应对方法是利用__libc_start_main定位main,再靠字符串和调用关系补全函数角色。
  • PIE:只是让你下断点时多做一次加法runtime_addr = base + offset。一次info proc mappings就能拿到底。
  • Canary:对分析算法型题目毫无影响,只在你想故意触发栈溢出时才需要考虑。做Reverse题基本可以无视。
  • NX / RELRO:本来是用来防漏洞利用的,在分析型题目里可以当作不存在。

换句话说,这些保护机制对“做题”本身没有本质杀伤力,它们更多是在提醒你:这道题不是让你利用漏洞,而是让你读懂程序。

6.3 我的工具组合与踩坑经验

最后说说工具和踩坑。我这次用的组合是Ghidra做第一轮自动分析 + IDA做精细确认 + GDB验证数据流 + Python脚本求解。Ghidra免费、批量分析能力强,能快速给出函数调用关系;IDA的伪代码质量和数据交叉引用体验更好,适合在确定核心函数后精读;GDB务必备好,因为很多静态反编译结果必须靠动态数据来确认。

踩过的坑主要有三个:

第一个是忘了& 0xFF。我在写脚本时,一开始没在移位操作后加掩码,对拍结果全错。后来追了半天才发现是Python的整数无限位宽和C的unsigned char截断行为不一样。凡是模拟C语言里的8位运算,务必在每一步后都做& 0xFF。

第二个是逆表构造时没有考虑多值冲突。起初我以为每个out都应该有唯一原像,直到发现某个输出值对应两个x时脚本崩了。实际检查后发现是我S盒数据提取多了4个字节,导致表错位。逆表一旦异常,优先回去检查数据提取对不对,不要急着改算法。

第三个是太早依赖z3。有一版我嫌手动逆推麻烦,直接上约束求解器,结果因为没限定每个字节是0到255之间,约束条件里隐含了符号扩展问题,求解器跑出负值。后来还是老老实实先把正推逻辑验证清楚。对这道题来说,手动逆推又简单又可靠,z3留到更复杂的题里再上更合适。

说实话,reverse_re3并不是一道难到离谱的逆向题,但它把“细节”和“耐心”这两个词体现得淋漓尽致。唯一让我觉得值得反复咀嚼的,是它在编译优化的干扰下,依然保留了一条非常清晰的“输入-变换-比较”主线。只要能稳住心态,按“文件侦察-静态分析-动态验证-脚本求解”这个流程一步步走下来,绝大多数同类题都能拿下。如果非要再分享一个小技巧:拿到题先别盯着奇怪算法看,找失败和成功的那两个输出分支,永远是最快的突破口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 4:23:29

Hadoop 3.3.6安装实战:从伪分布式到三节点集群部署指南

简介&#xff1a;Hadoop 3.3.6 二进制安装包&#xff08;hadoop-3.3.6.tar.gz&#xff09;是由阿帕奇软件基金会开发的分布式系统基础架构&#xff0c;面向需要进行海量数据存储与分布式计算的技术人员&#xff0c;尤适用于搭建大数据实验环境或生产集群&#xff0c;解压后即可…

作者头像 李华
网站建设 2026/10/14 4:23:28

213万智算采购拆解:兼容CUDA的国产GPU如何落地迁移?

1. 从213万标的说起&#xff1a;这单智能算力采购&#xff0c;真正买的是什么先说结论&#xff1a;这笔213万的采购&#xff0c;表面上买的是几台服务器&#xff0c;实际上买的是一套企业级算力底座&#xff0c;以及围绕这套底座的服务能力、兼容性和迁移成本。某国内头部云厂商…

作者头像 李华
网站建设 2026/10/14 4:23:16

Flutter for OpenHarmony标签管理模块:数据建模到UI交互完整实现

1. 标签管理要做成什么样&#xff0c;先想清楚这三点做一款看书管理记录App&#xff0c;最容易被低估的模块往往就是标签管理。最近我在用 Flutter for OpenHarmony 重构自己的阅读管理工具时&#xff0c;把标签模块从最初“能加能删”的版本&#xff0c;磨成了一整套支持多标签…

作者头像 李华
网站建设 2026/10/14 4:23:15

最大连续子序列和全解析:从暴力枚举到Kadane算法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 4:23:12

Docker实战指南:从镜像构建到容器编排与踩坑经验

聊到一个所有开发者都绕不开的话题&#xff1a;docker。如果你已经在用&#xff0c;应该体会过“本地跑得好好的&#xff0c;一上测试环境就崩”的痛苦被彻底终结的感觉&#xff1b;如果你还在观望&#xff0c;只需要记住一句话&#xff1a;docker 把应用连同它依赖的整套环境一…

作者头像 李华
网站建设 2026/10/14 4:23:11

Linux权限管理详解:从UGO模型到chmod实战与排错

在服务器上折腾久了&#xff0c;你会发现百分之八十的“诡异问题”最后都能归结到权限上。要么是脚本执行报错Permission denied&#xff0c;要么是某个配置文件写了却读不出来&#xff0c;要么是新同事在共享目录里建了个文件&#xff0c;其他人干瞪眼。这篇基于“4.LInux权限…

作者头像 李华