请注意,本文编写于 1443 天前,最后修改于 1438 天前,其中某些信息可能已经过时。
这里是新生赛出题的思路,题解与复现专栏。主要是写一下自己的想法,与学到的一些东西,和复现一下PWN题。本次比赛我主要负责了一道密码学与三道逆向题目。学弟们太强了,差点把我底裤给我撕了,差一点点就被打穿了,但是我觉得被打穿只是时间问题,要是最后一道题目再稍微的早一点点放出,我应该现在已经在裸奔了。本文将分为三个部分,第一部分题目思路+WP。第二部分出题时学习到的东西,第三部分PWN的复现(持续更新)。师傅们可以选择食用。
一道来自空指针公开赛的原题,具体传送门在这里。我对原本脚本进行了一定的魔改,但是并没有更改主要逻辑。删除了原先脚本的VSS加密逻辑,将其中的知识点直接进行了抽象。具体的考点就如同题目描述的一样,伪随机数的爆破,利用二维码的白边,获得被泄露出的随机数生成序列还原本来的种子生成序列。最终完成对图像的解密,具体的WP解密脚本如下:
from PIL import Image
from randcrack import RandCrack
import random
share = Image.open('Secret.png')
width = share.size[0]//2
res = Image.new('L', (width, width))
print(width)#width=444由于加密使得图片扩容,即一个像素生成2*2=4个像素
bits = ''#由于share2的构成的逻辑是当原图片是255像素
for idx in range(width*width-624*32, width*width):#取倒数624*32个数据
i,j = idx//width, idx % width#i,j分别代表行和列
#print(i,end=" ")
#print(j)
if share.getpixel((2*j, 2*i)) == 255:#取最小的一个像素点代表此处的方阵
bits += '0'
else:
bits += '1'#bits即为初始化的字符串
rc = RandCrack()
for i in range(len(bits), 0, -32):
rc.submit(int(bits[i-32:i], 2))#每次将32位放入初始化的rc一共放入624组,达到初始化最少数据
flipped_coins = [int(bit) for bit in bin(rc.predict_getrandbits(width*width-624*32))[2:].zfill(width*width-624*32)] + list(map(int, bits))#通过初始化数据与脚本猜测剩余随机数
data = []
for idx in range(width*width):
i, j = idx//width, idx % width
if share.getpixel((2*j, 2*i)) == 255:
data.append(0 if flipped_coins[idx] else 255)
else:
data.append(255 if flipped_coins[idx] else 0)
#if flipped_coins[idx]:
# data.append(255 if share.getpixel((2*j, 2*i)) == 255 else 0 )
#else:
# data.append(0 if share.getpixel((2*j, 2*i)) == 255 else 255 )
res.putdata(data)
res.save('ans.png')
还是希望大家能学会这种爆破的方法。
这道题目我是没想到解答数会比python字节码少的,一开始沟通的,我是想把这道题目放在签到题目中的。本题的主要想传达的一种逆向的思想,就是“七分靠猜,三分靠看”,不要被一些恶心的东西所迷惑。
这道题目很抱歉,出现了一小点点小问题,是我在看师傅们WP时候发现的,由于最后还原数组没有注意,导致少了一个字符,第一位flag字符没有卡住,出现了不是C也可以通过check的问题,我很抱歉,已经对题目文件进行了修改。
本题的主要逻辑如下:
这里是两次函数,这两个函数实际上是互为反函数的,一个数经过原先两个数处理后其实是不变的。所以整个解密逻辑只需要过两次异或即可。
这道题目的灵感是来自我高数的复习的时候,一开始我是想在其中加入反动调的,但是被你子洋师傅制止了。快感谢子洋师傅,这里可以使用动态调试观察输入与输出的结果,从而得到两次函数加密后没有被改变的结论。
当然,由于这个程序并没有对flag进行严格的密码学处理,导致各个flag位是独立的,也可以选择强行模拟程序通过过爆破的手段对flag进行爆破。
无论是哪种方法,都体现出来猜的思想,有些时候我们就需要大胆的去猜,毕竟猜错也不会掉一块肉。所以我觉得这道题目应该会比较的简单,可能放在入门区会有更多的师傅们做出来。
这道题目应该会比较的恶心,但是解题数居然会比上一题多是我没想到的。本题的想法主要是来自上次比赛周学长的题目,上次比赛认真复现的同学们会知道,当时的题目逻辑是一颗树,并且汇编语言不是x86架构的,导致IDA并不能反汇编。所以本次采取了python字节码的方式模拟了一棵树。鉴于大多数同学并没有接触过算法,所以我并没有选择排列树等比较复杂的树,而是根据flag的顺序构造出了一颗左枝非常非常长的盗版树最后采取了中序遍历的方式输出了整棵树,简化了整个程序的逻辑降低难度。主要目的是考察一下大家对一种自己不熟悉的语言的一种快速掌握能力。这里给出python的源码,大家可以对着进行一下复现。
def main1():
class Node:
def __init__(self, data):
self.left = None #左节点
self.right = None #右节点
self.data = data #值
def PrintTree(self):
if self.left:
self.left.PrintTree()
print(chr(self.data),end="")
if self.right:
self.right.PrintTree()
def insert(self,data):
if self.data:
if self.left is None:
self.left = Node(data)
elif self.right is None:
self.right = Node(data)
else:
self.left.insert(data)
def inorderTraversal(self, root):
res = []
if root:
res = self.inorderTraversal(root.left)
res.append(root.data)
res = res + self.inorderTraversal(root.right)
return res
str1=input("PLZ input flag:")
#str1="cumtctf{1e70a305fb378202c26dd6dafa14db17}"
flag=[ord(i) for i in str1]
root = Node(flag[0]) #创建节点
for i in range(1,len(flag)):
root.insert(flag[i])
F=root.inorderTraversal(root)
flag="7b}41ada16fdd2d262c70b8533f00ea{7t1tfuccm"
YES=1
for i, element in enumerate(F):
if ord(flag[i])!=element:
YES=0
print("wrong")
break
if YES:
print("wow~you are right!")
本题是裤衩题,开玩笑的呀,本题是一个游戏。很多时候我也会再想逆向手到底能干啥,制作游戏外挂就是一个方向。所以我找到了一个很老的很老的游戏,又不能让大家直接使用网上的现成的外挂直接出结果,所以我为大家规定了特定的触发flag条件,主要就是希望大家能真正的体会到身为一个逆向手可以掌控程序的快乐。本题属于一个沙盒题目,没有固定的解法,这里给出几个我想到的解法,当然不限于这些解法。主要的目的就是希望大家玩的开心,享受RE的快乐。具体题目的实现我会放在第二部分,有兴趣的同学可以看一下,毕竟这个知识我也是现学现卖,如有不足的地方,请多包涵。
解法1:
解法2:
解法3:
首先这道题目在出题方面上最难得就是他没有源码,所有的操作与修改必须在汇编层面上实现(自己给自己整了一道逆向题目)。所一整个程序我必须十分的熟悉。首先需要确定自己的出题思路,由于整个程序有很多关,而且每关有很多小关,要是以通关为flag判断条件一定会存在一些不必要的麻烦,比如在小关切换时就触发flag弹出条件,使整道题目丧失他的意义(非预期)。所以在斟酌后我选择了以球的数目为判断通关条件,flag形式由shellcode注入的弹窗实现。
由于整个程序读写权限分明,我必须选择有执行权限的段区植入shellcode,所以找到一块代码空间就成为了下一个目标。由于整个程序不一定全部会被使用,所以一定存在可以利用的代码段。最终我选择了这里。
这里是道具选择的一个case,一共有20个case跳转,假若我将所有道具的图标绑定在一个图片上,其余的空间就变成了无用代码,我们可以进行利用。本题的shellcode就被注入到了这个地方,同时在主程序中所有的图表都产生了变化---死亡图标,无形中又减少了直接靠技术通关的可能性。
由于每次程序启动的时候地址都是不确定的,所以MassageBox的函数地址也是不能被确定了,所以我们除了需要给MassageBox传参,还需要确定它的函数地址。这里我们需要使用到一个动态链接库----动态定位kernel32.dll
不同版本的操作系统,kernel32.dll的基地址也是不同的。Windows没有linux那样方便的中断机制来调用系统函数,所以只能通过基址+偏移地址来确定函数的位置。大概流程是通过FS段选择器找到TEB,通过TEB找到PEB,然后获取kernel和ntdll的地址。
具体可参考
在确定kernel32.dll后,我们就可以是使用他对windows的API进行查找了。Massageboxs在user32.dll中,而我们并不确定这个动态库是否被程序加载,所以我们需要使用LoadLibrary对user32.dll进行加载。
在加载相应的库之后,我们需要确定我们需要函数的API地址,我们可以使用查找的方式遍历库来实现。但由于每个函数名字都很长,对比起来需要占很大的空间,导致我们的shellcode变得十分的长,所以我们需要使用hash索引的方式,简化比较。我们可以先通过程序计算哈希值。
#include <stdio.h>
#include <windows.h>
DWORD GetHash(char *fun_name)
{
DWORD digest = 0;
while (*fun_name)
{
digest = ((digest << 25) | (digest >> 7));
digest += *fun_name;
fun_name++;
}
return digest;
}
int main()
{
DWORD hash;
hash = GetHash("LoadLibraryExA");
printf("0x%.8x\n", hash);
getchar();
return 0;
}
当然这里的算法不是固定的,只要能保证这里面算出来的值和你shellcode中对应的值相等即可,我们把他提取出来,我们接完成了准备工作。
对于shellcode的编写我们可以使用C中的asm,直接编写汇编代码,随后在OD中提取相应的字节码,直接注入到程序内即可。注意函数调用的规则等,不要让程序崩溃。
这里是shellcode编写代码:
#include <stdio.h>
#include <windows.h>
int main()
{
__asm {
// ===将索要调用的函数hash值入栈保存
CLD // 清空标志位DF
push 0x1E380A6A // 压入MessageBoxA-->user32.dll
push 0x4FD18963 // 压入ExitProcess-->kernel32.dll
push 0x0C917432 // 压入LoadLibraryA-->kernel32.dll
mov esi, esp // 指向堆栈中存放LoadLibraryA的地址
lea edi, [esi - 0xc] // 后面会利用edi的值来调用不同的函数
// ===开辟内存空间,这里是堆栈空间
xor ebx, ebx
mov bh, 0x04 // ebx为0x400
sub esp, ebx // 开辟0x400大小的空间
// ===将user32.dll入栈
mov bx, 0x3233
push ebx // 压入字符'32'
push 0x72657375 // 压入字符 'user'
push esp
xor edx, edx // edx=0
// ===查找kernel32.dll的基地址
mov ebx, fs: [edx + 0x30] // [TEB+0x30] -> PEB
mov ecx, [ebx + 0xC] // [PEB+0xC] -> PEB_LDR_DATA
mov ecx, [ecx + 0x1C] // [PEB_LDR_DATA+0x1C] -> InInitializationOrderModuleList
mov ecx, [ecx] // 进入链表第一个就是ntdll.dll
mov ebp, [ecx + 0x8] //ebp = kernel32.dll 的基地址
// === hash 的查找相关
find_lib_functions :
lodsd // eax=[ds*10H+esi],读出来是LoadLibraryA的Hash
cmp eax, 0x1E380A6A // 与MessageBoxA的Hash进行比较
jne find_functions // 如果不相等则继续查找
xchg eax, ebp
call[edi - 0x8]
xchg eax, ebp
// ===在PE文件中查找相应的API函数
find_functions :
pushad
mov eax, [ebp + 0x3C] // 指向PE头
mov ecx, [ebp + eax + 0x78] // 导出表的指针
add ecx, ebp // ecx=0x78C00000+0x262c
mov ebx, [ecx + 0x20] // 导出函数的名字列表
add ebx, ebp // ebx=0x78C00000+0x353C
xor edi, edi // 清空edi中的内容,用作索引
// ===循环读取导出表函数
next_function_loop :
inc edi // edi作为索引,自动递增
mov esi, [ebx + edi * 4] // 从列表数组中读取
add esi, ebp // esi保存的是函数名称所在的地址
cdq
// ===hash值的运算过程
hash_loop :
movsx eax, byte ptr[esi] // 每次读取一个字节放入eax
cmp al, ah // eax和0做比较,即结束符
jz compare_hash // hash计算完毕跳转
ror edx, 7
add edx, eax
inc esi
jmp hash_loop
// ===hash值的比较函数
compare_hash :
cmp edx, [esp + 0x1C]
jnz next_function_loop // 比较不成功则查找下一个函数
mov ebx, [ecx + 0x24] // ebx=序数表的相对偏移量
add ebx, ebp // ebx=序数表的绝对地址
mov di, [ebx + 2 * edi] // di=匹配函数的序数
mov ebx, [ecx + 0x1C] // ebx=地址表的相对偏移量
add ebx, ebp // ebx=地址表的绝对地址
add ebp, [ebx + 4 * edi] // 添加到EBP(模块地址库)
xchg eax, ebp // 将func addr移到eax中
pop edi // edi是pushad中最后一个堆栈
stosd
push edi
popad
cmp eax, 0x1e380a6a // 与MessageBox的hash值比较
jne find_lib_functions
// ===下方的代码,就是我们的弹窗
function_call :
xor ebx, ebx // 清空eb寄存器
push ebx
mov eax,0x151BAA25
xor eax,0x68699916
push eax
mov eax,0x4E7E413A
xor eax,0x20172654
push eax
mov eax,0x13481527
xor eax,0x20172654
push eax
mov eax,0xFA330061
xor eax,0x88567652
push eax
mov eax,0x1A36A911
xor eax,0x68699920
push eax
mov eax,0x595AF15B
xor eax,0x68699920
push eax
mov eax,0x51DF754
xor eax,0x63699420
push eax
mov eax,0xF90BE1B3
xor eax,0x956694D0
push eax
mov eax, esp
push ebx // push 0
push eax // push "flag"
push eax // push "flag"
push ebx // push 0
call[edi - 0x04] // call MessageBoxA
push ebx // push 0
call[edi - 0x08] // call ExitProcess
}
return 0;
}
最后将shellcode注入。由于注入的flag不能是明文,不然很可能直接被搜索字符串找到,我给每个字符串进行了把不同的异或。
这部分就十分的简单,我们可以使用CE,确定小球的内存地址,直接读取内存地址,比较跳转即可。
很荣幸可以参见这次的出题活动,每道题我还是很认真的思考过的。如果同学们这次玩的还比较开心,喜欢这种风格的话,我们不出意外应该明年上半学期的校赛会再次见面,届时我会单独负责RE板块,给大家带来我精心润色的题目(当然老张要是坚持办比赛的话),到之后的题难度一定会上升,毕竟有子洋师傅嘛!如果对此次这几道题有很大意见的师傅们,不用担心,因为12月份的校赛你不会再见到我啦!当然我还是希望师傅们给我提供宝贵的建议,第一次出题,如有不妥,请多多包涵。
全部评论 (共 8 条评论)
我永远单推炜昊师傅 (☆ω☆)