picoCTF 2024
picoCTF 2024
Re复现
1.packer
题目描述
Reverse this linux executable?
反转这个 Linux 可执行文件?
解题思路
题目附件是一个可执行的ELF64文件,将它先放入DIE里查壳,发现有UPX壳,首先要进行脱壳。

这是最简单的UPX壳。脱完后我们将它放入IDA里进行静态分析。
 
逻辑简单清晰,flag显而易见就是v13的内容。
构造exp解出flag即可。

exp
| 1 | a=bytes.fromhex("7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f35646565343434317d").decode() | 
2.FactCheck
题目描述
This binary is putting together some important piece of information… Can you uncover that information?Examine this file. Do you understand its inner workings?
这个二进制文件正在汇总一些重要信息… 你能发现这些信息吗?
检查这个文件 你了解它的内部结构吗?
解题思路
这个文件也是一个可执行的ELF64文件,首先放入DIE里查壳,发现没有壳。
然后放入IDA里进行静态分析,发现这是一个flag拼接。主要代码如下所示:
| 1 | unsigned __int64 v39; // [rsp+238h] [rbp-18h] | 
分析:这个flag分为两部分,一部分是v22=”picoCTF{wELF_d0N3_mate_”,另一部分需要进行条件拼接。
第一个拼接条件是v24="5"如果小于A(65)就为真,在v22字符串的后面追加v34=”f”。
第二个拼接条件是v35 = "6" ≠ 'A' 条件为 真,在v22字符串的后面追加v37=”d”。
第三个拼接条件是”Hello” == “World”,这是永假的,所以追加不执行,跳过追加操作。
第四个拼接条件是v19-v30==3,v19在上一行被赋值为3,v30是e,这是永假的,所以追加不执行,跳过追加操作。
接下来是无条件拼接,拼接了v25=”6”和v28=”5”
第五个拼接条件是v29=”a”和”G”(71)来比较是否相等,这是永假的,所以追加不执行,跳过追加操作。
然后是无条件追加,v27=”e”,v36=”e”,v23=”4”,v31=”e”和”}”。
编写exp解出flag。

