NewStarCTF 2024 Reverse Week1&Week2复现
1220725071034280 发表于 四川 CTF 348浏览 · 2024-11-21 15:45

Week1

Simple_encryption

ida 打开

主要逻辑是根据输入的字符串长度,如果j%3==0 ,那么就将输入的索引为j 的值减去 31,如果 j%3==1,加上41,如果 j%3==2,那么和 0x55 异或。然后值与 buffer 数组相比较,一样就成功,所以点进数组提取数据。

可以写出 python 脚本

buffer = [0x47, 0x95, 0x34, 0x48, 0xA4, 0x1C, 0x35, 0x88, 0x64, 0x16,
          0x88, 0x07, 0x14, 0x6A, 0x39, 0x12, 0xA2, 0x0A, 0x37, 0x5C,
          0x07, 0x5A, 0x56, 0x60, 0x12, 0x76, 0x25, 0x12, 0x8E, 0x28,
          0x00, 0x00]
for i in range(len(buffer)):
    if i % 3 == 0:
        buffer[i] += 0x1f
    if i % 3 == 1:
        buffer[i] -= 0x29
    if i % 3 == 2:
        buffer[i] ^= 0x55
    print(chr(buffer[i]), end='')

ezAndroidStudy

我记得之前在哪个师傅博客看到过说先看 .xml 文件的 activity 可以快速找到。

对于 apk 文件可以先拖进模拟器运行看看里面是什么样的,然后看看提示

所以查看 AndroidManifest.xml,看见 activity 只有 work.pangbai.ezandroidstudy.Homowork.pangbai.ezandroidstudy.MainActivity

拿到 flag1

看下一个

resources.arsc/res/value/string.xml 找到了

继续看下一个

/layout/activity_main.xml 里的 activity_main.xml

查找 flag4

打开 /res/raw 发现目录下有个 flag4.txt

flag 5

需要逆向 so 层,那么将 .so 提取出来,可以使用apktool

apktool d ezAndroidStudy.apk -o "需要生成的文件夹"

"/lib/x86_64/libezandroidstudy.so"丢进 ida 反编译就出来了

ez_debug

根据题目丢进 x64dbg 动调

首先查找字符串,看看有没有可以字符串,可以发现有一些 flag 字样

Decrypted flag 处下个断点运行,得到结果

Week2

Pangbai 泰拉记(1)

ida 打开反汇编查看,flag 和一个 key 异或

__int64 __fastcall main()
{
  std::ostream *v0; // rax
  std::ostream *v1; // rax
  int i; // [rsp+24h] [rbp+4h]

  j___CheckForDebuggerJustMyCode(&_6D15E8DE_Pangbai____1__Pangbai____1__1_cpp);
  v0 = std::operator<<<std::char_traits<char>>(std::cout, "Use your debugger to discover the hidden flag!");
  std::ostream::operator<<(v0, std::endl<char,std::char_traits<char>>);
  for ( i = 0; i < 32; ++i )
    flag[i] ^= key[i];
  v1 = std::operator<<<std::char_traits<char>>(std::cout, "Click on the flag to obtain?");
  std::ostream::operator<<(v1, std::endl<char,std::char_traits<char>>);
  return 0i64;
}

key 交叉引用能看见还没 main0 函数调用,进去看看

发现有两个函数 IsDebuggerPresent()CheckRemoteDebuggerPresent(),这是两个反调试函数

对于 CheckRemoteDebuggerPresent() 函数:

kernel32CheckRemoteDebuggerPresent()函数用于检测指定进程是否正在被调试. Remote 指同一个机器中的不同进程

BOOL WINAPI CheckRemoteDebuggerPresent(
  _In_    HANDLE hProcess,
  _Inout_ PBOOL  pbDebuggerPresent
);

如果说检测到正在被调试,那么 pbDebuggerPresent 指向的值会被设置为 0xffffffff

对于这道题,调试一下可以看得出来流程,进程会跳转到右边这里,这里执行的是将 key 替换为不正确的 key ,所以可以将 jz 指令改为 jnz 指令,让程序跳转不了

jz: Jump if Zero,零标志位 ZF=1 时,jz 指令执行跳转
jnz: Jump if Not Zero,零标志位 ZF=0 时,jnz 指令执行跳转

然后进行调试

提取数据,写脚本

