ctfshow:屏幕裂开了
屏幕裂开了
这是一道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

我们给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())
这里有个比较经典的坑,一开始没注意

如果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}
评论