exp
| 1 | def build_flag(): | 
3.weirdSnake
题目描述
I have a friend that enjoys coding and he hasn’t stopped talking about a snake recentlyHe left this file on my computer and dares me to uncover a secret phrase from it. Can you assist?
我有个朋友喜欢编程,最近一直在谈论一条蛇。 他把这个文件放在我的电脑里,问我能不能从中找出一个秘密短语。你能帮忙吗?
解题思路
下载题目附件得到一个无后缀名的文件,先进行DIE查壳,发现很特别的是文件类型是数据。这说明这个文件 不是一个有效的可执行文件(ELF / PE),它可能是:
- 被加壳或加密过的二进制(如 packer、crypter 加壳) 
- 是某个程序 dump 出来的数据片段 
- 是非标准格式(比如纯数据段、内存镜像等) 
- 根本不是可执行文件  
在这里,将它放到vscode里面可以读取到文件内容。
这个明显的是**file “snake.py”**的dis模块即这个文件对应的字节码。显而易见,这道题是让我们分析这个字节码模块来写出exp解出flag的。
关于dis模块的介绍:Python 代码先被编译为字节码后,再由Python虚拟机来执行字节码, Python的字节码是一种类似汇编指令的中间语言, 一个Python语句会对应若干字节码指令,虚拟机一条一条执行字节码指令, 从而完成程序执行。而dis模块可以帮助我们查看Python代码的字节码,它是python中内置的一个模块。
- 第一列表示当前字节码在源代码中的行号 
- 第二列是字节码的偏移量和对应的字节码,0表示当前字节码,LOAD_CONST是 Python 虚拟机要执行的操作,它是加载内容到栈上。 
- 第三列是字节码指令的参数。这是字节码指令的操作数。第一个 - LOAD_CONST指令的参数是- 0,表示它将第 0 个局部变量(即- 4)加载到栈上。
| 1 | 1 0 LOAD_CONST 0 (4) | 
对它进行分析:
1.行 1 (偏移量 0 - 82): input_list = [...]
- LOAD_CONST(0 to 78): 这些指令依次加载了 4、54、41、0、112、32、25、49、33、3、0、0、57、32、108、23、48、4、9、70、7、110、36、8、108、7、49、10、4、86、43、108、122、14、2、71、62、115、88、78 这些数字常量到栈上。
- BUILD_LIST 40(80): 从栈顶取出 40 个元素,构建一个列表。
- STORE_NAME 0 (input_list)(82): 将构建好的列表存储到名为- input_list的变量中。- 总结:这一部分创建了一个包含 40 个整数的列表,并将其赋值给变量 - input_list
2.行2-6行,是key_str被赋值为t_Jo3。
3.行 9 (偏移量 120 - 132): key_list = [ord(char) for char in key_str]
- LOAD_CONST 36 (<code object <listcomp> at 0x7f5ef6665d40, file "snake.py", line 9>)(120): 加载列表推导式(list comprehension)的代码对象。
- LOAD_CONST 37 ('<listcomp>')(122): 加载列表推导式的名称。
- MAKE_FUNCTION 0(124): 根据代码对象和名称创建一个函数对象(这里的函数就是列表推导式的内部实现)。
- LOAD_NAME 1 (key_str)(126): 加载- key_str(- t_Jo3)。
- GET_ITER(128): 获取- key_str的迭代器。
- CALL_FUNCTION 1(130): 调用前面创建的函数对象,并将迭代器作为参数传递。这个函数会遍历- key_str,对每个字符执行- ord(),并构建一个新列表。
- STORE_NAME 2 (key_list)(132): 将结果列表存储到- key_list变量中。- 总结: - key_list将包含字符串- t_Jo3中每个字符的 ASCII值。
4.行 11 (偏移量 134 - 160): while len(key_list) < len(input_list): key_list.extend(key_list)
- 这是一个 - while循环,用于扩展- key_list直到它的长度不小于- input_list的长度。
- LOAD_NAME 3 (len)(134): 加载内置函数- len。
- LOAD_NAME 2 (key_list)(136): 加载- key_list。
- CALL_FUNCTION 1(138): 调用- len(key_list)。
- LOAD_NAME 3 (len)(140): 加载- len。
- LOAD_NAME 0 (input_list)(142): 加载- input_list。
- CALL_FUNCTION 1(144): 调用- len(input_list)。
- COMPARE_OP 0 (<)(146): 比较- len(key_list) < len(input_list)。
- POP_JUMP_IF_FALSE 162(148): 如果比较结果为 False(即- key_list不小于- input_list的长度),则跳到偏移量 162 (循环结束)。
- LOAD_NAME 2 (key_list)(150): 加载- key_list。
- LOAD_METHOD 4 (extend)(152): 加载- key_list的- extend方法。
- LOAD_NAME 2 (key_list)(154): 再次加载- key_list作为- extend方法的参数。
- CALL_METHOD 1(156): 调用- key_list.extend(key_list),将- key_list自己追加到自身后面,使其长度翻倍。
- POP_TOP(158): 弹出方法调用的返回值(- None)。
- JUMP_ABSOLUTE 134(160): 无条件跳转回偏移量 134,开始下一次循环条件判断。- 总结: 这是一个经典的处理密钥流(key stream)不足长度的方法,通过重复密钥来匹配明文长度。 
5.行 15 (偏移量 162 - 180): result = [a ^ b for a, b in zip(input_list, key_list)]
- LOAD_CONST 38 (<code object <listcomp> at 0x7f5ef6665df0, file "snake.py", line 15>)(162): 加载第二个列表推导式的代码对象。
- LOAD_CONST 37 ('<listcomp>')(164): 加载名称。
- MAKE_FUNCTION 0(166): 创建函数对象。
- LOAD_NAME 5 (zip)(168): 加载内置函数- zip。
- LOAD_NAME 0 (input_list)(170): 加载- input_list。
- LOAD_NAME 2 (key_list)(172): 加载- key_list。
- CALL_FUNCTION 2(174): 调用- zip(input_list, key_list),它会生成一个迭代器,每次返回- input_list和- key_list中对应位置的元素对。
- GET_ITER(176): 获取- zip对象的迭代器。
- CALL_FUNCTION 1(178): 调用列表推导式的函数对象,将- zip迭代器作为参数。这个函数会遍历配对的元素,对每一对执行异或操作- ^。
- STORE_NAME 6 (result)(180): 将计算得到的列表存储到- result变量中。- 总结: 这一步执行了逐元素的异或 (XOR) 操作,这是对称加密(如一次性密码本、流密码)或哈希/校验和中常见的操作。 - result将是一个包含异或结果的整数列表。
6.行 18 (偏移量 182 - 200): result_text = ''.join(map(chr, result))
- LOAD_CONST 39 ('')(182): 加载空字符串- ''。
- LOAD_METHOD 7 (join)(184): 加载空字符串的- join方法。
- LOAD_NAME 8 (map)(186): 加载内置函数- map。
- LOAD_NAME 9 (chr)(188): 加载内置函数- chr。
- LOAD_NAME 6 (result)(190): 加载- result列表。
- CALL_FUNCTION 2(192): 调用- map(chr, result)。- map会将- result列表中的每个整数转换为对应的字符(ASCII/Unicode 字符)。
- CALL_METHOD 1(194): 调用- join方法,将- map对象生成的字符拼接成一个字符串。
- STORE_NAME 10 (result_text)(196): 将最终的字符串存储到- result_text变量中。
- LOAD_CONST 40 (None)(198): 加载- None。
- RETURN_VALUE(200): 返回- None(因为脚本没有明确的返回值,- main模块的执行隐式返回- None)。- 总结: 这一步将异或结果(整数列表)转换回字符,并将这些字符拼接成一个可读的字符串,这很可能是解密后的明文或某个关键信息。 
exp
| 1 | # original_input_list - 对应字节码分析中的 input_list (密文/加密后的数据) | 