flag = [0x63, 0x61, 0x6E, 0x20, 0x79, 0x6F, 0x75, 0x20, 0x66, 0x69,
        0x6E, 0x64, 0x20, 0x6D, 0x65, 0x20, 0x63, 0x61, 0x6E, 0x20,
        0x79, 0x6F, 0x75, 0x20, 0x66, 0x69, 0x6E, 0x64, 0x20, 0x6D,
        0x65, 0x3F]
key = [0x05, 0x0D, 0x0F, 0x47, 0x02, 0x02, 0x0C, 0x7F, 0x22, 0x5A,
       0x0C, 0x11, 0x47, 0x0A, 0x56, 0x52, 0x3C, 0x0C, 0x0F, 0x59,
       0x26, 0x5E, 0x06, 0x7F, 0x04, 0x08, 0x00, 0x0A, 0x45, 0x09,
       0x5A, 0x42]
for i in range(0, 32):
    flag[i] ^= key[i]
    print(chr(flag[i]), end="")

drink_tea

先读一遍流程,输入长度为 32 的字符串,进入 sub_140001180,然后再比较

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+20h] [rbp-28h]
  __int64 v5; // [rsp+28h] [rbp-20h]

  printf("Please Input: \n");
  sub_140001120("%32s", byte_140004700);
  v5 = -1i64;
  do
    ++v5;
  while ( byte_140004700[v5] );
  if ( v5 == dword_140004078 )
  {
    for ( i = 0; i < dword_140004078; i += 8 )
      sub_140001180(&byte_140004700[i], aWelcometonewst);
    if ( !memcmp(byte_140004700, &unk_140004080, dword_140004078) )
      printf("Right! \n");
    else
      printf("wrong!");
    return 0;
  }
  else
  {
    printf("Wrong! \n");
    return 0;
  }
}

进入 sub_140001180 函数查看

__int64 __fastcall sub_140001180(unsigned int *a1, _DWORD *a2)
{
  __int64 result; // rax
  unsigned int v3; // [rsp+0h] [rbp-38h]
  unsigned int v4; // [rsp+4h] [rbp-34h]
  int v5; // [rsp+8h] [rbp-30h]
  unsigned int i; // [rsp+Ch] [rbp-2Ch]

  v3 = *a1;
  v4 = a1[1];
  v5 = 0;
  for ( i = 0; i < 0x20; ++i )
  {
    v5 -= 1640531527;
    v3 += (a2[1] + (v4 >> 5)) ^ (v5 + v4) ^ (*a2 + 16 * v4);
    v4 += (a2[3] + (v3 >> 5)) ^ (v5 + v3) ^ (a2[2] + 16 * v3);
  }
  *a1 = v3;
  result = 4i64;
  a1[1] = v4;
  return result;
}

看到这个加密流程就能发现是 tea 算法加密找到key 和输入的值直接逆

脚本如下:

#include <stdio.h>
#include <stdint.h>

void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;
    uint32_t delta=0x9e3779b9;
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
    for (i=0; i<32; i++) {
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;
    }
    v[0]=v0; v[1]=v1;
}

int main()
{
    int i,j;
    unsigned char flag[] = {0x78, 0x20, 0xF7, 0xB3, 0xC5, 0x42, 0xCE, 0xDA, 0x85,
        0x59, 0x21, 0x1A, 0x26, 0x56, 0x5A, 0x59, 0x29, 0x02, 0x0D, 0xED, 0x07,
        0xA8,0xB9, 0xEE, 0x36, 0x59, 0x11, 0x87, 0xFD, 0x5C, 0x23, 0x24};
    unsigned char keys[]="WelcomeToNewStar";

    uint32_t *v = (uint32_t*)flag;
    uint32_t *k = (uint32_t*)keys;

    for(i=0;i<8;i+=2){  //每8个字节(2个 uint32)解密
        decrypt(v+i,k);
    }
    for(j=0;j<32;j++){
        printf("%c",flag[j]);
    }
    return 0;
}

ezencrypt

先查看 MainActivity 函数发现有加密逻辑,Enc enc = new Enc(tx),那就去 Enc 函数看看

Enc 的构造函数里进行第一次加密,ECB 模式的 AES 加密,密钥是 MainActivitytitle

doEncCheck 函数进行加密数据检查,发现有 native 关键字,在 Java 中,native关键字用于声明一个方法是由本地代码(通常是C或C++)实现的,所以说明函数是 C/C++ 编写的,所以主体在 so 文件,进行 so 提取

