CTF-WP-3
N1CTF2023
AdditionPlus
程序获取输入,然后八个字节八个字节的加密。
八个字节的加密方式是分别经过八个函数,每个函数得到一个字节的运算结果,然后进行比对。
每个函数有一堆运算由与、异或、左移1位组成。
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
解法一——Ponce
这里看了师傅的WP复现一下👉2023 N1CTF writeup by Arr3stY0u
顺便了解一下Ponce这个工具的使用。
首先,illera88/Ponce获取两个DLL
直接放在plugins
目录下即可。
进入IDA,由Ponce配置设置,这里红框中有时间的设置,在这题得设大一些。
这里每次八个字节进行符号执行,以前八个字节为例,先对程序进行Patch。
前八个字节,就先将index上限到8。
然后比对的时候,程序是十六个字节进行比对的,这里将or rdx, rax
改为or rax, rax
,即可改为只比对前八个字节。
在程序获取输入后,为前八字节进行操作:
接下来可以直接运行到判断前八字节比对结果的跳转指令处:
在这里,如果比对失败会走0x000055816786A38F
,比对成功会走0x000055816786A374
,这里由于输入的不是flag,会走0x000055816786A38F
,这里让Ponce解出走0x000055816786A374
的输入:
跑了小久,成功解出前八个字节的flag:
尝试一次跑十六个字节的,也是可以的:
之后就是如法炮制解出剩余flag:n1ctf{c327b78b-8ead-425H1N09d-89fb-1d2d0a8f3e7c}
N1LLua
N1CTF2023-N1LLua | OrientalGlass 👈 看这位师傅的WP
使用AssetBundleExtractor导出main.lua
:
根据luac
的文件头删去多余的前十六个字节:
使用unluac
来反编译:java -jar .\unluac.jar .\main.lua-resources.assets-13.dat > main.lua
1 | local L0_1, L1_1, L2_1, L3_1, L4_1 |
根据GPT的解释:
所以总体来说,这段代码通过 Lua 虚拟机实现对一段密文的解密,利用 luavm 模块解析并运行字节码来还原明文。流程就是将密文解析为字节码->运行解析->返回结果。
再把代码中的字节码dump成一个luac
文件,再次反编译:java -jar .\unluac.jar .\new_code.luac > new_code.lua
这是一段非标准的TEA加密:
Lua中^
是乘幂符号
但知道了也没用,写解密函数半天写不出来,看WP才知道乘幂操作被改了😫😫
1 | function encrypt(v, k, p) |
不知道是怎么改的,就只好改加密函数为解密函数,让程序自己解密了。
首先更改加密函数的Lua:java -jar .\unluac.jar --disassemble .\new_code.luac > .\new_code.luaasm
获取Lua的字节码
1 | .version 5.2 |
红框里为sum = 0
的操作,因此需要改k2=2654435769*32
从上到下分别对应循环中的三个加法,解密应该逆序,所以直接调换位置,add
也要改为sub
sum = sum + delta
v0 = v0 + bit32.bxor(bit32.bxor(v1 ^ 4 + k0, v1 + sum), v1 ^ 5 + k1)
v1 = v1 + bit32.bxor(bit32.bxor(v0 ^ 4 + k2, v0 + sum), v0 ^ 5 + k3)
改完后,java -jar .\unluac.jar --assemble .\new_code.luaasm --output .\new_code_dec.luac
编译会Luac文件,然后将其二进制字符串patch到main.luac
中:
使用AssetBundleExtractor将更改的main.luac
patch回去:
然后更改Assembly-CSharp.dll里的check方法的逻辑,使其使用密文作为输入进行解密。
1 | // Token: 0x06000002 RID: 2 |
然后运行程序获取flag: n1ctf{E5c4p3_N1Lva7ua!}
N1go
给了GO的源码,但混淆了,且一堆字符串需要运行时才能知道,用WP里的脚本(稍微改了一下可以在Windows下运行)解一下,思路是获取func() string
里的函数内容,配合Go的print
模板写入到一个go文件中,执行获取输出结果,替换回去。
1 | import os |
生成的新的Go文件从四万多行变到了一万六千多行,还行。
其逻辑主体大概如下:
1 | func main() { |
大概就是根据输入进入了不同的函数,如果输入的是flag则最后进入的是eicz56Kamp
,解法就是构造输入让程序一直走到成功。
有798个函数,如果用DFS遇到有几个函数可以绕圈圈的情况就出不来了,如果用BFS情况也不少,但想来出题师傅应该不会这么狠吧🙁
WP里用了networkx
,这个东西没见过但看上去蛮厉害的,把每个函数看成节点,flag执行路径就是找cMpCg2IkJ95
到eicz56Kamp
的路径。
这里贴一下:
1 | import re |
这里借用了WP里的两个辅助函数,写一下DFS的解法:
1 | import hashlib |
鹏程杯
bad_pe
ida打开提示有对不齐的节指针,去010editor中看下,把.ATOM修改正确:
修改之后,程序可以运行了,调试以下,发现以下异或:
异或出来的结果为MZ开头:
可知.ATOM为PE程序,dump出来,手动异或得到PE程序。
发现是个RC4加密:
调试一下,得到比较的结果,再当作自己的输入让其进行一次加密,得到flag:
安全编程
调试程序:
在sub_7FDDCEB2EC70中,有程序逻辑。
检测是否猜对数字:
如果猜对了次数加一,然后和100比较,若达到一百次,就解密图片:
直接该right_times为100就可以直接跑出来了。
babyre
用条件断点获取rand数据。
逻辑很清晰,直接脚本:
1 | import numpy as np |
ezapk
Apk里的逻辑很简单,加密逻辑都在so中。
进so逻辑都呈现出来了,但是需要分析,比赛的时候只看出来了XXTEA,没看出来DES😫
逻辑
获取输入,分为四个32bit数,进行XXTEA加密
1 | sscanf((const char *)input, "%8x%8x%8x%8x", &c0, &c1, &p2_, &c3); |
之后每两个数为一组进入sub_22B0
,根据a1
,进入不同模式的DES加密
1 | _QWORD *__fastcall sub_22B0(unsigned __int64 a1, __int64 a2) |
DES_cbc_enc
第一个条件分支,将a1异或后就进入了该函数
1 | __int64 __fastcall DES_cbc_enc(__int64 c2_, __int64 c2, unsigned int value_8, unsigned int *c1) |
while循环只执行了一次,里面有一个异或,猜测是模式的异或,猜测异或的是IV,那sub_16A0
应该是使用c1
生成密钥,存储在v19
,然后使用sub_1B90
加密c2
子密钥生成
猜测一下函数是子密钥生成,但逻辑上有些理不清
1 |
|
DES 64bit 加密
1 | __int64 __fastcall sub_1B90(__int64 a1, unsigned int *a2, _BYTE *a3) |
DES_ecb_enc
另一个条件分支的加密和上面差不多,只是少了明文一开始的异或
sub_16A0
初始化密钥,sub_1B90
进行加密。
exp
知道以上逻辑就可以写脚本解密flag了,借鉴了其他师傅WP的一部分👉2023第三届“鹏城杯”线上初赛WriteUp
1 | from Crypto.Cipher import DES |
StateMachine
看了网上师傅的WP👉2023 11.04 鹏城杯 wp - LaoGong
逻辑
先看获取opcode
的函数:
1 | __int64 __fastcall get_opcode(int index, unsigned int unk_5150_indexMul329) |
再看获取op_num
的函数:
1 | __int64 __fastcall get_op_num(int a1, unsigned int a2) |
get_opcode
和get_op_num
都是获取同一个数据里的数据,且最后返回的时候,经过了移位变换。
调试可以获取数据:
再来看其他函数,应该是控制流进行操作的函数了
1 | void __fastcall __noreturn start_routine(int *a1) |
利用网上师傅WP里的脚本可以讲逻辑输出,对于codes
为什么要二进制然后倒转这点我暂时没搞懂😭
1 | codes = open('./vm', 'rb').read()[0x3020: 0x3020 + 0x157] |
使用gcc -c ./main.s -o main.o
可以将输出的汇编代码转成二进制文件,更好分析。
先获取输入,然后将输入分为三组,每组两个四字节数进行TEA加密,最后进行比较。
1 | __int64 __fastcall 0x0000(__int64 a1, __int64 a2) |
密文在之前有一个初始化函数中:
exp
懒得写解密脚本了,借鉴一下网上师傅的,阿里嘎多
主要是前面的逻辑或多或少没理清
1 |
|
ACTF2023
native-app
blutter
1 | git clone https://github.com/worawit/blutter --depth=1 |
在x64 Native Tools Command Prompt for VS 2022
中:
1 | python ./blutter.py E:\CTFChallenge\攻防世界\ACTF2023\native_app\chall\lib\arm64-v8a ./out |
out
目录下:
1 | │ blutter_frida.js |
IDA
打开libapp.so
,使用addNames.py
恢复符号。
DASCTF X 0psu3十一月挑战赛
ezpython
PYC文件将前十六个字节换成Python11的文件头,原来是由Key的yuanshen
。
反编译以下,发现有些错误,但根据字节码让GPT反编译就可以补齐逻辑了:
1 | # Source Generated with Decompyle++ |
一个简单的DES加密,运行代码就可以获取IV。
1 | import pyDes |
Tetris
两段加密函数,分别构成整个FLAG:
1 | int __cdecl sub_E3B030(unsigned int *a1) |
第一段解密脚本:
1 | # DASCTF{we1c0###################} |
第二段解密脚本,直接那WP里的了:
1 | from z3 import * |
letsgo
在exe
的hookmain
中,有一个函数,检测按键按下,如果都被按下了调用r
,即DLL的加密函数:
1 | LRESULT __fastcall KeyboardHookProc(int code, WPARAM wParam, _DWORD *lParam) |
DLL进行UPX脱壳:
在main_Enc
中,
1 | // main.Enc |
以上可知计算出来的pi/4
就是种子,这里直接patch掉计算的部分,将rand_seed
的参数直接设置为计算好的:0x31a2818'
这里用的是WP里获取种子的大小,这个脚本其他不知道为啥,计算的key和flag不对。
1 | func hello() { |
然后获取加密的key
:[100, 111, 95, 121, 111, 117, 95, 116, 104, 105, 110, 107, 95, 116, 104, 105, 115, 95, 105, 115, 95, 107, 101, 121]
然后就可以进行AES解密了:
1 | import base64 |
古剑杯
re2
v3
解密数据,数据以ELF
开头,后面程序会自动dump出来的。
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
开启子进程执行ELF
,同时给父进程调式,父进程将0x6020AC
的数据改为7
,即子进程的密钥位置。
1 | __int64 sub_400BAE() |
子进程解flag逻辑,是个TEA解密,只是密钥需要父进程写入一下。
1 | __int64 sub_400914() |
exp
1 | def tea_dec(cipher, key): |
re3
pyc
文件反编译出字节码让GPT去生成代码:
1 | #!/usr/bin/env python |
有点不太对劲的,改改字节码,尝试执行:
1 | ┌──(biu㉿Biu)-[~/Desktop] |
1 | flag1输出:ifxMVX0e |
更改字节码,把flag1
的LOAD_FAST 1: result
改为LOAD_GLOBAL 0: str
,输出str的逆序:fjU1MmYyNWcyNmcyOTgyYj
然后,就看不懂了……
1 | # str = '=cWbi' + 'hGfyM' + 'zNllzZ' + '0cjZzMW' + 'N5cTM4Y' + 'jYygTOy' + 'cmNycWNyYmM1Ujf' |