firmware

一个关于路由器固件的逆向

思路

UE打开看到文件开头的TP-LINK technologies等字样,应该是路由器相关的。binwalk查看bin文件,经过相关查询,发现最后的

1
1180160       0x120200        Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 2774624 bytes, 519 inodes, blocksize: 131072 bytes, created: 2015-04-13 09:35:04

为一个文件系统,将其分离出来

1
dd if=firmware.bin skip=1180160 of=router-fs.squashfs bs=1

file得到文件大小后,再加上大小分离

1
dd if=firmware.bin skip=1180160 of=fs.squashfs bs=1 count=2774624

尝试使用unsquashfs解包,报错。查询相关问题,确认是版本不对,查询到相关解压缩工具

1
2
3
su apt-get install build-essential zlib1g-dev liblzma-dev python-magic
git clone https://github.com/mirror/firmware-mod-kit.git
cd firmware-mod-kit/src && ./configure && make

之后使用unsquashfs_all.sh尝试解包得到squashfs_root,

原以为按题目提示是在/home/ctf/flag.txt中,但是又说了是后门网站及端口,
寻找到backor文件解upx在字符串中即可找到网站再通过交叉引用即可发现端口。

crackME

有反调试手段的程序

思路

需要输入用户名及密码来进行校验,用户名已知。
开始时根据用户名建了个表,在函数fn_401090处。
之后进行check。

先将输入的密码每两个字节转化为一个字节存储

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
while ( i < strlen(pass) )
{
if ( isdigit(pass[i]) )
{
v9 = pass[i] - '0';
}
else if ( isxdigit(pass[i]) ) // a-f
{
if ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30u) + 24) + 12) != 2 ) //debug check
pass[i] = 0x22;
v9 = (pass[i] | 0x20) - 0x57;
}
else
{
v9 = ((pass[i] | 0x20) - 'a') % 6 + 10;
}
v10 = v9 + 0x10 * v10; // 每两个字节的输入转化为一个字节并将其存入V15中,eg:12abyz=>[0x12,0xab,0xab]
if ( !((signed int)(i + 1) % 2) )
{
v15[idx++] = v10;
len = idx;
v10 = 0;
}
++i;
}

之后将其与用户名建的表异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
while ( i_ < 8 )
{
v11 += user_rela[++j];
v13 = user_rela[j];
v8 = user_rela[v11];
user_rela[v11] = v13;
user_rela[j] = v8;

if ( *(_DWORD *)(__readfsdword(0x30u) + 0x68) & 0x70 )// debug check
v13 = v11 + j;
*(&v16 + i_) = user_rela[(unsigned __int8)(v8 + v13)] ^ v15[v5];
if ( *(_DWORD *)(__readfsdword(0x30u) + 2) & 0xFF )// debug check
{
v11 = 0xADu;
j = 0x2B;
}
sub_401710((int)&v16, (const char *)user, i_++);
v5 = i_;
if ( i_ >= (unsigned int)(&v15[strlen(v15) + 1] - &v15[1]) )
v5 = 0;
}

最后检测得到的结果是否为’dbappsec’

调试写脚本

1
2
3
4
5
6
7
8
9
10
re = 'dbappsec'
li = [0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd]

flag = [0 for _ in range(8)]
for i in range(8):
flag[i] = hex(ord(re[i])^li[i]).replace('0x','')
pwd = ''.join(flag)

import md5
print md5.new(pwd).hexdigest()

反调试手段

该程序使用的均为PEB结构的检查,通过FS:[30h]处得到PEB结构的基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

  1. 检测BeingDebugged

    1
    2
    3
    .text:00401B4D                 mov     eax, large fs:30h
    .text:00401B55 mov eax, [eax+2]
    .text:00401B5C test eax, eax
  2. 检测ProcessHeap属性
    PEB结构的0X18处,被设置为加载器为进程分配的第一个堆的位置,第一个堆头有一个属性字段用来告诉这个堆是否在调试器中创建(ForceFlags和Flags)
    在XP中ForceFlags位于堆头部偏移量0x10处,在win7他在0x44处
    同理在XP中偏移量0x0C或者win7系统中偏离量0x40中查看Flags属性

1
2
3
4
.text:00401985                 mov     eax, large fs:30h
.text:0040198B mov eax, [eax+18h]
.text:0040198E mov eax, [eax+0Ch]
.text:00401991 cmp eax, 2
  1. 检测NTGlobalFlag

PEB结构0x68处标志位,看是否为0x70

1
2
3
4
.text:00401ADA                 mov     eax, large fs:30h 
.text:00401AE0 mov eax, [eax+68h]
.text:00401AE3 and eax, 70h
.text:00401AE6 test eax, eax