IDA 打开 so 文件,找到 doEncCheck ,点进去查看

enc 函数的加密伪代码

__int64 __fastcall enc(char *a1)
{
  int i; // [rsp+0h] [rbp-20h]
  int v3; // [rsp+4h] [rbp-1Ch]

  v3 = __strlen_chk(a1, -1LL);
  for ( i = 0; i < v3; ++i )
    a1[i] ^= xork[i % 4];
  return encc(xork, a1);
}

又看见一个 encc 加密函数,(太好了,又有个算法加密,我们有救了!)

__int64 __fastcall encc(char *a1, char *a2)
{
  unsigned __int64 v2; // rcx
  __int64 result; // rax
  unsigned __int8 v4; // [rsp+Ch] [rbp-34h]
  int v5; // [rsp+14h] [rbp-2Ch]
  int v6; // [rsp+18h] [rbp-28h]
  int i; // [rsp+1Ch] [rbp-24h]

  init_sbox(a1);
  v5 = 0;
  v6 = 0;
  for ( i = 0; ; ++i )
  {
    v2 = __strlen_chk(a2, -1LL);
    result = i;
    if ( i >= v2 )
      break;
    v6 = (v6 + 1) % 256;
    v5 = (sbox[v6] + v5) % 256;
    v4 = sbox[v6];
    sbox[v6] = sbox[v5];
    sbox[v5] = v4;
    a2[i] ^= sbox[(sbox[v5] + sbox[v6]) % 256];
  }
  return result;
}

一个异或,一个 RC4 加密,找到了异或的字符串 “meow” 和要解密的数据

解密流程:RC4->异或->Base64->AES

#include<stdio.h>
#include<string.h>

char sbox[257] = {0};
char xork[] = "meow";
//s盒
void init_sbox(char*a1){
  int i,j,k,tmp;
  for ( i = 0; i < 0x100; i++ )
    sbox[i] = i;
  for ( i = 0; i < 0x100; i++ )
  {
    tmp = sbox[i];
    j = (a1[k] + tmp + j) % 256;
    sbox[i] = sbox[j];
    sbox[j] = tmp;
    if ( ++k >= strlen(a1))
      k = 0;
  }
}

//解密 RC4
void encc(char *a1,char*data){
  init_sbox(a1);
  int i,j,k,tmp;
  for ( i = 0;i<strlen(data) ; i++ )
  {
    j = (j + 1) % 256;
    k = (sbox[j] + k) % 256;
    tmp = sbox[j];
    sbox[j] = sbox[k];
    sbox[k] = tmp;
    data[i] ^= sbox[(sbox[j] + sbox[k]) % 256];
  }
}
//异或
void enc(char *a1){
    int i;
    int len=strlen(a1);
  for ( i = 0; i < len; ++i )
    a1[i] ^= xork[i % 4];
  encc(xork, a1);
}

int main(){
  int i;
  char mm[]={0xC2, 0x6C, 0x73, 0xF4, 0x3A, 0x45, 0x0E, 0xBA, 0x47, 0x81, 
      0x2A, 0x26, 0xF6, 0x79, 0x60, 0x78, 0xB3, 0x64, 0x6D, 0xDC, 0xC9, 0x04,
      0x32, 0x3B, 0x9F, 0x32, 0x95, 0x60, 0xEE, 0x82, 0x97, 0xE7, 0xCA, 0x3D,
      0xAA, 0x95, 0x76, 0xC5, 0x9B, 0x1D, 0x89, 0xDB, 0x98, 0x5D};
  enc(mm);
  for(i=0;i<44;i++){
    putchar(mm[i]);
  }
  puts("");
}

