本文最后更新于 2024年11月29日 下午
有 Java 代码审计,好难,Java 我讨厌你qwq,但是我觉得这些题出得太好啦,复现完学到了很多!!!
Pangbai 泰拉记(1) ida 打开反汇编查看,flag 和一个 key 异或
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __int64 __fastcall main () { std::ostream *v0; std::ostream *v1; int i; j___CheckForDebuggerJustMyCode (&_6D15E8 DE_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 0 i64; }
对 key 交叉引用能看见还没 main0 函数调用,进去看看
发现有两个函数 IsDebuggerPresent() 和 CheckRemoteDebuggerPresent(),这是两个反调试函数
对于 CheckRemoteDebuggerPresent() 函数:
kernel32 的 CheckRemoteDebuggerPresent() 函数用于检测指定进程是否正在被调试. Remote 指同一个机器中的不同进程
1 2 3 4 BOOL WINAPI CheckRemoteDebuggerPresent( _In_ HANDLE hProcess, _Inout_ PBOOL pbDebuggerPresent );
如果说检测到正在被调试,那么 pbDebuggerPresent 指向的值会被设置为 0xffffffff。
对于这道题,调试一下可以看得出来流程,进程会跳转到右边这里,这里执行的是将 key 替换为不正确的 key ,所以可以将 jz 指令改为 jnz 指令,让程序跳转不了
1 2 jz: Jump if Zero ,零标志位 ZF=1 时,jz 指令执行跳转jnz: Jump if Not Zero ,零标志位 ZF=0 时,jnz 指令执行跳转
然后进行调试
提取数据,写脚本
1 2 3 4 5 6 7 8 9 10 11 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,然后再比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int __cdecl main (int argc, const char **argv, const char **envp) { int i; __int64 v5; printf ("Please Input: \n" ); sub_140001120 ("%32s" , byte_140004700); v5 = -1 i64; 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 函数查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __int64 __fastcall sub_140001180(unsigned int *a1 , _DWORD *a2 ) { __int64 result unsigned int v3 unsigned int v4 int v5 unsigned int i v3 = *a1 v4 = a1 [1 ] v5 = 0 for ( i = 0 { 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 = 4 i64 a1 [1 ] = v4 return result }
看到这个加密流程就能发现是 tea 算法加密找到key 和输入的值直接逆
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #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 ){ 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 加密,密钥是 MainActivity 的 title
doEncCheck 函数进行加密数据检查,发现有 native 关键字,在 Java 中,native 关键字用于声明一个方法是由本地代码(通常是C或C++)实现的,所以说明函数是 C/C++ 编写的,所以主体在 so 文件,进行 so 提取
IDA 打开 so 文件,找到 doEncCheck ,点进去查看
enc 函数的加密伪代码
1 2 3 4 5 6 7 8 9 10 __int64 __fastcall enc(char *a1 ) { int i int v3 v3 = __strlen_chk(a1 , -1 LL); for ( i = 0 a1 [i] ^= xork[i % 4 ]; return encc(xork, a1 ); }
又看见一个 encc 加密函数,(太好了,又有个算法加密,我们有救了!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 __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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <string.h> char sbox[257 ] = {0 };char xork[] = "meow" ;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 ; } }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 判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 也有花指令,和上面的一样流程来去花
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 signed int __cdecl sub_401100 (int a1, int a2) { signed int v2; signed int result; int i; char v5[16 ]; 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 函数比较内容大概是:(我比较喜欢看汇编流程图,感觉比伪代码更方便 :) 嘻嘻)
解密脚本
1 2 3 4 5 6 7 8 9 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.txt 和 father.txt 放在同一目录下,打开 father.txt 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int __cdecl main (int argc, const char **argv, const char **envp) { int v4; int v5; int v6; int v7; int v8; const char **v9; char stat_loc[4 ]; __pid_t v11; unsigned int v12; int *p_argc; p_argc = &argc; v9 = argv; v12 = __readgsdword(0x14 u); 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 ); } return 0 ; }
对于fork()、execl() 函数:都是 Linux 中的进程控制函数
fork():创建新的进程,该进程几乎相当于当前进程的一个完全拷贝
execl():是函数族 exec() 之一,用来启动另外的进程以取代当前运行的进程
execl() 四个参数
参数
变量类型
解释
绝对路径
const char*
文件存储路径
标识符
const char*
大多数时候是文件名
参数
——
选项
NULL
——
NULL
所以 main() 函数中流程:fork() 创建子进程,返回的 pid 就是 v11,v11>0 为父进程,v11=0 为子进程,子进程中使用了 execl() 函数,启动当前目录下的 son 文件,传入 s 作为新进程的参数,这里的新进程替换掉之前的子进程,使自己变成子进程。
打开 son.txt 查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int __cdecl sub_600011AD (int a1, int a2) { signed int i; signed int j; char *s; signed int v6; 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 传入的 s。execl 执行的命令为 ./son s,而对于 son 文件的主函数而言,第一个参数是 a1 表示执行命令参数的个数,这里就是 2,而后面的 a2 真实类型为 const char*,代表的就是命令的各个参数,所以这里的 a2 + 4 执行的就是第二个参数,也就是 s.
大概流程为:将 s 中的每个字节循环移位来进行变化,最后与密文进行比较
对于 father 文件的 ptarce,ptrace 是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了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 文件中也出现了
1 2 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 进行逆向变换,脚本
1 2 3 4 5 6 7 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 脱壳方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int __cdecl main (int argc, const char **argv, const char **envp) { int status; 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 循环中,把s 和 data 进行比较
RC4 函数内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 __int64 __fastcall RC4 (const char *a1, __int64 a2) { __int64 result; unsigned __int8 v3; unsigned __int8 v4; unsigned int i; unsigned int v6; 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 ,把数据提取出来
1 2 3 4 5 6 7 8 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~