4.Classic Crackme 0x100
题目描述
A classic Crackme. Find the password, get the flag!Binary can be downloaded here.Crack the Binary file locally and recover the password. Use the same password on the server to get the flag!
Additional details will be available after launching your challenge instance.
经典的 Crackme 破解程序。找到密码,即可获得 flag!
二进制文件可在此处下载。
在本地破解二进制文件并恢复密码。在服务器上使用相同的密码即可获得 flag!
启动挑战实例后,您将获得更多详细信息。
解题思路
这道题要求我们先找到密码,然后进行验证密码,如果正确就能得到flag。
使用ida打开,进行分析主程序的逻辑,找到密码的加密逻辑。
 
由于i_0<len,len很明显是小于255的,所以i_0%255==i_0,这就简化了加密的逻辑。循环套循环,外面是循环3次,里面是在每一个大循环时进行每个字节的循环加密。在这里,random1和random2都是随着i_0变化而变化,(random2 & secret3) + (secret3 & (random2 >> 4))这就相当于一个偏移量,shift也随着i_0变化而变化。后面还有%26,是控制它的偏移量为26以内。
shift 是每个字符的加密偏移量,是通过固定公式和字符位置计算出来的整数,范围是 0~25,表示这个字符要偏移几个字母。
shift = ((random2 & secret3) + (secret3 & (random2 >> 4)))%26
解密的时候random1和random2还是一样的,shift也是和加密时是一样的,关键是解密时顺序不同。
密文=(明文-fix)%26+shift+fix
明文=(密文-fix-shift+26)%26+fix
括号里再加26是为了防止变成负数。
解密成功后拿到密钥xgxamyxzbathferiakznsetdpbhbvigjzioiexlslagntpgewt,然后在虚拟机中运行这个ELF文件,发现是successful但是输出的还是picoCTF{sample_flag},我一开始很疑惑,为什么都找到啦还是没有出现真正的flag。后来才发现这个需要和题目中的靶机远程连接nc一下,再输入密码就能拿到flag。