equation

jsfuck相关

思路

拿到脚本可以看到全是jsfuck来判断flag,看到里面有不少==判断,拿到在线解jsfuck的网站上对几个进行测试,可以确定是对flag中的值进行加减之后来校验,问题是太多了不可能一个个手动复制过去得到原等式。

查询看下别人的脚本,是将判断的那些先提取出到一个文件中,再对每一个’l[…]’(原先的…为正常的jsfuck的下标),修改为”l[‘+(…)+’]”,相当于对所有jsfuck表示的数字用”‘+(…)+””给包括住

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
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
typedef long long LL;
#define foru(i,a,b) for(int i=a;i<=b;i++)
#define ford(i,a,b) for(int i=a;i>=b;i--)
char s[100000],a[100000];
int cnt;
int main(){
freopen("raw.js","r",stdin);
freopen("out.js","w",stut);
scanf("%s",s);
int n=strlen(s);
printf("s='%c",s[0]);
foru(i,1,n-1){
if(s[i]=='[')
if(s[i-1]=='l'){
a[++cnt]='l';
printf("['+(");
}else{
a[++cnt]='[';
printf("[");
}
else if(s[i]==']'){
if(a[cnt]=='l') printf(")+']");
else printf("]");
cnt--;
}
else if(s[i-1]=='='&&s[i-2]=='=')printf("'+(%c",s[i]);
else if(s[i+1]=='&'&&s[i+2]=='&')printf("%c)+'",s[i]);
else printf("%c",s[i]);
}
printf(");\nconsole.log(s);");
}

之后将等式在浏览器控制台运行即可得到等式

将其复制出来加入z3的Solver,得到flag

1
2
3
4
5
6
7
8
9
10
from z3 import *


s = Solver()
l = [Int('l[%d]'%i) for i in range(0x2a)]

s.add(...)

s.check()
print s.model()

signin

RSA相关逆向?

思路

可以直观的看到是个离散对数问题,反正是不可能硬解的。
然后是按rsa的思路来,模数是两个大素数的积,直接在factordb上分解得到pq,然后计算(p-1)*(q-1)

使用python计算的时候用下gmpy2库

1
2
3
4
apt-get install libgmp-dev
apt-get install libmpfr-dev
apt-get install libmpc-dev
pip install gmpy2

写下解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
n = 103461035900816914121390101299049044413950405173712170434161686539878160984549
p = 282164587459512124844245113950593348271
q = 366669102002966856876605669837014229419

c= 0xad939ff59f6e70bcbfad406f2494993757eee98b91bc244184a377520d06fc35
e = 65537

phin = (p-1)*(q-1)

import gmpy2
d= gmpy2.invert(e,phin)
m = pow(c,d,n)
flag = ''
m = hex(m)[2:]
print m
for i in range(len(m)/2):
flag += chr(int('0x'+m[2*i:2*(i+1)],16))
print flag

[GWCTF 2019] re3

SMC程序的逆向

思路

输入之后判断长度,然后对402219为起始地址处开始每个字节异或0x99,之后运行该函数。
这里需要写个IDC脚本来帮忙

1
2
3
4
5
6
7
8
9
10
11
#include <idc.idc>

static main()
{
auto addr = 0x402219;
auto i = 0;
for(i=0;i<=223;i++)
{
PatchByte(addr+i,Byte(addr+i)^0x99);
}
}

之后f5将其转化为伪代码,发现内部函数不少,怀疑其为加密算法,后面发现有AES的S盒,推测为AES,找到生成的密钥
写个脚本

1
2
3
4
5
6
7
8
from Crypto.Cipher import AES

k='cb8d493521b47a4cc1ae7e62229266ce'.decode('hex')

c='BC0AADC0147C5ECCE0B140BC9C51D52B46B2B9434DE5324BAD7FB4B39CDB4B5B'.decode('hex')
aes=AES.new(k)
mess = aes.decrypt(c)
print mess

maze_behind_junk

又一道迷宫

思路

查壳发现upx壳直接脱壳,之后可能因为中间掺杂了数据不能看伪代码,不过汇编也不长可以直接看

可以分析出来就是正常的迷宫switch,上下和左右分别控制两个变量,给了两张表,如果拿到里面是4的话就直接去最后的检查

分析出来是wasd控制上下左右,因为最后对两个变量进行检查,所以控制下次数就ok了,不过flag肯定不唯一,就不用交了

BJD hamburger competition

unity 逆向

思路

