前言

Juggle是35C3 CTF中的一道中等难度的逆向题目。虽然由于多个非预期解的存在(也可能是作者故意为之),使得最后题目的动态分数只有90分,但题目整体的考察点比较全面,包括XSLT、VM逆向、二分查找算法、汇编代码编写和调试等。预期的解法也比较有意思,对于逆向基本功的练习十分有帮助。
这里我将自己的解法和几种非预期解整理了一下,供大家参考。

初识XSLT

题目给出了一个XSLT文件和一个DockerfileDockerfile用来配置XSLT环境,主要逻辑都在XSLT文件中。

XSLT是一种语言,用来处理XML格式的数据,将其转换为其他格式,例如HTML等。更多详细信息可以参考w3school

题目给出的docker中使用了xalanXSLT进行解释。本地测试时也可以使用Visual Studio(部分版本)进行调试,可以设置断点和观察变量等,十分方便。

非预期解1 —— XXE

首先打开XSLT文件,大致浏览一下,可以找到读取flag的位置:

<xsl:when test="count($c/दाल) = 1">
    <xsl:if test="count($chef-drinks) = 0">
        <xsl:copy-of select="document('/flag')"/>
    </xsl:if>
    <xsl:call-template name="consume-meal"> <!-- 递归调用consume-meal函数 -->
        <xsl:with-param name="chef-drinks" select="$chef-drinks"/>
        <xsl:with-param name="food-eaten" select="$food-eaten + 1"/>
        <xsl:with-param name="course" select="$r"/>
        <xsl:with-param name="drinks" select="$drinks"/>
    </xsl:call-template>
</xsl:when>

可以看到当满足count($c/दाल) = 1count($chef-drinks) = 0两个条件时,flag的内容就会被读取到转换后的文件并输出给我们。
这里便存在第一个非预期解——XXE,即XML外部实体注入。这里引用一下OpenToAll的wp

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///flag" >]><foo>&xxe;</foo>

即可成功读取到flag。网上有关XXE的资料很多,这里不过多介绍。

梳理程序逻辑

我们继续来看读flag的条件,其中的$c可以在前面找到:

<xsl:template name="consume-meal"> <!-- 函数consume-meal定义 -->
    <xsl:param name="chef-drinks"/> <!-- 函数参数1 -->
    <xsl:param name="food-eaten"/> <!-- 函数参数2 -->
    <xsl:param name="course"/> <!-- 函数参数3 -->
    <xsl:param name="drinks"/> <!-- 函数参数4 -->
    <xsl:if test="$food-eaten > 30000">
        <xsl:message terminate="yes">You ate too much and died</xsl:message>
    </xsl:if>
    <xsl:if test="count($drinks) > 200">
        <xsl:message terminate="yes">You cannot drink that much</xsl:message>
    </xsl:if>
    <xsl:if test="count($course) > 0">
        <xsl:variable name="c" select="$course[1]" /> <!-- 取course第一个元素 -->
        <xsl:variable name="r" select="$course[position()>1]" /> <!-- 剩余元素,递归处理 -->
            <xsl:choose> <!-- 对取出的c元素的类型进行判断,进行不同处理 -->
                <xsl:when test="count($c/宫保鸡丁) = 1">
                ...
                </xsl:when>
                <xsl:when test="count($c/paella) = 1">
                ...
                </xsl:when>
                ...
            </xsl:choose>
        </xsl:if>
    </xsl:template>

这里的xsl:template可以理解为函数,即定义了一个名为consume-meal的函数,其中的几个xsl:param为函数参数。

之前的c变量为course[1],即course数组中第一个元素(XSLT中数组下标从1开始)。而course数组剩下的元素被放到了r变量中。

在每个xsl:when分支判断中,通过count($c/...)是否为1判断了c的元素类型,并进行不同的处理,最后将r作为course参数,递归调用了consume-meal函数。

所以整个consume-meal相当于对course数组中的每个元素依次进行了一个很大的case分支判断。

那么这些case分支到底在干什么呢?我们可以再观察一下其他几个变量:

  • food-eaten在每次递归调用会加一,到30000后会报错退出,相当于限制了输入参数courses的长度。
  • drinks参数比较复杂,在许多分支中都会进行修改和判断,其中比较明显的有几个加减乘除的操作,与基于栈的四则运算很像。
  • chef-drinks只有在Борщ分支中会进行修改,在ラーメン分支中会与drinks进行比较,最终在दाल分支中判断为空便可以读取flag。

首先看Борщ分支:

<xsl:when test="count($c/Борщ) = 1">
    <xsl:variable name="arg0">
        <value>
            <xsl:value-of select="$drinks[1] + 0"/>
        </value>
    </xsl:variable>
    <xsl:call-template name="consume-meal">
        <xsl:with-param name="chef-drinks" select="$chef-drinks[position() > 1 or $chef-drinks[1] != $arg0]"/>
        <xsl:with-param name="food-eaten" select="$food-eaten + 1"/>
        <xsl:with-param name="course" select="$r"/>
        <xsl:with-param name="drinks" select="$drinks[position() > 1]"/>
    </xsl:call-template>
</xsl:when>

可以看出,当drinks[1] != chef-drinks[1]时,传入下一步的chef-drinks值为$chef-drinks[position() > 1 or 1],即原来的chef-drinks会被完整传入。
drinks[1] == chef-drinks[1]时,传入的是$chef-drinks[position() > 1 or 0],即删掉了chef-drinks的第一个元素。

所以Борщ分支判断了drinks[1]chef-drinks[1]是否相等,相等则删掉chef-drinks[1]

其实从这里我们就可以看出一点端倪了。读取flag的条件是count($chef-drinks) = 0,而这里又有一个类似于猜数的功能,猜对所有chef-drinks的值就可以将其删光,以满足flag读取的条件。

然后我们来看ラーメン分支:

<xsl:when test="count($c/ラーメン) = 1">
    <xsl:variable name="arg0">
        <value>
            <xsl:value-of select="$drinks[1] + 0"/>
        </value>
    </xsl:variable>
    <xsl:variable name="chefvalue">
        <value>
            <xsl:value-of select="$chef-drinks[1] + 0"/>
        </value>
    </xsl:variable>
    <xsl:variable name="newdrinks">
        <value>
            <xsl:value-of select="1 * ($arg0 > $chefvalue)"/>
        </value>
        <xsl:copy-of select="$drinks[position() > 1]"/>
    </xsl:variable>
    <xsl:call-template name="consume-meal">
        <xsl:with-param name="chef-drinks" select="$chef-drinks"/>
        <xsl:with-param name="food-eaten" select="$food-eaten + 1"/>
        <xsl:with-param name="course" select="$r"/>
        <xsl:with-param name="drinks" select="exsl:node-set($newdrinks)//value"/>
    </xsl:call-template>
</xsl:when>

可以看出,这里会将drinks[1] > chef-drinks[1]的结果与原来的drinks拼在一起,作为新的drinks参数。
到这里就基本一目了然了,这其实就相当于汇编语言中的cmp drinks[1], chef-drinks[1]指令,将比较的结果放到了drinks的第一位,所以这个大的分支判断其实就是一个基于栈的VM的解释器

其中分支中判断的菜名其实就是指令名,drinks就是栈,栈的初始值使我们可以控制的,我们需要通过VM中提供的指令猜出chef-drinks中所有的值,就可以成功拿到flag。

通过分析代码,我们可以把所有指令的作用还原:

  • 宫保鸡丁
    • 打印出drinks和chef-drinks的值,相当于打log
  • paella
    • push(const)
  • 불고기
点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