[DDCTF 2018] 黑盒破解

最近在学vm, 感觉大爹给的题难度有点高,拿点简单的找下手感先

ida中逻辑很清楚,首先输入password,然后输入passcode,目的是令程序输出Binggo.

题目附带了flag-48ee204317.txt,显然password是48ee204317

输入passcode后程序的关键逻辑在此,在Dispatcher函数内

光看伪代码很难发现他在干什么,但是我们如果看汇编的话会发现

这里基本可以确定,该函数通过call rax来跳转到虚拟机的各个case内

那么我们可以猜测第一层循环在分发opcode,第二层循环在检验对应的case是否应该被跳转,一共应该有9个case

我们尝试找出 *(a1 + *(a1 + 4 * (j + 72LL) + 8) + 408) 中 j 从 0 变化到 8 的所有数据,并找到所有满足条件的 v2 的取值,这样,通过byte_603900我们就能找到合法的opcode有哪些

考虑用idc脚本

#include<idc.idc>
static main()
{
	auto j;
	auto a1 = 0x23658C0;
	for(j = 0; j <= 8; ++j)
	{
		Message("0x%x,",Byte(a1 + Byte(a1 + 4 * (j + 72) + 8) + 408));
	}

}

在外层调用Dispatcher函数的地方断点并运行,得到

[0x2a,0x27,0x3e,0x5a,0x3f,0x4e,0x6a,0x2b,0x28]

然后用ida取出byte_603900的数据

byte_603900 = [0x02, 0x00, 0x00, 0x0E, 0x16, 0x54, 0x20, 0x18, 0x11, 0x45,
0x50, 0x59, 0x58, 0x53, 0x00, 0x08, 0x44, 0x2D, 0x46, 0x39,
0x00, 0x54, 0x42, 0x01, 0x3C, 0x0F, 0x00, 0x07, 0x17, 0x00,
0x56, 0x21, 0x00, 0x37, 0x6D, 0x2B, 0x2A, 0x6E, 0x59, 0x5D,
0x47, 0x3A, 0x4A, 0x34, 0x44, 0x48, 0x43, 0x6C, 0x3F, 0x59,
0x25, 0x33, 0x55, 0x2F, 0x31, 0x68, 0x27, 0x34, 0x7C, 0x28,
0x67, 0x59, 0x00, 0x52, 0x00, 0x26, 0x00, 0x3E, 0x56, 0x4E,
0x33, 0x21, 0x45, 0x6D, 0x60, 0x39, 0x46, 0x72, 0x6D, 0x4D,
0x54, 0x40, 0x00, 0x74, 0x57, 0x73, 0x72, 0x7A, 0x47, 0x45,
0x00, 0x71, 0x00, 0x4A, 0x35, 0x70, 0x3B, 0x36, 0x2E, 0x26,
0x2C, 0x6C, 0x4A, 0x00, 0x7C, 0x63, 0x35, 0x57, 0x4D, 0x41,
0x43, 0x62, 0x00, 0x68, 0x37, 0x00, 0x5A, 0x6A, 0x6B, 0x7C,
0x29, 0x69, 0x4C, 0x70, 0x50, 0x71, 0x26, 0x36, 0x3C, 0x06,
0x1B, 0x00, 0x3C, 0x30, 0x00, 0x00, 0x00, 0x4C, 0x0B, 0x4B,
0x48, 0x08, 0x54, 0x47, 0x12, 0x09, 0x24, 0x00, 0x00, 0x24,
0x40, 0x0D, 0x39, 0x06, 0x5C, 0x2C, 0x1A, 0x2D, 0x0A, 0x38,
0x35, 0x37, 0x16, 0x3B, 0x00, 0x24, 0x48, 0x00, 0x49, 0x00,
0x37, 0x08, 0x1F, 0x24, 0x45, 0x1D, 0x11, 0x40, 0x2F, 0x4A,
0x08, 0x15, 0x00, 0x11, 0x00, 0x1A, 0x22, 0x41, 0x52, 0x5B,
0x0B, 0x45, 0x31, 0x19, 0x43, 0x19, 0x1E, 0x0A, 0x21, 0x05,
0x4D, 0x59, 0x38, 0x34, 0x09, 0x36, 0x2F, 0x43, 0x02, 0x53,
0x12, 0x2F, 0x4C, 0x21, 0x0D, 0x3C, 0x31, 0x2E, 0x37, 0x08,
0x30, 0x29, 0x32, 0x2F, 0x00, 0x1A, 0x14, 0x41, 0x53, 0x15,
0x21, 0x00, 0x08, 0x13, 0x38, 0x5C, 0x36, 0x3B, 0x50, 0x00,
0x2F, 0x1E, 0x57, 0x00, 0x30, 0x2E, 0x0C, 0x2E, 0x37, 0x52,
0x1C, 0x33, 0x34, 0x11, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00]
passcode = [0x2a, 0x27, 0x3e, 0x5a, 0x3f, 0x4e, 0x6a, 0x2b, 0x28]
opcode = []
for i in passcode:
    opcode.append(chr(byte_603900.index(i)))