exp
| 1 | 
 | 
5.WinAntiDbg0x100
题目描述
This challenge will introduce you to ‘Anti-Debugging.’ Malware developers don’t like it when you attempt to debug their executable files because debugging these files reveals many of their secrets! That’s why, they include a lot of code logic specifically designed to interfere with your debugging process.Now that you’ve understood the context, go ahead and debug this Windows executable!This challenge binary file is a Windows console application and you can start with running it using cmd on Windows.Challenge can be downloaded here. Unzip the archive with the password
这项挑战将带你了解“反调试”。恶意软件开发者不喜欢你尝试调试他们的可执行文件,因为调试这些文件会暴露他们的许多秘密!正因如此,它们包含大量专门设计用来干扰你调试过程的代码逻辑。
既然你已经了解了背景,那就开始调试这个 Windows 可执行文件吧!
这个挑战二进制文件是一个 Windows 控制台应用程序,你可以先在 Windows 上使用 cmd 命令运行它。
挑战文件可以在这里下载。使用密码解压压缩包。
解题思路
根据题目我们就知道本题考察的是反调试。
什么是反调试?
反调试(Anti-Debugging)是一种技术手段,用于阻止别人使用调试器(如 GDB、x64dbg、OllyDbg、IDA、WinDbg 等)来分析和修改程序的行为。
简单的反调试往往是识别是否被调试,如果是则退出程序,封禁账号等等 (检测)
再复杂些可以在反汇编代码中插入花指令,使调试器的反汇编引擎无法正确解析反汇编指令(干扰)
门槛较高的反调试则可以是从驱动层将调试权限清零,使得调试器失效等等 (权限清零)
反调试的手段可以大致归纳为:检测、干扰、权限清零 三种
API 检查类(直接判断是否被调试)
| 方法 | 平台 | 示例 | 
|---|---|---|
| IsDebuggerPresent() | Windows | 简单快速,返回值为 true代表有调试器 | 
| CheckRemoteDebuggerPresent() | Windows | 可检测父进程是否调试它 | 
| ptrace(PTRACE_TRACEME, …) | Linux | 如果已被调试,则失败 | 
如何绕过反调试?
在汇编中找到 IsDebuggerPresent 的调用并修改其返回值(如 patch 为返回 0)
我们进行分析主函数,里面使用了大量的OutputDebugStringW输出提示信息到 调试器控制台。

这个主函数逻辑清晰,主要是要在动态调试中要绕过反调试,就能在Output窗口得到flag
下断点在 test eax, eax 处
1.经过反调试的动调

2.绕过反调试

手动修改EAX的值为0,可以绕过反调试
call IsDebuggerPresent
 → 调用 Windows API,返回值存在 EAX 中:
- 若正在调试,返回 EAX=1
- 若未调试,返回 EAX=0
test eax, eax
 → 实际就是判断 EAX 是否为 0
F8一直运行得到flag

Pwn复现
1.heap 0
题目描述
Are overflows just a stack concern?Download the binary here.Download the source here.
Additional details will be available after launching your challenge instance.
溢出仅仅是堆栈问题吗?
点击此处下载二进制文件。
点击此处下载源代码。
启动挑战实例后,您将获得更多详细信息。
解题思路
这是一道经典的 heap-based PWN(堆溢出)题目
堆的结构是什么?
- 堆的生长方向是从低地址向高地址生长的,而栈是从高地址向低地址生长的。
实际上堆可以申请到的内存空间比栈要大很多,在 linux 的 4G 的虚拟内存空间里最高可以达到 2.9 G 的空间。

