本文的目标是,记录一些不具备通用性的,或者比较进阶的题目。之前的另一篇文章则用于记录一些基础知识和通用性较强的基本手法。
文章目录
- 跨科题目
- buu fungame:reverse与pwn的结合
- reverse+web
- 反跟踪
- Easyhook:hook例题
- vm类型总结
- 一道稍特殊的vm:js
- angr进阶
- 例题:华为杯第一届研究生网络安全创新大赛 infantvm
- angr的能力范围
- 花指令
- push+ret=jmp
- call+pop=jmp
- 其它patch时常见的花指令
- 双进程保护
- mfc
- tea模板
- rc4模板
- 文件patch把代码写到哪
- 附录
- python循环移位
- 黑盒穷举
- 2023 ciscn感想
跨科题目
buu fungame:reverse与pwn的结合
从please input开始分析,是一个基本的字符串异或。输入后,程序退出。总感觉少了点啥,怀疑有其它代码偷偷执行了。
浏览了一下左边的函数,感觉是用户函数,但不知道是什么时候调用的。注意到一个b64串,解出来后是also pwn,这其实算是比较明显的提示了。
关键函数是下面这个,copy了两次,两个dst的区别就是容量。src的长度为16,此处存在栈溢出。
int __cdecl sub_4013BA(char *Source)
{
char Destination[12]; // [esp+1Ch] [ebp-Ch] BYREF
strcpy(Destination, Source);
strcpy(x, Source);
return 0;
}
reverse+web
例题非常简单,就是创建client、然后把flag发送到对面。如果按rev的思维进行调试(看内存、或插入print语句),做起来非常不顺。可以用“nc -l -p 端口号”监听本机端口,然后再尝试发起连接即可。
反跟踪
Easyhook:hook例题
攻防世界EASYHOOK。本题将writeFile hook为加密函数+writeFile。
本题的流程是:
- 用getProcAddress从指定的DLL(第一个参数,可以来源于loadLibrary)检索被hook函数的地址(第二个参数)。
- 保存被hook函数地址原来的内容(一般为jmp xxx)。
- 计算自定义函数地址和被hook函数地址的差。
- 将被hook函数地址的内容改为“jmp 上一步计算的差”。具体的操作是GetCurrentProcessId、OpenProcess、VirtualProtectEx、WriteProcessMemory。
- 自定义函数中往往会包含被hook函数(这样比较隐蔽),采用自定义逻辑-去除hook-被hook函数的结构。如果不去除hook,会死循环自定义逻辑。去除hook的具体操作是将先前保存的原内容写回,写回的api还是上述四个。
hook可以避免关键函数显示在调用图里。用户函数较少时,都看一遍就能找到关键函数了。保护作用有限。只是做题的话,只需要关心加密函数、check函数等,看见异或或者求模基本八九不离十了。
vm类型总结
vm的题目,第一步angr,第二步找指令序列。然后是指令翻译。
例题:GWCTF2019 babyvm,
vm的好处就是隐藏实际执行的代码,所以假flag是比较常见的,看见没有执行的关键函数,也要理所当然认为它是执行了的。跑angr的时候有一个实际问题,就是ida地址和实际地址的不同。目前的归纳经验是以0x400000作为base(elf 64符合此规律)。
找指令序列的时候有一个实际问题,怎么找?目前的归纳经验是:
- 寻找data段的长数据。夹杂了一些0的字节序列,并且为了方便,指令的编码通常是连号的(比如0xE1表示加法,0xE2表示减法)。
- 其所在函数的结构往往是,将eip设为指令序列起始地址,循环体{单指令函数}(退出条件是eip=终结指令,终结指令会在指令序列的末尾)。
- 单指令函数中,往往会出现将内存作为函数来执行的代码。其逻辑可能是遍历函数表,判断eip指令与当前函数表表项是否相同,相同则执行对应函数。
指令翻译的时候,也有一些归纳经验。
- 寄存器和内存之间需要有数据交流,也就是读写函数。寄存器往往不止一个。读写函数可能会全部封装在一起。
- 具体被执行的函数里,往往会有一句eip自增。这个可以印证eip所在内存位置的判断,也可以根据自增量来判断指令的长度。
一道稍特殊的vm:js
例题是攻防世界secret string 400。
js的背景知识:
- 原型。我个人理解有些像父类。属性/方法不存在时,会去原型里找,还找不到就去原型的原型里找。
- Function(str)。这样可以创建一个callable对象。比如f = Function(“alert(1);”),然后f()就可以调用了。
这题第一关是解压(虽然不解压也能看见格式混乱的代码)。教训是遇到未知bin文件先解压。
js有个特殊的地方,就是能进行源码级别的编辑。做js题,找到源码就是成功了一大半。在源码中加入console.log,然后用浏览器运行。不动调很难做出。
对这一题来说,可以看见虚拟机运行的每一条指令(本题是通过eval(字符串)和Function(字符串)执行自定义函数,所以可以在执行前console log),这使得它不那么像一道虚拟机题了。
angr进阶
angr的最简单使用一般的wp都会有,一般是二十行代码左右。
我认为,angr理论上可以媲美ollydbg,至少是linux elf的ollydbg。但实际使用中,其泛用性似乎远远不如od。angr失败的原因是什么?
除了环境/架构支持问题,主要是两个,一个是状态爆炸,另一个是信息不足。
状态爆炸的体现,就是一直不出结果。
信息不足的体现,就是一堆warning,然后很快提醒你not found。
这两个问题,其实是有解决方法的。局部分析可以避免状态爆炸;利用gdb提供context可以解决信息不足。
“扫描gdb context然后自动生成Angr模拟的状态空间,这样子就可以绕开不重要的、但是Angr会受卡的代码,从某个break stop开始,将变量或者内存符号化,然后使用Angr的SimulatorManger进行单步模拟或者explore探索。配合Instrumentation && hook机制,可以在模拟执行的时候从不同的level搜集想要获得的dynamic trace。只有能够自由地在模拟执行中操控(增删改查)expr和constraint,才算真正入门Angr。”
CLE负责装载二进制对象以及它所依赖的库,生成地址空间。
claripy是经angr封装后的z3,可以单独拿出来当z3用。
PyVex是中间语言,Angr使用Valgrind的中间语言——VEX来完成这方面的内容。VEX中间语言抽象了几种不同架构间的区别,允许在他们之上进行统一的分析。SimuVEX模块是中间语言VEX执行的模拟器。
API使用概览可以看https://www.cnblogs.com/murkuo/p/15316469.html。
接下来是github的jakespringer/angr_ctf上的十几道例题(dist目录下是题目,solution目录下有答案)。代码我就不贴了,15道rev题的代码都很有参考价值。
00是find的使用,也是网上大多数题解的模样。需要知道成功代码的地址。
01是avoid的使用。avoid用于剪枝。例题里重复执行了很多代码。需要知道导致失败的代码的地址。
02是condition,用于不知道成功/失败代码地址的情况。此外,我认为返回布尔值的函数有很大的自定义空间,而不只是判断“标准输出内容是否等于成功/失败字符串”。
03是手工指定register,使用blank state(而非entry state)来实现任意地址(解题的话一般是用户输入之后)开始执行。blank state一定要指定fill option。例题是一个多输入的题目。
04是栈修复。与03并列,都是对initial state进行自己的设置,03设置initial_state.reg.寄存器名,04设置initial_state.stack。起始地址的选择很重要,应选择在scanf执行完毕、调用者退栈之后。栈修复主要用于输入内容被存储在栈中的情况(严格来说,是初次访问输入时,是通过栈取的)。05的输入内容被存储在常量地址,可以设置initial_state.memory。06的输入内容被存储在malloc的动态地址,也是设置initial_state.memory,但有点绕,因为要先往模拟的指针地址存入指针,然后往指针存入符号变量。
07与之前不同。之前是标准输入。07则是从文件读入。因此需要用angr来模拟一个与代码中fopen内同名的文件。然后从fopen开始执行。
01-07解决的主要是自定义起点时,initial state信息不足的问题,最重要是那个填充选项。从此可以让分析从关键的地方开始。
08-12解决的是状态爆炸问题。08自定义终点(一般定在对用户输入的所有处理结束之后),然后添加一个“处理后的用户输入等于内置结果”的约束。09则利用hook替换中间的某个函数(实际实现相当于范围patch,有时patch掉一个函数call,有时patch掉几行),前提是理解了这个函数的作用,hook应当起到人工给angr减负的作用。10也是hook,但不是汇编行级hook,而是符号级hook(也可以理解为,angr模拟了函数),这是为了应对需要patch的地方过多的情况。11是模拟scanf函数。
12讲了angr的一个选项,veritesting。Veritesting结合了静态符合执行与动态符号执行,减少了路径爆炸的影响。
13则是用angr自己实现的库函数来hook本来的库函数,从而加速执行。还给了一个可替换的库函数的总列表,srand,java的,非常强大。这题的另一个启示是,静态编译能增加函数数量,从而些许增加逆向难度。
14是用call state来对so进行模拟执行。
至于15-17,算是pwn。
例题:华为杯第一届研究生网络安全创新大赛 infantvm
我用网上的angr模板试过,当时结果是无解。在wp里居然看见了angr。多番尝试,我发现了玄学。
如果发现有解,但解明显不是flag(比如\x00),这个时候一定要试,狠狠试。一样的代码也能跑出不同的结果。目前可以确认的是,跟flag长度关系不大(因此可以设成50),跟veritesting也没有关系(开不开都有可能出)。
import angr
import sys
import claripy
path_to_binary = r'C:\Users\ysnb\Downloads\infantvm' # :string
project = angr.Project(path_to_binary)
# start_address = 0x080492DC
initial_state = project.factory.entry_state(
# addr=start_address,
add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
class ReplacementScanf(angr.SimProcedure):
def run(self, format_string, scanf_address):
scanf = claripy.BVS('scanf', 50*8)
self.state.memory.store(scanf_address, scanf, endness=project.arch.memory_endness)
self.state.globals['solution'] = scanf
scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())
simulation = project.factory.simgr(initial_state, veritesting=True)
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good job' in stdout_output # :boolean
def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again' in stdout_output
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found:
solution_state = simulation.found[0]
stored_solution = solution_state.globals['solution']
solution = solution_state.solver.eval(stored_solution)
print(solution)
from Crypto.Util.number import long_to_bytes
# print(long_to_bytes(solution))
print(long_to_bytes(solution).decode()[::-1])
else:
raise Exception('Could not find the solution')
angr的能力范围
从之前的题目中知道,常量异或肯定能解,
base64能解吗?base58能解吗?
md5能解吗?aes能解吗?
有些情况解决不了,比如:
- 预期输入的状态数很多,比如输入的flag是字符画
- 找不到终点(check函数的位置,或者正确提示字符串),比如有壳程序
花指令
keypatch插件可以搜索指定的超过一行的汇编片段。这对花指令分析或许有用。
需要注意的是,如果jmp $+1,实质作用也就成了nop。因此所有的jmp花指令都可以改写为nop花指令。
push+ret=jmp
牢记ret就是pop eip。
例题:ISCTF2022 final。动调的时候会发现,程序通过 “有效代码 + push下一处有效代码位置 + ret” 的方法实现控制流的跳转,每次只执行一句汇编。
这种写法会影响静态分析。解决的方法是,把所有的有效代码 + push + ret组合都patch成有效代码 + far jmp(0xEA)。
自定义patch的时候注意,如果patch成0x90有问题,可以试试patch成0x00。
call+pop=jmp
牢记call就是push+jmp。
判断一个call是不是花指令,主要看操作数。如果是一个在附近的地址,多半是花指令。
例题:ISCTF2022 simpleflower。这里立下一个原则,就是如果超过三处,就应当写脚本来patch了,而不是手改。此处留下一个模式匹配patch的idc代码。
#include <idc.idc>
static main()
{
auto i,j,from,size;
from=0x00407000; //起始地址
size=0x0040F201-0x00407000;//扫描数据块大小
for ( i=0; i < size;i++ ) {
//查找 EB 03 C3 CD 03 ,替换90
if ((Byte(from)==0xeB)&&(Byte(from+1)==0x03)&&(Byte(from+2)==0xC3)&&(Byte(from+3)==0xCD)&&(Byte(from+4)==0x03))
{
for(j=0;j<5;j++)
{
PatchByte(from,0x90);
from++;
}
continue;
}
//查找 E8 01 00 00 00 E9 ,替换90
if ((Byte(from)==0xe8)&&(Byte(from+1)==0x01)&&(Byte(from+2)==0x00)&&(Byte(from+3)==0x00)&&(Byte(from+4)==0x00)&&(Byte(from+5)==0xE9))
{
for(j=0;j<12;j++)
{
PatchByte(from,0x90);
from++;
}
continue;
}
//查找 E8 00 00 00 00 call $+5,替换90
if ((Byte(from)==0xe8)&&(Byte(from+1)==0x00)&&(Byte(from+2)==0x00)&&(Byte(from+3)==0x00)&&(Byte(from+4)==0x00))
{
for(j=0;j<17;j++)
{
PatchByte(from,0x90);
from++;
}
continue;
}
from++;
}
Message("\n" + "OK\n");
}
本题的花法是:
call $+5 (对应机器码为E8 00 00 00 00)
pop ebp
这两条指令啥都没做,不过会影响静态分析。
下面这五条同样等效于nop,对应上面idc代码的第三种。
call $+5
xchg eax, ds:[esp]
lea eax, [eax+0Ch]
xchg eax, ds:[esp]
retn
下面这几条同样等效于nop,对应上面idc代码的第一种。这里需要注意,jmp 常数是相对于jmp指令尾部而言的。下面的ret和int 3会被直接跳过。
EB是short jmp,后面操作数只有一字节。EB 03 xx xx xx中无论xx是什么,都会被跳过。
E9是near jmp,后面操作数为两字节。
EA是far jmp,后面操作数为四字节。
short jmp 3
ret
int 3
伪代码中如果出现以下代码,可能也是nop:
interlockedExchange(a,b) 能以原子操作的方式交换俩个参数a, b,并返回a以前的值。
__writeeflags() 将指定的值写入程序状态和控件 (EFLAGS) 寄存器。就是pushf。
__readeflags() 读取程序状态和控件 (EFLAGS) 寄存器。就是popf。
其它patch时常见的花指令
jz和jnz同一个地方;
E8被识别为call;
[addr处] jmp addr+1;
双进程保护
这里不是指“防止关闭”。这里指的是软件作者自己写了一个调试器“占坑”,用父进程调试子进程,以阻止破解者调试、并利用异常处理的路径分叉(调试进程和被调试进程都可能是处理者)来增加跟踪难度的行为。“双进程保护”这个名字来自《加密与解密》。
这类题的标志性特征就是createprocess并使用了DEBUG_ONLY_THIS_PROCESS
(0x00000002)或者DEBUG_PROCESS(0x00000001)的creation flag。代码的结构可以分为“简易调试器”和“被调试代码”两部分。可以先搜索一下“windows简易调试器的实现”,能获取一些必要的背景知识。
解决的思路仍然是用nop和jmp回避整块简易调试器的代码(不能让这个调试器启动起来,否则就无法动调),但不止于此。因为调试进程并不是什么都没做的,调试进程会处理异常。出题人会故意在汇编中制造一些异常,比如:
- 0xC0000094 Integer division by zero
- 0xc000001d Illegal Instruction
对于前者,一种处理异常的方法是EIP+=2(div指令长度为2,也就是直接下一条的意思)。
对于后者,也可以写出类似的异常处理(例题中为eip+=3)。
由于出题人的调试器会妥善处理这些异常,所以直接运行时不会有问题。这就导致了一个两难局面:不去掉出题人的调试器,无法动调;去掉出题人的调试器,无法F9。
如果去掉出题人的调试器后直接拿自己的调试器F9,会不断停在出题人设置的异常(比如int 3和div)上,需要你经常set ip、反复去patch。
如果想让题目更难的话,可以用更复杂的异常处理方式(而不只是eip增),也可以在父进程(调试进程)中增加更复杂的、有意义的内容,以增加直接nop掉的成本(破解者需要模拟执行这些内容后,才能毫无顾虑地nop)。
mfc
如果有输入框,通过resource hacker查找输入框的id(所得id为10进制),在ida中搜索对应十六进制id,就能找到getText,从而找到check函数等关键位置。
如果有按钮,xspy对button按钮进行检测,检测出id之后,对整个MFC窗口进行检测,就能用id查到点击按钮后触发的函数(下图的onCommand)。
如果没有提供上述交互(输入框、按钮),看看有没有自定义消息(如下图)。可以自己写代码来发送这个msg来进行交互。
// 1.cpp : 定义控制台应用程序的入口点。
//注意是VS的MFC运行。所有的MFC 的.cpp文件第一条语句都是#include“stdafx.h”.
#include "stdafx.h"
#include<stdio.h>
#include<string.h>
#include "windows.h"
int main()
{
HWND h = FindWindowA("944c8d100f82f0c18b682f63e4dbaa207a2f1e72581c2f1b",NULL);
//HWND h = FindWindowA(NULL, "Flag就在控件里");
//HWND h = FindWindowA("944c8d100f82f0c18b682f63e4dbaa207a2f1e72581c2f1b", "Flag就在控件里");
//这里用到两个关键函数,一个是获取窗口句柄函数,第二个就是根据句柄发送消息函数。获取句柄的FindWindowA中第一个可以传入类名,第二个可以传入标题,因为我们两个都有,所以任意一个都可以锁定程序窗口。
if (h)
{
SendMessage(h, 0x464, NULL, NULL);
//发送函数中第二个是区别其他消息的常量值,这里题目用了自定义常量值,所以我们要对应一致。
}
getchar();
return 0;
}
tea模板
特征就是移位、两部分互相运算。
from libnum import *
l = [2936232810,479881757,1187579365,1870219775]
keys = [2232106884, 443933925, 887867854, 2475959571, 656951854, 2429565456, 564163628, 1528392103, 3056784222, 3676191895, 3057416514, 3711600247, 3128233222, 3382368592, 2469769916, 868161824, 1736323680, 2744845574, 1194723888, 2480028295, 665089334, 2885264336, 1475561420, 1027060899, 2054121846, 3361673682, 2428380120, 2845300551, 1395633862, 846084946, 1692169952, 272236806, 544473676, 425754791, 851509650, 439900147, 879800366, 2024180243, 4048360562, 3369892084, 2444816952, 3765763143, 3236559074, 858681714, 1717363516, 1683076390, 3366152872, 3820967107]
mask = 0xffffffff
def rol(a, b):
return ((a << b) & mask) | (a >> (32-b) )
def ror(a, b):
return ((a << (32-b) ) & mask) | (a >> b)
def dec(a, b):
aa = a
bb = b
for i in range(24):
bb += 0x100000000
bb -= (rol(aa, 19) + keys[47-i*2] ) ^ rol(aa, 7)
bb &= 0xffffffff
aa += 0x100000000
aa -= (rol(bb, 7) + keys[47-i*2-1] ) ^ rol(bb, 19)
aa &= 0xffffffff
return n2s(aa)[::-1] + n2s(bb)[::-1]
flag = b''
for i in range(0, len(l), 2):
flag += dec(l[i], l[i+1] )
print('flag{' + flag.hex() + '}')
rc4模板
rc4特征就是box[(box[x] + box[y]) % 256]),非常有辨识度。
下面代码对应的题目是这样的,获取系统分钟和秒钟作为密钥,所以解题时穷举3600次密钥即可。
如果题目更简单一些,密钥大概率就是个常量字符串了。
from hashlib import sha256
def rc4(data, key):
x = 0
box = list(range(256))
for i in range(256):
x = (x + box[i] + (key[i % len(key)])) % 256
box[i], box[x] = box[x], box[i]
x = y = 0
out = []
for char in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
out.append(chr((char) ^ box[(box[x] + box[y]) % 256]))
return ''.join(out)
enc = open('flag.txt.enc', 'rb').read()
for i in range(60*60):
k = sha256(str(i).encode()).digest()
d = rc4(enc, k)
if 'flag{' in d:
print(d, i)
文件patch把代码写到哪
有人说,401000,直接写在text段开始。
对于linux elf,常见于awd的patch,可以写在eh_frame段(似乎gcc编译就会有这个段)。
附录
python循环移位
def ror8(inte, bit):
if inte != inte & 0xff:
print('input warning')
inte = inte & 0xff
part1 = inte >> bit
part2 = (inte << (8-bit)) & 0xff
return (part1 | part2) & 0xff
黑盒穷举
import subprocess
binary = '/home/rookie19/Desktop/mx'
ctr = 0
for ch1 in range(20, 128):
# for ch2 in range(20, 128):
# cand = 'flag{' + chr(ch1) + chr(ch2)
cand = 'flag' + chr(ch1)
obj = subprocess.Popen(["qemu-mips", binary], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
obj.stdin.write(str.encode(cand + '\n'))
# obj.stdin.flush()
obj.stdin.close()
response = obj.stdout.readline()
obj.stdout.close()
response = response.decode()
# print(response)
# ctr += 1
# if ctr % 100 == 0:
# print(response)
# if response[0] == '│':
# print(cand)
# exit(0)
2023 ciscn感想
题目形式很新。可信计算(cry)、量子通信(cry)、目标检测对抗样本生成(misc)。逆向有一道berkley snap少儿编程。
ctf常常需要现场学习的能力。比如逆向出一道小众语言;或者像这次的crypto一样,附件里塞一个很长的讲BB84的word文档;抑或是awd现场搜特定版本cms的漏洞;或者像这次的misc一样,现场搜目标检测对抗样本论文。
ctf也需要灵活的头脑。比如给了shell的题目有时有非预期解;利用好flag格式有时可以直接穷举;awd中可以偷窃别人的攻击流量进行重放。