AES 密钥是`MainActivity.title 也就是 “IamEzEncryptGame” ,厨子梭出来

Dirty_flowers

不能 f5,有花指令,那就按下 space 键看文本流程,直接将 push-pop 的指令也就是 0x4012f1~0x401302 的指令全部 nop 掉 ,然后在 main 函数头按下 U 和 P 重新编译

伪代码大概流程:输入 36 长度字符串,进入 sub_401100 加密,再与 sub_4011D0 判断

if ( strlen(v4) == 36 )
  {
    sub_401100(v4, 36);
    if ( sub_4011D0(v4, 36) )
      printf("success!\n");
    else
      printf("no!\n");
    return 0;
  }
  else
  {
    printf("wrong length!\n");
    return 0;
  }

sub_401100 也有花指令,和上面的一样流程来去花

signed int __cdecl sub_401100(int a1, int a2)
{
  signed int v2; // kr00_4
  signed int result; // eax
  int i; // [esp+1Ch] [ebp-1Ch]
  char v5[16]; // [esp+24h] [ebp-14h] BYREF

  strcpy(v5, "dirty_flower");
  v2 = strlen(v5);
  result = v2;
  for ( i = 0; i < a2; ++i )
  {
    result = i + a1;
    *(_BYTE *)(i + a1) ^= v5[i % v2];
  }
  return result;
}

sub_4011D0 函数比较内容大概是:(我比较喜欢看汇编流程图,感觉比伪代码更方便 :) 嘻嘻)

解密脚本

enc = [0x02, 0x05, 0x13, 0x13, 0x02, 0x1e, 0x53, 0x1f, 0x5c, 0x1a, 0x27, 0x43, 0x1d, 0x36, 0x43,
       0x07, 0x26, 0x2d, 0x55, 0x0d, 0x03, 0x1b, 0x1c, 0x2d, 0x02, 0x1c, 0x1c, 0x30, 0x38, 0x32,
       0x55, 0x02, 0x1b, 0x16, 0x54, 0x0f, 0x00]
str = "dirty_flower"
flag = ""
for i in range(len(enc)):
    enc[i] ^= ord(str[i % len(str)])
    flag += chr(enc[i])
print(flag)

Ptrace

ida 居然还能反编译.txt ,真的是 tql。第一次见这种类型的题目,看 wp 理解了半天,复现了俩小时(

记得将 son.txtfather.txt 放在同一目录下,打开 father.txt 文件

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp-14h] [ebp-30h]
  int v5; // [esp-10h] [ebp-2Ch]
  int v6; // [esp-Ch] [ebp-28h]
  int v7; // [esp-8h] [ebp-24h]
  int v8; // [esp-4h] [ebp-20h]
  const char **v9; // [esp+0h] [ebp-1Ch]
  char stat_loc[4]; // [esp+8h] [ebp-14h] BYREF
  __pid_t v11; // [esp+Ch] [ebp-10h]
  unsigned int v12; // [esp+10h] [ebp-Ch]
  int *p_argc; // [esp+14h] [ebp-8h]

  p_argc = &argc;
  v9 = argv;
  v12 = __readgsdword(0x14u);
  puts("Please input your flag:");
  __isoc99_scanf("%32s", &s, v4, v5, v6, v7, v8, v9);
  v11 = fork();     // 创建子进程
  if ( v11 )
  {//父进程
    if ( v11 <= 0 ) 
    {
      perror("fork");
      return -1;
    }
    wait(stat_loc);
    ptrace(PTRACE_POKEDATA, addr, addr, 3);
    ptrace(PTRACE_CONT, 0, 0, 0);
    wait(0);
  }
  else
  {//子进程
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    execl("./son", "son", &s, 0); //把 s 作为新进程的参数
  }
  return 0;
}

对于fork()execl() 函数:都是 Linux 中的进程控制函数

fork():创建新的进程,该进程几乎相当于当前进程的一个完全拷贝

execl():是函数族 exec() 之一,用来启动另外的进程以取代当前运行的进程

execl() 四个参数

参数 变量类型 解释
绝对路径 const char* 文件存储路径
标识符 const char* 大多数时候是文件名
参数 ------ 选项
NULL ------ NULL

所以 main() 函数中流程:fork() 创建子进程,返回的 pid 就是 v11v11>0 为父进程,v11=0 为子进程,子进程中使用了 execl() 函数,启动当前目录下的 son 文件,传入 s 作为新进程的参数,这里的新进程替换掉之前的子进程,使自己变成子进程。

打开 son.txt 查看

int __cdecl sub_600011AD(int a1, int a2)
{
  signed int i; // [esp+2h] [ebp-28h]
  signed int j; // [esp+6h] [ebp-24h]
  char *s; // [esp+Ah] [ebp-20h]
  signed int v6; // [esp+Eh] [ebp-1Ch]

  s = *(char **)(a2 + 4);
  v6 = strlen(s);
  for ( i = 0; i < v6; ++i )
    byte_60004080[i] = ((int)(unsigned __int8)s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));
  for ( j = 0; j < v6; ++j )
  {
    if ( byte_60004080[j] != byte_60004020[j] )
    {
      puts("this is Wrong~");
      return 0;
    }
  }
  puts("this is right~");
  return 0;
}

这里的 s = *(char **)(a2 + 4),其实它就是指向 father 传入的 sexecl 执行的命令为 ./son s,而对于 son 文件的主函数而言,第一个参数是 a1 表示执行命令参数的个数,这里就是 2,而后面的 a2 真实类型为 const char*,代表的就是命令的各个参数,所以这里的 a2 + 4 执行的就是第二个参数,也就是 s.

大概流程为:将 s 中的每个字节循环移位来进行变化,最后与密文进行比较

对于 father 文件的 ptarceptrace 是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。

而这里查看子进程,可以发现使用 ptrace(PTRACE_TRACEME, 0, 0, 0);,它就是允许父进程对自身进行调试的语句,然后在父进程中,使用 PTRACE_POKEDATA 对数据进行修改,然后使用 PTRACE_CONT 让子进程继续执行。因此我们要关注的就是父进程对子进程的什么数据进行了修改

我们能看到有语句ptrace(PTRACE_POKEDATA, addr, addr, 3);,就是将 addr 所指向的地址进行了数据修改,更改为了 3,点进去 addr 指向的就是 0x60004040 位置的数据

这个地址在 son 文件中也出现了

for ( i = 0; i < v6; ++i )
    byte_60004080[i] = ((int)(unsigned __int8)s[i] >> dword_60004040) | (s[i] << (8 - dword_60004040));

所以这个 ptrace 修改的是偏移值,将 4 改为了 3

因此,按照偏移 3 进行逆向变换,脚本

enc = [0xCC, 0x8D, 0x2C, 0xEC, 0x6F, 0x88, 0xED, 0xEB, 0x2F, 0xED,
       0xAE, 0xEB, 0x4E, 0xAC, 0x2C, 0x8D, 0x8D, 0x2F, 0xEB, 0x6D,
       0xCD, 0xED, 0xEE, 0xEB, 0x0E, 0x8E, 0x4E, 0x2C, 0x6C, 0xAC,
       0xE7, 0xAF]
for i in range(len(enc)):
    enc[i] = (enc[i] << 3 | enc[i] >> 5)&0xff
    print(chr(enc[i]), end='')

UPX

看了下 wp 需要脱壳,但是我拿到的附件已经是脱好壳的了,所以直接看伪代码

一般进行 upx 脱壳方法

upx -d "文件路径"
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int status; // [rsp+Ch] [rbp-4h]

  puts("Please input your flag:");
  __isoc99_scanf("%22s", s);
  RC4(s, key);
  for ( status = 0; status <= 21; ++status )
  {
    if ( s[status] != data[status] )
    {
      puts("this is Wrong~");
      exit(status);
    }
  }
  puts("this is right~");
  return 0;
}

输入的字符串长度为 22,然后进入 RC4 函数加密,后续进行 for 循环中,把sdata 进行比较

RC4 函数内容:

__int64 __fastcall RC4(const char *a1, __int64 a2)
{
  __int64 result; // rax
  unsigned __int8 v3; // [rsp+14h] [rbp-Ch]
  unsigned __int8 v4; // [rsp+15h] [rbp-Bh]
  unsigned int i; // [rsp+18h] [rbp-8h]
  unsigned int v6; // [rsp+1Ch] [rbp-4h]

  v3 = 0;
  v4 = 0;
  init_sbox(a2);
  v6 = strlen(a1);
  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i >= v6 )
      break;
    v4 += sbox[++v3];
    swap(&sbox[v3], &sbox[v4]);
    a1[i] ^= sbox[(unsigned __int8)(sbox[v3] + sbox[v4])];
  }
  return result;
}

RC4 加密,好讨厌写解密算法。。。

看 wp 可以进行动调,又学到了,直接下断点

这是 elf 文件,所以只能远程动调,连接 kali

随便输入字符串,断点断在了加密函数,查看数据就是我们输入的字符串

然后找到 data ,把数据提取出来

from ida_bytes import *
addr = 0x55FE34CF0040       //s的起始地址
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(22):
    patch_byte(addr + i, enc[i])
print('Done')

点进 run 之后,s 就会有变化,然后 f9 运行又会断在与 data 比较的地方,这个时候可以看 s 的值,与之前又不一样,按 a 可以转化为字符串(又学到了!)

over~

0 条评论
某人
表情
可输入 255

没有评论