ctfshow:屏幕裂开了

reverseyulabu2026-10-12 5 次阅读

屏幕裂开了

这是一道APK逆向题,大致思路也是反汇编出伪代码逆向,找出flag,而这份题用jadx读出java源码后,调用了来自native的checkflag,需要用IDA再读lib文件,最终整合出完整的flag-check过程

0x00 jadx读java源码

找到mainActivty,阅读其中的oncreate

java 复制代码
public class MainActivity extends AppCompatActivity {
    private Button btn;
    private Button btn2;
    private EditText input;
    int hit_count = 0;
    String key = "InfinityLoop";
    char[] s = new char[256];
    char[] k = new char[256];

    public native boolean checkflag(char[] cArr, String str);

    static {
        System.loadLibrary("native-lib");
    }

    @Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        for (char i = 0; i < 256; i = (char) (i + 1)) {
            this.s[i] = i;
        }
        for (int i2 = 0; i2 < 256; i2++) {
            char[] cArr = this.k;
            String str = this.key;
            cArr[i2] = str.charAt(i2 % str.length());
        }
        this.btn = (Button) findViewById(R.id.button);
        this.btn2 = (Button) findViewById(R.id.button2);
        this.btn.setOnClickListener(new View.OnClickListener() { // from class: com.zxc.ClickStorm.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                MainActivity.this.hit_count++;
                char j = 0;
                for (char i3 = 0; i3 < 256; i3 = (char) (i3 + 1)) {
                    j = (char) (((MainActivity.this.s[i3] + j) + MainActivity.this.k[i3]) % 256);
                    char tmp = MainActivity.this.s[i3];
                    MainActivity.this.s[i3] = MainActivity.this.s[j];
                    MainActivity.this.s[j] = tmp;
                }
                MainActivity.this.btn.setText(String.valueOf(MainActivity.this.hit_count));
                if (MainActivity.this.hit_count >= 99999) {
                    MainActivity.this.btn2.setEnabled(true);
                }
            }
        });
        this.btn2.setOnClickListener(new View.OnClickListener() { // from class: com.zxc.ClickStorm.MainActivity.2
            @Override // android.view.View.OnClickListener
            public void onClick(View v) {
                MainActivity mainActivity = MainActivity.this;
                if (mainActivity.checkflag(mainActivity.s, MainActivity.this.input.getText().toString())) {
                    MainActivity.this.btn2.setText("O");
                } else {
                    MainActivity.this.btn2.setText("X");
                }
            }
        });
        this.input = (EditText) findViewById(R.id.InputFlag);
    }
}

一点点解读,第一个循环很好懂:s数组被初始化为0-255。我们接着看第二个循环:

java 复制代码
for (int i2 = 0; i2 < 256; i2++) {
    char[] cArr = this.k;
    String str = this.key;
    cArr[i2] = str.charAt(i2 % str.length());
}

这个稍微绕了一点,有点反直觉,不过仔细推一下不难看出cArr被赋予数组k的头指针,然后用key作为密钥重复填入数组,直到255

接着还剩下的就是btn和btn2的点击监听器了,btn2应该是类似提交的功能,把输入的内容调用check-flag来判断一遍,所以我们重点看btn的内容

循环先不看,先看收尾,99999次点击后才激活btn2,说明要点够99999次才给提交去check,接下来的思路就是将循环中的逻辑用python复现,外层套循环执行99999次,得到提交去check的内容

python 复制代码
key = "InfinityLoop"

s = list(range(256))
k = [ord(key[i % len(key)]) for i in range(256)]

def onclick():
    j = 0
    for i in range(256):
        j = (s[i] + j + k[i]) % 256
        s[i], s[j] = s[j], s[i]

for _ in range(99999):
    onclick()

print(s)

貌似是复现出来了,但是不知道有没有用,得再看看check-flag,但是这是写在lib里面的,所以我们需要导出对应文件,放到IDA里查看,找到这个check-flag(导出so文件时要选择电脑对应cpu架构的文件)

0x01 IDA读so文件

c 复制代码
__int64 __fastcall Java_com_zxc_ClickStorm_MainActivity_checkflag(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  int j; // [rsp+10h] [rbp-180h]
  int i; // [rsp+14h] [rbp-17Ch]
  __int64 CharArrayElements; // [rsp+18h] [rbp-178h]
  const char *StringUTFChars; // [rsp+20h] [rbp-170h]
  char v9; // [rsp+2Bh] [rbp-165h]
  int v10; // [rsp+30h] [rbp-160h]
  int v11; // [rsp+34h] [rbp-15Ch]
  _BYTE v14[264]; // [rsp+80h] [rbp-110h]
  unsigned __int64 v15; // [rsp+188h] [rbp-8h]

  v15 = __readfsqword(0x28u);
  LOBYTE(v11) = 0;
  LOBYTE(v10) = 0;
  StringUTFChars = _JNIEnv::GetStringUTFChars(a1, a4, 0);
  if ( strlen(StringUTFChars) == 63 )
  {
    CharArrayElements = _JNIEnv::GetCharArrayElements(a1, a3, 0);
    for ( i = 0; i < 256; ++i )
      v14[i] = *(CharArrayElements + 2LL * i);
    for ( j = 0; j < 63; ++j )
    {
      v11 = (v11 + 1);
      v10 = (v14[v11] + v10);
      v9 = v14[v11];
      v14[v11] = v14[v10];
      v14[v10] = v9;
      if ( (v14[(v14[v10] + v14[v11]) % 256] ^ StringUTFChars[j]) != answer[j] )
        return 0;
    }
    return 1;
  }
  else
  {
    return 0;
  }
}

