buzhifou01 发表于 2020-3-9 14:11

JarvisOJ APK_500题解

# 使用工具
ida
apktool
010editor
Hex Workshop
PKID
jadx-gui
d2j-dex2jar
APKSign
# 解题过程

1.查壳,无壳,放到模拟器中运行,输入信息之后出现提示信息。


2.使用apktool进行反编译,发现AndroidManifest.xml不能被解密,那么反汇编终止,把生成AndroidManifest.xml放到010editor中,发现是空文件,接着往dex文件下手,由于使用apktool出现异常那么dex文件也有可能有问题


3.接着对apk进行解压缩,也生成了AndroidManifest.xml,拖入到010editor中发现是二进制文件,从中看不出什么,但里面有数据,说明该题的作者对AndroidManifest.xml进行了修改导致了apk不能正常反编译。

4.使用Hex Workshop打开AndroidManifest.xml文件,接着对比了一下该网站(https://bbs.pediy.com/thread-194206.htm)上的图片发现,Magic Number不为00080003并且StylePoolOffset也不为0。

5.把这两个值进行修改之后,重新打包成压缩文件,把文件名后缀改为.apk,签名,签名软件用的是APKSign。


6.接着使用apktool进行反编译和解压缩,然后使用d2j-dex2jar.bat对classes-dex进行反编译,随后把生成的jar文件拖到jadx-gui中,从静态代码块中可以看到程序具有反调式的功能并且加载了动态链接库so,从clickHandler方法中看到输入的口令长度小于16,helloworld为关键方法,但该方法在native层,那么就要看so文件了。
public class MainActivity extends AppCompatActivity {
    static {
        //反调式代码
      if (!Debug.isDebuggerConnected()) {
            System.loadLibrary("easy");
      }
    }
    public void clickHandler(View view) {
      TextView textView = (TextView) findViewById(R.id.textView2);
      String charSequence = ((TextView) findViewById(R.id.editText)).getText().toString();
      if (charSequence.isEmpty()) {
            Toast.makeText(this, "口令不能为空", 1).show();
      } else if (charSequence.length() > 16) {
            Toast.makeText(this, "输入的口令太长了", 1).show();
      } else if (helloworld(charSequence)) {
            Toast.makeText(this, "{FLAG:" + charSequence + "}", 1).show();
      } else {
            Toast.makeText(this, "输入的口令不正确", 1).show();
      }
    }

    public native boolean helloworld(String str);

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
      super.onCreate(bundle);
      setContentView((int) R.layout.activity_main);
      ((TextView) findViewById(R.id.editText)).setInputType(1);
    }
}

7.用ida加载so文件,由于加了反调试,所以只能静态分析so文件了,先看一下里面的函数,没找到 Java_类名_方法名 的函数和helloword函数,接着看到一个比较关键的函数JNI_OnLoad,程序加载so的时候,会执行JNI_OnLoad函数,由于主Activity里面有反调试,那么JNI_OnLoad里面可能也有。

8.在该函数内部看到一个字符串处理函数被调用了3次,进入该函数可以看出,该函数对字符串s1进行异或操作,接着往下看可以看到里面有几个字符串变量。


9.在下面看到一个if判断语句,里面涉及了2个函数,括号内是return -1,显然这两个函数跟新建东西或者初始化有关,分析发现这2个函数跟native注册有关。

10.上面有个函数sub_1198,看上去有点像c++里面的回调函数,进去,在里面看到反调式程序,下面就是求解flag的关键代码,里面的算法思路我都加了注释,大家应该看得懂。


