Back

babymips

UTCTF2020 babymips

[UTCTF2020]babymips

0x00

MIPS的指令:

image-20220118095524636

IDA可以直接反编译出MIPS的伪代码,所以我们直接看伪代码即可,可以看到是C++语法:

 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 v3; // $v0
  char v5[24]; // [sp+18h] [+18h] BYREF
  char v6[24]; // [sp+30h] [+30h] BYREF
  char v7[84]; // [sp+48h] [+48h] BYREF

  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v5, argv, envp);
  v3 = std::operator<<<std::char_traits<char>>(&std::cout, "enter the flag");
  std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  std::operator>><char>(&std::cin, v5);
  memcpy(v7, &unk_4015F4, sizeof(v7));
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v6, v5);
  sub_401164(v7, v6);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v6);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v5);
  return 0;
}

关键函数:sub_401164传入两个参数,一个是v7,将unk_4015F4复制,一个是v6,就是输入的flag

导出unk_4015F4:

image-20220118102349919

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned char ida_chars[] =
{
  0x62, 0x6C, 0x7F, 0x76, 0x7A, 0x7B, 0x66, 0x73, 0x76, 0x50, 
  0x52, 0x7D, 0x40, 0x54, 0x55, 0x79, 0x40, 0x49, 0x47, 0x4D, 
  0x74, 0x19, 0x7B, 0x6A, 0x42, 0x0A, 0x4F, 0x52, 0x7D, 0x69, 
  0x4F, 0x53, 0x0C, 0x64, 0x10, 0x0F, 0x1E, 0x4A, 0x67, 0x03, 
  0x7C, 0x67, 0x02, 0x6A, 0x31, 0x67, 0x61, 0x37, 0x7A, 0x62, 
  0x2C, 0x2C, 0x0F, 0x6E, 0x17, 0x00, 0x16, 0x0F, 0x16, 0x0A, 
  0x6D, 0x62, 0x73, 0x25, 0x39, 0x76, 0x2E, 0x1C, 0x63, 0x78, 
  0x2B, 0x74, 0x32, 0x16, 0x20, 0x22, 0x44, 0x19, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00
};

sub_401164,整体逻辑很简单,显示判断a2的size是否为0x4E,是则进入for循环,对a2挨个字符进行异或与a1相比,最终都相等则输出correct:

 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
int __fastcall sub_401164(int a1, int a2)
{
  int v2; // $v0
  int result; // $v0
  int v4; // $v0
  unsigned int i; // [sp+1Ch] [+1Ch]

  if ( std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a2) != 0x4E )
  {
LABEL_2:
    v2 = std::operator<<<std::char_traits<char>>(&std::cout, "incorrect");
    result = std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
  }
  else
  {
    for ( i = 0; i < std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a2); ++i )
    {
      if ( (*(char *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i) ^ (i + 23)) != *(char *)(a1 + i) )
        goto LABEL_2;
    }
    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "correct!");
    result = std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
  }
  return result;
}

根据以上即可写出exp:

 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
#include <iostream>
using namespace std;

int main() {
	unsigned char res[84] = { 0 };
	unsigned char ida_chars[] =
	{
	  0x62, 0x6C, 0x7F, 0x76, 0x7A, 0x7B, 0x66, 0x73, 0x76, 0x50,
	  0x52, 0x7D, 0x40, 0x54, 0x55, 0x79, 0x40, 0x49, 0x47, 0x4D,
	  0x74, 0x19, 0x7B, 0x6A, 0x42, 0x0A, 0x4F, 0x52, 0x7D, 0x69,
	  0x4F, 0x53, 0x0C, 0x64, 0x10, 0x0F, 0x1E, 0x4A, 0x67, 0x03,
	  0x7C, 0x67, 0x02, 0x6A, 0x31, 0x67, 0x61, 0x37, 0x7A, 0x62,
	  0x2C, 0x2C, 0x0F, 0x6E, 0x17, 0x00, 0x16, 0x0F, 0x16, 0x0A,
	  0x6D, 0x62, 0x73, 0x25, 0x39, 0x76, 0x2E, 0x1C, 0x63, 0x78,
	  0x2B, 0x74, 0x32, 0x16, 0x20, 0x22, 0x44, 0x19, 0x00, 0x00,
	  0x00, 0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
	  0x00, 0x00
	};

	for (int i = 0; i < 0x4E; i++) {
		res[i] = ida_chars[i] ^ (i + 23);
	}

	cout << res << endl;
}

