NewstarCTF2023 week4 re
旨在详细记录一下 (毕竟官方WP有些讲的也不清楚) (终于全部补完了...)
k00h_slT
IDA打开 先查看main
这里读入flag后 CreateThread
先进入startaddress查看
貌似什么都没干就退出了
这里tab看一下汇编
发现这里没有识别出来(灰色部分)
这是因为401779提前retn了
所以我们想办法将40176E后到40177A的nop掉 然后让未识别的反编译
由于这里直接ctrl+N的话patch不准确
会patch掉我们需要的部分
所以利用ida_python/idc patch
#include<idc.idc>
static main(){
auto st=0x40176E,ed=0x40177A,i;
for(i=st;i<=ed;i++){
PatchByte(i,0x90);
}
}
然后再将未识别的c一下
发现下面还有一些要处理
可以看出我们还需要将40179C到4017A8的nop掉 一样的用idcpatch
然后反编译就能看到完整代码
其实这里的wcscmp对我们的进程列表作了比较(细心看可以对比出跟之前有不同) 所以如果不把这段正常化后面调试是进行不了的
将patch后的applytoexe
注意到函数列表还有一个Tls_callback 查看
分析可以知道:
- case0: check
- case1: 反调试
- case2: 注意到144=0x90 所以这里检测的是Tlscallback函数自身有没有被nop 若有则修改一些关键数据 所以我们不能nop来反调试
- case3: 这里其实是对sub4010500中调用的函数进行了修改
看到这个virtualprotect就应该意识到有修改的可能
首先要绕过case1的反调试
汇编查看
有个简单方法就是将jz和jnz互改 将跳转条件反一下就可以防止while(1)
了
关于Tls_callback参数:
Tls_callback的第二个参数Reason可以有四个值
分别是DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH、DLL_PROCESS_DETACH
因此对应顺序为 1-2-3-0
整个程序流程:
- 首先程序启动,先通过case 1检测是否存在调试器,如果存在则直接卡死进程
- 然后通过main函数中的CreateThread创建了一个线程,然后由于有WaitForSingleObject的存在,main函数卡在这条指令等待线程执行结束。线程进入后先通过case 2的nop检测,然后对4A7000进行修改。
动调进入sub_4013B0 运行到此处即可得到key的值 - 随后反调试之进程检测,然后调用了sub_401500,此时这个函数的内容对应的是sub_401170函数即base64函数。
- 调用完后退出线程的时候进入case 3,sub_401500的call被修改成为了sub_4013B0
- 退出线程后,进入main函数中的sub_401500,进行了xxtea的加密。下面比较不相同因此不会触发假flag的提示。
- 程序所有进程退出,进入了Tls的case 0,case 0是最后的check函数,通过检测后程序输出Congratulations。
大致分析过后开始动调
这里注意下断点在case3的时候要找到 switch case3的地方 跟tab不是对应的地方
动调进入case3可以在汇编中看到
这里call 401170被改为了4013B0
对应这段P后反编译发现是一个xxtea加密
找key:
加密数据的话在case0的check中可以提取出来
然后针对xxtea写个解密即可(网上找个脚本)
#include <stdio.h>
#include <stdint.h>
#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<3) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
void btea(uint32_t *v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if (n > 1) /* Coding Part */
{
rounds = 6 + 52/n;
sum = 0;
z = v[n-1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p=0; p<n-1; p++)
{
y = v[p+1];
z = v[p] += MX;
}
y = v[0];
z = v[n-1] += MX;
}
while (--rounds);
}
else if (n < -1) /* Decoding Part */
{
n = -n;
rounds = 6 + 52/n;
sum = rounds*DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p=n-1; p>0; p--)
{
z = v[p-1];
y = v[p] -= MX;
}
z = v[n-1];
y = v[0] -= MX;
sum -= DELTA;
}
while (--rounds);
}
}
int main()
{
uint32_t v[]= {0x3400A0D0, 0xB23CFFEB, 0xCDE69111, 0x032D0771, 0xFA1D9E6C, 0x9D15360A, 0x933EBF03, 0x9F12DDA6,
0x8C58DDA1, 0x46BEE3E0, 0x04476F65, 0x3C44CEF9};
uint32_t const k[4]= {0x75,0x404,0xBF,0X2652};
int n= -12;
btea(v, n, k);
for(int i=0;i<12;i++)
for(int j=0;j<4;j++){
printf("%c",(v[i]>>(8*j))&0xff);
}
return 0;
}
总之拿到base变表(一样的动调提取即可 )加密后的
48Pt4WXo+yhqh0GzFAbRg0XqgFhq4UOl+UwqL8wUHy6rEFSQ
用base64变表解一下就可以得到flag
最后再整体动调走一遍逻辑
-
读取flag
-
CreateThread
-
StartAddress中调用sub401500
此时查看发现是一个base64
可以提取出变表
-
调用TlsCallback_0
1-2-3-0
case3修改了call的函数为xxtea加密 -
回到main 再次调用修改后的sub_401500
然后进行假的check(由于xxtea加密了所以肯定不会触发假flag的提示)若相同则输出fakeflag
-
最后的case0动调看不出来 但是前面理论分析了在结束thread的时候会调用 所以case0进行最后真正的check检测
补充:这里其实是可以看到case0的调用的
在case0下个断点 在几乎都跑完的时候(前面调用了系统的dll)
可以看到左下角debugger的提示 thread exited 此时刚好是回到case0处
这样一来整道题的逻辑就清楚了(收获良多)
iwannarest
jadx打开
这里加载了一个libarary
在边栏找到
发现在资源文件内
将.apk改为.zip解压
然后ida打开
逐个函数查看
在这个函数内找到 关键加密算法
一眼..tea
最终变化后的v5和key都告诉我们了
直接逆即可 (中间调一调注意下细节 比如初始sum的值)
#include<bits/stdc++.h>
#include<stdint.h>
using namespace std;
signed main(){
unsigned __int64 v7[8]={0xEC339492,0xBB7D6A5F,0x5BD9F28A,0x412C450B,0xC5982391,0x3355F5FD,0x1503E350,0x3945171};
// unsigned __int64 v7[8]={0xdc091d87,0xc19ea29c,0xf69a5c7a,0x91f6e33b,0x8a5b94e1,0x93529f20,0x23b0e340,0xf91d069b};
// v7[0] = 0xDC091F87C19EA29CLL;
// v7[1] = 0xF69A5C7A91F6E33BLL;
// v7[2] = 0x8A5B94E193529F20LL;
// v7[3] = 0x23B0E340F91D069BLL;
unsigned int a2[8]={1,1,4,5};
// unsigned int a2[4]={0x74617221,0x4e657753,0x74617221,0x4e657753};
// 0x4e657753746172214e65775374617221
//481B081D3A1E0C27
unsigned int *result; // rax
unsigned int v3; // [rsp+4h] [rbp-24h]
unsigned int v4; // [rsp+8h] [rbp-20h]
unsigned int v5; // [rsp+Ch] [rbp-1Ch]
unsigned int i; // [rsp+10h] [rbp-18h]
for(int j=0;j<4;j+=1){
// cout<<j<<" ";
v4 = v7[2*j];
v5 = v7[2*j+1];
v3 = -1640531527*33;
for ( i = 0; i <= 31; ++i )
{
v3 += 1640531527;
v5 -=((((v4 >> 5) ^ (4 * v4)) + ((v4 >> 3) ^ (16 * v4))) ^ ((v3 ^ v4) + (v4 ^ a2[(unsigned int)((v3>>2)&3) ^ 1])));
v4 -= (((v5 >> 5) ^ (4 * v5)) + ((v5 >> 3) ^ (16 * v5))) ^ ((v3 ^ v5) + (v5 ^ a2[(unsigned int)((v3>>2)&3) ]));
}
v7[j*2] = v4;
// result = a1;
v7[j*2+1] = v5;
// return result;
}
for(int i=0;i<8;i++){
for(int m=0;m<=3;m++)
printf("%c",(v7[i]>>(8*m))&0xff);
}
}
茶
虚拟机逆向
动调导出指令流
大概框架:
这里的0x9E...
是通过动调进入VM内部得到的
然后注意下这里每次reg[4]
的初始赋值情况
开始是reg[4]=reg[1]=v[1]
后面是reg[4]=reg[0]=变换后的v[1]
中间还是3个异或
k[]
数组的值:
这里容易把8当作一个值 其实convert看看
实际上是2,0,2,3
参数都找全了 40轮逆向即可
补:利用程序自动化导出opcode
#include<bits/stdc++.h>
using namespace std;
unsigned int k[8] = {
0x00000002, 0x00000000, 0x00000002, 0x00000003, 0x00000000, 0x00000000, 0x00000000, 0x00000000
};
void F0(){
cout<< "Init \n";
}
void F1(int a1,int a2,int a3){
if(a3==1){
printf("reg[%d] = reg[%d]\n",a1,a2);
}
if(a3==2){
printf("reg[%d] = v[%d]\n",a1,a2);
}
if(a3==0){
printf("reg[%d] = %d\n",a1,a2);
}
}
void F2(int a1,int a2,int a3){
if(a3==1){
printf("reg[%d] += reg[%d]\n",a1,a2);
}
if(a3==2){
printf("reg[%d] += k[%d]\n",a1,a2);
}
if(a3==0){
printf("reg[%d] += %d\n",a1,a2);
}
}
void F3(int a1,int a2){
printf("reg[%d] <<= %d\n",a1,a2);
}
void F4(int a1,int a2){
printf("reg[%d] ^= reg[%d]\n",a1,a2);
}
void F5(int a1,int a2){
printf("reg[%d] >>= %d\n",a1,a2);
}
int main(){
unsigned int v2[208] = {
};
int v3;
v3=0;
int cnt=0;
while ( v2[v3] != 255 )
{
switch ( v2[v3] )
{
case 240:
F0();
++v3;
break;
case 241:
F1(v2[v3 + 1], v2[v3 + 2], v2[v3 + 3]);
v3 += 4;
break;
case 242:
F2(v2[v3 + 1], v2[v3 + 2], v2[v3 + 3]);
v3 += 4;
break;
case 243:
F3(v2[v3 + 1], v2[v3 + 2]);
v3 += 3;
break;
case 244:
F4(v2[v3 + 1], v2[v3 + 2]);
v3 += 3;
break;
case 245:
F5(v2[v3 + 1], v2[v3 + 2]);
v3 += 3;
break;
case 246:
if ( cnt==40 )
++v3;
else
v3 = 17;
cnt++;
return 0;
break;
default:
v3++;
}
}
}
导出后
这比手动动调抄还是简便得多(而且更具普适性)
solution.c
#include<bits/stdc++.h>
#include<stdint.h>
using namespace std;
signed main(){
unsigned __int64 v7[]={0x76B9621A, 0xCCE4ADDE, 0x25C8BFC8, 0x16C2D472, 0xF317D53A, 0xF2A111A1, 0xDF89F0E6, 0xDCCDA623,
0x21C2F409, 0xDBD88D63};
unsigned int k[8]={2,0,2,3};
// unsigned int a2[4]={0x74617221,0x4e657753,0x74617221,0x4e657753};
// 0x4e657753746172214e65775374617221
//481B081D3A1E0C27
unsigned int *result; // rax
unsigned int v3; // [rsp+4h] [rbp-24h]
unsigned int v4; // [rsp+8h] [rbp-20h]
unsigned int v5; // [rsp+Ch] [rbp-1Ch]
unsigned int i; // [rsp+10h] [rbp-18h]
for(int j=0;j<5;j+=1){
// cout<<j<<" ";
v4 = v7[2*j];
v5 = v7[2*j+1];
//<-------- encrypt --------------->
// v3 = 0;
// unsigned int delta=0x9e3779b9;
// for ( i = 0; i < 40; ++i )
// {
// v3 += delta;
// v4 += ((v5>>5)+k[1])^((v5<<5)+k[0])^(v5+v3);
// v5 += ((v4>>5)+k[3])^((v4<<5)+k[2])^(v4+v3);
// }
// <-------- decrypt --------------->
unsigned int delta=0x9e3779b9;
v3 += 40*delta;
for ( i = 0; i < 40; ++i )
{
v5 -= ((v4>>5)+k[3])^((v4<<5)+k[2])^(v4+v3);
v4 -= ((v5>>5)+k[1])^((v5<<5)+k[0])^(v5+v3);
v3 -= delta;
}
v7[j*2] = v4;
// result = a1;
v7[j*2+1] = v5;
// return result;
}
for(int i=0;i<10;i++){
for(int m=0;m<=3;m++)
printf("%c",(v7[i]>>(8*m))&0xff);
}
}
最后 想知道怎么用angr一把梭呐???
VM好多题都可以angr做 但是这道我尝试了没成功 有空研究问一下再补
easy_js
用在线网站反混淆
https://tool.yuanrenxue.cn/decode_obfuscator
function _0x4a2285(_0x2b5a00) {
var _0x37bb73 = new Array(256);
var _0x53324d = new Array(256);
var _0x563e78, _0x1be492, _0x5b4a53;
// RC4
for (_0x563e78 = 0; _0x563e78 < 256; _0x563e78++) {
_0x37bb73[_0x563e78] = _0x563e78;
_0x53324d[_0x563e78] = _0x2b5a00["charCodeAt"](_0x563e78 % _0x2b5a00["length"]);
}
for (_0x563e78 = _0x1be492 = 0; _0x563e78 < 256; _0x563e78++) {
_0x1be492 = (_0x1be492 + _0x37bb73[_0x563e78] + _0x53324d[_0x563e78]) % 256;
_0x5b4a53 = _0x37bb73[_0x563e78];
_0x37bb73[_0x563e78] = _0x37bb73[_0x1be492];
_0x37bb73[_0x1be492] = _0x5b4a53;
}
return _0x37bb73;
}
function _0x569454(_0x1f637f, _0x46bf6a) {
return String["fromCharCode"](_0x1f637f["charCodeAt"](0) + 13) + String["fromCharCode"](_0x46bf6a["charCodeAt"](0) - 1) + "wstar" + '_' + String["fromCharCode"](_0x46bf6a['charCodeAt'](1) + 1) + String["fromCharCode"](_0x1f637f["charCodeAt"](3) + 1) + 's';
}
function _0x221c90(_0x2acf55, _0x549db3) {
var _0x300c36 = '',
_0x22f2ea = new Array(256);
_0x22f2ea = _0x4a2285(_0x2acf55);
var _0x37dfd2, _0x3c262d, _0x20fab4;
_0x37dfd2 = _0x3c262d = 0;
for (var _0x4ff9a6 = 0; _0x4ff9a6 < _0x549db3['length']; _0x4ff9a6++) {
_0x37dfd2 = (_0x37dfd2 + 1 + 1) % 256;
_0x3c262d = (_0x3c262d + _0x22f2ea[_0x37dfd2]) % 256;
_0x20fab4 = _0x22f2ea[_0x37dfd2];
_0x22f2ea[_0x37dfd2] = _0x22f2ea[_0x3c262d];
_0x22f2ea[_0x3c262d] = _0x20fab4;
_0x300c36 += String["fromCharCode"](_0x549db3["charCodeAt"](_0x4ff9a6) ^ _0x22f2ea[(_0x22f2ea[_0x37dfd2] + _0x22f2ea[_0x3c262d]) % 256] ^ 3);
}
return _0x300c36;
}
window["_0x54cd23"] = _0x53816c;
var _0x24eb58 = window['_0x54cd23'];
function _0x53816c(_0x3cc7e4) {
return btoa(_0x3cc7e4) === "Cn8RHIJEVdvlrRESjETCscwQZdlhRfsRkWoHCTa0HcfLPg==";
}
function _0x499d16() {
alert("来试试吧");
var _0x2fd441 = document["getElementById"]("username")['value'];
var _0x387488 = document["getElementById"]("password")['value'];
var _0x130920 = document['getElementById']("flagtext")["value"];
if (_0x2fd441 === "admin" && _0x387488 === "123456") {
alert("正在验证账号密码------");
alert("账号密码正确!再接再厉");
alert("正在验证flag------");
var _0x271a69 = _0x221c90(_0x569454(_0x2fd441, _0x387488), _0x130920);
_0x24eb58(_0x271a69) ? alert("flag正确!") : alert("不行不行!");
} else {
alert('不行不行');
}
}
这里是一个RC4加密 但是有些细节变了
巨坑。。。
然后就是找 enc数组
这里可以看到
js里的btoa 是base64...
然后将这段解码成hex 就是flag 加密后的
这里的key发现是对 admin 和 123456进行了一些变换
找一个js在线运行 函数打上去传这两个参数就可以得到key
对着写一个RC4 把表得到后逆向xor一下就有flag了
solution
#include<bits/stdc++.h>
using namespace std;
signed main(){
int base64_table[] = {0x0A ,0x7F ,0x11 ,0x1C ,0x82 ,0x44 ,0x55 ,0xDB ,0xE5 ,0xAD ,0x11,
0x12 ,0x8C ,0x44 ,0xC2 ,0xB1 ,0xCC ,0x10 ,0x65 ,0xD9 ,0x61 ,0x45 ,0xFB ,0x11 ,0x91 ,0x6A ,0x07 ,0x09 ,0x36 ,0xB4 ,0x1D ,0xC7 ,0xCB ,0x3E };
string key = "n0wstar_3js";
int s[256],k[256];
int j=0;
for (int i = 0; i < 256; i++) {
s[i] = i;
k[i] = key[i % key.length()];
}
for (int i2 = 0; i2 < 256; i2++) {
j = (s[i2] + j + k[i2]) & 255;
int temp = s[i2];
s[i2] = s[j];
s[j] = temp;
}
int j2 = 0;
int i3 = 0;
for (int i4 : base64_table) {
i3 = (i3 + 1 +1) & 255;
j2 = (s[i3] + j2) & 255;
int temp2 = s[i3];
s[i3] = s[j2];
s[j2] = temp2;
int rnd = s[(s[i3] + s[j2]) & 255];
cout<<((char) (i4 ^ rnd^3));
}
}
简单的跨
jadx打开
可以看到是一个RC4 和加载了 antidbg
解包apk后打开
一个base64换表
这里变表也告诉了 直接base64解成hex数组
但是这里直接RC4解密是不行的
因为涉及到C和Java char类型的区别
这里有个方法 利用python
enc = b64()
enc = enc.replace(b'\xc0\x80', b'\x00')
# print(enc.decode())
print(enc.hex())
for x in enc.decode():
print(ord(x),end=',')
print('\n')
print(''.join([chr(x) for x in encrypt(enc.decode(), "runrunrun")]))
至于为什么这里要将 b'\xc0\x80'换成b'\x00'也不太清楚 是字节的问题吧 不换的话decode会出错
经过python转换得到的新的enc表为
注释掉的为旧表 可以发现两者不一样
然后跑个RC4即可
#include<bits/stdc++.h>
using namespace std;
signed main(){
// int base64_table[] = {0xc3,0xa0,0x27,0x00,0x71,0xc2,0xa0,0x55,0xc3,0x8c,0xc2,0xa3,0xc3,0x92,0x79,0xc2,0xb6,0xc2,0x83,0xc2,0xb8,0x1e,0x4f,0x3b,0xc2,0x80,0x4a,0xc3,0xbc,0xc3,0xad,0x2e,0xc3,0xad,0x1c,0xc3,0xa3,0x48,0x2a,0x53,0x28,0xc2,0x87,0x4e,0x26,0xc3,0x9e,0xc3,0xb9,0xc2,0x90,0xc3,0x97,0x13,0xc2,0xa4,0xc3,0xaa,0xc2,0x99};
int base64_table[] = {224,39,0,113,160,85,204,163,210,121,182,131,184,30,79,59,128,74,252,237,46,237,28,227,72,42,83,40,135,78,38,222,249,144,215,19,164,234,153};
// string base64_table = "à'q UÌ£Òy¶¸O;Jüí.íãH*S(N&Þùפê";
string key = "runrunrun";
int s[256],k[256];
int j=0;
for (int i = 0; i < 256; i++) {
s[i] = i;
k[i] = key[i % key.length()];
}
for (int i2 = 0; i2 < 256; i2++) {
j = (s[i2] + j + k[i2]) & 255;
int temp = s[i2];
s[i2] = s[j];
s[j] = temp;
}
int j2 = 0;
int i3 = 0;
for (int i4 : base64_table) {
i3 = (i3 + 1 ) & 255;
j2 = (s[i3] + j2) & 255;
int temp2 = s[i3];
s[i3] = s[j2];
s[j2] = temp2;
int rnd = s[(s[i3] + s[j2]) & 255];
cout<<((char) (i4 ^ rnd));
}
}
可以看到 根本不需要将 '\xc0\x80'换成'x\00' 直接utf-8转后即可(注意给他hex转回来。。。)就是这个忘了卡了好久...