第一次碰到unity逆向,还是老八的小汉堡…,像是B站某个up做的…。看着程序挺正常。
使用dnspy逆向BJD hamburger competition_Data\Managed\Assembly-CSharp.dll,能够找到后门的关键函数(不太清楚是不是unity的关键函数都在这个dll里)

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
string name = this.toSpawn.name;
if (name == "汉堡底" && Init.spawnCount == 0)
{
Init.secret += 997;
}
else if (name == "鸭屁股")
{
Init.secret -= 127;
}
else if (name == "胡罗贝")
{
Init.secret *= 3;
}
else if (name == "臭豆腐")
{
Init.secret ^= 18;
}
else if (name == "俘虏")
{
Init.secret += 29;
}
else if (name == "白拆")
{
Init.secret -= 47;
}
else if (name == "美汁汁")
{
Init.secret *= 5;
}
else if (name == "柠檬")
{
Init.secret ^= 87;
}
else if (name == "汉堡顶" && Init.spawnCount == 5)
{
Init.secret ^= 127;
string str = Init.secret.ToString();
if (ButtonSpawnFruit.Sha1(str) == "DD01903921EA24941C26A48F2CEC24E0BB0E8CC7")
{
this.result = "BJDCTF{" + ButtonSpawnFruit.Md5(str) + "}";
Debug.Log(this.result);
}
}

选择不同的配料会对secret进行处理,最后对其sha1值进行比对,若相同,则计算其md5值即可得到flag,这里完全可以在线破解得到secret为1001。查了下出题人意思是除了写脚本得到选择的配料以外,还可以按老八原视频的顺序加…,魔鬼出题人无疑。

[RoarCTF2019]polyre

控制流平坦化的一道逆向

思路

