阿里CTF2026-License记录下第一次搞协议逆向，很遗憾比赛过程中卡在最后一步了总体协议流程如下数据包=长度（4字节）+nonce（12字节）+数据流（长度-12字节）数据流做AES-GCM解密zstd解压缩base64解码protobuf解析出来四个字段，enc_data、password、salt、sha256_hashAES-160解密（魔改了SBOX、mixcolumn、加密解密互换了，同时是CBC模式加PKC#7），密钥是password、salt做PBKDF2-HMAC-SHA256得到48字节，前28字节作为密钥，后20字节作为iv解密后是一个json格式字符串，要求包含license_code和sign字段，license_code就是license启动时随机生成的字符串，sign是RSA4096签名（比赛分析到这里了，签名没分析出来，题目应该是给了个d，需要用密码学攻击还原e，但实际上e就是65537）上面全部通过读取和打印FLAG环境变量ELF加载直接分析license可以发现他和build_token建立了通信运行过程中打印了license code: xxxx-uuid这样的字符串，但并没有在license文件里找到，猜测是从build_token发送elf回来加载到主elf内存里执行代码直接上调试，ida里按照run.sh里设置--no-redirect -c 127.0.0.1:12345，本地跑./build_token -p 'r&FGW9RpqTc*aqof' -s -l 0.0.0.0:12345，从main函数return 0开始调试刚开始可以看到retn后来到了新的区域慢慢调试可以发现有一些脱壳的感觉，在解密一些数据代码，按照经验不停的跳过循环，最终可以发现来到一块非常大的函数单步进入反编译后可以看到license、flag等字符串，说明这里才是真正的check逻辑数据包结构开头部分是在生成随机的UUID，慢慢调试直到要求输入，发现下图红框位置要求输入数据，分别要求4、12长度（注意回车符也算输入）第三处要求输入长度的正是第一处输入的数值-12（不太可控，因为手动输入只能输可打印字符），因此调试到这里时我手动在第三处输入前把要求长度修改掉，从而得以输入可控长度的测试数据，我们这里成为数据流数据流AES-GCM解密数据包结构分析完后，下图中sub_403D10函数会返回CPU架构是否支持AVX并设置一个bool字节（随后很多if-else会根据这个值选择进入到的代码，这些分支代码逻辑是相同的，可以省略很多分析过程）。我的CPU支持AVX，进入到下图红框中的数据初始化，AI分析可知是密钥扩展，但这里的密钥通过一定运算才能得到所以直接来到最后最终轮密钥结果处，提取出来前32字节即可获得密钥为f6778d8728d8f17ce8c5c81f45c3d5fd869ca851b7575be540776f4f26c1140dif-else出来后发现对16个0字节做了加密void __fastcall sub_40C1B0(__m128i *_RDI, const __m128i *a2, _OWORD *a3) { _XMM0 = _mm_xor_si128(_mm_loadu_si128(a2), *_RDI); __asm { aesenc xmm0, xmmword ptr [rdi+10h] aesenc xmm0, xmmword ptr [rdi+20h] aesenc xmm0, xmmword ptr [rdi+30h] aesenc xmm0, xmmword ptr [rdi+40h] aesenc xmm0, xmmword ptr [rdi+50h] aesenc xmm0, xmmword ptr [rdi+60h] aesenc xmm0, xmmword ptr [rdi+70h] aesenc xmm0, xmmword ptr [rdi+80h] aesenc xmm0, xmmword ptr [rdi+90h] aesenc xmm0, xmmword ptr [rdi+0A0h] aesenc xmm0, xmmword ptr [rdi+0B0h] aesenc xmm0, xmmword ptr [rdi+0C0h] aesenc xmm0, xmmword ptr [rdi+0D0h] aesenclast xmm0, xmmword ptr [rdi+0E0h] } *a3 = _XMM0; }这种模式符合AES-256-GCM模式，里面包含2部分，CTR+GHASH随后的调试里都可以看到特征，比如下图里检查了数据流最后16字节是否等于一组结果可以写一个代码来加密数据，结果放入数据包中的数据流部分，从而调试通过这部分AES解密zstd解压缩解密完的数据发现检查了大小，要求不小于13字节在往下调试的过程中发现报错Unknown frame descriptor，搜索字符串可以定位一堆报错caseconst char *__fastcall sub_7FFFF7D68AA6(unsigned __int64 a1) { int v1; // r8d const char *result; // rax v1 = 0; if ( a1 > 0xFFFFFFFFFFFFFF88LL ) v1 = -(int)a1; switch ( v1 ) { case 0: result = "No error detected"; break; case 1: result = "Error (generic)"; break; case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 11: case 13: case 15: case 17: case 18: case 19: case 21: case 23: LABEL_38: result = "Unspecified error code"; break; case 10: result = "Unknown frame descriptor"; break; case 12: result = "Version not supported"; break; case 14: result = "Unsupported frame parameter"; break; case 16: result = "Frame requires too much memory for decoding"; break; case 20: result = "Data corruption detected"; break; case 22: result = "Restored data doesn't match checksum"; break; case 24: result = "Header of Literals' block doesn't respect format specification"; break; default: switch ( v1 ) { case 30: result = "Dictionary is corrupted"; break; case 32: result = "Dictionary mismatch"; break; case 34: result = "Cannot create Dictionary from provided samples"; break; case 40: result = "Unsupported parameter"; break; case 41: result = "Unsupported combination of parameters"; break; case 42: result = "Parameter is out of bound"; break; case 44: result = "tableLog requires too much memory : unsupported"; break; case 46: result = "Unsupported max Symbol Value : too large"; break; case 48: result = "Specified maxSymbolValue is too small"; break; case 49: result = "This mode cannot generate an uncompressed block"; break; case 50: result = "pledged buffer stability condition is not respected"; break; case 60: result = "Operation not authorized at current processing stage"; break; case 62: result = "Context should be init first"; break; case 64: result = "Allocation error : not enough memory"; break; case 66: result = "workSpace buffer is not large enough"; break; case 70: result = "Destination buffer is too small"; break; case 72: result = "Src size is incorrect"; break; case 74: result = "Operation on NULL destination buffer"; break; case 80: result = "Operation made no progress over multiple calls, due to output buffer being full"; break; case 82: result = "Operation made no progress over multiple calls, due to input being empty"; break; case 100: result = "Frame index is too large"; break; case 102: result = "An I/O error occurred when reading/seeking"; break; case 104: result = "Destination buffer is wrong"; break; case 105: result = "Source buffer is wrong"; break; case 106: result = "Block-level external sequence producer returned an error code"; break; case 107: result = "External sequences are not valid"; break; default: goto LABEL_38; } break; } return result; }搜索可知是Zstandard解码器报错，解码器在输入数据开头没有识别出合法的魔数（28 B5 2F FD，可以IDA搜索到多个比较），所以才报错python有zstandard库可以直接帮我们压缩数据，把压缩完的数据传给AES-GCM再去构造数据包即可保证这里解压不在报错Base64解码这里非常明显，多处base64查表，而且上一步骤中解压的数据字符不是4的倍数会报错padding不对等等，确认这里在进行base64解码所以只需要把我们对数据做一次base64编码再去zstd压缩、再去AES-GCM加密，最后构造包即可通过这部分Protobuf解析来到下图这里时会出现新的报错，结合AI分析以及代码里switch-case取字段的逻辑，可知是protobuf解析sub_40B960里要求了wire必须等于2，一共四个字段enc_data、password、salt和sha256_hash所以这里要构造符合protobuf的数据结构AES-160解密发现sha256加密通过可控数据调试以及上一步骤中字段名，发现password和salt最先被读取并做了PBKDF2-HMAC-SHA256，iter正好是1000，生成了48字节调试过程中到上图ROR8的位置时发现检查enc_data字段数据长度是否为20的倍数，不是就会跳转LABEL_80（ud2，也就是BUG()）当我修改完enc_data长度为20倍数后，到下图时发现解密完的数据最后检查了最后1字节是否小于20，如果大于20会报错invalid padding到这里可以分析出是某种对称加密（分组大小为20字节）+PKCS#7填充回过头分析对称加密算法，下图中调试经过unk_409DC0后会发现off_6EA318指向了一个256字节大小的sbox值为AES_SBOX = bytes.fromhex( "637C699066329A0E6441CBA99FFAD5AA6524F777371D83EB981A2A7DBD2502EEE" "5E7455029C4ECA7CCF05C4D1396A2099EFF5AA1C76FE9150C1BC5975614A5B620" "D62111700D7F4E4652354BA4C9011E310F2F17FCDB7430DE481C950653D36718F" "D2D1F7A8D8775B426E071A3825807D4BADAA8B5D99CCFF960D81200798904C2B8" "3C614276DF6CEA495462E8B3F50BF1287ED2CD23F28E80F836E3D722DDF34A2E5" "510C0B15943AC683FBB6DAFCAC638B973AEDCBC9DC3D14CFEA63B92E42B5BFB2C" "F6C1B25D8FEF78915F9472ED4088B7443427E16A0586C8938A7B8451E63D990A3" "3BF39038C086B3E8519CEB08BABA0E247BE4F5E9B57AD6E81163AD0F4" )和标准AES不一样，但类似AES，搜索发现确实存在20字节分组的算法。调试发现unk_409DC0里面存在表生成的函数__int64 __fastcall sub_409DC0(volatile signed __int32 *a1, int a2, __int64 a3, __int64 a4) { __int64 result; // rax int v5; // ett int v6; // ecx unsigned int v7; // ebp int v8; // ecx unsigned __int32 v9; // ebp bool v10; // al __int64 v11; // [rsp+86h] [rbp-58h] BYREF __int64 v12; // [rsp+9Eh] [rbp-40h] __int64 v13; // [rsp+A6h] [rbp-38h] v13 = a4; v12 = a3; result = *(unsigned int *)a1; if ( a2 ) { do { while ( 1 ) { v6 = result & 3; if ( (unsigned int)(v6 - 2) < 2 ) break; if ( v6 != 1 ) return result; v7 = result; if ( (result & 4) == 0 ) { v7 = result | 4; result = (unsigned int)_InterlockedCompareExchange(a1, result | 4, 1); if ( (_DWORD)result != 1 ) continue; } v11 = 0LL; while ( *a1 == v7 && ((__int64 (__fastcall *)(__int64, volatile signed __int32 *, __int64, _QWORD, _QWORD, _QWORD, int))unk_4C70D6)( 202LL, a1, 137LL, v7, 0LL, 0LL, -1) < 0 && *(_DWORD *)((__int64 (*)(void))unk_4C4D28)() == 4 ) ; result = *(unsigned int *)a1; } v5 = result; result = (unsigned int)_InterlockedCompareExchange(a1, result & 4 | 1, result); } while ( v5 != (_DWORD)result ); v10 = v6 == 2; } else { LABEL_15: while ( 2 ) { v8 = result & 4; v9 = result; switch ( result & 3 ) { case 0LL: return result; case 1LL: while ( 2 ) { if ( v8 || (v9 |= 4u, result = (unsigned int)_InterlockedCompareExchange(a1, v9, 1), (_DWORD)result == 1) ) { v11 = 0LL; while ( *a1 == v9 && ((__int64 (__fastcall *)(__int64, volatile signed __int32 *, __int64, _QWORD, _QWORD, _QWORD, int))unk_4C70D6)( 202LL, a1, 137LL, v9, 0LL, 0LL, -1) < 0 && *(_DWORD *)((__int64 (*)(void))unk_4C4D28)() == 4 ) ; v9 = *a1; result = *a1 & 3; v8 = *a1 & 4; switch ( *a1 & 3 ) { case 0: return result; case 1: continue; case 2: goto LABEL_23; case 3: goto LABEL_14; } } goto LABEL_15; } case 2LL: LABEL_23: BUG(); case 3LL: LABEL_14: result = (unsigned int)_InterlockedCompareExchange(a1, v8 | 1, v9); if ( v9 != (_DWORD)result ) continue; v10 = 0; break; } break; } } LODWORD(v11) = 0; BYTE4(v11) = v10; (*(void (__fastcall **)(__int64, __int64 *))(v13 + 32))(v12, &v11); result = (unsigned int)_InterlockedExchange(a1, v11); if ( (result & 4) != 0 ) return off_6E9E98(202LL, a1, 129LL, 0x7FFFFFFFLL); return result; } __int64 __fastcall sub_41EBE0(__int64 a1) { __int64 i; // rbp _UNKNOWN **v3; // [rsp+0h] [rbp-148h] BYREF _UNKNOWN ***v4; // [rsp+8h] [rbp-140h] BYREF _OWORD v5[19]; // [rsp+10h] [rbp-138h] BYREF memset(v5, 0, 256); for ( i = 0LL; i != 256; ++i ) { if ( dword_6EA310 ) { v3 = &off_6EA010; v4 = &v3; sub_409DC0(&dword_6EA310, 1, (__int64)&v4, (__int64)&unk_6E95E8); } *((_BYTE *)v5 + i) = __ROL1__(*((_BYTE *)&off_6EA010 + i + 512), 3) ^ __ROL1__(*((_BYTE *)&off_6EA010 + i + 512), 1) ^ __ROL1__(*((_BYTE *)&off_6EA010 + i + 512), 4) ^ *((_BYTE *)&off_6EA010 + i + 512) ^ __ROL1__(*((_BYTE *)&off_6EA010 + i + 512), 2) ^ 0x63; } off_6E9ED8(a1, v5, 256LL); *(_QWORD *)(a1 + 256) = 10LL; *(_QWORD *)(a1 + 264) = 11LL; *(_QWORD *)(a1 + 272) = 12LL; *(_QWORD *)(a1 + 280) = 13LL; *(_QWORD *)(a1 + 288) = 14LL; *(_QWORD *)(a1 + 296) = 11LL; *(_QWORD *)(a1 + 304) = 11LL; *(_QWORD *)(a1 + 312) = 12LL; *(_QWORD *)(a1 + 320) = 13LL; *(_QWORD *)(a1 + 328) = 14LL; *(_QWORD *)(a1 + 336) = 12LL; *(_QWORD *)(a1 + 344) = 12LL; *(_QWORD *)(a1 + 352) = 12LL; *(_QWORD *)(a1 + 360) = 13LL; *(_QWORD *)(a1 + 368) = 14LL; *(_QWORD *)(a1 + 376) = 13LL; *(_QWORD *)(a1 + 384) = 13LL; *(_QWORD *)(a1 + 392) = 13LL; *(_QWORD *)(a1 + 400) = 13LL; *(_QWORD *)(a1 + 408) = 14LL; *(_QWORD *)(a1 + 416) = 14LL; *(_QWORD *)(a1 + 424) = 14LL; *(_QWORD *)(a1 + 432) = 14LL; *(_QWORD *)(a1 + 440) = 14LL; *(_QWORD *)(a1 + 448) = 14LL; *(_DWORD *)(a1 + 456) = 17170948; return a1; } __int64 __fastcall sub_41EE90(_QWORD **a1, __int64 a2) { __int64 v2; // r14 void (__fastcall **v3)(_BYTE *); // rbx char v5; // al __int64 v6; // rcx char v7; // si bool v8; // sf char v9; // al char v10; // si __int64 i; // rax _OWORD v12[15]; // [rsp+0h] [rbp-630h] BYREF __int128 v13; // [rsp+F0h] [rbp-540h] _OWORD v14[15]; // [rsp+100h] [rbp-530h] BYREF __int128 v15; // [rsp+1F0h] [rbp-440h] _OWORD v16[16]; // [rsp+200h] [rbp-430h] BYREF void (__fastcall **v17)(_BYTE *); // [rsp+308h] [rbp-328h] __int64 v18; // [rsp+310h] [rbp-320h] _BYTE v19[784]; // [rsp+320h] [rbp-310h] BYREF v3 = (void (__fastcall **)(_BYTE *))**a1; **a1 = 0LL; if ( !v3 ) BUG(); if ( *(_BYTE *)(a2 + 4) == 1 ) { ((void (*)(void))unk_404120)(); v18 = v2; v17 = v3; v15 = 0LL; memset(v14, 0, sizeof(v14)); memset(v12, 0, sizeof(v12)); v13 = 0LL; v5 = 1; v6 = 0LL; do { *((_BYTE *)v12 + v6) = v5; *((_BYTE *)v14 + (unsigned __int8)v5) = v6++; v7 = 2 * v5; v8 = v5 < 0; v9 = (2 * v5) ^ 0x8D; if ( !v8 ) v9 = v7; v10 = 2 * v9; v8 = v9 < 0; v5 = (2 * v9) ^ 0x8D; if ( !v8 ) v5 = v10; } while ( v6 != 255 ); HIBYTE(v13) = v12[0]; memset(v16, 0, sizeof(v16)); for ( i = 3LL; i != 258; i += 3LL ) { *((_BYTE *)&v15 + i + 14) = *((_BYTE *)v12 + (*((unsigned __int8 *)&v13 + i + 14) ^ 0xFFLL)); *((_BYTE *)&v15 + i + 15) = *((_BYTE *)v12 + (*((unsigned __int8 *)&v13 + i + 15) ^ 0xFFLL)); *((_BYTE *)v16 + i) = *((_BYTE *)v12 + (*((unsigned __int8 *)v14 + i) ^ 0xFFLL)); } ((void (__fastcall *)(_QWORD **, _OWORD *, __int64))unk_4C9CBF)(a1, v14, 256LL); ((void (__fastcall *)(_QWORD **, _OWORD *, __int64))unk_4C9CBF)(a1 + 32, v12, 256LL); ((void (__fastcall *)(_QWORD **, _OWORD *, __int64))unk_4C9CBF)(a1 + 64, v16, 256LL); return (__int64)a1; } else { (*v3)(v19); return off_6E9ED8(v3, v19, 768LL); } }交给AI分析可知生成了3张256字节表，用于方便后续AES计算，其中有log、exp、inv表。需要注意的是生成方式和标准不一样，标准xtime异或是0x1b，sub_41EE90里是异或了0x8d最后出来后又在sub_41EBE0中生成了sbox（这里魔改了）出来后qword_6EA498值为13，符合AES-160的NK+6（密钥是28字节224bit，NK=7）此外在一轮轮调试数据还发现到mixcolumn时结果变了，检查发现数值和标准的不一样到这里应该是全部的魔改点了。调试可以发现AES-160采用的是CBC模式，前面PBKDF2-HMAC-SHA256生成的48字节，前28字节作为密钥，后20字节作为iv。最后还有一点，前面提到有padding所以应该是解密，但是上面的流程是按照加密算才和调试拿到的数据对应的，所以有可能是加密解密算法对调了下。解密后会检查sha256加密后的值是否等于protobuf里解析出来的sha256_hash值（32字节）时间戳校验+RSA签名前面步骤全部正确后会在最终check前再次进入一个很大的函数，整体AI分析了一遍可知是在做json数据解析，要求包含license_code和sign字段，license_code值等于最开始随机生成的UUID，而sign字段经过一定加密要求最后16字节等于UUID（去除-并转为字节）通过观察报错定位到第一个是时间戳校验（license expired）上图中计算了时间戳差值并检查是否小于60s时间戳校验后有一大串常量数据（1024字节）继续往下走发现从上面数据中初始化了512字节，之后跳转到一个处理hex的逻辑其中v204是单一字符，取自v196字符串，查看发现正是我们设置的sign值。下图每次读取两个字符拼接为一字节之后是一个while循环，逻辑仍然是处理hex值，转为字节存储到了v206又提取了固定的512字节（来自上面常量）接着进入到一个函数，传递了sign和512字节，AI分析是大整数取模运算，符合RSA特性通过调试拿到输入输出值测试发现是在计算n = bytes.fromhex("B75815AF28D17CCE2ECA787C6004EC6F1A51AC14B5A7AF746C7B3DD1354517C3CF38B35BE17E64BAB76A47FEA92396DDA947CC268EE0DACC097911912AB64B4C572D7518003C8B24D9DF5E950D44FE9613805ABDC47EA1F693EB6B04D56A124522126770E9FC771B185EC44F308D57FAA6D3C67A585A5E1672F5F89FADB9B073E913166A99D5A3896BDC6430B1C4A5AECDA7FEFA15AEB41A37E76D61698FF36A2F1B2B0A0CBC1AA0AD068C8ECF9388558B0257335EADC4831397917CE1C9B5E2B033C6935F1B57A06BC830B3D03CC8C8D758A38CD8D85583435B3594A7599F1B692B9FBF0E98B388E6A96D20D6245EF5503F79693552987CCAEC2C86D481A45EEA1C573D33FA15109962E5C8D2A02A6923FB375C6B1F05FC4CDCCC17055AADBA17085FC8C22563DA4FFD05FEBD07A485B5B28C3203890FDABDDD6693C40FCECE05D4FEA9EE46F1447416B7FF4D914A6A5787917637977A3330659D8191CD102093F15ED4D2444E60A4950AE51EBF616721F8785F5656130CFABE2174DBB9F9E5121F8F10670EC0538465B283A02C187989E06C07E3BD3792C5E5E7C49752ADCCFA573DF668C90AB67C61CFC5E46D5CC387D51078D27ADBD8E3AE3CDA5C7A00E741B60E8DD1AD61D0F7633FC2E9F30A3BAC74897001B1AEB1B7C27A0C25A75620B8FD1374084F86186C56AF651FBEEE0DFEDA25FC110EFCD8BBB0C05222326BBF") n = int.from_bytes(n, "little") print(hex((1<<4096)%n-1))到这之后基本确认是RSA-4096签名生成的sign，比赛时卡这里是因为n、d端序提取错了，导致RSA签名完的数据一直对不上调试拿到的数据所以总结来说这里sign经过n、d（私钥）解密后最后16字节要等于uuid，所以我们需要用n、e（公钥）加密uuid作为sign值构造json总结到这里就是一步步往回逆向，具体流程如下远程连接获取license_code，构造json数据，包含license_code和sign字段，sign字段由uuid值做rsa加密生成得到的json数据做魔改后的AES-160加密，密钥和iv由后面的password、salt做PBKDF2-HMAC-SHA256加密生成AES-160加密完的结果放到enc_data中，password、salt可以我们自己构造，sha256_hash值（hex）要等于json数据sha256加密的结果构造protobuf，四个字段对protobuf字节流Base64编码对Base64字符串做zstd压缩对zstd数据流做AES-GCM加密加密完的结果作为数据流，构造最终的数据包=长度（4字节）+nonce（12字节）+数据流（长度-12字节）全流程代码如下import hashlib from base64 import b64encode import zstandard as zstd def zstd_compress(data): return zstd.compress(data) def xtime(a) -> int: a &= 0xFF return (((a << 1) ^ 0x8D) & 0xFF) if (a & 0x80) else ((a << 1) & 0xFF) def gf_mul(a, b) -> int: a &= 0xFF b &= 0xFF res = 0 for _ in range(8): if b & 1: res ^= a a = xtime(a) b >>= 1 return res & 0xFF # ---------------- S-box / inverse S-box (standard AES S-box) ---------------- AES_SBOX = bytes.fromhex( "637C699066329A0E6441CBA99FFAD5AA6524F777371D83EB981A2A7DBD2502EEE" "5E7455029C4ECA7CCF05C4D1396A2099EFF5AA1C76FE9150C1BC5975614A5B620" "D62111700D7F4E4652354BA4C9011E310F2F17FCDB7430DE481C950653D36718F" "D2D1F7A8D8775B426E071A3825807D4BADAA8B5D99CCFF960D81200798904C2B8" "3C614276DF6CEA495462E8B3F50BF1287ED2CD23F28E80F836E3D722DDF34A2E5" "510C0B15943AC683FBB6DAFCAC638B973AEDCBC9DC3D14CFEA63B92E42B5BFB2C" "F6C1B25D8FEF78915F9472ED4088B7443427E16A0586C8938A7B8451E63D990A3" "3BF39038C086B3E8519CEB08BABA0E247BE4F5E9B57AD6E81163AD0F4" ) INV_SBOX = bytearray(256) for i, v in enumerate(AES_SBOX): INV_SBOX[v] = i INV_SBOX = bytes(INV_SBOX) # ---------------- Rijndael-160 params ---------------- def rijndael_rounds(Nb, Nk) -> int: return max(Nb, Nk) + 6 # same as Rijndael # ---------------- Key schedule (uses your xtime/rcon field) ---------------- def rot_word(w) -> int: return ((w << 8) & 0xFFFFFFFF) | ((w >> 24) & 0xFF) def sub_word(w, sbox) -> int: return ( (sbox[(w >> 24) & 0xFF] << 24) | (sbox[(w >> 16) & 0xFF] << 16) | (sbox[(w >> 8) & 0xFF] << 8) | (sbox[(w >> 0) & 0xFF] << 0) ) & 0xFFFFFFFF def rcon(i) -> int: # i starts from 1, stored in MSB byte c = 1 for _ in range(i - 1): c = xtime(c) return (c << 24) & 0xFFFFFFFF def key_expansion(key, Nb, Nk, sbox): if len(key) != 4 * Nk: raise ValueError(f"key length must be {4*Nk} bytes for Nk={Nk}, got {len(key)}") Nr = rijndael_rounds(Nb, Nk) W_words = Nb * (Nr + 1) w = [0] * W_words # first Nk words (big-endian as in your code) for i in range(Nk): w[i] = int.from_bytes(key[4*i:4*i+4], "big") for i in range(Nk, W_words): temp = w[i - 1] if i % Nk == 0: temp = sub_word(rot_word(temp), sbox) ^ rcon(i // Nk) elif Nk > 6 and (i % Nk) == 4: temp = sub_word(temp, sbox) w[i] = (w[i - Nk] ^ temp) & 0xFFFFFFFF return w # ---------------- State helpers (Nb=5 => 4x5, column-major) ---------------- def bytes_to_state(block, Nb): if len(block) != 4 * Nb: raise ValueError("bad block size") s = [[0]*Nb for _ in range(4)] for c in range(Nb): for r in range(4): s[r][c] = block[4*c + r] return s def state_to_bytes(s, Nb): out = bytearray(4*Nb) for c in range(Nb): for r in range(4): out[4*c + r] = s[r][c] & 0xFF return bytes(out) def add_round_key(s, round_w, round_idx, Nb) -> None: base = round_idx * Nb for c in range(Nb): w = round_w[base + c] s[0][c] ^= (w >> 24) & 0xFF s[1][c] ^= (w >> 16) & 0xFF s[2][c] ^= (w >> 8) & 0xFF s[3][c] ^= (w >> 0) & 0xFF def sub_bytes(s, sbox, Nb) -> None: for r in range(4): for c in range(Nb): s[r][c] = sbox[s[r][c]] # 你对齐出来的是“右移”版本 def shift_rows(s, Nb) -> None: for r in range(1, 4): k = r % Nb row = s[r] s[r] = row[-k:] + row[:-k] def inv_shift_rows(s, Nb) -> None: for r in range(1, 4): k = r % Nb row = s[r] s[r] = row[k:] + row[:k] # ---------------- MixColumns (your matrix) + auto inverse ---------------- MIX_MAT = [ [4, 1, 6, 2], [2, 4, 1, 6], [6, 2, 4, 1], [1, 6, 2, 4], ] def gf_mat_inv_4x4(mat): # Gauss-Jordan over GF(2^8) with xor add and gf_mul # Build [mat | I] A = [[mat[r][c] & 0xFF for c in range(4)] + [1 if c == r else 0 for c in range(4)] for r in range(4)] def gf_inv(x) -> int: # brute-force inverse in GF(2^8) for this field x &= 0xFF if x == 0: raise ZeroDivisionError("no inverse for 0") for y in range(1, 256): if gf_mul(x, y) == 1: return y raise ZeroDivisionError("no inverse found (should not happen)") for col in range(4): # find pivot pivot = None for r in range(col, 4): if A[r][col] != 0: pivot = r break if pivot is None: raise ValueError("matrix not invertible") if pivot != col: A[col], A[pivot] = A[pivot], A[col] inv_p = gf_inv(A[col][col]) # scale pivot row for j in range(8): A[col][j] = gf_mul(A[col][j], inv_p) # eliminate other rows for r in range(4): if r == col: continue factor = A[r][col] if factor == 0: continue for j in range(8): A[r][j] ^= gf_mul(factor, A[col][j]) inv = [[A[r][4 + c] & 0xFF for c in range(4)] for r in range(4)] return inv INV_MIX_MAT = gf_mat_inv_4x4(MIX_MAT) def mix_single_column(col): a0, a1, a2, a3 = [x & 0xFF for x in col] out = [] for r in range(4): out.append( gf_mul(MIX_MAT[r][0], a0) ^ gf_mul(MIX_MAT[r][1], a1) ^ gf_mul(MIX_MAT[r][2], a2) ^ gf_mul(MIX_MAT[r][3], a3) ) return [x & 0xFF for x in out] def inv_mix_single_column(col): a0, a1, a2, a3 = [x & 0xFF for x in col] out = [] for r in range(4): out.append( gf_mul(INV_MIX_MAT[r][0], a0) ^ gf_mul(INV_MIX_MAT[r][1], a1) ^ gf_mul(INV_MIX_MAT[r][2], a2) ^ gf_mul(INV_MIX_MAT[r][3], a3) ) return [x & 0xFF for x in out] def mix_columns(s, Nb) -> None: for c in range(Nb): col = [s[r][c] for r in range(4)] mc = mix_single_column(col) for r in range(4): s[r][c] = mc[r] def inv_mix_columns(s, Nb) -> None: for c in range(Nb): col = [s[r][c] for r in range(4)] mc = inv_mix_single_column(col) for r in range(4): s[r][c] = mc[r] # ---------------- Block encrypt/decrypt (20B) ---------------- def rijndael160_encrypt_block(block20, key28, Nb = 5, Nk = 7): Nr = rijndael_rounds(Nb, Nk) w = key_expansion(key28, Nb, Nk, AES_SBOX) s = bytes_to_state(block20, Nb) add_round_key(s, w, 0, Nb) for rnd in range(1, Nr): sub_bytes(s, AES_SBOX, Nb) shift_rows(s, Nb) mix_columns(s, Nb) add_round_key(s, w, rnd, Nb) sub_bytes(s, AES_SBOX, Nb) shift_rows(s, Nb) add_round_key(s, w, Nr, Nb) return state_to_bytes(s, Nb) def rijndael160_decrypt_block(ct20, key28, Nb = 5, Nk = 7): Nr = rijndael_rounds(Nb, Nk) w = key_expansion(key28, Nb, Nk, AES_SBOX) s = bytes_to_state(ct20, Nb) add_round_key(s, w, Nr, Nb) for rnd in range(Nr - 1, 0, -1): inv_shift_rows(s, Nb) sub_bytes(s, INV_SBOX, Nb) add_round_key(s, w, rnd, Nb) inv_mix_columns(s, Nb) inv_shift_rows(s, Nb) sub_bytes(s, INV_SBOX, Nb) add_round_key(s, w, 0, Nb) return state_to_bytes(s, Nb) # ---------------- PKCS#7 padding for 20-byte blocks ---------------- BLOCK_SIZE = 20 def pkcs7_pad(data, block_size = BLOCK_SIZE): pad_len = block_size - (len(data) % block_size) if pad_len == 0: pad_len = block_size return data + bytes([pad_len]) * pad_len def pkcs7_unpad(padded, block_size = BLOCK_SIZE): if not padded or (len(padded) % block_size) != 0: raise ValueError("bad padded length") pad_len = padded[-1] if pad_len < 1 or pad_len > block_size: raise ValueError("bad padding") if padded[-pad_len:] != bytes([pad_len]) * pad_len: raise ValueError("bad padding bytes") return padded[:-pad_len] # ---------------- High-level encrypt/decrypt with PBKDF2 key + post-xor mask ---------------- def derive_key_and_mask(password_b, salt, ITER): enc_data_48 = hashlib.pbkdf2_hmac("sha256", password_b, salt, ITER, dklen=48) key28 = enc_data_48[:28] mask20 = enc_data_48[28:] return key28, mask20 def xor_bytes(a, b): return bytes(x ^ y for x, y in zip(a, b)) # ---------------- CBC 模式下的加密/解密逻辑 ---------------- def encrypt(data, password_b, salt, ITER): """ CBC 模式加密 (逻辑互换版)： 1. PKCS7 Padding 2. 使用 mask20 作为初始 IV 3. 每个块：Plaintext XOR Previous_Ciphertext -> Decrypt_Core -> Ciphertext """ key28, iv = derive_key_and_mask(password_b, salt, ITER) # 自动进行 PKCS7 填充 data_p = pkcs7_pad(data, BLOCK_SIZE) out = bytearray() prev_block = iv # 初始 IV for i in range(0, len(data_p), BLOCK_SIZE): plaintext_block = data_p[i:i + BLOCK_SIZE] # CBC 核心：明文先与前一个密文块异或 mixed = xor_bytes(plaintext_block, prev_block) # 逻辑互换：调用原始的 decrypt 核心作为加密引擎 ciphertext_block = rijndael160_decrypt_block(mixed, key28, Nb=5, Nk=7) out += ciphertext_block prev_block = ciphertext_block # 更新 IV 为当前密文块 return bytes(out) def decrypt(ciphertext, password_b, salt, ITER): """ CBC 模式解密 (逻辑互换版)： 1. 使用 mask20 作为初始 IV 2. 每个块：Ciphertext -> Encrypt_Core -> XOR Previous_Ciphertext -> Plaintext 3. 移除 Padding """ if len(ciphertext) % BLOCK_SIZE != 0: raise ValueError("密文长度必须是 20 字节的倍数") key28, iv = derive_key_and_mask(password_b, salt, ITER) out = bytearray() prev_block = iv # 初始 IV for i in range(0, len(ciphertext), BLOCK_SIZE): ciphertext_block = ciphertext[i:i + BLOCK_SIZE] # 逻辑互换：调用原始的 encrypt 核心作为解密引擎 decrypted_core = rijndael160_encrypt_block(ciphertext_block, key28, Nb=5, Nk=7) # CBC 核心：解密后的数据与前一个密文块异或得到明文 plaintext_block = xor_bytes(decrypted_core, prev_block) out += plaintext_block prev_block = ciphertext_block # 更新 IV 为当前密文块（注意是解密前的密文） # 移除 PKCS7 填充 return pkcs7_unpad(bytes(out), BLOCK_SIZE) def encode_varint(x: int) -> bytes: if x < 0: raise ValueError("varint encoder expects non-negative int") out = bytearray() while True: b = x & 0x7F x >>= 7 if x: out.append(b | 0x80) else: out.append(b) break return bytes(out) def encode_key(field_number: int, wire_type: int) -> bytes: return encode_varint((field_number << 3) | wire_type) def field_length_delimited(field_number: int, data) -> bytes: if isinstance(data, str): data = data.encode("utf-8") return encode_key(field_number, 2) + encode_varint(len(data)) + data MASK128 = (1 << 128) - 1 R = 0xE1000000000000000000000000000000 # GCM reduction constant (SP800-38D) # ---------------- AES-ECB 单块 ---------------- def aes_ecb_encrypt_block(key: bytes, block16: bytes) -> bytes: if len(block16) != 16: raise ValueError("block16 must be 16 bytes") if len(key) not in (16, 24, 32): raise ValueError("key must be 16/24/32 bytes") from Crypto.Cipher import AES # type: ignore return AES.new(key, AES.MODE_ECB).encrypt(block16) # ---------------- GCM: inc32 ---------------- def inc32(counter16: bytes) -> bytes: """只对最后32-bit（big-endian）+1，前12字节不变。""" if len(counter16) != 16: raise ValueError("counter16 must be 16 bytes") prefix = counter16[:12] c = int.from_bytes(counter16[12:], "big") c = (c + 1) & 0xFFFFFFFF return prefix + c.to_bytes(4, "big") # ---------------- GF(2^128) 乘法 (GCM) ---------------- def gf128_mul_gcm(x: int, y: int) -> int: """ 标准 GHASH 乘法：按 big-endian bit 顺序。 """ z = 0 v = x for i in range(128): if (y >> (127 - i)) & 1: z ^= v if v & 1: v = (v >> 1) ^ R else: v >>= 1 return z & MASK128 def ghash(H: bytes, aad: bytes, c: bytes) -> bytes: """ GHASH_H(A, C)，最后包含 len(A)||len(C) (bit) 的长度块。 """ if len(H) != 16: raise ValueError("H must be 16 bytes") H_int = int.from_bytes(H, "big") X = 0 def iter_blocks(data: bytes): for i in range(0, len(data), 16): blk = data[i:i+16] if len(blk) < 16: blk = blk + b"\x00" * (16 - len(blk)) yield blk for blk in iter_blocks(aad): X = gf128_mul_gcm(X ^ int.from_bytes(blk, "big"), H_int) for blk in iter_blocks(c): X = gf128_mul_gcm(X ^ int.from_bytes(blk, "big"), H_int) len_block = (len(aad) * 8).to_bytes(8, "big") + (len(c) * 8).to_bytes(8, "big") X = gf128_mul_gcm(X ^ int.from_bytes(len_block, "big"), H_int) return X.to_bytes(16, "big") # ---------------- GCM: CTR 加密/解密 ---------------- def gcm_ctr_crypt(key: bytes, J0: bytes, data: bytes) -> bytes: """ GCM 的加解密（CTR）：从 counter=inc32(J0) 开始。 """ if len(J0) != 16: raise ValueError("J0 must be 16 bytes") out = bytearray() counter = inc32(J0) for off in range(0, len(data), 16): block = data[off:off+16] ks = aes_ecb_encrypt_block(key, counter) out.extend(bytes(b ^ k for b, k in zip(block, ks[:len(block)]))) counter = inc32(counter) return bytes(out) # ---------------- GCM: 由 nonce(12) + plaintext 计算 ciphertext 和 tag ---------------- def gcm_encrypt_and_tag(key: bytes, nonce12: bytes, plaintext: bytes, aad: bytes = b""): """ 返回 (ciphertext, tag16) - J0 = nonce || 0x00000001 (nonce长度=12时标准写法) - H = AES_K(0^128) - C = CTR(J0, P) - S = AES_K(J0) - tag = S XOR GHASH(H, AAD, C) """ if len(nonce12) != 12: raise ValueError("nonce must be 12 bytes") J0 = nonce12 + b"\x00\x00\x00\x01" H = aes_ecb_encrypt_block(key, b"\x00" * 16) ciphertext = gcm_ctr_crypt(key, J0, plaintext) S = aes_ecb_encrypt_block(key, J0) g = ghash(H, aad, ciphertext) tag = bytes(x ^ y for x, y in zip(S, g)) return ciphertext, tag # ---------------- 拼包：4字节长度 + 12字节nonce + (ciphertext||tag) ---------------- def build_packet(nonce12: bytes, ciphertext: bytes, tag16: bytes) -> bytes: if len(nonce12) != 12: raise ValueError("nonce must be 12 bytes") if len(tag16) != 16: raise ValueError("tag must be 16 bytes") body = nonce12 + ciphertext + tag16 length = len(body) # = 12 + len(ciphertext) + 16 return length.to_bytes(4, "little") + body def make_packet_from_nonce_plain(key: bytes, nonce12: bytes, plaintext: bytes, aad: bytes = b""): ciphertext, tag16 = gcm_encrypt_and_tag(key, nonce12, plaintext, aad=aad) pkt = build_packet(nonce12, ciphertext, tag16) return pkt n = int.from_bytes(bytes.fromhex("BF6B322252C0B0BBD8FC0E11FC25DAFE0DEEBE1F65AF566C18864F087413FDB82056A7250C7AC2B7B1AEB101708974AC3B0AF3E9C23F63F7D061ADD18D0EB641E7007A5CDA3CAEE3D8DB7AD27810D587C35C6DE4C5CF617CB60AC968F63D57FACCAD5297C4E7E5C59237BDE3076CE08979182CA083B2658453C00E67108F1F12E5F9B9DB7421BEFA0C1356565F78F8216761BF1EE50A95A4604E44D2D45EF1932010CD91819D6530337A9737769187576A4A914DFFB7167444F146EEA9FED405CECE0FC49366DDBDDA0F8903328CB2B585A407BDFE05FD4FDA6325C2C85F0817BAAD5A0517CCDC4CFC051F6B5C37FB23692AA0D2C8E562991015FA333D571CEA5EA481D4862CECCA7C98523569793F50F55E24D6206DA9E688B3980EBF9F2B691B9F59A794355B438355D8D88CA358D7C8C83CD0B330C86BA0571B5F93C633B0E2B5C9E17C91971383C4AD5E3357028B558893CF8E8C06ADA01ABC0C0A2B1B2F6AF38F69616DE7371AB4AE15FAFEA7CDAEA5C4B13064DC6B89A3D5996A1613E973B0B9AD9FF8F572165E5A587AC6D3A6FA578D304FC45E181B77FCE97067122245126AD5046BEB93F6A17EC4BD5A801396FE440D955EDFD9248B3C0018752D574C4BB62A91117909CCDAE08E26CC47A9DD9623A9FE476AB7BA647EE15BB338CFC3174535D13D7B6C74AFA7B514AC511A6FEC04607C78CA2ECE7CD128AF1558B7"), "big") d = int.from_bytes(bytes.fromhex("32C892BD6E6CF6B66F83B78BE7F4771C0DC0382A8644B54DEA57BFA20381C63F523D0B0D16397F6D52B380FC5BC9EBED41A0CF434628A131FED3DB548BF2CA41C3B269C4369600E42C0556997E07214F6A721C29A49D3744E9DB04C25709C14CA57E9A39EFA082621F3FB09E09BB45FAD2E8A9F64FDA457A8CE9982899C90EBA69CF0E12FDC572304E81D6D7056F478D3D2B3E9448B9BD27A5F13DEB1D32AF2E944440F58888A46EDC497AD2D91F14E4092C0D4EBF37E8BA220C4D00469377D6AE9E16AAD55C6619D73F65DF364B03A28AF910A0C442FC8871ECF9F8AA4624147F8F3C21BBC5BAF0A5B00A3CE67367AA665D4BDB8036F3289E8EE6192FFDEB8A800A60196CF0C73E75EA5B397057451ADFD512AEE8F1E2C04F959B74270DD7A2F434A3C378C50734570CFFF1880C3D0879A4378AB2E06E2A51C4021205F8CC8D5A24878DB2457984F0BDA467D617CBC92AE65D154C1C4A98740007F4DB588DEBABD1579EA1E5162E3A7B716FB2ABC99274A8E252C2DF32AD674C85219B55213BF4C7621E5E7D583F04F69A68391F1F93FC472425900C9B51DECBDE9B2F742753AF51B9F1C146119D628D379A427D92C8F860D4D662DAFB0E9AF766B0FE580E3325E90F364F8C747095D4A259803139C3937B58EF20E601E8B78B70C2093A4D7335847EF18E6CFD8694BD6928350D66F3809B573C73A00889411C0B97C1204F6986"), "big") e = 65537 def gen_packet(license_code): password_b = b"mypassword" salt = bytes.fromhex("1020304050") ITER = 1000 TARGET_HEX = "".join(license_code.split("-")) calculated_sig = pow(int.from_bytes(bytes.fromhex(TARGET_HEX), "big"), e, n) raw_data = b'{"license_code":"'+license_code.encode()+b'","sign":"'+calculated_sig.to_bytes((calculated_sig.bit_length() + 7) // 8, 'big').hex().encode()+b'"}' ct = encrypt(raw_data, password_b, salt, ITER) protobuf_msg = b"".join([ field_length_delimited(1, ct), # enc_data = 20 bytes (派生) field_length_delimited(2, password_b), # password field_length_delimited(3, salt), # salt field_length_delimited(4, hashlib.sha256(raw_data).digest()), # sha256_hash ]) b64 = b64encode(protobuf_msg) buffer = zstd_compress(b64) key = bytes.fromhex("f6778d8728d8f17ce8c5c81f45c3d5fd869ca851b7575be540776f4f26c1140d") nonce12 = b"b" * 12 packet = make_packet_from_nonce_plain(key, nonce12, buffer, aad=b"") return packet if __name__ == "__main__": packet = gen_packet("612e2577-5b06-4bc3-b84b-b3c126b43662") print(int.from_bytes(packet[:4], byteorder="little")) print(packet[16:].hex())远程连接的exp如下from pwn import * import subprocess from solve import * # 设置调试模式 context.log_level = 'debug' host = '223.6.249.127' port = 11240 io = remote(host, port) # 1. 精确提取挑战指令 log.info("正在解析挑战信息...") io.recvuntil(b"plz run this command to get solve result") cmd = "" while True: line = io.recvline().decode().strip() if "hashcash" in line: cmd = line break log.info(f"提取到的命令: {cmd}") # 2. 执行并发送结果 try: solution = subprocess.check_output(cmd, shell=True).strip() log.success(f"计算结果: {solution.decode()}") io.sendline(solution) except Exception as e: log.error(f"本地执行失败: {e}") io.close() exit() # 3. 等待关键提示并停顿 try: io.recvuntil(b"license code:") code = io.recvline().strip() log.success(f"收到 License Code: {code.decode()}") try: payload = gen_packet(code.decode()) log.info(f"正在发送长度为 {len(payload)} 的字节流...") io.send(payload) except ValueError: log.error("输入的不是有效的十六进制字符串！") # -------------------------- except EOFError: log.error("服务器在验证后断开连接。") io.interactive()拿到远程flag
Windows核心编程-学习笔记13SEH：结构化异常处理（structed exception handling），包含两方面的功能：终止处理异常处理终止处理程序终止处理程序确保不管一个代码块（被保护代码）是如何退出的，另一个代码块（终止处理程序），总会得以调用和执行__try { // Guarded body } __finally { // Termination handler }注意在循环里的try中提前退出包括continue、break等，也会进入finally，之后循环内尚余的代码就不会执行了。所以最好在try、finally中不要包含return、continue、break、goto语句异常处理程序与软件异常异常处理程序如下，只要没有异常，except里面的都不会执行__try { // Guarded body } __except (exception filter) { // Exception handler }注意try后不能同时有finally块和except块，也不能有多个finally块或except块，但可以互相嵌套EXCEPTION_EXECUTE_HANDLER（1）EXCEPTION_EXECUTE_HANDLER告诉系统程序知道这个异常，并预计这个异常会在某种情况下发生，可以执行代码来处理它try-except很容易造成漏洞，比如在try中加入stcpy，加入抛出异常却不知道什么原因，比如可能是参数为NULL或非法地址，或者缓冲区大小不够等等，这时候如果处理了异常，进程继续执行，但却是在有受损状态下执行，可能会导致程序稍后因为不明原因崩溃，更有可能造成容易被利用的安全漏洞。全局展开：try抛出后，检查异常处理者；找不到就继续找外面一层的try代码块EXCEPTION_CONTINUE_EXECUTION（-1）EXCEPTION_CONTINUE_EXECUTION表示从异常发生点继续执行实际上可以调用一个函数来决定返回异常标识符中的哪一个，但如果选择EXCEPTION_CONTINUE_EXECUTION要谨慎，因为不知道从哪条指令重新执行，可能会造成新的异常。例如下面汇编是*pchBuffer=TEXT('J')的汇编，pchBuffer没有初始化而是指向了NULLMOV EAX, DWORD PTR[pchBuffer] MOV WORD PTR[EAX], 'J'这时第二条指令抛出异常，pchBuffer指向的是NULL，就算经过except调用函数的处理修复，pchBuffer虽然不再指向NULL，但寄存器EAX依然是NULL，所以陷入死循环。EXCEPTION_CONTINUE_SEARCH（0）EXCEPTION_CONTINUE_SEARCH表示不处理，继续向上找别的try处理块GetExceptionCode该函数可以返回发生异常的类型下面列举一些异常与内存相关的异常：EXCEPTION_ACCESS_VIOLATION：线程试图读写一个虚拟内存地址，但在这个地址并不具备相应权限EXCEPTION_DATATYPE_MISALIGNMENT：线程试图从没有提供自动对齐机制的硬件中读写没有对齐的数据EXCEPTION_ARRAY_BOUNDS_EXCEEDED：线程在支持边界检查的硬件上访问越界的数组元素EXCEPTION_IN_PAGE_ERROR：因文件系统或设备驱动返回一个读取错误，造成页面错误不能满足EXCEPTION_GUARD_PAGE：线程试图访问具有PAGE_GUARD保护属性的内存页。该内存页是可访问的，但同时引发了EXCEPTION_GUARD_PAGE异常EXCEPTION_STACK_OVERFLOW：线程耗尽了系统分配给它的栈空间EXCEPTION_ILLEGAL_INSTRUCTION：线程执行了一条无效指令。这个异常由具体的CPU架构所定义，在不同CPU上执行一条无效指令可能会导致陷阱错误EXCEPTION_PRIV_INSTRUCTION：线程试图执行在当前机器模式下不允许的指令与异常本身相关的异常：EXCEPTION_INVALID_DISPOSITION：异常过滤程序返回EXCEPTION_EXECUTE_HANDLER、EXCEPTION_CONTINUE_EXECUTION、EXCEPTION_CONTINUE_SEARCH之外的值EXCEPTION_NONCONTINUABLE_EXCEPTION：异常过滤程序返回EXCEPTION_CONTINUE_EXECUTION以响应不可继续的异常与调试相关的异常：EXCEPTION_BREAKPOINT：执行到一个断点EXCEPTION_SINGLE_STEP：由跟踪中断或者其他单步执行机制发出的一条指令已经执行完的信号EXCEPTION_INVALID_HANDLE：向函数传入了一个无效句柄与整型相关的异常：EXCEPTION_INT_DIVIDE_BY_ZERO：线程试图在整数除法运算中除以0EXCEPTION_INT_OVERFLOW：整型运算的结果超过了该类型规定的范围与浮点类型相关的异常：EXCEPTION_FLT_DENORMAL_OPERAND：浮点运算的其中一个操作数是非规格化值，太小以至于不能作为一个标准浮点值EXCEPTION_FLT_DIVIDE_BY_ZERO：线程试图在浮点除法运算中以浮点数0作为除数EXCEPTION_FLT_INEXACT_RESULT：浮点运算的结果不能精确地表示为十进制小数EXCEPTION_FLT_INVALID_OPERATION：表示任何没有在此列出的其他浮点数异常EXCEPTION_FLT_OVERFLOW：浮点运算结果的指数部分超过了该类型允许的最大值EXCEPTION_FLT_STACK_CHECK：浮点运算造成栈上溢或下溢EXCEPTION_FLT_UNDERFLOW：浮点运算结果的指数部分小于该类型允许的最小值注意，GetExceptionCode只能在异常过滤程序中（__except之后的圆括号中）或者异常处理程序的代码中调用，但不能在异常过滤程序的函数中调用GetExceptionInformation异常发生时，操作系统向发生异常的线程的栈中压入三个结构：EXCEPTION_RECORD、CONTEXT、EXCEPTION_POINTERSEXCEPTION_RECORD：包含与引发的异常有关的信息，这些信息的内容与CPU无关CONTEXT：包含关于异常但与CPU也有关的信息EXCEPTION_POINTERS：仅包含两个指针数据成员，分别指向被压入栈中的EXCEPTION_RECORD和CONTEXT结构可以使用GetExceptionInformation来获取这些信息，该函数会返回指向一个EXCEPTION_POINTERS结构的指针和GetExceptionCode限制类似，GetExceptionInformation只能在异常过滤程序中调用，一旦程序控制流被转移到异常处理程序或者别的地方，栈上的数据就被销毁了软件异常之前讨论的都是硬件异常，即CPU捕获事件并引发异常。其实也可以在代码中强制引发异常VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, CONST ULONG_PTR *pArguments);dwExceptionCode：要抛出异常的标识符，最好按照标准Windows错误码格式位31和位30描述严重性位29是1，0为Microsoft创建的异常保留位28是0位27~位16是Microsoft预定义的设备代码位15~位0是选择用来标识引发异常的程序段的任意值dwExceptionFlags：必须设为0或EXCEPTION_NONCONTINUABLE，用来指出异常过滤程序在响应这个异常时能否返回EXCEPTION_CONTINUE_EXECUTION。如果不向RaiseException传递EXCEPTION_NONCONTINUABLE标志，过滤程序就能返回EXCEPTION_CONTINUE_EXECUTION。正常情况下会导致线程重新执行引发软件异常的那条CPU指令。但Microsoft做了修改，使得程序从RaiseException函数调用的后面继续执行。如果传递EXCEPTION_NONCONTINUABLE，相当于告诉系统出现异常程序便不能继续，这个标志在OS内部用来传递极为严重的出错信号nNumberOfArguments、pArguments：传递和引发的异常有关的附加信息，可以传递NULL未处理异常如果每个异常过滤程序都返回EXCEPTION_CONTINUE_SEARCH，会发生未处理异常。Windows提供了SetUnhandledExceptionFilter函数，提供了处理异常的最后机会。PTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(PTOP_LEVEL_EXCEPTION_FILTER pTopLevelExceptionFilter);通常应在进程初始化阶段调用该函数。一旦调用了这个函数，进程中任意线程抛出未处理异常，都会通过SetUnhandledExceptionFilter里的异常过滤函数，该函数必须符合以下原型：LONG WINAPI TopLevelUnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);返回值是三个异常标识符之一即可UnhandledExceptionFilterLONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionInfo);同样返回值是三个异常标识符之一。该函数式系统在“异常最终没人处理”时会调用的默认入口，它内部会去调用 SetUnhandledExceptionFilter 设置进去的那个回调（如果有）UnhandledExceptionFilter处理异常时会按照顺序执行5个步骤：允许向资源写入并继续执行：如果是因为线程的写操作而引起访问违例，UnhandledExceptionFilter会检查该线程是不是试图修改exe或DLL模块中的资源。这些资源默认只读，试图修改他们会引发访问违例。UnhandledExceptionFilter调用VirtualProtect将资源页的保护属性设为PAGE_READWRITE，并返回EXCEPTION_CONTINUE_EXECUTION，以允许失败的指令再次执行将未处理异常报告给调试器：UnhandledExceptionFilter首先检查应用程序是否在调试器控制之下，是则返回EXCEPTION_CONTINUE_SEARCH。此时，由于异常未处理，Windows会通知当前连接的调试器。调试器接收为这个异常生成的EXCEPTION_RECORD结构的ExceptionInformation成员，并通过这个信息来定位代码中引发异常的指令，并通知你引发了什么样的异常通知设定的全局异常过滤函数：如果已调用SetUnhandledExceptionFilter设定了一个全局异常过滤程序，UnhandledExceptionFilter将调用它。如果过滤函数返回EXCEPTION_CONTINUE_EXECUTION、EXCEPTION_EXECUTE_HANDLER，UnhandledExceptionFilter直接返回该值给系统。如果过滤函数返回EXCEPTION_CONTINUE_SEARCH，则转到步骤4将未处理异常报告给调试器（再次）静默终止进程
Windows核心编程-学习笔记12DLL注入在Windows中，每个进程都有自己私有的地址空间。用指针来引用内存的时候，指针的值表示的是进程自己地址空间的一个内存地址。进程不能创建一个指针来引用属于其他进程的内存。但有些情况需要应用程序跨越进程边界来访问另一个进程的地址空间：想要从另一个进程创建的窗口派生出子类窗口需要辅助调试想要为另一个进程安装挂钩注册表注入DLLWindows注册表中默认提供了AppInit_DLLs与LoadAppInit_DLLs两个注册表项：将要注入的DLL路径字符串写入AppInit_DLLs（空格或逗号分隔文件名），然后LoadAppInit_DLLs值设为1这个方法缺点是自定义的DLL会映射到所有使用了User32.dll的进程中，但CUI程序不会使用；同时映射到的进程很多，可能会导致bugWindows挂钩注入DLL一个例子，进程A为了查看系统中各窗口处理了哪些消息，安装了WH_GETMESSAGE挂钩，通过调用SetWindowsHookEx安装HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0);WH_GETMESSAGE是安装挂钩的类型GetMsgProc是DLL里的一个函数的地址，在窗口即将处理一条消息时，系统调用这个函数hInstDll标识了一个含有GetMsgProc函数的DLL最后一个0表示要为哪个线程安装挂钩。一个线程可能调用SetWindowsHookEx并传入系统中另一个线程的线程标识符。通过为这个参数传递0，告诉系统为所有系统中的GUI线程安装挂钩接着来看会发生什么：进程B中的一个线程准备向一个窗口发送一条消息系统检查该线程是否已安装了WH_GETMESSAGE挂钩系统检查GetMsgProc所在的DLL是否已被映射到进程B的地址空间如果DLL尚未映射，系统会强制将该DLL映射到进程B的地址空间，并将进程B中该DLL的锁计数器递增由于DLL的hInstDll是在进程B中映射的，因此系统会对它进行检查，看它与该DLL在进程A中的位置是否相同。如果hInstDll相同，那么两个进程的地址空间中，GetMsgProc函数位于相同的位置，系统可以直接在进程A的地址空间中调用GetMsgProc。如果hInstDll不同，那么系统必须确定GetMsgProc函数在进程B中地址空间中的虚拟内存地址。地址公式为GetMsgProc B= hInstDll B + (GetMsgProc A - hInstDll A)系统在进程B中递增该DLL的锁计数器系统在进程B的地址空间中调用GetMsgProc函数GetMsgProc返回时，系统递减该DLL在进程B中的锁计数器注意当系统把挂钩过滤函数所在DLL注入或映射到地址空间中时，会映射整个DLL，而不仅仅是挂钩过滤函数相比注册表注入DLL方法，挂钩允许我们在不需要该DLL时可以从进程的地址空间中撤销对它的映射，只需调用UnHookWindowsHookExBOOL UnhookWindowsHookEx(HHOOK hHook);当一个线程调用UnhookWindowsHookEx时，系统会遍历自己内部的一个已经注入过该DLL的进程列表，并将该DLL锁计数器递减。当锁计数器减到0时，系统会自动从进程的地址空间中撤销对该DLL的映射远程线程注入DLLWindows提供了一些函数让一个进程对另一个进程进行操作。从根本上说，DLL注入技术要求目标进程中的一个线程调用LoadLibrary来加载目标DLL。由于不能轻易控制其他进程中的线程，所以可以在目标进程中创建一个新的线程，由我们自己控制：HANDLE CreateRemoteThread(HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId);除了hProcess，其他参数和CreateThread完全相同，hProcess用来表示新创建的线程归哪个进程所有。参数pfnStartAddr是线程函数的内存地址。对于LoadLibrary来说，实际上是：LoadLibraryA和LoadLibraryW，唯一区别在传给函数的参数类型，前者ANSI后者UNICODE。那么使用下面一行代码实现远程线程注入DLL：HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL);这样新线程在远程进程中创建时，会立即调用LoadLibraryW并传入DLL路径名。上面方法还有两个问题：CreateRemoteThread直接引用LoadLibraryW，该引用会被解析为我们模块的导入段中的LoadLibraryW转换函数的地址，传到远程线程很可能就是访问违例。因此需要先调用GetProcAddress来获得LoadLibraryW的确切位置。但Kernel32.dll都被映射到进程地址空间中的同一内存地址，所以可以修改代码如下PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL);DLL路径字符串问题：字符串"C:\MyLib.dll"是在调用进程的地址空间中，远程访问这个内存地址是没法找到的，此时远程就会崩溃终止。为解决这个问题，需要使用VirtualAllocEx函数在远程进程的地址空间中分配一个内存块来存储DLL路径字符串（VirtualFreeEx用来释放内存）。此外还需要方法将字符串从进程的地址空间复制到远程进程的地址空间中。Windows提供了ReadProcessMemory和WriteProcessMemory来进行读写：BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID pvAddressRemote, PVOID pvBufferLocal, SIZE_T dwSize, SIZE_T* pdwNumBytesRead); BOOL WriteProcessMemory(HANDLE hProcess, LPCVOID pvAddressRemote, PVOID pvBufferLocal, SIZE_T dwSize, SIZE_T* pdwNumBytesWritten);hProcess：远程进程pvAddressRemote：远程进程中的地址pvBufferLocal：本地进程中的内存地址dwSize：要传输的字节数pdwNumBytesRead和pdwNumBytesWritten：实际传输的字节数，函数返回时可查看完整代码如下#include <windows.h> #include <tchar.h> BOOL InjectDll(HANDLE hProcessRemote, const wchar_t* dllPath) { SIZE_T bytesWritten = 0; SIZE_T dllPathBytes = (wcslen(dllPath) + 1) * sizeof(wchar_t); // 1) 获取 LoadLibraryW 在本进程中的真实地址 HMODULE hKernel32 = GetModuleHandleW(L"Kernel32.dll"); if (!hKernel32) return FALSE; LPTHREAD_START_ROUTINE pfnLoadLibraryW = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryW"); if (!pfnLoadLibraryW) return FALSE; // 2) 在远程进程中申请内存保存 DLL 路径 LPVOID remoteBuf = VirtualAllocEx( hProcessRemote, NULL, dllPathBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!remoteBuf) return FALSE; // 3) 把 DLL 路径写入远程进程 if (!WriteProcessMemory(hProcessRemote, remoteBuf, dllPath, dllPathBytes, &bytesWritten) || bytesWritten != dllPathBytes) { VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return FALSE; } // 4) 创建远程线程调用 LoadLibraryW(remoteBuf) HANDLE hThread = CreateRemoteThread( hProcessRemote, NULL, 0, pfnLoadLibraryW, remoteBuf, 0, NULL); if (!hThread) { VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return FALSE; } // 可选：等待线程结束 WaitForSingleObject(hThread, INFINITE); // 清理 CloseHandle(hThread); VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return TRUE; }其他方法注入DLL木马DLL：直接替换，但不自动适用版本变化，应尽量避免使用将DLL作为调试器注入：麻烦使用CreateProcess来注入代码：如果要注入代码的进程是由我们进程生成的，父进程可以挂起子进程，通过子进程的主线程的句柄，对线程执行的代码进行修改。可以设置线程的指令指针，让它执行内存映射文件中的代码：让进程生成一个被挂起的子进程从exe模块的文件头中取得主线程的起始内存地址将位于该内存地址处的机器指令保存起来强制将一些手动编写的shellcode写入内存地址。里面应调用LoadLibrary来加载一个DLL让子进程的主线程恢复运行，从而执行shellcode将保存起来的原始指令恢复到起始地址处让进程从起始地址继续执行，好像什么都没发生过一样API拦截覆盖代码来拦截API在内存中定位要拦截的函数，得到它的内存地址将这个哈数起始的几个字节保存到我们自己的内存中用CPU的一条JUMP指令来覆盖这个函数的起始几个字节，这条jump指令用来跳转到替代函数的内存地址。当然，替代函数的签名必须与要拦截的函数的签名完全一致：所有参数必须相同，返回值必须相同，调用约定必须相同撤销只需将保存的字节放回去这个方法存在严重不足CPU依赖性，指令不同在抢占式、多线程环境下根本不能工作：一个线程覆盖另一个函数起始位置的代码需要时间，这时候另一个线程可能会试图调用同一个函数，所以必须确保任一时刻只能有一个函数试图调用这个函数修改模块的导入段来拦截API这个方法可以很好的解决上一种方法的两个问题。首先需要理解模块的导入段，它包含了一组DLL以及一个符号表，其中列举出了该模块从各DLL中导入的符号。当该模块调用一个导入函数的时候，线程实际会先从模块的导入表中得到相应的导入函数底子，然后再跳转所以要拦截函数很简单，修改这个函数在模块导入段中的地址即可。完全不存在对CPU的依赖，且没有修改函数代码，不必担心线程同步问题。
Windows核心编程-学习笔记11DLL基础Windows API提供的所有函数都包含在DLL中Kernel32.dll：包含用来管理内存、进程和线程的函数User32.dll：包含用来执行与用户界面相关的任务，如窗口创建、发送消息等函数GDI32.dll：包含用来绘图和显示文本的函数其他：AdvAPI32.dll包含的函数与对象的安全性、注册表的操控和事件日志有关；ComDlg32.dll包含一些常用的对话框（如打开文件和保存文件）；ComCtl32.dll支持所有常用的窗口控件DLL和进程的地址空间创建DLL必须为链接器指定/DLL开关，这样生成的文件映像才能被识别为DLL。在DLL函数能被调用之前，DLL的文件映像必须映射到调用进程的地址空间中隐式加载时链接显式运行时链接一旦系统将DLL的文件映像映射到调用进程的地址空间，进程中的所有线程就可以调用该DLL中的函数。线程调用DLL中的一个函数时，该DLL函数会在线程栈中取得传给它的参数，并使用线程栈来存放它需要的局部变量。此外，由DLL中的函数创建的任何对象都为调用线程或调用进程所拥有，DLL绝对不会拥有任何对象。生成DLL可执行模块：一个从DLL中导入函数和变量的模块DLL模块：导出函数和变量供可执行文件使用的模块如果一个可执行模块需要从一个DLL模块导入函数和变量，必须首先生成DLL模块，然后生成可执行模块。生成DLL：必须创建一个头文件，在其中包含要从DLL导出的函数原型、结构以及符号创建C/C++源代码模块来实现DLL模块中的函数和变量生成DLL模块会造成编译器处理每个源代码模块并生成一个.obj模块，每个源文件模块对应一个.obj模块所有.obj模块都创建完毕后，链接器合并所有.obj模块的内容并生成单一的DLL映像文件。该映像文件包含DLL的所有二进制代码以及全局/静态数据变量如链接器检测到DLL的源文件至少导出了一个函数或变量，还会生成一个.lib文件（只是列出所有被导出的函数和变量符号名称）一旦生成了DLL模块，就可以生成可执行模块：在所有引用了函数、变量、数据结构或符号的源代码模块中，必须包含由DLL开发人员创建的头文件创建C/C++源代码模块来实现想要包含在可执行模块中的函数和变量。当然，代码可以引用在DLL的头文件中定义的函数和变量生成可执行模块会造成编译器处理每个源代码模块并生成一个.obj模块所有.obj模块创建完毕后，链接器合并所有.obj模块的内容并生成一个可执行映像文件。该映像文件包含可执行文件的所有二进制代码以及全局变量/静态变量。可执行模块还包含一个导入段，其中列出了所有它需要的DLL模块的名称。此外，针对列出的每个DLL名称，该段还记录了可执行文件的二进制代码从中引用的函数和变量的符号名称。一旦DLL和可执行模块都生成完毕就可以执行了加载程序为新进程创建一个虚拟地址空间，并将可执行模块映射到新进程的地址空间。加载程序接着解析可执行模块的导入段，针对导入段列出的每个DLL名称，加载程序都会在用户的系统中定位该DLL模块，并将该DLL映射到进程的地址空间。注意DLL模块有可能从其他DLL模块导入函数和变量，因此加载程序需要解析每个模块的导入段，将所有要求的DLL都加载到进程的地址空间，所以进程的初始化可能非常耗时生成DLL模块创建DLL实际是在创建一组可供某个可执行模块（或其他DLL）调用的函数。DLL可向其他模块导出变量、函数或C++类。头文件（MyLib.h）：#ifdef MYLIBAPI #else // 表示所有函数变量都要导入 #define MYLIBAPI extern "C" __declspec(dllimport) #endif // 定义数据结构和符号、导出的变量、导出的函数原型 在DLL每个源代码文件（MyLibFile1.cpp）中必须包含上面的头文件#include<windows.h> #define MYLIBAPI extern "C" __declspec(dllexport) #include "MyLib.h" // 源代码extern "C"用来告诉编译器不要对变量名或函数名进行改编，保证C/C++等任何编程语言编写可执行模块都能访问导出的真正含义：当Microsoft的C/C++编译器看到__declspec(dllexport)修饰的变量、函数原型或C++类时，会在生成的.obj文件中嵌入一些额外的信息，链接DLL时，链接器会检测到这些信息并自动生成一个.lib文件（列出了DLL导出的符号），此外还会在生成的DLL文件中嵌入一个导出符号表（导出段），按字母顺序列出了导出的变量、函数和类的符号名。链接器还会保存相对虚拟地址（RVA），指出每个符号可在DLL模块的什么位置找到。生成可执行模块很简单，导入DLL的符号#include "MyLib\MyLib.h"运行可执行模块启动一个可执行模块时，操作系统加载程序会先为进程创建虚拟地址空间，接着将可执行模块映射到进程的地址空间。然后，加载程序会检查可执行模块的导入段，尝试定位所需的DLL并将其映射到进程的地址空间中。由于导入段只包含DLL名称，不包含DLL路径，所以加载程序必须在用户的磁盘上搜索DLL，搜索顺序如下可执行映像文件所在目录Windows系统目录，由GetSystemDirectory返回16位系统目录，即Windows目录中的System子目录Windows目录，由GetWindowsDirectory返回进程的当前目录PATH环境变量中列出的目录5目录在2目录后原因是防止加载程序在应用程序的当前目录中找到并加载伪造的系统DLLDLL高级技术DLL模块的显式加载和符号链接隐式加载DLL就是上一节的做法，显式加载需要应用程序在运行期间自主决定调用某个DLL的某个函数显式加载DLL模块调用函数可以是下面两个HMOUDLE LoadLibrary(PCTSTR pszDLLPathName); HMOUDLE LoadLibraryEx(PCTSTR pszDLLPathName, HANDLE hFile, DWORD dwFlags);这两个函数会采用上面搜索DLL的算法定位DLL文件映像，并尝试将该映像映射到调用进程的地址空间，返回值就是文件映像映射到的虚拟内存地址。LoadLibraryEx有额外两个参数：hFile：设为NULL，留待未来扩充使用dwFlags：可设为0或多种标志的组合DONT_RESOLVE_DLL_REFENCES：只需将DLL映射到调用进程的地址空间，不会对额外DLL自动加载进程的地址空间LOAD_LIBRARY_AS_DATAFILE：将DLL当做一个数据文件映射到进程地址空间，不分配页面保护属性（比如加载一个exe文件的资源）LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE：DLL文件以独占访问模式打开，禁止其他任何应用程序在当前应用程序使用该DLL文件时对其进行修改LOAD_LIBRARY_AS_IMAGE_RESOURCE：与LOAD_LIBRARY_AS_DATAFILE相似，但使用这个时，系统加载DLL会使用RVA进行修复LOAD_WITH_ALTERED_SEARCH_PATH：改变LoadLibraryEx在定位指定DLL时所采用的搜索算法如果pszDLLPathName不包含\字符，就用标准搜索路径来定位DLL如果pszDLLPathName包含\字符，取决于路径是否为完整路径完整路径：直接尝试加载不是完整路径：函数会将下列文件夹与pszDLLPathName连接起来进程的当前目录Windows系统目录16位系统目录——Windows目录下的System子目录Windows目录PATH环境变量中列出的目录如果出现.或..也会考虑进去拼接一个相对路径如果不想用LOAD_WITH_ALTERED_SEARCH_PATH，可以调用SetDllDirectory，并将库文件夹作为参数传入（传递""表示将当前路径从搜索步骤中删除，传递NULL恢复默认算法）。这个函数告诉LoadLibrary和LoadLibraryEx在搜索时使用以下算法：应用程序的当前目录通过SetDllDirectory设置的目录Windows系统目录16位Windows系统目录Windows目录PATH环境变量中列出的目录LOAD_IGNORE_CODE_AUTHZ_LEVEL：用来关闭WinSafer提供的验证（对代码在执行过程中可以拥有的特权加以控制）显式卸载DLL模块BOOL FreeLibrary(HMODULE hInstDll); VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode);后者包含了FreeLibrary和ExitThread每个DLL在进程中都有一个与之对应使用计数，LoadLibrary和LoadLibraryEx函数会递增该使用计数，而FreeLibrary和FreeLibraryAndExitThread会递减该使用计数。注意不同进程使用计数互不影响线程可以调用GetModuleHandle来检测一个DLL是否已被映射到进程的地址空间，可以检查未映射再LoadLibraryHMODULE GetModuleHandle(PCTSTR pszModuleName);HMODULE hInstDll = GetModuleHandle(TEXT("MyLib")); if (hInstDll == NULL) { hInstDll = LoadLibrary(TEXT("MyLib")); }如果向GetModuleHandle传递NULL，函数会返回应用程序的可执行文件的句柄如果只有DLL或exe的HINSTANCE/HMODULE，可使用GetModuleFileName函数获取DLL的完整路径：DWORD GetModuleFileName(HMODULE hInstModule, PTSTR pszPathName, DWORD cchPath);hInstModule：DLL或exe的HMODULE；如果传递NULL在pszPathName中返回当前正在运行的应用程序的可执行文件的文件名pszPathName：一个缓冲区地址，函数会将文件映像的完整路径保存到这个缓冲区中cchPath：指定了缓冲区的大小一旦显式加载了一个DLL模块，线程必须调用GetProcAddress来得到想引用的符号的地址FARPROC GetProcAddress(HMODULE hInstDll, PCSTR pszSymbolName);hInstDll：指定了包含符号的那个DLL的句柄，是先前LoadLibrary（Ex）或GetModuleHandle返回的pszSymbolName：两种形式，第一种是以0为终止符的字符串（注意传递ANSI字符串，不要带TEXT）；第二种是用序号来指定要获得地址的符号，但不常用如果DLL不包括指定符号，GetProcAddress返回NULL，表示调用失败DLL的入口点函数DLL调用入口点函数主要是用来通知执行一些针对当前进程或线程的初始化和清理工作BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL映射到进程地址空间 break; case DLL_THREAD_ATTACH: // 创建了一个线程 break; case DLL_THREAD_DETACH: // 线程干净地退出 break; case DLL_PROCESS_DETACH: // DLL从进程地址空间撤销映射 break; } return (TRUE); // 仅用于DLL_PROCESS_ATTACH }hInstDll：类似_tWinMain的hInstExe参数，该值标识了一个虚拟内存地址，DLL文件映像被映射到进程地址空间中的这个位置。通常将该参数保存在一个全局变量中，以便在调用资源加载函数时使用。fdwReason：系统调用入口点函数的原因fImpLoad：DLL隐式加载时值非0，显式加载值为0注意：避免在DllMain里调用其他DLL导入的函数（可能没初始化）；避免调用LoadLibrary（Ex）和FreeLibrary（可能会产生循环依赖）DLL_PROCESS_ATTACH系统首次将一个DLL映射到进程地址空间时会调用DllMain函数，并为fdwReason传递DLL_PROCESS_ATTACH，DllMain主要执行任何与进程相关的初始化操作。在DllMain处理DLL_PROCESS_ATTACH通知时，DllMain返回值指出该DLL的初始化是否成功，其他fdwReason值时系统会忽略DllMain的返回值隐式加载DLL时程序运行-具体步骤：系统创建新进程，分配进程地址空间并将exe文件映像以及所需DLL文件映像映射到进程的地址空间中系统创建进程的主线程，用这个线程来调用每个DLL的DllMain函数并传递DLL_PROCESS_ATTACH所有已映射的DLL都完成了对该通知的处理后，系统会先让进程的主线程开始执行可执行模块的c/c++运行库，然后执行可执行模块的入口点函数（_tmain或_tWinMain）。如果任何一个DLL的DllMain返回FALSE，系统会将所有文件映像从地址空间中清除，向用户显示一个消息框来指出进程无法启动，然后终止整个进程显式加载DLL时程序运行-具体步骤：进程中的一个线程调用LoadLibrary（Ex）时，系统定位指定的DLL并将该DLL映射到进程地址空间系统调用LoadLibrary（Ex）的线程来调用DLL的DllMain函数，并传入DLL_PROCESS_ATTACH当DLL的DllMain完成了对通知的处理时，系统会让LoadLibrary（Ex）调用返回，线程和往常一样继续执行。若DllMain返回FALSE，系统自动从进程地址空间撤销对DLL文件映像的映射并让LoadLibrary（Ex）返回NULLDLL_PROCESS_DETACH系统将一个DLL从进程地址空间中撤销映射时会调用DllMain函数，并为fdwReason传递DLL_PROCESS_DETACH，DllMain执行与进程相关的清理工作。按照撤销映射原因分类：进程要终止：调用ExitProcess函数的线程将负责执行DllMain函数的代码。正常情况下，这个线程就是应用程序的主线程。入口点函数返回到C/C++运行库的启动代码后，启动代码会显式调用ExitProcess来终止进程进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread：发出调用的线程将负责DllMain函数中的代码。如果调用的是FreeLibrary，那么在DllMain处理完DLL_PROCESS_DETACH通知前，线程不会从该调用中返回的；如果调用TerminateProcess，系统不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数，即没有执行清理代码DLL_THREAD_ATTACH当进程创建一个线程的时候，系统会检查当前映射到该进程的地址空间中的所有DLL文件映像，并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数，告诉DLL需要执行与线程相关的初始化。新建的线程负责执行所有DLL的DllMain函数代码。只有在所有DLL都完成了对该通知的处理后，系统才会让新线程开始执行它的线程函数。系统将一个新的DLL映射到进程地址空间时，如果进程中已有多个线程在运行，系统不会让任何现有的线程用DLL_THREAD_ATTACH来调用该DLL的DllMain函数。只有创建新线程，而且DLL已被映射到进程地址空间，系统才会用DLL_THREAD_ATTACH来调用DLL的DllMain函数。DLL_THREAD_DETACH让线程终止的首选方式是让它的线程函数返回，系统会调用ExitThread来终止线程，然后系统会让这个将终止的线程用DLL_THREAD_DETACH来调用所有已映射的DLL的DllMain函数，进行线程相关的清理。DLLMain和C/C++运行库如果使用Microsoft的链接器并指定了/DLL开关，链接器会认为入口点函数的名称是_DllMainCRTStartup。该函数包含在C/C++运行库文件中，并在链接DLL时静态链接到DLL文件映像中系统将DLL的文件映像映射到进程地址空间时，实际调用的是_DllMainCRTStartup函数，该函数先处理DLL_PROCESS_ATTACH通知，初始化C/C++运行库，并确保在_DllMainCRTStartup收到DLL_PROCESS_ATTACH通知时，所有全局或静态C++对象都已构造完毕。在C/C++运行时的初始化完成后，函数才会调用DllMain函数。链接DLL时，如果链接器无法在DLL的.obj文件中找到一个名为DllMain的函数，那么它会链接C/C++运行库的DllMain函数。如果不提供自己的DllMain，C/C++运行库会认为不关心DLL_THREAD_ATTACH和DLL_THREAD_DETACH的通知。所以为了提升创建线程和销毁线程的性能，C/C++运行库会在它提供的DllMain函数中调用DisableThreadLibraryCalls延迟加载DLL延迟加载DLL：隐式链接，系统一开始不会将该DLL加载，只有在代码试图引用DLL中包含的一个符号时，系统才会实际加载该DLL。主要在下列情况下非常有用：如果应用程序使用了多个DLL，它的初始化可能会比较慢，因为加载程序要将所有必需的DLL映射到进程地址空间。缓解该问题的一个办法是将DLL的加载过程延伸到进程的执行过程中如果在代码中调用一个新函数，然后试图在一个不提供该函数的老版本OS中运行该应用程序，加载程序会报错并不允许应用程序运行已知的DLL系统对OS提供的某些DLL进行了特殊处理，这些DLL被称为已知的DLL。在注册表中，有一个注册表项HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs，其中包含了多个DLL名称，每个数值名称的数值数据正好等于数值名称加上.dll扩展名（并非肯定如此）。当LoadLibrary或LoadLibraryEx被调用时，函数首先检查是否传入了一个包含扩展名的DLL名称，如果没有包含会用正常的搜索规则来搜索这个DLL。如果指定了.dll扩展名，这两个函数会先将扩展名去掉，然后再KnownDLLs注册表项中搜索，看其中是否有与之相符的数值名称。如果没有数值名称与之相符，就使用正常的搜索规则。但如果找到了与之相符的数值名称，系统就会查看与数值名称对应的数值数据，并试图用该数据来加载DLL。系统会从C:\Windows\System32开始搜索DLL
Windows核心编程-学习笔记10内存映射文件内存映射文件允许预定一个地址空间区域并为区域调拨物理存储，但这个物理存储来自磁盘上已有的文件，而不是系统分页文件内存映射文件用于：加载并执行exe和dll，节省分页文件的空间和应用程序的启动时间访问磁盘上的数据文件，避免文件I/O操作和缓冲文件内容在同一台机器的不同进程之间共享数据映射到内存的可执行文件和DLL系统在线程调用CreateProcess时：系统先定位调用CreateProcess时指定的可执行文件。找不到exe文件，进程就不会创建，返回FALSE系统新建一个进程内核对象系统为该进程创建一个私有地址空间系统预定一个足以容纳exe文件的地址空间区域，该区域首选的位置已在exe文件中指定。默认x86 0x400000，x64 0x140000000系统会标注预定区域的后备物理存储来自磁盘上的exe文件而不是系统的分页文件系统将exe文件映射到进程的地址空间之后，会访问exe文件中的一个段，其中列出了一些DLL文件，包含在exe中调用的函数。然后系统针对每个dll都调用LoadLibrary；其中的任何DLL用到了其他DLL，系统同样会调用LoadLibrary来加载DLL。对于DLL：系统预定一个足以容纳DLL文件的地址空间区域。默认x86 0x10000000，x64 0x180000000。所有与windows一起发布的标准系统dll都有不同的基地址，这样即使加载到同一个地址空间也不会发生重叠如果系统无法在DLL文件指定的基地址预定区域，这可能是因为该区域已被另一个DLL或exe占用，也可能区域不够大，这时系统会尝试在其他地址为DLL预定地址空间区域。这样不好有两点，一是DLL去除了重定位信息，系统可能无法加载DLL；二是系统在DLL内部需要重定位，占用分页文件额外的存储，还会增大加载DLL所需的时间系统会进行标注，表明预定区域的后备物理存储来自磁盘上的DLL文件，而非来自系统的分页文件。如果windows不能将DLL加载到他首选的基地址而必须重定位，系统还会另行标注，表明DLL中有一部分物理存储被映射到了分页文件完成exe文件映射后，系统会负责所有换页、缓冲和高速缓存操作。例如，如果exe中的代码导致它跳转到一条尚未载入内存的指令的地址，就会引发页面错误。系统检测到错误自动将代码页从文件映像加载到一个RAM中。然后系统将该RAM页映射到进程地址空间中的适当位置，并让线程继续执行，就好像代码页早已自如内存一样，这一切对于应用程序来说都是透明的。同一个可执行文件或DLL的多个实例不会共享静态数据如果应用程序的一个实例修改了数据页面中的一些全局变量，应用程序所有实例的内存都会被修改。系统通过内存管理系统的写时复制特性来防止这种情况发生，每次当应用程序试图向内存映射文件写入时，系统都会截获此类尝试，接着为应用程序试图写入的内存页分配一个新的内存块，复制页面内容，然后让应用程序向刚才分配的内存块写入。这样应用程序的其他实例不会受任何影响。在同一个可执行文件或DLL的多个实例间共享静态数据默认不行，但可以利用这个共享数据设计一个变量来保存正在运行实例的数量，新实例只需检查这个全局变量的值就可以确定当前运行实例数目。每个exe或DLL文件映像由多个段组成，每个标准段名称都以点号开始。段名目的.bss未初始化的数据.CRT只读的C运行时数据.data已初始化的数据.debug调试信息.didata延迟导入的名字表.edata导出的名字表.idata导入的名字表.rdata只读的运行时变量.reloc重定位表信息.rsrc资源.textexe或DLL的代码.textbss当启用增量链接选项时，由C++编译器生成.tls线程局部存储.xdata异常处理表每个段都有与之关联的属性：READ：可以读取数据WRITE：可以写入数据EXECUTE：可以执行该段内容SHARED：该段内容为多个实例所共享除了上面编译器和链接器创建的标准段，还可以自己定义段#pragma data_seg("sectionname")例如下面创建Shared段，只包含一个LONG变量#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_seg()如果没有初始化g_lInstanceCount就会放到Shared段之外的其他段。但可以借助allocate声明标识符将未初始化的数据放到任何希望的段中#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_seg() __declspec(allocate("Shared")) int d;要想共享变量，还需要告诉链接器共享一个特定段的变量。可以通过在链接器的命令行汇总使用/SECTION开关来实现/SECTION:name,attributes例如（S为SHARED）/SECTION:Shared,RWS也可以直接将链接器开关嵌入源代码#pragma comment(linker, "/SECTION:Shared,RWS")映射到内存的数据文件Windows操作系统允许将数据文件映射到进程的地址空间，这样操控大数据流时会很方便。例如，实现对文件中所有字节进行反转：一个文件，一个缓冲区：分配一块足够大的内存来存放整个文件，然后用交换算法。缺点是文件比较大，无法调拨很大物理存储；其次反转顺序后的内容写回文件可能损坏两个文件，一个缓冲区：先打开现有文件，并创建一个长度为0的新文件，接着分配一个小的内部缓冲区，比如8KB，然后文件指针定位到原始文件末尾减去8KB的地方，对这部分反转写入新文件直到达到起始位置。缺点是需要操作指针，处理速度慢；其次消耗大量空间（最后2倍原文件）一个文件，两个缓冲区：初始化分配两个8KB缓冲区，读取前8KB和后8KB，都反转然后第一个写回文件尾，最后一个写回文件头，以此类推，注意最后一部分不是8KB倍数需要特殊处理。一个文件，零个缓冲区：使用内存映射文件，打开文件并向系统预定一个虚拟地址空间区域使用内存映射文件创建并打开一个文件内核对象，该对象标识了想要用作内存映射文件的磁盘文件CreateFile创建文件内核对象，必须以只读或可读可写方式来打开文件创建一个文件映射内核对象，向系统说明文件的大小以及准备如何访问文件调用CreateFileMappingHANDLE CreateFileMapping(HANDLE hFile, PSECURITY_ATTRIBUTES psa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, PCTSTR pszName);hFile：需要映射到进程地址空间的文件的句柄，前面CreateFile返回的psa：指针，通常传递NULL表示默认fdwProtect：指定保护属性dwMaximumSizeHigh/dwMaximumSizeLow：CreateFileMapping主要目的是确保有足够物理存储可供文件映射对象使用，这两个参数告诉系统内存映射文件的最大大小。要用当前文件大小创建一个文件映射对象，为这个两个参数传递0即可；如果磁盘文件大小为0字节，就不能传两个0，否则系统认为错误返回NULLpszName：以0为终止符字符串，用来为文件映射对象指定名称告诉系统将文件映射对象的部分或全部映射到进程的地址空间中调用MapViewOfFile为文件的数据预定一个地址空间区域PVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap);将文件映射到进程的地址空间时，不必一下子映射整个文件。可以每次只将文件的一小部分映射到地址空间中。文件被映射到进程地址空间的部分称为视图（View）hFileMappingObject：文件映射对象的句柄，前面调用CreateFileMapping或OpenFileMapping返回的dwDesiredAccess：指定数据访问方式dwFileOffsetHigh、dwFileOffsetLow：告诉系统应该将数据文件中的哪个字节映射到视图中的第一个字节dwNumberOfBytesToMap：告诉系统要将数据文件中的多少映射到地址空间用完后，清理步骤如下：告诉系统从进程地址空间中取消对文件映射内核对象的映射调用UnmapViewOfFile来释放内存区域BOOL UnmapViewOfFile(PVOID pvBaseAddress);pvBaseAddress指定了要返还区域的基地址，必须和MapViewOfFile的返回值相同关闭文件映射内核对象关闭文件内核对象和第2步一样都是CloseHandle用内存映射文件在进程间共享数据Windows提供了多种机制在应用程序间快速、方便地共享数据和信息，包括RPC、COM、OLE、DDE、窗口消息、剪贴板、邮槽、管道、套接字等。在WIndows中，在同一台机器上共享数据最底层的机制就是内存映射文件。这种数据共享机制是通过让两个或更多进程映射同一个文件映射对象的视图来实现的，这意味着它们共享相同的物理存储页面。所以，一个进程在共享文件映射对象的视图中写入数据时，其他进程也可以看到变化（注意所有进程必须为文件映射对象使用完全相同的名称）堆堆非常适合分配大量小数据块。例如，链表和树最好是用堆来管理，优点在于不必理会分配粒度和页面边界，缺点在于分配和释放内存块的速度比其他方式慢，而且无法再对物理存储的调拨和撤销调拨进行直接控制略过，不太重要
Windows核心编程-学习笔记9探索虚拟内存系统信息操作系统的许多值由主机决定，比如页面大小和分配粒度等。可以使用GetSystemInfo函数获取主机相关值VOID GetSystemInfo(LPSYSTEM_INFO psi);必须将一个SYSTEM_INFO结构的地址传给该函数typedef struct _SYSTEM_INFO { union { struct { WORD wProcessorArchitecture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberofProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO, *LPSYSTEM_INFO;系统启动时会确定这些成员的值。对于任何给定的系统，这些值始终一样，所以在任何进程中只需调用一次即可。SYSTEM_INFO结构中与内存有关的成员：dwPageSize：表示CPU的页面大小。x86、x64为4096字节，IA-64为8192字节lpMinimumApplicationAddress：给出每个进程的可用地址空间中最小内存地址lpMaximumApplicationAddress：给出每个进程的私有地址空间中可用的最大内存地址dwAllocationGranularity：显示地址空间预定区域的分配粒度为了能让32位程序在64位版本Windows上运行，Microsoft提供了一个称为Windows 32-bit On Windows 64-bit（WOW64）的仿真层32位程序通过WOW程序运行时，GetSystemInfo返回值和原生64位程序返回值可能不一样。可以调用IsWow64Process函数确定进程是否在WOW64上运行BOOL IsWow64Process(HANDLE hProcess, PBOOL pbWow64Process);hProcess：当前正在运行的应用程序pbWow64Process：如果WOW64，pbWow64Process指向的布尔值为TRUE新的IsWow64Process2可以告诉我们进程机器架构BOOL IsWow64Process2( [in] HANDLE hProcess, [out] USHORT *pProcessMachine, [out, optional] USHORT *pNativeMachine );虚拟内存状态VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);GlobalMemoryStatus用来获取当前内存状态的动态信息，传入MEMORYSTATUS结构地址typedef struct _MEMORYSTATUS { DWORD dwLength; // 整个结构大小（字节数） DWORD dwMemoryLoad; SIZE_T dwTotalPhys; SIZE_T dwAvailPhys; SIZE_T dwTotalPageFile; SIZE_T dwAvailPageFile; SIZE_T dwTotalVirtual; SIZE_T dwAvailVirtual; } MEMORYSTATUS, *LPMEMORYSTATUS;如果预计程序会在有4GB内存的机器上运行，或者分页文件的大小可能超过4GB，则使用GlobalMemoryStatusExVOID GlobalMemoryStatusEx(LPMEMORYSTATUSEX pmst);传入MEMORYSTATUSEX结构地址typedef struct _MEMORYSTATUSEX { DWORD dwLength; DWORD dwMemoryLoad; DWORDLONG ullTotalPhys; DWORDLONG ullAvailPhys; DWORDLONG ullTotalPageFile; DWORDLONG ullAvailPageFile; DWORDLONG ullTotalVirtual; DWORDLONG ullAvailVirtual; DWORDLONG ullAvailExtendedVirtual; } MEMORYSTATUSEX, *LPMEMORYSTATUSEX;除了所有表示大小的成员变为64位之外其他与原来的MEMORYSTATUS一样，最后一个成员ullAvailExtendedVirtual表示当前进程的虚拟地址空间的超大内存部分中未预定的内存大小NUMA机器中的内存管理NUMA-非统一内存访问（Non-Uniform Memory Access）机器中的CPU既能访问自己节点的内存，也能访问其他节点的内存。但是CPU访问自己节点的内存比访问外部节点的内存要快得多。默认情况下，当线程调拨物理存储时，操作系统会尽量用CPU自己节点的RAM来支持物理存储以提升访问性能。只有在没有足够RAM情况下才会使用外部节点的RAM来支持物理存储。调用GlobalMemoryStatusEx时，通过ullAvailPhys参数返回的值是所有节点可用内存的总量。要想知道某个特定NUMA节点的内存容量需要调用BOOL GetNumaAvailableMemoryNode(UCHAR uNode, PULONGLONG pulAvailableBytes);uNode标识了节点，pulAvailableBytes指向的LONGLONG变量用来返回该节点的内存容量。可以调用GetNumaProcessorNode函数轻松判断CPU驻留在哪个NUMA节点上BOOL GetNumaProcessorNode(UCHAR Processor, PUCHAR NodeNumber);可调用GetNumaHighestNodeNumber获取系统的最高节点编号：BOOL GetNumaHighestNodeNumber(PULONG pulHighestNodeNumber);对于任何给定的节点，可调用获得驻留在该节点中的CPU列表：pulProcessorMask返回位掩码，哪一位被设置了，与该位对应的CPU就属于该节点BOOL GetNumaNodeProcessorMask(UCHAR uNode, PULONGLONG pulProcessorMask);确定地址空间的状态Windows提供了VirtualQuery来查询与地址空间中的内存地址有关的特定信息，比如大小、存储类型和保护属性DWORD VirtualQuery(LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);Windows还提供了允许一个进程查询另一个进程内存信息的函数DWORD VirtualQueryEx(HANDLE hProcess, LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);后者允许传递要查询其地址空间的一个进程的句柄，常用于调试器等调用VirtualQuery（Ex）函数时，pvAddress必须指定要查询的虚拟内存地址。参数pmbi指向一个必须由你分配的MEMORY_BASIC_INFORMATION结构typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; WORD PartitionId; SIZE_T RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;最后一个参数制定MEMORY_BASIC_INFORMATION结构的大小，VirtualQuery（Ex）返回值是它复制到MEMORY_BASIC_INFORMATION结构缓冲区中的字节数在应用程序中使用虚拟内存Windows提供三种机制来操控内存：虚拟内存：最适合用来管理大型对象或结构的数组内存映射文件：最适合用来管理大型数据流（通常来自文件），以及在同一机器上运行的多个进程之间共享数据堆：最适合用来管理大量的小型对象首先是虚拟内存，Windows提供了一些用来操控虚拟内存的函数，可通过这些函数直接预定地址空间区域，为区域调拨物理存储，以及根据需要设置页面的保护属性。预定地址空间区域VirtualAlloc来预定PVOID VirtualAlloc(PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect);pvAddress：内存地址，告诉系统在什么地方预定地址空间。大多数时候传递NULL，让系统自己去找合适的区域来预定dwSize：想预定区域大小，以字节为单位fdwAllocationType：告诉系统是要预定区域还是调拨物理存储fdwProtect：为区域分配的保护属性为预定的区域调拨物理存储预定区域后还需要为区域调拨物理存储，这样才能访问其中的内存地址。系统会从分页文件中调拨物理存储给区域。调拨物理存储时，起始地址始终都是页面大小的整数倍，大小也是页面大小的整数倍。为了调拨物理存储，必须再次调用VirtualAlloc，但需要传递MEM_COMMIT标识符而不是MEM_RESERVE来作为fdwAllocationType参数的值。为物理存储指定页保护属性时，通常使用和预定区域时相同的保护属性（大多时候是PAGE_READWRITE），但也可以指定一个不同的保护属性。同时预定和调拨物理存储要想预定区域的同时为区域调拨物理存储，只需调用VirtualAlloc一次即可PVOID pvMem = VirtualAlloc(NULL, 99*1024, MEM_RESERVE|MEM_COMMIT, PAGE_READWRITE);何时调拨物理存储一个很不错的案例是电子表格：如果通过链表实现电子表格，只需要某个单元格存放了数据时才会创建与之对应的CELLDATA结构，由于里面大多数单元格没有用到，所以这种方法可以节省大量物理存储，但是读取难度增大，必须要遍历链表来找到对应单元格。因此利用虚拟内存技术：预定足够大的区域来容纳CELLDATA结构的整个数组，只预定区域不消耗物理存储当用户在某个单元格输入数据时，首先确定CELLDATA结构在区域中的内存地址。这是还没有为改地址映射物理存储，所以试图访问该内存地址会引发访问违例为第2步的内存地址调拨一个CELLDATA结构所需的物理存储设置新的CELLDATA结构的成员上面技术存在的问题在于，必须确定何时调拨物理存储如果用户在一个单元格中输入数据，然后只是编辑或修改其中的数据，就没必要调拨物理存储。因为第一次输入数据时已经调拨了由于系统始终按照页面粒度来调拨物理存储，试图为一个CELLDATA结构调拨物理存储时，系统实际上为整个页面调拨了物理存储。此时相邻单元格输入数据就不用再调拨物理存储了可通过4种方法来确定是否需要为区域中的某个部分调拨物理存储：总是尝试调拨物理存储：最简单但性能降低使用VirtualQuery查询是否为某个地址空间调拨了物理存储：更糟糕了，还要查询记录哪些页面已经调拨，哪些为被调拨：运行的更快，但需要实现记录页面调拨信息的方法使用结构化异常处理（SEH）：最佳方案，该方法可以让系统在发生某种情况时通知应用程序。设置一个SEH，当程序试图访问未被调拨物理存储的内存地址时，系统通知应用程序。应用程序调拨物理存储，并告诉系统重新执行那条引发异常的指令撤销调拨物理存储并释放区域可以调用VirtualFreeBOOL VirtualFree(LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);释放一个已预定的区域：pvAddress：区域的基地址，预定区域时VirtualAlloc所返回的地址dwSize：只能传0fdwFreeType：MEM_RELEASE，告诉系统撤销映射到区域的所有物理存储，并释放所预定的全部区域要撤销调拨给区域的一部分物理存储，但不想释放整个区域：pvAddress：标识了要撤销调拨的第一个页面地址dwSize：想要释放的字节数fdwFreeType：MEM_DECOMMIT和调拨物理存储一样，撤销调拨物理存储也是基于页面粒度的更改保护属性BOOL VitualProtect(PVOID pvAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD pflOldProtect);pvAddress：内存基地址dwSize：要改变保护属性的区域的大小（以字节为单位）flNewProtect：除了PAGE_WRITECOPY和PAGE_EXECUTE_WRITECOPY之外的任何PAGE_*属性pflOldProtect：函数在里面填充原来和pvAddress地址处的字节关联的保护属性同样保护属性与整个物理存储页关联，而非单个字节如果若干连续的物理存储页跨越了不同的区域，VitualProtect无法一次性改变他们的保护属性，必须多次调用
Windows核心编程-学习笔记8Windows内存架构进程的虚拟地址空间每个进程都有自己的虚拟地址空间。对32位进程，地址空间大小为4GB，64位大小为16EB由于每个进程都有私有的地址空间，当进程中的各个线程运行时，它们只能访问属于该进程的内存。线程既看不到属于其他进程的内存，也无法访问它们。虚拟地址空间的分区每个进程的虚拟地址空间被划分为许多分区（partition）。由于地址空间的分区依赖于OS底层实现，所以会随Windows内核的不同而略有变化空指针赋值分区：0~0xFFFF闭区间，如果进程中的线程试图读取或写入位于这一分区的内存地址，会引发访问违例。例如：int* pnSomeInteger = (int*)malloc(sizeof(int)); *pnSomeInteger = 5;malloc可能返回 NULL（例如内存不足），此时解引空指针就会触发访问违例。正确安全做法是先检查malloc返回值是否为NULL用户模式分区：这一分区大小取决于CPU架构。一个进程无法通过指针来读取、写入或以其他任何方式访问驻留在这一分区中其他进程的数据。进程的大部分数据都保留在这一分区。由于每个进程都有自己的数据分区，所以一个应用程序破坏另一个应用程序的可能性非常小，使整个系统更加健壮在Windows中，每个exe、dll模块都在这个区域加载。每个进程都可能将DLL加载到这一分区的不同地址。系统还在这一分区映射该进程可以访问的所有内存映射文件。内核模式分区：这一分区是OS代码所在的地方。与线程调度、内存管理、文件系统支持、网络支持以及设备驱动程序相关的代码都加载到这一分区。驻留在该分区的一切均由所有进程共享。虽然这一分区就在每个进程中用户模式分区的上方，该分区中的所有代码和数据都被完全保护起来。应用程序试图读取或写入位于这一分区的内存地址会引发访问违例。地址空间中的区域系统创建一个进程并赋予它地址空间时，可用地址空间中的大部分都是闲置的或尚未分配的。为了使用这部分地址空间，必须调用VirtualAlloc来分配其中的区域。分配区域的操作被称为预定。应用程序预定地址空间的一个区域时，系统会确保区域的起始地址在分配粒度（allocation granularity）的边界上。分配粒度随不同的CPU平台而不同。区域大小是系统页面大小的整数倍。页面（page）是一个内存单位，系统通过它来管理内存。和分配粒度类似，页面大小会因不同CPU而变化，x86和x64系统页面大小为4KB，IA-64系统页面大小为8KB。所以假如x86或x64系统中，预定一个10KB地址空间，系统会预定12KB区域当程序不再需要访问预定的地址空间区域时，应释放该区域。这个过程称为释放地址空间区域，通常调用VirtualFree函数完成为区域调拨物理存储预定的地址空间区域要想使用，必须分配物理存储，并将该存储映射到预定的区域。这个过程称为调拨物理存储。物理存储始终以页面为单位来调拨。我们调用VirtualAlloc函数将物理存储调拨给所预定的区域为区域调拨物理存储时，并不需要为整个区域都调拨物理存储。程序不再需要访问预定区域中已调拨的物理存储时，应将其释放。这个过程称为撤销物理存储，通过调用VirtualFree完成物理存储和分页文件如今的OS使磁盘空间看起来像内存一样，一种称为分页文件的磁盘文件包含可供任何进程使用的虚拟内存。当线程试图访问存储中的一个字节时，CPU必须知道该字节是在RAM中还是在磁盘上一个线程试图访问所属进程的地址空间中的一个数据块时，有两种情况：线程要访问的数据就在RAM中。CPU会先将数据的虚拟内存地址映射到内存的物理地址，接着就可以访问内存中的数据线程要访问的数据不在RAM中，而是在分页文件的某处。这次不成功的访问称为页面错误（page fault）。发生这个错误时，CPU会通知OS，OS在RAM中找到一个闲置页面。如果找不到，OS必须先释放一个。如果待释放的页面没有修改过，OS可直接释放该页面。但如果系统需要释放一个修改过的页面，必须先将页面从RAM中复制到分页文件，接着OS对它内部的表项进行更新，以指出该数据的虚拟内存地址现已映射到了RAM中对应的物理内存地址。这时，CPU会重试那条引发页面错误的指令。但这一次CPU能将虚拟内存地址映射到物理RAM地址，并成功访问数据块不在分页文件中维护的物理存储：并不是每运行一个程序，系统就必须为该进程的代码和数据预定地址空间区域，为这些区域调拨物理存储，再将硬盘上的程序文件中的代码和数据复制到分页文件中已调拨的物理存储中。相反，用户执行一个应用程序时，系统会打开应用程序的exe文件，确定应用程序的代码和数据大小，然后系统预定一个地址空间，注明与该区域关联的物理存储就是exe文件本身。这样一来不但能快速加载应用程序，分页文件还可以保持在合理的大小。将程序位于磁盘上的文件映像用作一个地址空间区域的物理存储时，这个文件映像就是所谓的内存映射文件。页面保护属性可为每个已分配的物理存储页指定不同的页面保护属性保护属性描述PAGE_NOACCESS试图读取页面、写入页面或执行页面中的代码将引发访问违例PAGE_READONLY试图写入页面或执行页面中的代码将引发访问违例PAGE_READWRITE试图执行页面中的代码将引发访问违例PAGE_EXECUTE试图读取页面或写入页面将引发访问违例PAGE_EXECUTE_READ试图写入页面将引发访问违例PAGE_EXECUTE_READWRITE对页面执行任何操作都不会引发访问违例PAGE_WRITECOPY试图执行页面中的代码将引发访问违例。试图写入页面将使系统为进程单独创建一份该页面的私有副本（以分页文件为后备存储）PAGE_EXECUTE_WRITECOPY对页面执行任何操作都不会引发访问违例。试图写入页面将使系统为进程单独创建一份该页面的私有副本（以分页文件为后备存储）Windows数据执行保护（Data Execution Protection，DEP）提供了针对写入内存区域恶意代码的防护。如果启用了DEP，OS只为那些真正需要执行代码的额内存区域使用PAGE_EXECUTE_*保护属性。其他保护属性（如PAGE_READWRITE）用于只存放数据的内存区域写时复制：上表最后两个属性是为了节省RAM的使用和分页文件的空间。Windows支持一种允许两个或更多进程共享同一个存储块的机制，提升了系统的性能，同时要求所有实例将存储视为只读或只执行为避免混乱，OS会为共享存储块分配写时复制保护。一个exe或dll被映射到地址空间时，系统会计算有多少页面可写，然后从分页文件中分配存储空间来容纳可写页面。除非模块的可写页面被实际写入，否则不会用到分页文件存储。进程中的一个线程试图向共享块写入时：系统在RAM中找到一个闲置页面（都有对应的分页空间文件）系统将线程想要修改的页面内容复制到第一步找到的闲置页，系统会为该闲置页分配PAGE_READWRITE或PAGE_EXECUTE_READWRITE，原始页的保护和数据不会发生任何修改系统更新进程的页面表，这样访问的虚拟地址就对应到RAM中的一个新页此外，使用VirtualAlloc函数预定地址空间或调拨物理存储时，不能传递PAGE_WRITECOPY或PAGE_EXECUTE_WRITECOPY，否则会导致调用失败，此时GetLastError返回ERROR_INVALID_PARAMETER。这两个属性是OS在映射exe和dll是用的特殊访问保护属性标志：PAGE_NOCACHE：禁止对已调拨页面进行缓存PAGE_WRITECOMBINE：对单个设备的多次写操作可以组合在一起，提高性能PAGE_GUARD：使应用程序在页面中有任何字节被写入时得到通知内存区域类型Free：该区域的虚拟地址没有任何后备存储。该地址空间尚未预定，应用程序既可以从显示的基地址开始预定区域，也可以从闲置区域中的任何地方开始预定Private：该区域的虚拟地址由系统的分页文件提供后备存储Image：该区域的虚拟地址最初由映像文件（exe或DLL）提供后备存储，但之后就不一定了。例如，如果模块映像中的一个全局变量被写入，那么“写时复制”机制会改用分页文件作为特定页面的后备存储Mapped：区域的虚拟地址最初由内存映射文件提供后备存储，但之后就不一定了。例如，数据文件可能使用“写时复制”保护来映射。向文件的任何写入都会造成特定页面由分页文件提供原始存储。为节省磁盘空间，链接器会尽量压缩生成的PE文件。但是当Windows将PE文件映射到进程的虚拟地址空间时，每个section必须从它自己的页面边界开始，而且大小必须是系统页面大小的整数倍。这意味着PE文件通常需要比文件大小更多的虚拟地址空间。数据对齐的重要性只有在访问正确对齐的数据时，CPU执行效率最高。将数据的地址模数据的大小，结果为0则数据为对齐的。例如，一个WORD值的起始地址应该能被2整除，一个DWORD值的起始地址应该能被4整除如果CPU要访问的数据没有对齐：CPU引发异常CPU通过多次访问已对齐的内存，取得完整的错位数据
Windows核心编程-学习笔记7同步和异步设备I/O线程发出一个同步设备I/O请求时，会被临时挂起，直至I/O请求完成。此类挂起会损害性能，我们不希望线程被阻塞，因此需要各个线程就它们正在执行的操作进行通信，这种机制在Microsoft中被称为I/O完成端口。利用该机制，应用程序的线程在读写设备时不必等待设备响应，从而显著提升吞吐量。打开和关闭设备设备以及打开设备的函数：文件：永久存储任意数据；CreateFile（pszName为路径名或UNC路径名）目录：属性和文件压缩设置；CreateFile（pszName为目录名或UNC目录名）逻辑磁盘驱动器：格式化驱动器；CreateFile（pszName为\\.\x:，x是盘符）物理磁盘驱动器：访问分区表；CreateFile（pszName为\\.\PHYSICALDRIVEx，x是物理驱动器编号）串口：通过电话线传输数据；CreateFile（pszName为COMx）并口：将数据传输至打印机；CreateFile（pszName为LPTx）邮槽：一对多数据传输，通常是通过网络传到另一台Windows机器；服务器CreateMailslot（pszName为\\.\mailslot\mailslotname），客户端CreateFile（pszName为\\servername\mailslot\mailslotname）命名管道：一对一数据传输，通常是通过网络传到另一台Windows机器；服务器CreateNamedPipe（pszName为\\.\pipe\pipename），客户端CreateFile（pszName为\\servername\pipe\pipename）匿名管道：单机上的一对一数据传输，不经过网络；CreatePipe用来打开服务器和客户端套接字：报文或数据流传输，通常是通过网络传到任何支持套接字的机器上；Socket，accept或AcceptEx控制台：文本窗口屏幕缓冲区；CreateConsoleScreenBuffer或GetStdHandler上面每个函数都可以返回一个用来标识设备的句柄CreateFile：HANDLE CreateFile(PCTSTR pszName, DWORD dwDesiredAccess, DWORD dwShareMode, PSECURITY_ATTRIBUTES psa, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hFileTemplate)pszName：既可标识设备类型，也可标识设备的特定实例dwDesiredAccess：指定设备的数据传输标志0：不打算从设备读取数据或者写入数据。如果只想改变设备的配置（比如只是修改文件的时间戳），就可以传入0GENERIC_READ：允许对设备进行只读访问GENERIC_WRITE：允许对设备进行只写访问GENERIC_READ|GENERIC_WRITE：允许对设备进行读写操作dwShareMode：指定设备共享特权0：要求独占对设备的访问。如果设备已经打开，CreateFile调用失败；如果成功打开设备，后续的CreateFile调用就会失败FILE_SHARE_READ：要求引用设备的其他任何内核对象不得修改由该设备维护的数据。如果设备以只写或独占方式打开，CreateFile调用失败；如果成功打开设备，后续使用了只写访问标志的CreateFile调用失败FILE_SHARE_WRITE：要求引用设备的其他任何内核对象不得读取由该设备维护的数据。如果设备以只读或独占方式打开，CreateFile调用失败；如果成功打开设备，后续使用了只读访问标志的CreateFile调用失败FILE_SHARE_READ|FILE_SHARE_WRITE：不关心引用设备的其他内核对象是从设备读取还是向设备写入。如果设备已以独占方式打开，CreateFile调用失败FILE_SHARE_DELETE：进行文件操作时，不关心文件是否被逻辑删除或移动psa：指向一个SECURITY_ATTRIBUTES结构，可用来指定安全信息以及CreateFile返回的句柄是否可继承，一般传NULLdwCreationDisposition：打开是文件时使用CREATE_NEW：创建新文件，如存在同名文件则失败CREATE_ALWAYS：无视存在直接创建OPEN_EXISTING：打开现有文件或设备，如不存在则失败OPEN_ALWAYS：打开现有文件或设备，如不存在则新建一个TRUNCATE_EXISTING：打开现有文件并将文件大小截断为0字节，如不存在则失败dwFlagsAndAttributes：设置标志来微调与设备之间的通信；如果设备是文件，还能设置文件的属性高速缓存标志：FILE_FLAG_NO_BUFFERING：表示在访问文件时不使用任何数据缓冲机制FILE_FLAG_SEQUENTIAL_SCAN：将以顺序方式访问文件，会读取超出请求量的数据FILE_FLAG_RANDOM_ACCESS：高速系统不要预读文件数据FILE_FLAG_WRITE_THROUGH：禁止对文件写入操作进行缓存，以减少数据丢失的可能性通信标志：FILE_FLAG_DELETE_ON_CLOSE：使用这个标志，文件系统会在文件的所有句柄都关闭后删除文件。通常和FILE_ATTRIBUTE_TEMPORARY一起使用，应用程序可以创建一个临时文件，向文件中写入数据，从文件中读取数据，最后关闭文件。文件关闭后，系统就会自动删除该文件FILE_FLAG_BACKUP_SEMANTICS：一般用于备份和恢复软件FILE_FLAG_POSIX_SEMANTICS：Windows查找文件名时不区分大小写，但POSIX子系统（现在没了）需要区分。这个标志要求CreateFile创建或打开文件时区分大小写来查找文件名FILE_FLAG_OPEN_REPARSE_POINT：忽略文件可能存在的重解析属性FILE_FLAG_OPEN_NO_RECALL：请求文件数据，但它应继续位于远程存储中。 不应将其传输回本地存储。 此标志供远程存储系统使用FILE_FLAG_OVERLAPPED：要以异步方式访问设备；异步I/O指调用一个函数来告诉系统要读取或写入数据，但这个函数调用不会等待系I/O操作完成，而是立即返回，OS自己在线程中完成I/O操作，完成后告诉我们文件属性标志：FILE_ATTRUTE_ARCHIVE：文件是一个存档文件。应用程序使用此属性标记文件以供备份或删除。FILE_ATTRIBUTE_ENCRYPTED：文件已加密FILE_ATTRIBUTE_HIDDEN：文件已隐藏FILE_ATTRIBUTE_NORMAL：文件没有设置其他属性，只有单独使用才能生效FILE_ATTRIBUTE_OFFLINE：文件的数据不会立即可用。 此属性指示文件数据在物理上移动到脱机存储。 此属性由远程存储（分层存储管理软件）使用FILE_ATTRIBUTE_READONLY：文件只读，无法写入或删除FILE_ATTRIBUTE_SYSTEM：文件是OS的一部分或独占使用FILE_ATTRIBUTE_TEMPORARY：文件临时存储，会将数据保存在内存中hFileTemplate：既可以标识一个已经打开的文件句柄，也可以是NULL注意CreateFile失败返回INVALID_HANDLE_VALUE，值为-1使用文件设备取得文件大小获取文件大小，pliFileSize是一个LARGE_INTEGER联合体地址BOOL GetFileSizeEx(HANDLE hFile, PLARGE_INTEGER pliFileSize);另一个获取文件大小的函数是GetCompressedFileSize，该函数返回文件的物理大小DWORD GetCompressedFileSize(PCTSTR pszFileName, PDWORD pdwFileSizeHigh);但GetCompressedFileSize传入的是文件名字符串，此外文件大小的低32位是函数的返回值，高32位放在pdwFileSizeHigh指向的DWORD中，需要使用ULARGE_INTEGER结构定位文件指针调用CreateFile会使系统创建一个文件内核对象来管理文件操作。该内核对象内部有一个文件指针，是一个64位偏移量，表示该在哪里执行下一次同步读取或写入操作。该文件指针最初被设为0，所以如果在CreateFile后立即调用ReadFile，会从偏移为0的位置读取文件，并随着读取数目更新文件指针要想自定义访问文件不同位置，需要更改与文件内核对象关联的文件指针，为此要调用SetFilePointerExBOOL SetFilePointerEx(HANDLE hFile, LARGE_INTEGER liDistanceToMove, PLARGE_INTEGER pliNewFilePointer, DWORD dwMoveMethod);hFile：更改哪个文件内核对象的文件指针liDistanceToMove：要移动多少字节，这个值会与文件指针偏移值相加，负数可以使文件指针后移dwMoveMethod：指定移动文件指针时的起始位置FILE_BEGIN：文件对象指针被设为liDistanceToMove指定的值（无符号64位）FILE_CURRENT：文件对象指针将与liDistanceToMove相加（有符号，因为要有负数）FILE_END：文件对象指针被设为文件的逻辑大小+liDistanceToMove（有符号，因为要有负数）SetFilePointerEx更新了文件对象指针后，会在pliNewFilePointer指向的LARGE_INTEGER结构中返回文件指针的新值，不感兴趣就传NULL需要注意SetFilePointerEx一些事实：将文件指针值设为超过文件当前大小是正当操作。除非在该位置向文件写入数据或者调用SetEndOfFile，否则这样做不会增加文件在磁盘上的实际大小如果SetFilePointerEx操作的文件是用FILE_FLAG_NO_BUFFERING标志打开的，那么文件指针只能被设为扇区大小的整数倍Windows没有提供一个GetFilePointerEx函数，但可以通过SetFilePointerEx将文件指针移动0字节，并利用pliNewFilePointer获取文件指针值设置文件尾通常在关闭文件时，系统会负责设置文件尾（EOF）。但有时需要强制使文件变得更小或更大，可以调用SetEndOfFileBOOL SetEndOfFile(HADNLE hFile);SetEndOfFile会根据文件对象指针所在位置来截短或增大文件执行同步设备I/O对设备数据读写BOOL ReadFile(HANDLE hFile, PVOID pvBuffer, DWORD nNumBytesToRead, PDWORD pdwNumBytes, OVERLAPPED* pOverlapped); BOOL WriteFile(HANDLE hFile, CONST VOID *pvBuffer, DWORD nNumBytesToWrite, PDWORD pdwNumBytes, OVERLAPPED* pOverlapped);hFile：要访问设备的句柄pvBuffer：指向一个缓冲区，函数会将设备数据读取到这个缓冲区或者将缓冲区的数据写入设备nNumBytesToRead/nNumBytesToWrite：读取或者写入多少字节pdwNumBytes：指向一个DWORD地址，函数会在其中填充设备成功收发的字节数pOverlapped：如果同步I/O应该设为NULL二者调用成功都会返回TRUE将数据回写到设备BOOL FlushFileBuffers(HANDLE hFile);此函数强制将与hFile参数所标识的设备相关联的所有缓存数据回写（flush）到设备。设备必须是通过GENEIC_WRITE标志打开的，这样才能正常工作，成功返回TRUE同步I/O取消执行同步I/O的函数很容易使用，但除非请求完成，否则会阻塞住来自同一个线程的其他操作。如线程因为等待CreateFile返回而被阻塞，窗口消息将无法得到处理，该线程创建的所有窗口都会被冻住。应用程序有时停止响应就是因为要等待同步I/O操作完成而被阻塞缓解措施：异步执行I/O操作提供取消功能，比如Web请求可以点击X取消控制台程序Ctrl+C拿回控制权可以使用CancelSynchronousIo来取消一个线程尚未完成的同步I/OBOOL CancelSynchronousIo(HANDLE hThread);hThread参数是因为等待同步I/O请求完成而被挂起的线程句柄，该句柄必须是用THREAD_TERMINATE访问权限创建的，否则CancelSynchronousIo调用会失败。异步设备I/O基础和计算机执行的其他大多数操作相比，设备I/O是最慢、最不可预测的操作之一。CPU从文件或跨网络读取数据的速度，以及CPU向文件或跨网络写入数据的速度，比它执行算术运算的速度，甚至比它绘制屏幕的速度都要慢得多。但是使用异步I/O可以更好地利用资源并构建更高效的应用程序。要以异步方式访问设备，必须先调用CreateFile，并在dwFlagsAndAttributes参数中指定FILE_FLAG_OVERLAPPED标志来打开设备，该标志告诉系统要以异步方式访问设备。ReadFile、WriteFile会检查hFile所标识的设备是不是用FILE_FLAG_OVERLAPPED打开，是则执行异步设备I/O执行异步I/O，需要在ReadFile、WriteFile里的pOverlapped参数传递一个已初始化的OVERLAPPED结构typedef struct _OVERLAPPED { DWORD Internal; // out 错误码 DWORD InternalHigh; // out 传输比特数 DWORD Offset; // in DWORD OffsetHigh; // in HANDLE hEvent // in } OVERLAPPED, *LPOVERLAPPED;Offset和OffsetHigh构成一个64位偏移量，表示当访问文件时应该从哪里开始进行I/O操作；为避免对同一个文件内核对象进行多个异步调用时发生混乱，必须在异步I/O请求中指定起始偏移量hEvent：在里面存储一个C++对象地址Internal：容纳已处理的I/O错误码，初始为STATUS_PENDING，表明没有错误。可以用来定义宏来检查一个异步I/O操作是否已经完成，#define HasOverlappedIoCompleted(pOverlapped) (pOverlapped)->Internal != STATUS_PENDINGInternalHigh：异步I/O请求完成时，容纳已传输的字节数异步设备I/O注意事项：设备驱动程序不一定以先入先出的方式来处理队列中的I/O请求：比如，为了降低磁头的移动和寻道时间，文件系统驱动程序会在I/O请求队列中寻找那些要访问物理磁盘上相邻位置的请求如何正确检查错误：大多数Windows函数返回FALSE表示失败，或返回非零值表示成功；但ReadFile、WriteFile不一样如果请求同步I/O：返回非零值如果请求异步I/O或调用发生错误：返回FALSE，此时需要调用GetLastError来检查，如果返回ERROR_IO_PENDING，表明I/O请求已经成功入队；如果返回其他值，表明I/O请求无法添加到设备驱动程序的队列中在异步I/O请求完成前，一定不能移动或销毁在发出异步I/O请求时所用的数据缓存和OVERLAPPED结构，因为传递这些都是用的地址VOID ReadData(HANDLE hFile) { OVERLAPPED o = {0}; BYTE b[100]; ReadFile(hFile, b, 100, NULL, &o); }这个代码在异步I/O请求下会返回，导致o和b释放取消队列中的设备I/O请求：调用CancelIo来取消由给定句柄对应线程的I/O请求（队列中的），除非该句柄与一个I/O完成端口关联可关闭设备句柄，从而取消已入队的所有I/O请求线程终止时，系统自动取消该线程发出的所有I/O请求，除非请求的句柄和一个I/O完成端口关联如果需要取消发往给定文件句柄的一个指定的I/O请求，使用CancelIoEx；pOverlapped为NULL时取消全部待处理I/O请求BOOL CancelIoEx(HANDLE hFile, LPOVERLAPPED pOverlapped);接收I/O请求完成通知触发设备内核对象线程触发异步I/O请求后会继续执行，完成其他有用的工作。但即便如此最终还是要与I/O操作的完成同步（比如在某个点上，除非来自设备的数据已载入缓冲区，否则线程无法继续执行）在Windows中，设备内核对象用来线程同步。ReadFile和WriteFile将I/O请求添加到队列之前将设备内核对象设为未触发状态。设备驱动程序完成请求后，该驱动程序将设备内核对象设为触发状态线程可以通过调用WaitForSingleObject或WaitForMultipleObjects来检查异步I/O请求是否已完成触发事件内核对象设备内核对象触发没法很好的处理多个I/O请求，一个操作完成对象就会被触发，不知道是哪个操作触发。因此可以利用OVERLAPPED结构的最后一个成员hEvent来标识一个事件内核对象。必须调用CreateEvent来创建该事件对象。异步I/O请求完成时，设备驱动程序会检查OVERLAPPED结构的hEvent成员是否为NULL。如果hEvent不为NULL，驱动程序会调用SetEvent来触发事件。要想同时执行多个异步设备I/O请求，必须为每个请求创建单独的事件对象，初始化每个请求的OVERLAPPED结构中的hEvent成员，然后调用ReadFile和WriteFile。当运行到代码中的那个点，必须与I/O请求的完成状态进行同步的时候，只需调用WaitForMultipleObjects并传入与每个待处理I/O请求的OVERLAPPED结构相关联的事件句柄。可提醒I/O略异步过程调用：Asynchronous Procedure Call，APCI/O完成端口背景：并发模型缺陷1：并发模型系统中会有多个线程并发执行，由于所有线程都处于可运行状态，所以WIndows内核在这些可运行线程之间切换上下文花费太长时间，以至于各个线程都没有多少CPU时间完成自己的任务缺陷2：需要为每个客户请求创建新线程，虽然和创建一个有自己虚拟地址空间的进程相比，创建线程的开销要低很多，但依然不容忽视I/O完成端口背后的理论：并发运行的线程数量必须有一个上限在应用程序初始化时创建一个线程池，让线程池中的线程在应用程序运行期间一直保持可用状态，那么服务应用程序的性能就能提高创建I/O完成端口：HANDLE CreateIoCompletionPort(HANDLE hFile, HANDLE hExistingCompletionPort, ULONG_PTR CompletionKey, DWORD dwNumberOfConcurrentThreads);该函数执行两个不同的任务：不仅会创建一个I/O完成端口，还会将一个设备与一个I/O完成端口关联起来如果只是想创建一个I/O完成端口，为CreateIoCompletionPort的前三个参数分别传递INVALID_HANDLE_VALUE、NULL和0即可dwNumberOfConcurrentThreads参数告诉I/O完成端口在同一时间最多能有多少线程处于可运行状态（传递0默认使用主机CPU数量）创建I/O完成端口时，系统内核实际上会创建5个不同的数据结构：设备列表I/O完成队列（先入先出）等待线程队列（后入先出）已释放线程列表已暂停线程列表将设备与I/O完成端口关联：设备列表：标识了与端口关联的一个或多个设备。将设备与端口关联需要调用CreateIoCompletionPort函数，需要传入现有I/O完成端口的句柄（之前CreateIoCompletionPort的返回值）、设备的句柄、完成键I/O完成队列：设备的一个异步I/O请求完成时，系统会检查设备是否与一个I/O完成端口关联，是则将已完成的I/O请求项追加到完成端口的I/O完成队列的末尾。队列中每一项包含的信息由：已传输的字节数、设备与端口关联时所设置的完成键的值、指向I/O请求的OVERLAPPED结构的指针以及一个错误码。I/O完成端口的周边架构：线程池多少线程：标准经验法则是CPU数量乘以2池中所有线程执行相同的函数，通常该函数先进行一些初始化，然后进入一个循环。当服务进程被告知要停止的时候，该循环就应终止。在循环内部，线程进入睡眠状态，等待设备I/O请求完成并进入完成端口。调用GetQueuedCompletionStatus可以达到目的BOOL GetQueuedCompletionStatus(HANDLE hCompletionPort, PDWORD pdwNumberOfBytesTransferred, PULONG_PTR pCompletionKey, OVERLAPPED** ppOverlapped, DWORD dwMilliseconds);第一个参数hCompletionPort表示线程希望对哪个完成端口进行监视。GetQueuedCompletionStatus的任务基本就是将调用线程切换到睡眠状态，直到指定完成端口的I/O完成队列中出现一项，或者等待超时等待线程队列：当线程池中的每个线程调用GetQueuedCompletionStatus时，调用线程的线程标识符会被添加到这个等待线程队列，这使I/O完成端口内核对象始终都知道当前有哪些线程正在等待处理完成的I/O请求。当端口的I/O完成队列中出现一项的时候，该完成端口会唤醒等待线程队列中的一个线程。该线程会获得已完成I/O项中的所有信息：已传输的字节数、完成键以及OVERLAPPED结构的地址。这些信息通过GetQueuedCompletionStatus的pdwNumberOfBytesTransferred、pCompletionKey、ppOverlapped参数来返回给线程注意等待线程队列采用后入先出方式唤醒，也就是最后一个调用GetQueuedCompletionStatus的线程会被唤醒I/O完成端口如何管理线程池：已释放线程列表：使完成端口记住哪些线程已被唤醒，并监视其执行情况。如果一个已释放的线程调用的任何函数将该线程切换到等待状态，完成端口会检测到这一情况并更新内部的数据结构，将该线程的线程标识符从已释放线程列表中移除，并添加到已暂停线程列表已暂停线程列表完成端口的目标是根据在创建完成端口时指定的并发线程的数量，将尽可能多的线程保持在“已释放线程列表”。如果一个已释放线程因为任何原因进入等待状态，“已释放线程列表”会缩减，完成端口就可以释放另一个正在等待的线程。如果一个已暂停的线程被唤醒，就会离开“已暂停线程列表”并重新进入“已释放线程列表”小结这学的啥啊，好难理解
Hgame 2025复现Compress dot newdeepseek一把梭，自己还分析啥# 哈夫曼树结构 huffman_tree = {"a":{"a":{"a":{"a":{"a":{"s":125},"b":{"a":{"s":119},"b":{"s":123}}},"b":{"a":{"s":104},"b":{"s":105}}},"b":{"a":{"s":101},"b":{"s":103}}},"b":{"a":{"a":{"a":{"s":10},"b":{"s":13}},"b":{"s":32}},"b":{"a":{"s":115},"b":{"s":116}}}},"b":{"a":{"a":{"a":{"a":{"a":{"s":46},"b":{"s":48}},"b":{"a":{"a":{"s":76},"b":{"s":78}},"b":{"a":{"s":83},"b":{"a":{"s":68},"b":{"s":69}}}}},"b":{"a":{"a":{"s":44},"b":{"a":{"s":33},"b":{"s":38}}},"b":{"s":45}}},"b":{"a":{"a":{"s":100},"b":{"a":{"s":98},"b":{"s":99}}},"b":{"a":{"a":{"s":49},"b":{"s":51}},"b":{"s":97}}}},"b":{"a":{"a":{"a":{"s":117},"b":{"s":118}},"b":{"a":{"a":{"s":112},"b":{"s":113}},"b":{"s":114}}},"b":{"a":{"a":{"s":108},"b":{"s":109}},"b":{"a":{"s":110},"b":{"s":111}}}}}} # 编码数据 encoded_data = "00010001110111111010010000011100010111000100111000110000100010111001110010011011010101111011101100110100011101101001110111110111011011001110110011110011110110111011101101011001111011001111000111001101111000011001100001011011101100011100101001110010111001111000011000101001010000000100101000100010011111110110010111010101000111101000110110001110101011010011111111001111111011010101100001101110101101111110100100111100100010110101111111111100110001010101101110010011111000110110101101111010000011110100000110110101011000111111000110101001011100000110111100000010010100010001011100011100111001011101011111000101010110101111000001100111100011100101110101111100010110101110000010100000010110001111011100011101111110101010010011101011100100011110010010110111101110111010111110110001111010101110010001011100100101110001011010100001110101000101111010100110001110101011101100011011011000011010000001011000111011111111100010101011100000" # 解码函数 def decode_huffman(tree, encoded_data): decoded_text = [] current_node = tree for bit in encoded_data: if bit == '0': current_node = current_node['a'] else: current_node = current_node['b'] if 's' in current_node: decoded_text.append(chr(current_node['s'])) current_node = tree return ''.join(decoded_text) # 解码 decoded_text = decode_huffman(huffman_tree, encoded_data) print(decoded_text)Turtle64位手脱下，sub_401550和sub_40175A一眼rc4__int64 sub_401876() {//... _BYTE v1[256]; // [rsp+20h] [rbp-60h] BYREF sub_401C20(); strcpy(v8, "yekyek"); Buf2[0] = -51; Buf2[1] = -113; Buf2[2] = 37; Buf2[3] = 61; Buf2[4] = -31; qmemcpy(v5, "QJ", sizeof(v5)); v2[0] = -8; v2[1] = -43; v2[2] = 98; v2[3] = -49; v2[4] = 67; v2[5] = -70; v2[6] = -62; v2[7] = 35; v2[8] = 21; v2[9] = 74; v2[10] = 81; v2[11] = 16; v2[12] = 39; v2[13] = 16; v2[14] = -79; v2[15] = -49; v2[16] = -60; v2[17] = 9; v2[18] = -2; v2[19] = -29; v2[20] = -97; v2[21] = 73; v2[22] = -121; v2[23] = -22; v2[24] = 89; v2[25] = -62; v2[26] = 7; v2[27] = 59; v2[28] = -87; v2[29] = 17; v2[30] = -63; v2[31] = -68; v2[32] = -3; v2[33] = 75; v2[34] = 87; v2[35] = -60; v2[36] = 126; v2[37] = -48; v2[38] = -86; v2[39] = 10; v13 = 6; v12 = 7; v11 = 40; j_printf("plz input the key: "); j_scanf("%s", Source); j__mbscpy(Dest, Source); v10 = 7; sub_401550(v8, v13, v1); sub_40163E(Source, v10, v1); if ( !j_memcmp(Source, Buf2, v12) ) { j_printf("plz input the flag: "); j_scanf("%s", Buf1); v9 = 40; sub_401550(Dest, v10, v1); sub_40175A(Buf1, v9, v1); if ( !j_memcmp(Buf1, v2, v11) ) j_puts(Buffer); else j_puts(aWrongPlzTryAga); } else { j_puts(aKeyIsWrong); } return 0LL; }解密def KSA(key): """ Key-Scheduling Algorithm (KSA) 密钥调度算法""" S = list(range(256)) j = 0 for i in range(256): j = (j + S[i] + key[i % len(key)]) % 256 S[i], S[j] = S[j], S[i] return S def PRGA(S): """ Pseudo-Random Generation Algorithm (PRGA) 伪随机数生成算法""" i, j = 0, 0 while True: i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] K = S[(S[i] + S[j]) % 256] yield K def RC4_sub(key, text): """ RC4 encryption/decryption """ S = KSA(key) keystream = PRGA(S) res = [] for char in text: res.append((char + next(keystream))&0xff) return bytes(res) def RC4(key, text): """ RC4 encryption/decryption """ S = KSA(key) keystream = PRGA(S) res = [] for char in text: res.append(char ^ next(keystream)) return bytes(res) key = b"yekyek" text = [0xcd, 0x8f, 0x25, 0x3d, 0xe1, ord("Q"), ord("J")] key = RC4(key, text) print(key) text = [248, 213, 98, 207, 67, 186, 194, 35, 21, 74, 81, 16, 39, 16, 177, 207, 196, 9, 254, 227, 159, 73, 135, 234, 89, 194, 7, 59, 169, 17, 193, 188, 253, 75, 87, 196, 126, 208, 170, 10] print(RC4_sub(key, text)) # hgame{Y0u'r3_re4l1y_g3t_0Ut_of_th3_upX!}Delta Erro0000orsint __fastcall main(int argc, const char **argv, const char **envp) { HMODULE LibraryA; // rax DWORD LastError; // eax char *v5; // rbx char *v6; // rdi __int64 v7; // rsi int v8; // ecx __int64 v9; // r8 __int128 v11; // [rsp+20h] [rbp-138h] __int64 v12; // [rsp+30h] [rbp-128h] __int128 v13; // [rsp+38h] [rbp-120h] __int64 v14; // [rsp+48h] [rbp-110h] __int128 v15; // [rsp+50h] [rbp-108h] BYREF __int64 v16; // [rsp+60h] [rbp-F8h] __int128 v17; // [rsp+70h] [rbp-E8h] BYREF __int64 v18; // [rsp+80h] [rbp-D8h] char Destination[16]; // [rsp+90h] [rbp-C8h] BYREF __int128 v20; // [rsp+A0h] [rbp-B8h] int v21; // [rsp+B0h] [rbp-A8h] char v22; // [rsp+B4h] [rbp-A4h] char Buffer[16]; // [rsp+B8h] [rbp-A0h] BYREF __int128 v24; // [rsp+C8h] [rbp-90h] __int64 v25; // [rsp+D8h] [rbp-80h] char Str1[16]; // [rsp+E0h] [rbp-78h] BYREF __int128 v27; // [rsp+F0h] [rbp-68h] __int128 v28; // [rsp+100h] [rbp-58h] __int128 v29; // [rsp+110h] [rbp-48h] __int128 v30; // [rsp+120h] [rbp-38h] __int128 v31; // [rsp+130h] [rbp-28h] int v32; // [rsp+140h] [rbp-18h] LibraryA = LoadLibraryA("msdelta.dll"); hLibModule = LibraryA; if ( LibraryA ) { off_7FF691DE5180 = (__int64 (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD))GetProcAddress(LibraryA, "ApplyDeltaB"); DeltaFree = (BOOL (__stdcall *)(LPVOID))GetProcAddress(hLibModule, "DeltaFree"); } else { puts("LoadLibrary Error"); } *(_OWORD *)Str1 = 0LL; v27 = 0LL; v28 = 0LL; v29 = 0LL; v30 = 0LL; v31 = 0LL; v32 = 0; *(_OWORD *)Destination = 0LL; v20 = 0LL; v21 = 0; v22 = 0; *(_OWORD *)Buffer = 0LL; v24 = 0LL; v25 = 0LL; printf("input your flag:"); scanf("%43s", Str1); if ( !strncmp(Str1, "hgame{", 6uLL) && BYTE10(v28) == 125 ) { strncpy(Destination, &Str1[6], 0x24uLL); LODWORD(v14) = 0; *(_QWORD *)&v13 = Destination; *((_QWORD *)&v13 + 1) = 37LL; LODWORD(v12) = 0; *(_QWORD *)&v11 = &unk_7FF691DE50A0; *((_QWORD *)&v11 + 1) = 69LL; v15 = v11; v16 = v12; v17 = v13; v18 = v14; if ( off_7FF691DE5180(0LL, &v17, &v15, &lpMemory) ) { printf("%s", (const char *)lpMemory); } else { puts("ApplyDelta Error"); LastError = GetLastError(); RaiseException(LastError, 1u, 0, 0LL); } puts("Seven eats the hash and causes the program to appear to have some kind of error."); puts("Seven wants to make up for the mistake, so she's giving you a chance to patch the hash."); printf("input your MD5:"); scanf("%32s", Buffer); v5 = Buffer; v6 = (char *)&unk_7FF691DE50B4; v7 = 16LL; do { sub_7FF691DE10E0(v5, "%02x", (_DWORD)v6++); v5 += 2; --v7; } while ( v7 ); word_7FF691DE50C4 = 0x7A01; v17 = v11; v18 = v12; v15 = v13; v16 = v14; if ( !off_7FF691DE5180(0LL, &v15, &v17, &lpMemory) ) { puts("You didn't take advantage of this opportunity."); sub_7FF691DE14E0(); exit(0); } v8 = 0; v9 = 0LL; do { if ( byte_7FF691DE3438[v9] != ((unsigned __int8)Str1[v9] ^ *((_BYTE *)lpMemory + v8 % (unsigned __int64)qword_7FF691DE5198)) ) { puts("Flag is error!!"); sub_7FF691DE14E0(); exit(0); } ++v8; ++v9; } while ( v8 < 43 ); } puts(aGreat); DeltaFree(lpMemory); FreeLibrary(hLibModule); return 0; }奇怪的题目，调用了个msdelta.dll，主函数可以patch掉getlasterror附近的一个jmp即可看完整逻辑，发现两处输入第一处会调用applydelta但会抛出错误第二处要求输入md5，因此在dll文件里memcmp处下了断点（attach到主进程下）看谁来和输入比较的找到值44D292FFE2E91730AE69EB50AE11D04A（其实第一次也有）再回到exe主函数，下断点看lpMemory值（异或值），注意长度为28，因此包括最后的0xor = [0x53, 0x65, 0x76, 0x65, 0x6E, 0x20, 0x73, 0x61, 0x79, 0x73, 0x20, 0x79, 0x6F, 0x75, 0x27, 0x72, 0x65, 0x20, 0x72, 0x69, 0x67, 0x68, 0x74, 0x21, 0x21, 0x21, 0x21, 0x00] cmp = [0x3B, 0x02, 0x17, 0x08, 0x0B, 0x5B, 0x4A, 0x52, 0x4D, 0x11, 0x11, 0x4B, 0x5C, 0x43, 0x0A, 0x13, 0x54, 0x12, 0x46, 0x44, 0x53, 0x59, 0x41, 0x11, 0x0C, 0x18, 0x17, 0x37, 0x30, 0x48, 0x15, 0x07, 0x5A, 0x46, 0x15, 0x54, 0x1B, 0x10, 0x43, 0x40, 0x5F, 0x45, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] for i in range(43): print(chr(cmp[i]^xor[i%len(xor)]), end="")这个题目需要直到msdelta.dll的作用，直接去定位哈希值，拿到补丁（lpMemory）尊嘟假嘟pass不想看了，会做了Nop'd一眼launcher有问题，打开后在init_array里发现了利用ptrace实现的修改内存__int64 __fastcall sub_1B9B(__pid_t pid) { unsigned int v1; // ebx char i; // bl int v3; // ebx __int64 v4; // rax __int64 v5; // r15 char v6; // dl __int64 v7; // r15 __int64 (__fastcall *v8)(); // rax unsigned int pida; // [rsp+4h] [rbp-64h] __int64 v11; // [rsp+18h] [rbp-50h] BYREF _BYTE v12[4]; // [rsp+22h] [rbp-46h] BYREF unsigned __int8 v13; // [rsp+26h] [rbp-42h] unsigned __int8 v14; // [rsp+27h] [rbp-41h] unsigned __int64 v15; // [rsp+28h] [rbp-40h] v1 = pid; v15 = __readfsqword(0x28u); waitpid(pid, 0LL, 0); ptrace(PTRACE_SETOPTIONS, (unsigned int)pid, 0LL, 0x100000LL); while ( ptrace(PTRACE_SYSCALL, v1, 0LL, 0LL) >= 0 ) { waitpid(v1, 0LL, 0); ptrace(PTRACE_GETREGS, v1, 0LL, &qword_55E0); sub_134B(PTRACE_PEEKTEXT, v1, (__int64)byte_55C0, *(&qword_55E0 + 16) - 8, 6uLL); if ( (byte_55C0[0] & 0xFC) != 0x48 || byte_55C0[1] != 15 || byte_55C2 != 31 || (byte_55C3 & 0xC7) != 0x44 || byte_55C5 != 127 ) { v11 = *(&qword_55E0 + 16); goto LABEL_41; } memset(byte_5540, 0, sizeof(byte_5540)); qword_5520 = 0LL; pida = v1; for ( i = 0; ; i = 1 ) { v5 = qword_5520; sub_134B(PTRACE_PEEKTEXT, pida, (__int64)v12, qword_5660 + 6 * qword_5520, 6uLL); if ( (v12[0] & 0xFC) != 0x48 || v12[1] != 15 || v12[2] != 31 || (v12[3] & 0xC7) != 0x44 || v14 == 126 ) break; v3 = (v14 - 1) % 128; if ( v13 > 0x3Fu ) { sub_1464(pida, (__int64)&v11, qword_5678 + ((8 * (v13 & 7 | (unsigned __int8)(8 * (v12[0] & 1)))) & 0x78), 8uLL); qword_5120[(unsigned __int8)v3] = v11; } else { switch ( v13 & 7 | (unsigned __int8)(8 * (v12[0] & 1)) ) { case 0: v4 = qword_5658; break; case 1: v4 = qword_5638; break; case 2: v4 = qword_5640; break; case 3: v4 = qword_5608; break; case 4: v4 = qword_5678; break; case 5: v4 = qword_5600; break; case 6: v4 = qword_5648; break; case 7: v4 = qword_5650; break; case 8: v4 = qword_5628; break; case 9: v4 = qword_5620; break; case 10: v4 = qword_5618; break; case 11: v4 = qword_5610; break; case 12: v4 = qword_55F8; break; case 13: v4 = qword_55F0; break; case 14: v4 = qword_55E8; break; case 15: v4 = qword_55E0; break; } qword_5120[(unsigned __int8)v3] = v4; } byte_5540[(unsigned __int8)v3] = 1; qword_5520 = v5 + 1; } v6 = i; v1 = pida; v11 = *(&qword_55E0 + 16); if ( v6 ) { v7 = -38LL; if ( byte_5540[0] ) { if ( qword_5120[0] <= 9uLL ) { v8 = funcs_1DBD[qword_5120[0]]; if ( v8 ) v7 = ((int (__fastcall *)(_QWORD, __int64 *, char *, __int64 *))v8)(pida, qword_5120, byte_5540, &v11); } *(&qword_55E0 + 15) = -1LL; ptrace(PTRACE_SETREGS, pida, 0LL, &qword_55E0); } ptrace(PTRACE_SYSCALL, pida, 0LL, 0LL); waitpid(pida, 0LL, 0); ptrace(PTRACE_GETREGS, pida, 0LL, qword_5040); qword_5040[10] = v7; qword_5040[16] = v11; ptrace(PTRACE_SETREGS, pida, 0LL, qword_5040); } else { LABEL_41: ptrace(PTRACE_SYSCALL, v1, 0LL, 0LL); waitpid(v1, 0LL, 0); } } return 0LL; }通过sub_134B获取game寄存器rip值-8地址的字节并比较是否等于x48x0fx1fx44x00x7f，去game里可以搜到这串指令正是nop qword ptr [rax+rax+7Fh]，分析可知从nop qword ptr [rax+rax+7Fh]开始到nop qword ptr [rax+rax+7Eh]结束，根据中间内容略...出的太好了研究了好一会，自己留着了实际上实现了个魔改chacha20加密import struct # 宏定义：循环左移（左移操作后，右边溢出的部分重新回到左边） def ROTL(a, b): return ((a << b) & 0xFFFFFFFF) | (a >> (32 - b)) # 宏定义：Chacha20的四分之一轮（Quarter Round）操作 def QR(a, b, c, d): a = (a + b) & 0xFFFFFFFF d ^= a d = ROTL(d, 16) c = (c + d) & 0xFFFFFFFF b ^= c b = ROTL(b, 12) a = (a + b) & 0xFFFFFFFF d ^= a d = ROTL(d, 8) c = (c + d) & 0xFFFFFFFF b ^= c b = ROTL(b, 7) return a, b, c, d # ChaCha20加密算法中的块函数 def chacha20_block(output, input_state): x = list(input_state) # 进行20轮加密操作，每轮执行四分之一轮操作 for _ in range(10): # 奇数轮 x[0], x[4], x[8], x[12] = QR(x[0], x[4], x[8], x[12]) x[1], x[5], x[9], x[13] = QR(x[1], x[5], x[9], x[13]) x[2], x[6], x[10], x[14] = QR(x[2], x[6], x[10], x[14]) x[3], x[7], x[11], x[15] = QR(x[3], x[7], x[11], x[15]) x = [x[i] for i in [0, 1, 2, 3, 5, 6, 7, 4, 10, 11, 8, 9, 15, 12, 13, 14]] # 偶数轮 x[0], x[4], x[8], x[12] = QR(x[0], x[4], x[8], x[12]) x[1], x[5], x[9], x[13] = QR(x[1], x[5], x[9], x[13]) x[2], x[6], x[10], x[14] = QR(x[2], x[6], x[10], x[14]) x[3], x[7], x[11], x[15] = QR(x[3], x[7], x[11], x[15]) x = [x[i] for i in [0, 1, 2, 3, 7, 4, 5, 6, 10, 11, 8, 9, 13, 14, 15, 12]] # 将加密结果与原始输入状态相加，输出最终结果 for i in range(16): output[i] = (x[i] + input_state[i]) & 0xFFFFFFFF # ChaCha20加密函数 def chacha20_encrypt(out, in_data, in_len, key, nonce, counter): state = [ 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574, # 固定常量（ASCII编码：expand 32-byte k） key[0], key[1], key[2], key[3], # 256位密钥（8个32位字） key[4], key[5], key[6], key[7], counter, nonce[0], nonce[1], nonce[2] # 计数器和nonce ] block = [0] * 16 # 存储每次生成的64字节的加密块 while in_len > 0: # 生成一个加密块 chacha20_block(block, state) state[12] = (state[12] + 1) & 0xFFFFFFFF # 每次加密后递增计数器 block_size = min(in_len, 64) # 计算当前块的大小 tmp = 0x61C88646&0xff for i in range(block_size): out[i] = tmp ^ in_data[i] ^ (block[i // 4] >> (8 * (i % 4)) & 0xFF) # 将输入数据与加密块异或得到密文 tmp = in_data[i] # 更新剩余输入数据的长度和指针 in_len -= block_size in_data = in_data[block_size:] out = out[block_size:] # 主函数 def main(): key = [0x73277449, 0x6C6C6120, 0x69727720, 0x6E657474, 0x206E6920, 0x20656874, 0x6B6F6F42, 0x20666F20] nonce = [0x74616857, 0x79207327, 0x2072756F] ciphertext = [0x64, 0x6A, 0x50, 0x17, 0x81, 0x7D, 0x6F, 0x1A, 0x87, 0xB1, 0xA4, 0x00, 0x09, 0x03, 0xF8, 0x8D, 0xF8, 0x6B, 0xDF, 0x32, 0x5F, 0x40, 0x90, 0x9C, 0xB8, 0x3D, 0x86, 0x13, 0x26, 0xB7, 0x63, 0xF7, 0x74, 0xE8, 0x53, 0xED, 0x58, 0x20, 0x4F, 0xD9, 0x99, 0x26, 0x21, 0x37, 0xDE, 0x35, 0x76, 0xC8, 0xBC, 0xD0, 0x6E] decrypted = bytearray(len(ciphertext)) chacha20_encrypt(decrypted, ciphertext, len(ciphertext), key, nonce, 0) print(decrypted.hex()) # 解密后的数据应当与原始明文相同 print(decrypted.decode('utf-8')) # https://blog.csdn.net/weixin_59166557/article/details/144060806 if __name__ == "__main__": main() Fast and frustrating给了PDB调试信息和符号表，定位FastAndFrustrating_FastAndFrustrating_Program__Main把Base64串和main代码传给GPT，thinking模式下9min梭了可以。你给的那段 Base64（H4sI...）解出来其实是 gzip 压缩的 JSON，里面是一个 27×27 的整数矩阵 mat_a 和一个长度 27 的向量 vec_b，程序用它们对输入 key 做线性约束校验。我按程序逻辑把整条链还原并算出来了：1) 先解压你给的 Base64 → 得到约束解 gzip 后是：mat_a：27×27vec_b：27校验本质是解线性方程组 A·x = b（x 是 key 的 27 个 ASCII 字节）。解出来的 key（ASCII）是：CompressedEmbeddedResources2) 从 exe 静态区取另外两个常量在你那段大 Base64 附近还能找到：密文 Base64：GFxmVucV6MVUXiWCMAnWpyvzXoLdHc5CmFeim+JjUBszB8HFX8Ku8NMc201AGZ9XHKDF 的 info 字符串：HGAME20253) HKDF 派生 AES 参数 + 解密程序用 HKDF-SHA256 派生 Key || IV（注意顺序是 Key 在前，IV 在后）：输出长度 = 32（AES-256 key） + 16（CBC IV）= 48 bytesKey = 前 32 bytesIV = 后 16 bytes 然后 AES-256-CBC 解密并做 PKCS7 去 padding，得到 flag。✅ flaghgame{F4st_4nd_frustr4t1ng_A0T_compilat1on}import base64, hmac, hashlib, math from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes, padding as cpadding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend ikm=b"CompressedEmbeddedResources" info=b"HGAME2025" keysize_bits=256 blocksize_bits=128 length=(keysize_bits+blocksize_bits)//8 okm=HKDF(algorithm=hashes.SHA256(), length=length, salt=None, info=info, backend=default_backend()).derive(ikm) key=okm[:keysize_bits//8] iv=okm[keysize_bits//8:keysize_bits//8+blocksize_bits//8] ct=base64.b64decode("GFxmVucV6MVUXiWCMAnWpyvzXoLdHc5CmFeim+JjUBszB8HFX8Ku8NMc201AGZ9X") cipher=Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) pt_padded=cipher.decryptor().update(ct)+cipher.decryptor().finalize() # unpad pkcs7 unpadder=cpadding.PKCS7(blocksize_bits).unpadder() pt=unpadder.update(pt_padded)+unpadder.finalize() print(pt)Mysterious signals没环境了，就是个TEA类魔改Signin出现了硬件断点检测__int64 __fastcall sub_14000F730(__int64 a1, __int64 a2, __int64 a3) { __int64 result; // rax HANDLE CurrentThread; // rax LPCONTEXT lpContext; // [rsp+28h] [rbp+8h] j___CheckForDebuggerJustMyCode(&unk_1400C70A3, a2, a3); lpContext = (LPCONTEXT)VirtualAlloc(0LL, 0x4D0uLL, 0x1000u, 4u); if ( lpContext ) { sub_1400034FE(lpContext, 1232LL); lpContext->ContextFlags = 1048592; CurrentThread = GetCurrentThread(); if ( GetThreadContext(CurrentThread, lpContext) ) { qword_1400BB880[0] = lpContext->Dr0; qword_1400BB880[1] = lpContext->Dr1; qword_1400BB880[2] = lpContext->Dr2; qword_1400BB880[3] = lpContext->Dr3; if ( qword_1400BB880[0] || qword_1400BB880[1] || qword_1400BB880[2] || (result = 24LL, qword_1400BB880[3]) ) { j_puts("Debug error."); j_exit(0); } } else { return 0LL; } } else { j_puts("VirtualAlloc failed."); return 0LL; } return result; }类似CRC检测__int64 __fastcall sub_140008670(__int64 a1, __int64 a2, __int64 a3) { __int64 v3; // rdx __int64 v4; // rcx __int64 v5; // r8 __int64 v6; // r8 void *v8; // [rsp+48h] [rbp+28h] int i; // [rsp+64h] [rbp+44h] j___CheckForDebuggerJustMyCode(&unk_1400C70A3, a2, a3); v8 = j_j_j__malloc_base(0x10000uLL); j_memset(v8, 0, 0x10000uLL); j_memcpy(v8, main, 0x10000uLL); sub_1400011D6(v4, v3, v5); for ( i = 0; i < 4; ++i ) dword_1400BB2A0[i] = sub_140001AB4((__int64)v8 + 0x4000 * i, 0x4000uLL, v6); return 1LL; } // attributes: thunk __int64 __fastcall sub_1400011D6(__int64 a1, __int64 a2, __int64 a3) { return sub_1400088D0(a1, a2, a3); } __int64 __fastcall sub_1400088D0(__int64 a1, __int64 a2, __int64 a3) { __int64 result; // rax unsigned int v4; // [rsp+24h] [rbp+4h] int i; // [rsp+44h] [rbp+24h] int j; // [rsp+64h] [rbp+44h] result = j___CheckForDebuggerJustMyCode(&unk_1400C70A3, a2, a3); for ( i = 0; i < 256; ++i ) { v4 = i; for ( j = 0; j < 8; ++j ) { if ( (v4 & 1) != 0 ) v4 = (v4 >> 1) ^ 0xEDB88320; else v4 >>= 1; } dword_1400BD1A0[i] = v4; result = (unsigned int)(i + 1); } return result; } // attributes: thunk __int64 __fastcall sub_140001AB4(__int64 a1, unsigned __int64 a2, __int64 a3) { return sub_140008760(a1, a2, a3); } __int64 __fastcall sub_140008760(__int64 a1, unsigned __int64 a2, __int64 a3) { unsigned int v4; // [rsp+24h] [rbp+4h] unsigned __int64 i; // [rsp+48h] [rbp+28h] j___CheckForDebuggerJustMyCode(&unk_1400C70A3, a2, a3); v4 = -1; for ( i = 0LL; i < a2; ++i ) v4 = dword_1400BD1A0[(unsigned __int8)(*(_BYTE *)(i + a1) ^ v4)] ^ (v4 >> 8); return ~v4; }所以需要保证断点不能影响main后面的代码，这样才能拿到正确秘钥from ctypes import c_uint32 import struct v = [0x23, 0xEA, 0x50, 0x30, 0x00, 0x4C, 0x51, 0x47, 0xEE, 0x9C, 0x76, 0x2B, 0xD5, 0xE6, 0x94, 0x17, 0xED, 0x2B, 0xE4, 0xB3, 0xCB, 0x36, 0xD5, 0x61, 0xC0, 0xC2, 0xA0, 0x7C, 0xFE, 0x67, 0xD7, 0x5E, 0xAF, 0xE0, 0x79, 0xC5] v = [struct.unpack("<I", bytes(v[i:i+4]))[0] for i in range(0, len(v), 4)] k = [0x97A25FB5, 0xE1756DBA, 0xA143464A, 0x5A8F284F] def xxtea_decrypt(n, v, key): # 全部转为c_unit32格式 v = [c_uint32(i) for i in v] r = 11 v0 = v[0].value delta = 0 total = c_uint32(delta * r) for i in range(r): e = (total.value >> 2) & 3 for j in range(n-1, 0, -1): v1 = v[j-1].value v[j].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(j & 3) ^ e] ^ v1))) v0 = v[j].value v1 = v[n-1].value v[0].value -= ((((v1 >> 5) ^ (v0 << 2)) + ((v0 >> 3) ^ (v1 << 4))) ^ ((total.value ^ v0) + (key[(0 & 3) ^ e] ^ v1))) v0 = v[0].value total.value -= delta return [i.value for i in v] v = xxtea_decrypt(len(v), v, k) # 输出解密后的数据 str_list = [] for i in range(len(v)): str_list.append(struct.pack('<I', v[i]).decode()) print('decrypted: %s' % ''.join(str_list))flag为hgame{3fe4722c-1dbf-43b7-8659-c1c4a0e42e4d}Middlemenjava层没啥看的，native层注册了middlemen，可以看到flag格式以及做了syscall 172操作，具体是利用sscanf把32个hex字符转成16个字节，写进 v12bool __fastcall sub_22DC(JNIEnv *a1, __int64 a2, __int64 a3) { _QWORD v4[14]; // [xsp+0h] [xbp-C0h] BYREF __int64 v5; // [xsp+70h] [xbp-50h] const char *v6; // [xsp+78h] [xbp-48h] __int64 v7; // [xsp+80h] [xbp-40h] __int64 v8; // [xsp+88h] [xbp-38h] JNIEnv *v9; // [xsp+90h] [xbp-30h] bool v10; // [xsp+98h] [xbp-28h] char v11[4]; // [xsp+9Ch] [xbp-24h] BYREF _QWORD v12[4]; // [xsp+A0h] [xbp-20h] BYREF v12[3] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); v9 = a1; v8 = a2; v7 = a3; memset(v12, 0, 20); v4[12] = v12; v4[11] = &v12[1]; v6 = (const char *)sub_2454(a1, a3, (__int64)v11); v4[13] = v4; v10 = sscanf( v6, "hgame{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x}", v12, (char *)v12 + 1, (char *)v12 + 2, (char *)v12 + 3, (char *)v12 + 4, (char *)v12 + 5, (char *)v12 + 6, (char *)v12 + 7, &v12[1], (char *)&v12[1] + 1, (char *)&v12[1] + 2, (char *)&v12[1] + 3, (char *)&v12[1] + 4, (char *)&v12[1] + 5, (char *)&v12[1] + 6, (char *)&v12[1] + 7) && (v5 = syscall(172LL, v12[0], *(_QWORD *)((char *)v12 + 4), v12[1], *(_QWORD *)((char *)&v12[1] + 4), 0x221221LL), v5 >= 0x1000000000001LL); _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); return v10; }信息很少不太对，直接看init_array发现猫腻函数sub_2020__int64 sub_2020() { __int64 result; // x0 __int128 v1; // [xsp+0h] [xbp-40h] BYREF __int64 v2; // [xsp+10h] [xbp-30h] BYREF int v3; // [xsp+18h] [xbp-28h] BYREF __int64 (__fastcall *v4)(__int64, __int64, __int64); // [xsp+20h] [xbp-20h] __int64 v5; // [xsp+28h] [xbp-18h] __int64 v6; // [xsp+38h] [xbp-8h] v6 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); v1 = *(_OWORD *)&off_48D8; result = prctl(PR_SET_NO_NEW_PRIVS, 1LL, 0LL, 0LL);// 禁止通过 execve 提升权限（如禁用 setuid）‌ if ( (_DWORD)result != -1 ) { result = prctl(PR_SET_SECCOMP, 2LL, &v1); // 启用 seccomp 模式，限制进程可调用的系统调用（沙盒安全）‌ if ( (_DWORD)result != -1 ) { sigfillset((sigset_t *)&v2); // 初始化一个信号集，使其包含所有定义的信号 v4 = sub_1ECC; v5 = v2; v3 = 4; result = sigaction(SIGSYS, (const struct sigaction *)&v3, 0LL);// 当进程收到 SIGSYS 时，会跳转到v3里定义的handler也就是sub_1ECC } } _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); return result; }先看prctl函数，发现启用了seccomp模式，所以v1实际上没有识别出来是sock_fprog结构体，按下Y键可以手动恢复结构体__int64 sub_2020() { __int64 result; // x0 sock_fprog v1; // [xsp+0h] [xbp-40h] BYREF __int64 v2; // [xsp+10h] [xbp-30h] BYREF int v3; // [xsp+18h] [xbp-28h] BYREF __int64 (__fastcall *v4)(__int64, __int64, __int64); // [xsp+20h] [xbp-20h] __int64 v5; // [xsp+28h] [xbp-18h] __int64 v6; // [xsp+38h] [xbp-8h] v6 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); v1 = prog; result = prctl(PR_SET_NO_NEW_PRIVS, 1LL, 0LL, 0LL);// 禁止通过 execve 提升权限（如禁用 setuid）‌ if ( (_DWORD)result != -1 ) { result = prctl(PR_SET_SECCOMP, 2LL, &v1); // 启用 seccomp 模式，限制进程可调用的系统调用（沙盒安全）‌ if ( (_DWORD)result != -1 ) { sigfillset((sigset_t *)&v2); // 初始化一个信号集，使其包含所有定义的信号 v4 = sub_1ECC; v5 = v2; v3 = 4; result = sigaction(SIGSYS, (const struct sigaction *)&v3, 0LL);// 当进程收到 SIGSYS 时，会跳转到v3里定义的handler也就是sub_1ECC } } _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); return result; }跳转prog，我们先要了解sock_fprog的结构，第一个数值是filter的大小，filter是指针指向sock_filter结构体数组struct sock_fprog { unsigned __int16 len; struct sock_filter *filter; }; struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; };ida最开始是没法识别出filter指针的，需要我们手动操作，来到filter指向的地址，首先先转为8字节的qword，再设置type，然后在这个结构体右键按下Array，设置数组大小0x2a，即可得到sock_filter数组seccomp的sock_filter没法用seccomp-tools dump，所以直接我们手动把这个42个qword大小保存为code.bpf，然后seccomp-tools disasm code.bpf得到反汇编代码 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x26 0xc00000b7 if (A != ARCH_AARCH64) goto 0040 0002: 0x20 0x00 0x00 0x00000020 A = args[2] // v12[1] 0003: 0x02 0x00 0x00 0x00000000 mem[0] = A 0004: 0x20 0x00 0x00 0x00000028 A = args[3] // v12[1]+4 0005: 0x02 0x00 0x00 0x00000001 mem[1] = A 0006: 0x64 0x00 0x00 0x00000004 A <<= 4 0007: 0x04 0x00 0x00 0x65766573 A += 0x65766573 0008: 0x02 0x00 0x00 0x00000002 mem[2] = A 0009: 0x60 0x00 0x00 0x00000001 A = mem[1] 0010: 0x07 0x00 0x00 0x00000000 X = A 0011: 0x00 0x00 0x00 0x22122122 A = 571613474 0012: 0x0c 0x00 0x00 0x00000000 A += X 0013: 0x07 0x00 0x00 0x00000000 X = A 0014: 0x60 0x00 0x00 0x00000002 A = mem[2] 0015: 0xac 0x00 0x00 0x00000000 A ^= X 0016: 0x07 0x00 0x00 0x00000000 X = A 0017: 0x60 0x00 0x00 0x00000000 A = mem[0] 0018: 0x0c 0x00 0x00 0x00000000 A += X 0019: 0x15 0x00 0x14 0x93cd6340 if (A != 2479711040) goto 0040 0020: 0x02 0x00 0x00 0x00000000 mem[0] = A 0021: 0x74 0x00 0x00 0x00000005 A >>= 5 0022: 0x04 0x00 0x00 0x6e6e6e6e A += 0x6e6e6e6e 0023: 0x02 0x00 0x00 0x00000002 mem[2] = A 0024: 0x60 0x00 0x00 0x00000000 A = mem[0] 0025: 0x07 0x00 0x00 0x00000000 X = A 0026: 0x00 0x00 0x00 0x22122122 A = 571613474 0027: 0x0c 0x00 0x00 0x00000000 A += X 0028: 0x07 0x00 0x00 0x00000000 X = A 0029: 0x60 0x00 0x00 0x00000002 A = mem[2] 0030: 0xac 0x00 0x00 0x00000000 A ^= X 0031: 0x07 0x00 0x00 0x00000000 X = A 0032: 0x60 0x00 0x00 0x00000001 A = mem[1] 0033: 0x0c 0x00 0x00 0x00000000 A += X 0034: 0x15 0x00 0x05 0xb5f40d3f if (A != 3052670271) goto 0040 0035: 0x20 0x00 0x00 0x00000000 A = sys_number 0036: 0x15 0x00 0x03 0x000000ac if (A != aarch64.getpid) goto 0040 0037: 0x20 0x00 0x00 0x00000030 A = args[4] 0038: 0x15 0x00 0x01 0x00221221 if (A != 0x221221) goto 0040 // 对应syscal 172里传入参数的第四个 0039: 0x06 0x00 0x00 0x00030000 return TRAP 0040: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0041: 0x06 0x00 0x00 0x00050000 return ERRNO(0)接着我们要分析的是sigaction函数里第二个参数，结构体是sigactionstruct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };SIGSYS, generated (since Linux 3.5) when a seccomp filter returns SECCOMP_RET_TRAP, fills in si_call_addr, si_syscall, si_arch, si_errno, and other fields as described in seccomp(2).当传入SIGSYS时，会判断seccomp也就是上面反汇编出来的东西是否为SECCOMP_RET_TRAP，然后才会进入handler其中的handler参数类型具体如下void handler(int sig, siginfo_t *info, void *ucontext) { ... }结合文档可以把sub_1ECC函数的第三个参数设置为ucontext_t前面分析知道必须要return SECCOMP_RET_TRAP，回到反汇编代码里分析，大概简单化可以发现检查了两个等式mem[0]=args[2] mem[1]=args[3] mem[2]=(args[3]<<4)+0x65766573 ((mem[1]+571613474)^mem[2])+mem[0]==2479711040 mem[0]=2479711040 mem[2]=(2479711040>>5)+0x6e6e6e6e ((mem[0]+571613474)^mem[2])+mem[1]==3052670271z3求解可以得到两个数值分别为1293539468、4011896661from z3 import * s = Solver() x = BitVec('x', 32) y = BitVec('y', 32) mem = [0]*3 mem[0]=x mem[1]=y mem[2]=(y<<4)+0x65766573 s.add(((mem[1]+571613474)^mem[2])+mem[0]==2479711040) mem[0]=2479711040 mem[2]=(2479711040>>5)+0x6e6e6e6e s.add(((mem[0]+571613474)^mem[2])+mem[1]==3052670271) if s.check() == sat: print(s.model())所以前面在做的就是seccomp 把 syscall(172) 变成一个会触发 SIGSYS 的“私有入口”，AES 的 key 从该入口触发时的寄存器快照里取出，并在 SIGSYS handler 内完成 AES 校验，再通过修改 ucontext 伪造 syscall 返回值，把结果传回调用者。传入sub_1ECC，__int64 __fastcall sub_1ECC(__int64 a1, __int64 a2, ucontext_t *a3) { int v3; // w9 __int64 result; // x0 int i; // [xsp+14h] [xbp-5Ch] char v7[24]; // [xsp+30h] [xbp-40h] BYREF char v8[16]; // [xsp+48h] [xbp-28h] BYREF _QWORD v9[3]; // [xsp+58h] [xbp-18h] BYREF v9[2] = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); strcpy(v7, "Sevenlikeseccmop"); v9[0] = a3->uc_mcontext.gregs[4]; // 这里结构体可能还有点问题，不过已经可以看出，v9[0]、v9[1]就是接收前面的16字节 v9[1] = a3->uc_mcontext.gregs[6]; // 这里是后8字节，需要满足seccomp里的过滤条件，同时被用来生成正确的aes key for ( i = 0; i <= 15; ++i ) { if ( i <= 0 ) v3 = -(-i & 7); else v3 = i & 7; v7[i] ^= *((_BYTE *)&v9[1] + v3); } aes_enc((__int64)v7, 0x10u, v9, v8, 0x10u); result = strncmp(v8, byte_5B58, 0x10u); if ( (_DWORD)result ) { result = getpid(); a3->uc_mcontext.gregs[4] = (int)result; } else { a3->uc_mcontext.gregs[4] = 0x1145141919810LL; } _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)); return result; }计算了新的秘钥dfbd6f283bc34984e9ab7c2e36c24f9fx = 1293539468 y = 4011896661 key = list(b"Sevenlikeseccmop") xor = int.to_bytes(x, length=4, byteorder='little')+int.to_bytes(y, length=4, byteorder='little') for i in range(len(key)): key[i] ^= xor[i%8] print(bytes(key).hex())然后把解密的hex转回"hgame{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x}"格式即可