参考文章 https://www.anquanke.com/post/id/163971
什么是堆溢出?
堆溢出(Heap Overflow)是指在程序执行过程中,向堆(Heap)区域写入超出其分配内存边界的数据,因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。。这可能导致程序崩溃、执行意外行为或者被攻击者利用。
关键步骤
参考文章 https://ggb0n.github.io/2020/06/01/%E5%A0%86%E6%BA%A2%E5%87%BA%E5%9F%BA%E7%A1%80/
1.寻找堆分配函数
通常来说堆是通过调用glibc函数malloc进行分配的,在某些情况下会使用calloc分配。calloc与malloc的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的。
2.寻找危险函数
通过寻找危险函数,我们快速确定程序是否可能有堆溢出,以及有的话,堆溢出的位置在哪里。常见的危险函数如下:
| 输入 | 输出 | 字符串处理函数 | 
|---|---|---|
| gets | sprintf | strcpy,字符串复制,遇到'\x00'停止 | 
| scanf | strcat,字符串拼接,遇到'\x00'停止 | |
| vscanf | bcopy | 
3.确定填充长度
这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离。
先放入IDA里面进行静态分析
程序初始化堆数据:
序在堆上开辟了两个大小为 5 字节的内存块。
input_data指向“pico”
safe_var指向“bico”
| 1 | input_data = malloc(5); | 
功能菜单:
选项 1:打印堆内容(查看地址和内容)
| 1 | printf("%p -> %s\n", input_data, input_data); | 
选项 2:向 input_data 写入字符串
scanf(“%s”, input_data);
input_data 指向的 buffer 只有 5 字节(包括末尾 \0),但是 scanf(“%s”) 不限制输入长度,这就导致了 堆溢出。
选项3:打印safe_var上的变量
选项4:输出flag
| 1 | case 4: | 

堆溢出覆盖临近变量的值,接下来要确定填充长度
使用gdb进行调试计算从input_data到safe_var的偏移
 
选择菜单 2,输入32字节的字符串
用你控制的字符 覆盖 safe_var 指向的内容为 "flag"
然后选择菜单 4,调用 check_win() → 打印 flag!
 
2.format string 0
格式化字符串函数:格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式
| 函数 | 基本介绍 | 
|---|---|
| printf | 输出到stdout | 
| fprintf | 输出到指定FILE流 | 
| vprintf | 根据参数列表格式化输出到 stdout | 
| vfprintf | 根据参数列表格式化输出到指定 FILE 流 | 
| sprintf | 输出到字符串 | 
| snprintf | 输出指定字节数到字符串 | 
| vsprintf | 根据参数列表格式化输出到字符串 | 
| vsnprintf | 根据参数列表格式化输出指定字节到字符串 | 
| 格式化字符 | 说明 | 
|---|---|
| %d | 输出十进制整数 | 
| %s | 从内存中读取字符串 | 
| %x | 输出十六进制数 | 
| %c | 输出字符 | 
| %p | 指针地址 | 
| %n | 到目前为止所写的字符数 | 
| %hhn | 写1个字节 | 
| %hn | 写2个字节 | 
| %ln | 写4个字节 | 
| %lln | 写8个字节 | 

main()的作用 :
- 打开 - flag.txt,读取 flag 到全局变量- flag中;
- 注册了一个 signal handler 处理段错误( - signal(11, sigsegv_handler));
- 调用 - serve_patrick()。 
用户输入推荐的“汉堡名”。如果输入的字符串在菜单中存在,程序会直接将你的输入当成格式化字符串传给 printf
如果你输入的字符串中包含了 % 字符,**printf(format) 就会当格式化字符串解析它!**
serve_patrick() — 第一关
如果你输入的是 Gr%114d_Cheese,就能通过 on_menu 检查。接着它会把你的输入 当作格式化字符串 传入 printf
printf(“Gr%114d_Cheese”);
%114d 是一个合法的格式符,printf 会试图从栈中读取整数值,但你没有提供这些值,它会打印垃圾值或导致崩溃。
serve_bob() — 第二关
必须要输入菜单选项里的其中一个,输入Cla%sic_Che%s%steak,会把你的输入 当作格式化字符串 传入 printf
printf(“Cla%sic_Che%s%steak”);
%s 会从栈上读取地址,但没提供参数,一旦读到 NULL 或非法地址,会触发 SIGSEGV,这会调用你在 main() 中注册的段错误处理函数 sigsegv_handler,sigsegv_handler() 中会打印 flag。