几百行的while(1),人都看傻了。肯定不能就这么看,找到个基于angr的去控制流平坦化的脚本(https://github.com/cq674350529/deflat),效果还行,处理完分析程序流程。
程序里面还残留着不少相同的while(…)和if(…),看的时候基本上可以直接忽略掉,重点函数

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
for ( i = 0; ; ++i )                          // i=0;i<6;i++
{

v18 = i;
v19 = v18 < 6;

if ( !v19 )
break;
v20 = s;
v4 = *(_QWORD *)&v20[8 * i];
v7 = 0;
while ( 1 )
{
v21 = v7;
v22 = v21 < 64; // 64次
if ( !v22 )
break;
v23 = v4;
v24 = v4 < 0;
if ( v4 >= 0 ) // 乘2
{
v27 = v4;
v28 = 2 * v27;
v4 = v28;
}
else
{
v25 = 2 * v4;
v26 = v25;
v4 = v26 ^ 0xB0004B7679FA26B3LL;
}
v29 = v7;
v7 = v29 + 1;

}
v30 = 8 * i;
v31 = &s1[8 * i];

*(_QWORD *)v31 = v4;
*(_QWORD *)v31 = v4;

i_ = i + 1;
}

把那些多余的去掉,剩下这些基本上可以看出来在干啥,基本上就是取出来八个字节一组然后进行64次循环,里面会通过对大小进行判断然后做处理。

这些处理完之后最后应该是与data段的一块数据做对比,所以逆向流程写脚本就可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
re = [0x96, 0x62, 0x53, 0x43, 0x6D, 0xF2, 0x8F, 0xBC, 0x16, 0xEE, 
0x30, 0x05, 0x78, 0x00, 0x01, 0x52, 0xEC, 0x08, 0x5F, 0x93,
0xEA, 0xB5, 0xC0, 0x4D, 0x50, 0xF4, 0x53, 0xD8, 0xAF, 0x90,
0x2B, 0x34, 0x81, 0x36, 0x2C, 0xAA, 0xBC, 0x0E, 0x25, 0x8B,
0xE4, 0x8A, 0xC6, 0xA2, 0x81, 0x9F, 0x75, 0x55]

flag = ''
for i in range(0,48,8):
num = (re[i+7]<<56) | (re[i+6] <<48) | re[i+5]<<40 | re[i+4]<<32 | re[i+3]<<24 | re[i+2]<<16 | re[i+1]<<8 | re[i]
for _ in range(64):
if num&1:
num = (num ^ 0xB0004B7679FA26B3)//2
num += 0x8000000000000000
else:
num = num//2
meta = hex(num)[2:]
for j in range(len(meta)//2-1,-1,-1): #小端序表示,需要倒序输出
flag += chr(int('0x'+meta[j*2:(j+1)*2],16))
print(flag)

xxor

思路

本来挺简单的,但是因为python数据一般不会自动控制大小,所以写的时候麻烦了点。前面用z3解下后面的结果

1
2
3
4
5
6
7
8
9
10
from z3 import *

s = Solver()

re = [Int('re[%d]'%i) for i in range(6)]

s.add(re[0] == 0xDF48EF7E, re[5] == 0x84F30420 , re[1] == 0x20CAACF4)
s.add(re[2] - re[3] == 0x84A236FF , re[3] + re[4] == 0xFA6CB703 , re[2] - re[4] == 0x42D731A8)
s.check()
print(s.model())

拿到之后对处理函数反过来写一下,主要是在中间每一步运算之后就要将其控制成四个字节,然后最后尝试使用了下libnum的n2s直接转化十六进制为字符串。

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
re = [0 for _ in range(6)]
re[2] = 3774025685
re[3] = 1548802262
re[4] = 2652626477
re[1] = 550153460
re[5] = 2230518816
re[0] = 3746099070

v5 = 0
for i in range(64):
v5 += 0x458BCD42
v5 %= 0x100000000

print(hex(v5)) #0x62F35080

import libnum

flag = ''
a = [0 for _ in range(2)]
for i in range(3):
v3 = re[i*2]
v4 = re[i*2+1]
v5 = 0x62F35080
for j in range(64):
v4 -= ((v3 + v5 + 20) ^ ((v3 << 6) + 3) ^ ((v3 >> 9) + 4) ^ 0x10)%0x100000000
v4 %= 0x100000000
v3 -= ((v4 + v5 + 11) ^ ((v4 << 6) + 2) ^ ((v4 >> 9) + 2) ^ 0x20)%0x100000000
v3 %= 0x100000000
v5 -= 0x458BCD42
v5 %= 0x100000000


flag += libnum.n2s(v3)
flag += libnum.n2s(v4)

print(flag)

[FlareOn5]Ultimate Minesweeper

挺有意思的.NET 逆向

思路

打开是个扫雷程序,随便点了几次全都是雷…。查壳没有,dnspy打开,基本上没做混淆,看了下几个函数,发现主要都在MainForm里面,注意到了有个GetKey函数,看着有随机数,寻找下调用,发现上面的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void SquareRevealedCallback(uint column, uint row)
{
if (this.MineField.BombRevealed)
{
this.stopwatch.Stop();
Application.DoEvents();
Thread.Sleep(1000);
new FailurePopup().ShowDialog();
Application.Exit();
}
this.RevealedCells.Add(row * MainForm.VALLOC_NODE_LIMIT + column);
if (this.MineField.TotalUnrevealedEmptySquares == 0)
{
this.stopwatch.Stop();
Application.DoEvents();
Thread.Sleep(1000);
new SuccessPopup(this.GetKey(this.RevealedCells)).ShowDialog();
Application.Exit();
}
}

这里就是检测有没有碰到雷的函数,可以看到就是成功的时候会调用GetKey应该就可以拿到flag。也可以看到计算位置的时候就是row*30+column。寻找下初始化扫雷方块的函数,找到了分配内存的函数,可以发现大多数都是直接为true,应该就是为地雷,而特殊的有专门得到flag为false的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void AllocateMemory(MineField mf)
{
for (uint num = 0u; num < MainForm.VALLOC_NODE_LIMIT; num += 1u)
{
for (uint num2 = 0u; num2 < MainForm.VALLOC_NODE_LIMIT; num2 += 1u)
{
bool flag = true;
uint r = num + 1u;
uint c = num2 + 1u;
if (this.VALLOC_TYPES.Contains(this.DeriveVallocType(r, c))) //here
{
flag = false;
}
mf.GarbageCollect[(int)num2, (int)num] = flag;
}
}
}

查看下跟这句话有关的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private uint DeriveVallocType(uint r, uint c)
{
return ~(r * MainForm.VALLOC_NODE_LIMIT + c);
}


private static uint VALLOC_TYPE_HEADER_PAGE = 4294966400u;
private static uint VALLOC_TYPE_HEADER_POOL = 4294966657u;
private static uint VALLOC_TYPE_HEADER_RESERVED = 4294967026u;

private uint[] VALLOC_TYPES = new uint[]
{
MainForm.VALLOC_TYPE_HEADER_PAGE,
MainForm.VALLOC_TYPE_HEADER_POOL,
MainForm.VALLOC_TYPE_HEADER_RESERVED
};

也就是当计算的值为这几个时就为false,反过来计算下就行

1
2
3
4
5
6
7
8
def getrc(num):
num = ~num & 0xffffffff
c = num % 30
r = num // 30
return r,c

print(map(getrc,[4294966400,4294966657,4294967026]))
#[(29, 25), (21, 8), (8, 29)]

点击这三个方块即可拿到flag