while ( v12 - str < len )
{
    v = *(unsigned __int8 *)v12++;
    v19 = len;
    v20 = v16;
    sprintf((char *)&offest, v16, v);      // 把字符串的每个字符转换成16进制
    strcat(c1, (const char *)&offest);          // 拼接字符串'0'
    len = v19;
    v16 = v20;
}
str = (char *)str;
ii = 0;
while ( ii < len1 )                           // 循环遍历字符串
{
    xor = *str ^ ii++;                      // 对字符串进行异或处理
    *str++ = xor;                           // 把异或后的结果赋值给字符串
}
v24 = sub_10A4();
if ( v24 )
{
    tstr = str;
    i2 = 0;
    offest = v24 - v44 + 0x1010101;             // 计算偏移值
    do
    {
      v28 = *((_BYTE *)&offest + i2++);         // 把str赋值给v28
      *tstr++ += v28;                           // 把v28赋值给tstr数组
    }
    while ( i2 != 4 );
}
if ( len1 > 7 )
{
    i1 = 7;
    do                                          // 下面的循环结构把str字串赋值给c2字符串
    {
      v29 = &c2;                            // 引用变量c2
      v30 = str;
      *(v29 - 7) = v30;
    }
    while ( i1 != len1 );
    v31 = (int)(str - 1);
    v32 = &c2;
    do                                          // 下面的循环结构把str前7个字符c2
    {
      v33 = *(_BYTE *)(v31++ + 1);            // str数组中每个数加1
      (v32++) = v33;
    }
    while ( (const char *)v31 != str + 6 );   // 是否为第7个字符
    c2 = 0;                               // c2结尾符
}
for ( i = 0; i < len1; ++i )
    str = c2 ^ str_4004;               // 与str_4004进行异或操作
c1 = 0;
v46 = 0;
v35 = str;
v36 = ::strlen(str);
while ( v35 - str < v36 )
{
    v37 = *(unsigned __int8 *)v35++;
    sprintf(&v45, (const char *)&x_, v37);      // 把str中的每个字符进行16进制转换
    strcat(c1, &v45);                           // 把转换后的字符拼接到c1中
}
v38 = &ans;
v39 = &str_2C87;                              // str_2C87为Z!s't&v~q~r},x,14`17c`gX
do                                          // 下面的循环结构把str_2C87赋值给ans
{
    v40 = *v39;
    v39 += 2;
    v41 = *(v39 - 1);
    *(_DWORD *)v38 = v40;
    *((_DWORD *)v38 + 1) = v41;
    v38 += 8;
}
while ( v39 != &dword_2CA7 );               // 是否为结尾符
strdeal(&ans, 32, 57);                        // 异或处理ans
return strcmp(&ans, c1) == 0;
}
11.到了最后,可以看到一个字符数组比较函数,那么ans就为一个关键数组,接着求出该数组,在上面的代码中可以看到对ans进行处理的算法:先是把str_2C87字符串赋值给ans,接着使用strdeal函数进行异或处理,使用如下idapython脚本可以把ans字符串求出。
# -*- coding:utf-8 -*-
def GetStr():
        #str_2C87字符串所在的位置
        str_addr=0x2C87
        tstr=''
        while(1):
                #判断循环是否结束
                if chr(Byte(str_addr))=='g' and chr(Byte(str_addr+1))=='X':
                        break
                #叠加字符串字符生成字符串
                tstr+=chr(Byte(str_addr))
                str_addr+=1
        #加上最后两个字符
        return tstr+'gX'
print GetStr()
#对应ida中的strdeal函数
def strdeal(str,c):
        index=0
        s=''
        while(index<len(str)):
                v=ord(c)+index
                t=str
                s+=chr(ord(t)^v)
                index+=1
        return s
s=GetStr()
print strdeal(s,'9')
运行结果如下:

12.那么接着就是str_4004字符串的问题了,在ida中可以看出它只有三个字符,但在第10步的代码中可以看出它的长度不止3,可能是哪个地方对它后面数进行赋值,接下来使用idapython脚本查找引用它的地方。

# -*- coding:utf-8 -*-
def GetStrPos():
        #查找引用str_4004的地方
        for refhs in XrefsTo(0x4004, flags=0):
                print "x: %s x.frm 0x%x"%(refhs,refhs.frm)

GetStrPos()
运行结果如下:

在输出窗口点击x.frm后面的地址就可跳到调用str_4004字符串的地方,比Ctrl+X方便多了!
13.在输出窗口中点击0x1434,可以看到这里对str_4004-str_4004进行赋值,显然这是不够的,接下来,使用如下脚本查看str_4007的引用情况,

# -*- coding:utf-8 -*-
def GetStrPos():
        #查找引用str_4004的地方
        for refhs in XrefsTo(0x4007, flags=0):
                print "x: %s x.frm 0x%x"%(refhs,refhs.frm)

GetStrPos()
运行结果:

14.在上面输出的地址中,看到一个离引用str_4004非常近的地址0x143e,随后分析了0x1430到0x1444处的地址,分析情况如下,那么str_4004的字符串为:str_4004+'839C6C83'+str_4007。
.text:00001430 1D 4B                           LDR             R3, =(str_4004 - 0x1438)
.text:00001432 84 B0                           SUB             SP, SP, #0x10
.text:00001434 7B 44                           ADD             R3, PC; str_4004
.text:00001436 03 33                           ADDS            R3, #3; R3+3
.text:00001438 18 46                           MOV             R0, R3
.text:0000143A 19 4B                           LDR             R3, =0x839C6C83 ; 把0x839C6C83放入R3,即为:str_4004-str_4004为0x839C6C83
.text:0000143C 19 46                           MOV             R1, R3
.text:0000143E 01 60                           STR             R1, ; 为str_4007字符串,str_4004后面拼接str_4007
.text:00001440 6B 46                           MOV             R3, SP
.text:00001442 18 46                           MOV             R0, R3
.text:00001444 00 22                           MOVS            R2, #0; 数组结束符
15.接下来使用idapython脚本,取出str_4007子串,并转换成十进制数,使用idapython脚本取值有一个非常大的好处,就是省的一个一个复制粘贴到代码中,脚本代码如下:
def GetV():
        #str_2C87字符串所在的位置
        str_addr=0x400B
        tstr=''
        i=0
        while i<9:
                print Byte(str_addr),
                i+=1
                str_addr+=1
               
GetV()
运行结果如下:

16.在上面当中,提到了ans字符数组跟c1一样,那c1字符数组到底为多少呢?从175行代码中看到c1字符数组的长度为32,从第11步看出ans的长度为31,在107行有对'0'进行拼接的操作,那么c1的可能就有32种,求解flag的代码中,每个都试过去,求解flag的代码如下:


s = "ddedd4ea2e7bef168491a6cae2bc660"
c1 = []
str_4004 =
#把十六进制数转换成十进制
def ChrToOrd(c,j):
        t=0x10
        #判断是否是个位数
        if j==2:
                t=0x1
        Hex=['a','b','c','d','e','f']
        Ord=
        #如果是0-9的数
        if '0'<=c and '9'>=c:
                return (ord(c)-ord('0'))*t
        #如果是a-f的数
        for i in range(6):
                if Hex==c:
                        return Ord*t
#把ans字符串中十六进制数转换成十进制
def getStrOrd(i):
        ord=[]
        j=0
        ans=0
        t=i
        #每两位进行转换
        while j<31:
                #ans追踪转换后的数值
                ans=0
                if j==t:
                        ans+=0
                        #如果j==i,把t标记为-1
                        t=-1
                else:
                        ans+=ChrToOrd(s,1)
                        j+=1
                        if j==31:
                                ord.append(ans)
                                break
                if j==t:
                        ans+=0
                        #如果j==i,把t标记为-1
                        t=-1
                else:
                        ans+=ChrToOrd(s,2)
                        j+=1
                ord.append(ans)
        return ord
def getC1Sz():
        for i in range(32):
                t=getStrOrd(i)
                c1.append(t)
#判断是否为可见字符
def isSee(t):
        if(t>32 and t<127):
                return 0
        else:
                return 1
def getFlag():
        getC1Sz()
        for t in c1:
                XorC1 = []
                flag = ''
                print 't ',t
                for i in range(16):
                        t1 = t^str_4004
                        #print 'p ',p
                        XorC1.append(t1)
                #XorC1对应str
                XorC1 = XorC1[-7:] + XorC1[:-7]
                #把str中的前四个元素减1
                for i in range(4):
                        XorC1 = XorC1- 1
                #把str中的每个元素进行异或处理
                for i in range(16):
                        t2 = XorC1 ^ i
                        #如果不是可见字符则退出
                        if(isSee(t2)==1):
                                break
                        flag+= chr(t2)
                else:
                        print flag
                        break
getFlag()
运行结果如下:


chxi 发表于 2020-3-9 14:24

只是加分了

HighBox 发表于 2020-3-9 14:29

打打CTF

lhhtys 发表于 2020-3-9 14:31

只是加分了

诺世成长日记 发表于 2020-3-9 15:26

渣渣来向大佬学习了

纯甄大狗哥 发表于 2020-3-9 15:27

小白来学习一下,谢谢分享

泡影 发表于 2020-3-9 23:00

楼主厉害啊

cdevil 发表于 2020-3-10 08:27

学习了,楼主厉害
页: [1]
查看完整版本: JarvisOJ APK_500题解