说真的这里c语言伪代码里塞java读着是真难受,我们主要先看这个:

c 复制代码
__int64 __fastcall Java_com_zxc_ClickStorm_MainActivity_checkflag(
    __int64 a1,
    __int64 a2,
    __int64 a3,
    __int64 a4
)

为啥会有四个参数?明明在java读的时候只有两个的,这里就要涉及到一个东西:JNIEnv

我们回到源码可以看到如何写的:

java 复制代码
public native boolean checkflag(char[] cArr, String str);

JNIEnv是java给C/C++代码提供的接口,其中a1是JNIEnv*,是与java交互的工具接口,a2是当前的 MainActivity 对象,由java自己传入,后面的a3a4才是我们在源码中看到的输入

知道这一点后,我们查一下这两个函数的用法,就可以知道它们做了什么:

c 复制代码
StringUTFChars = _JNIEnv::GetStringUTFChars(a1, a4, 0);

将java字符串转换成C/C++可以访问的字符串,a4是input,至于:

c 复制代码
CharArrayElements = _JNIEnv::GetCharArrayElements(a1, a3, 0);

作用就是把输入的s(在java中)复制出来,至此我们可以开始看具体的check逻辑了

在复制完后还没完,下面有个循环再复制了一遍:每次相对位移2 * i个字节(java中,每个char占2个字节),到了这一步,才算是把我们java中的s读成我们熟悉的数组

c 复制代码
v14[i] = *(CharArrayElements + 2LL * i);

循环里面做了一个类似交换的逻辑,大概把逻辑捋清楚后,找answer

image-20261010194851530

我们给answer的内容提取出来:

python 复制代码
answer = bytes.fromhex("""
    A6 3D 54 B0 74 CC BD 2A 4A DE
    BD 35 D1 1D 80 32 5F 64 2F C5
    DD 11 3E 95 CC 17 13 E5 5E 65
    CE 42 9E 47 C8 F3 4D 8A A6 1F
    F0 50 27 A2 28 81 24 A7 B4 90
    FC 93 8A C1 77 D5 16 1E FD 87
    C7 BB B3 00
    """)

判断条件是异或计算,我们现在有answer,又模拟出了点击99999次后的s,异或就可以得到flag了

解题脚本如下:

python 复制代码
key = "InfinityLoop"

s = list(range(256))
k = [ord(key[i % len(key)]) for i in range(256)]

def onclick():
    j = 0
    for i in range(256):
        j = (s[i] + j + k[i]) % 256
        s[i], s[j] = s[j], s[i]

for _ in range(99999):
    onclick()


def check_flag(str):
    index1 = 0
    index2 = 0
    s = [i for i in str]
    answer = bytes.fromhex("""
    A6 3D 54 B0 74 CC BD 2A 4A DE
    BD 35 D1 1D 80 32 5F 64 2F C5
    DD 11 3E 95 CC 17 13 E5 5E 65
    CE 42 9E 47 C8 F3 4D 8A A6 1F
    F0 50 27 A2 28 81 24 A7 B4 90
    FC 93 8A C1 77 D5 16 1E FD 87
    C7 BB B3 00
    """)
    flag = []
    for i in range(63):
        index1 += 1
        index2 = (s[index1] + index2 ) & 0xff
        temp = s[index1]
        s[index1] = s[index2]
        s[index2] = temp
        flag.append(s[(s[index2] + s[index1]) % 256] ^ answer[i])
    return flag
print(bytes(check_flag(s)).decode())

这里有个比较经典的坑,一开始没注意

image-20261010202313463

如果index2不用0xff取低八位(就是0-255),那会报错,我看了整个逻辑,index2的计算是没问题的,踩坑的点在于没注意到这个:

c 复制代码
LOBYTE(v11) = 0;
LOBYTE(v10) = 0;

这两个索引用的是LOBYTE,代表最低的八位,所以我们每次算完要取低八位,这样就没问题了,这样刚好保证范围在0-255,符合算法

得到flag:flag{i_hope_you_didnt_click_the_button_99999__justRE_in_Static}

评论

正在加载评论区…