print(opcode)

可以发现:合法的opcode有

[’$’, ‘8’, ‘C’, ‘t’, ‘0’, ‘E’, ‘u’, ‘#’, ‘;’]

一共9种,这也验证了我们"共有9个case"的猜想

在没有找到这些case在哪时,我们考虑对main函数内的byte_603F00进行交叉引用,进入修改了该check变量的函数,并再对该函数进行交叉引用

再次进行调试,这次我们输入的passcode仅为上述9个字符,可以得出opcode和case的关系

$ -> sub_400DC1(func1)
8 -> sub_400E7A(func2)
C -> sub_400F3A(func3)
t -> sub_401064(func4)
0 -> sub_4011C9(func5)
E -> sub_40133D(func6)
u -> sub_4012F3(func7)
# -> sub_4014B9(func8)
; -> sub_400CF1(func9)

接下来就该分析每个case的作用了

我们先把伪代码进行初步的简化

func1:
	if *(a1 + 288) < *(a1 + 292)
		*(a1 + 665) = *((a1 + 8) + (int)(a1 + 288));

func2:
	if *(a1 + 288) < *(a1 + 292)
        *((int)(a1 + 288) + (a1 + 8)) = *(a1 + 665)

func3:
	if *(a1 + 288) < *(a1 + 292)
		*(a1 + 665) = *(a1 + 665) + *(a1 + 664) - 33

func4
	if *(a1 + 288) < *(a1 + 292)
		*(_BYTE *)(a1 + 665) = *(_BYTE *)(a1 + 665) - *(_BYTE *)(a1 + 664) + 33
		if (a1 + 665)
			++(a1 + 665)

func5:
	if *(a1 + 288) < *(a1 + 292)
		++*(a1 + 288)

func6:
	check()

func7:
	if *(a1 + 288) > 0
		--*(a1 + 288)

func8:
	++(a1 + 288)
	if *(a1 + 288) < *(a1 + 292) and *(a1 + 664) - 48 <= 41
		*((a1 + 280) + (int)(a1 + 288) + a1) = *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48) - 49

func9:
	for (i = 0; i < *(a1 + 664); ++i)
    	++*(a1 + 288);
    if *(a1 + 664) - 16 <= 89
    	*((a1 + 280) + (int)(a1 + 288) + a1) = *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48) - 49
    	
check()
	if (a1 + 664) == 's'
		s = ((a1 + 8) + (a1 + 288)), len = 20
		if (right(s))
			success!

其中,check()的逻辑是

if (a1 + 664) == 's'
		s = ((a1 + 8) + (a1 + 288)), len = 20
		if (right(s))
			success!

然后进行动态调试,可以发现

  • +655: 临时变量tmp
  • +8: str = "PaF0!&Prv}H{ojDQ#7v="
  • +288: pt
  • +16: 我们输入的passcode(input)
  • +664: 下一位输入(next, 即input[i + 1])

并且,一些奇怪的东西如下:

  • *((a1 + 280) + (int)(a1 + 288) + a1): str[pt]
  • *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48): input[pt + next - 48]

那么,可以拿出伪代码的进一步分析了

func1($):
	temp = str[pt]
func2(8):
	str[pt] = temp
func3(C):
	temp = temp + next - 33
func4(t):
	temp = temp - next + 33
func5(0):
	++pt
func6(E):
	check()
func7(u):
	--pt
func8(#):
	str[pt] = input[pt + next - 48] - 49
func9(;):
	for (i = 0; i < next; ++i) ++pt;
	str[pt] = input[pt + next - 48] - 49

并且,在check()中,我们还发现:程序的目的是将str = "PaF0!&Prv}H{ojDQ#7v=" 转换成 str = "Binggo",然后我们才可以得到flag

显然地,str的位数远大于"Binggo",我们得考虑在PaF0!&P的第二个P处用'\0'将字符串截断

从头开始构造吧

首先看P -> B,我们先取出'P',用'$'使得temp = str[pt],此时pt = 0,则temp = 'P'

两者的ASCII分别是80, 66,考虑用't操作'

\(80 - next + 33 = 66\),显然\(next = 47\),即'/'

然后分别用'8''0'来进行更新,并且将pt指针后移一位

综上,第一步的opcode应该是$t/80

同理,PaF0!&转换成Binggo需要的opcode应该是$t/80$C)80$CI80$CX80$Cg80$Cj80

现在该考虑截断操作了

这里我们不能用func3来构造了,这样得到的是一个不可见字符

那只能考虑func8来进行一下复杂操作了

我们考虑在P -> B的时候额外插入一个111来让pt指向第七个字符时直接构造'\0',最后调整pt到str前端,最后用'E'调用check()并用's'满足check()的条件

最终的passcode就是

$t/81110$C)80$CI80$CX80$Cg80$Cj80#0uuuuuuuuEs

得到flag:
DDCTF{ThisIs_AnExample}

posted @ 2022-03-16 21:01  iPlayForSG  阅读(85)  评论(0编辑  收藏  举报