得到:

1
utflag{mips_cpp_gang_5VDm:~`N]ze;\)5%vZ=C'C(r#$q=*efD"ZNY_GX>6&sn.wF8$v*mvA@'}

image-20220118105019420

image-20220118105150796

到这里解题就结束了,接下来就是关于MIPS指令的学习

0x01 MIPS指令

首先需要知道MIPS和x86指令最本质的区别就是,MIPS属于RISC(精简指令集),x86属于CISC(复杂指令集)。

精简指令集与复杂指令集的区别就是指令的数量,精简指令集是以最大化程度减少指令的数量,那么相应的,某些功能如果通过精简指令集进行表达,则需要多条指令结合才能达到目的。而复杂指令集相反,增加指令的类型和数量,减少某些功能执行指令的数量。可以说前者是牺牲时间换空间,后者则是牺牲空间换时间,在某些简单操作上,精简指令集是比较占优势的,而对于复杂操作,复杂指令集的效率往往会更高,目前我们常用的Linux、Windows系统都是属于x86或者arm架构的复杂指令集。

MIPS,全称为Microcompute without InterLocked Pipeline Stages,中文翻译为无互锁流水级的微处理器

MIPS包含32个通用寄存器($0-$31):

编号 助记符 用法
0 zero 不管传入什么数据,该寄存器的值永远都为0
1 at 用作汇编器的暂时变量
2-3 v0, v1 子函数调用返回结果
4-7 a0-a3 子函数调用的参数
8-15 t0-t7 暂时变量,子函数调用时不需要保存与恢复
24-25 t8-t9 暂时变量,子函数调用时不需要保存与恢复
16-23 s0-s7 子函数寄存器变量,在返回之前子函数必须保存和恢复使用过的变量,
26-27 k0, k1 通常被中断或异常处理程序使用作为保存一些系统参数
28 gp 全局指针,一些运行系统维护这个指针来更方便的存取static和extern变量
29 sp 堆栈指针
30 s8/fp 第9个寄存器变量/框架指针
31 ra 子函数的返回地址

MIPS三种指令格式:

  • R格式 Register Format 所有其他
  • I格式 Immediate Format 用于有立即数的指令,lw,sw,
  • J格式 Jump Format 无条件跳转j,并连接jal

R指令

0x02 实例

根据功能可以分为五类:

1.算数运算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
move $t5, $t1       // $t5 = $t1;
add $t0, $t1,       // $t2 $t0 = $t1 + $t2; 带符号数相加
sub $t0, $t1,       // $t2 $t0 = $t1 - $t2; 带符号数相减
addi $t0, $t1, 5    // $t0 = $t1 + 5;
addu $t0, $t1, $t2  // $t0 = $t1 + $t2; 无符号数相加
subu $t0, $t1, $t2  // $t0 = $t1 - $t2; 无符号数相减
mult $t3, $t4       // $t3 * $t4, 把64-Bits 的积,存储到Lo,Hi中。即: (Hi, Lo) = $t3 * $t4;
div $t5, $t6        // Lo = $t5 / $t6 (Lo为商的整数部分); Hi = $t5 mod $t6 (Hi为余数)
mfhi $t0            // $t0 = Hi
mflo $t1            // $t1 = Lo

2.分支跳转:

1
2
3
4
5
6
7
b target 无条件的分支跳转,将跳转到target 标签处
beq $t0, $t1, target       // 如果 $t0 == $t1, 则跳转到target 标签处
blt $t0, $t1, target       // 如果 $t0 < $t1,  则跳转到target 标签处
ble $t0, $t1, target       // 如果 $t0 <=$t1,  则跳转到target 标签处
bgt $t0, $t1, target       // 如果 $t0 > $t1,  则跳转到target 标签处
bge $t0, $t1, target       // 如果 $t0 >= $t1, 则跳转到target 标签处
bne $t0, $t1, target       // 如果 $t0 != $t1, 则跳转到target 标签处

3.跳转:

1
2
j target          // 无条件的跳转, 将跳转到target 标签处
jr $t3            // 跳转到t3寄存器所指向的地址处(Jump Register)

4.数据加载/存储

1
2
3
4
5
la $t0, val_1 复制val_1表示的地址到t0寄存器中     注: val_1是个Label
lw $t2, ($t0) t0寄存器中的值作为地址,把这个地址起始的Word 复制到t2 中
lw $t2, 4($t0) t0寄存器中的值作为地址, 把这个地址再加上偏移量4后 所起始的Word 复制到t2 中
sw $t2, ($t0) 把t2寄存器中值(1 Word),存储到t0的值所指向的RAM中
sw $t2, -12($t0) 把t2寄存器中值(1 Word),存储到t0的值再减去偏移量12, 所指向的RAM 中

5.子函数调用

1
2
3
4
jal sub_routine_label 执行步骤:
  a. 复制当前的PC(Program Counter)到$ra寄存器中。 因为当前的PC 值就是子函数执行完毕后的返回
       地址。
  b. 程序跳转到子程序标签sub_routine_label处

根据以上实例大概可以了解到,对于存在立即数的指令,指令的助记符必须加上i,例如addi $t0 $t1 5subi $t0 $t1 5,对于无符号数操作,与x86中类似,在助记符后加u即可,例如addu $t0 $t1,如果同时存在,则加上iu,例如 subiu $t0 $t1 5

0x03 实例练习

在该题中,首先根据graph流程图,找到循环的位置:

image-20220118145824586

在进循环前执行:sw $zero, 0x28+var_C($fp),在反编译中,进循环前第一步就是int i = 0,由于$zero寄存器中的值永远为0,所以可以知道该指令就是对内存的赋值,对于0x28+var_c($fp)中的值进行赋值,那么该变量就是此时的i,可以发现sw就是从左往右赋值。在每次循环结束时,i++,根据关键字寻找可以发现,跳转前执行以下指令:

1
2
3
4
lw      $v0, 0x28+var_C($fp)
addiu   $v0, 1
sw      $v0, 0x28+var_C($fp)
b       loc_4011FC

sw相反的就是lw,从右往左赋值,将临时变量i存入寄存器v0中,然后将v0的值加1,通过sw将值存回i中。

对于lw称为数据加载,将数据从内存加载到寄存器中,而sw称为数据存储,将数据从寄存器存储到内存中,然后调用分支指令b跳转到判断的位置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
lw      $a0, 0x28+arg_4($fp)
la      $v0, _ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE4sizeEv  # std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(void)
move    $t9, $v0
jalr    $t9 ; std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(void)  # std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(void)
nop
lw      $gp, 0x28+var_18($fp)
move    $v1, $v0
lw      $v0, 0x28+var_C($fp)
sltu    $v0, $v1
andi    $v0, 0xFF
beqz    $v0, loc_4012E0

首先加载地址到a0寄存器中,通过之前的了解我们知道a0-a3都是子函数的参数,所以继续往下,la应该就是将子函数地址给到v0寄存器,然后赋值到t9寄存器中,通过jalr指令进行子函数调用,此时a0作为参数,其实就是输入的flag的地址,调用完之后,将strlen的结果加载到v0寄存器中,往下就是将v0的结果复制到v1,然后取i加载到v0,执行sltuv0v1进行比较,根据前面的学习,sltu无符号数操作,所以此时sltu $v0, $v1的含义是,if $v0 < $v1, $v0 = 1, else $v0 = 0,往下的beqz就是如果v0=0则跳转到loc_4012E0的位置继续执行,该位置就是提示成功的位置了:

image-20220118155755028

往下便如法炮制了,不再做深入了解,通过本题大概学习了MIPS的指令格式以及其与x86架构的一些区别,后续如果遇到其他相关题目,再做深入的学习

参考文献

[1]https://blog.csdn.net/qq_41191281/article/details/85933985

[2]https://blog.csdn.net/ben_chong/article/details/51794093

Licensed under CC BY-NC-SA 4.0
YuSec Github Blog
Built with Hugo
Theme Stack designed by Jimmy